code-foundry 1.46.3 → 1.46.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +40 -0
- package/.github/workflows/release.yml +82 -4
- package/.gitignore +5 -0
- package/CHANGELOG.md +15 -15
- package/docs/CONFIGURATION.md +21 -0
- package/docs/WORKFLOWS.md +17 -0
- package/package.json +1 -1
- package/src/commands/doctor.mjs +25 -2
- package/src/commands/sync.mjs +144 -1
- package/src/templates/gitignore +5 -0
package/.githooks/pre-commit
CHANGED
|
@@ -6,6 +6,46 @@ if [ -z "$(git diff --cached --name-only)" ]; then
|
|
|
6
6
|
exit 0
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
|
+
# core.hooksPath names a single directory, so pointing it at .githooks would
|
|
10
|
+
# silence every machine-level hook behind it: secret guards, git-lfs, signing.
|
|
11
|
+
# Chain to the machine-level pre-commit when one exists. The target is
|
|
12
|
+
# discovered at run time — never baked into this committed file — from the
|
|
13
|
+
# repository's previous hooks directory (recorded by sync), then the global
|
|
14
|
+
# hooksPath, then the checkout's default hooks directory. The machine hook
|
|
15
|
+
# runs first so cheap secret guards reject before the heavier project gate.
|
|
16
|
+
machine_hook() {
|
|
17
|
+
hook_name="$1"
|
|
18
|
+
for dir in \
|
|
19
|
+
"$(git config --get code-foundry.previousHooksPath 2>/dev/null || true)" \
|
|
20
|
+
"$(git config --global --get core.hooksPath 2>/dev/null || true)" \
|
|
21
|
+
"$(git rev-parse --git-common-dir 2>/dev/null || printf '.git')/hooks"
|
|
22
|
+
do
|
|
23
|
+
case "$dir" in
|
|
24
|
+
'' | .githooks | ./.githooks | */.githooks) continue ;;
|
|
25
|
+
esac
|
|
26
|
+
candidate="$dir/$hook_name"
|
|
27
|
+
if [ -f "$candidate" ] && ! [ "$candidate" -ef "$0" ]; then
|
|
28
|
+
printf '%s' "$candidate"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if [ -z "${CODE_FOUNDRY_HOOK_CHAIN:-}" ]; then
|
|
36
|
+
if machine="$(machine_hook pre-commit)"; then
|
|
37
|
+
# The marker travels through the machine hook into anything it delegates
|
|
38
|
+
# back to: a guard that chains to .githooks/pre-commit (a mutual-delegation
|
|
39
|
+
# pair) re-enters this hook, sees the marker, and runs the gate instead of
|
|
40
|
+
# chaining again — machine policy once, project policy once, no loop.
|
|
41
|
+
if [ -x "$machine" ]; then
|
|
42
|
+
CODE_FOUNDRY_HOOK_CHAIN=1 "$machine" "$@" || exit $?
|
|
43
|
+
else
|
|
44
|
+
CODE_FOUNDRY_HOOK_CHAIN=1 sh "$machine" "$@" || exit $?
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
9
49
|
# Prefer a runtime the repository already depends on. A repository opts into
|
|
10
50
|
# the gate by depending on code-foundry; one that does not keeps the
|
|
11
51
|
# whitespace guard and stays offline. Nothing is fetched at commit time.
|
|
@@ -447,6 +447,9 @@ jobs:
|
|
|
447
447
|
# when it validated, github.token after fallback. The token value
|
|
448
448
|
# lives only in this masked env expression, never in step outputs.
|
|
449
449
|
AUTO_MERGE: ${{ steps.credentials.outputs.auto_merge }}
|
|
450
|
+
# Read Actions ownership with the workflow credential, not an
|
|
451
|
+
# automation token that may only have release/PR write permissions.
|
|
452
|
+
VALIDATION_TOKEN: ${{ github.token }}
|
|
450
453
|
GH_TOKEN: ${{ steps.credentials.outputs.token_source == 'configured' && (secrets.CODE_FOUNDRY_TOKEN) || github.token }}
|
|
451
454
|
run: |
|
|
452
455
|
set -euo pipefail
|
|
@@ -464,12 +467,87 @@ jobs:
|
|
|
464
467
|
fi
|
|
465
468
|
|
|
466
469
|
for pr in "${release_prs[@]}"; do
|
|
467
|
-
|
|
470
|
+
pr_state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" \
|
|
471
|
+
--json isDraft,headRefOid,headRefName \
|
|
472
|
+
--jq '[.isDraft, .headRefOid, .headRefName] | @tsv')
|
|
473
|
+
read -r PR_DRAFT PR_HEAD PR_BRANCH <<< "$pr_state"
|
|
474
|
+
if [[ ! "$PR_DRAFT" =~ ^(true|false)$ || ! "$PR_HEAD" =~ ^[a-f0-9]{40}$ ]]; then
|
|
475
|
+
echo 'Unable to verify generated release PR state.' >&2
|
|
476
|
+
exit 1
|
|
477
|
+
fi
|
|
478
|
+
case "$PR_BRANCH" in
|
|
479
|
+
release-please--branches--main|release-please--branches--main--*) ;;
|
|
480
|
+
*) echo 'Release PR head left the generated namespace; refusing readiness mutation.' >&2; exit 1 ;;
|
|
481
|
+
esac
|
|
468
482
|
if [ "$AUTO_MERGE" = true ]; then
|
|
469
483
|
if [ "$PR_DRAFT" = false ]; then
|
|
470
|
-
# A
|
|
471
|
-
#
|
|
472
|
-
#
|
|
484
|
+
# A ready head already receives synchronize validation. Preserve
|
|
485
|
+
# its live/completed owner instead of cancelling and reallocating
|
|
486
|
+
# CI every time another queued main release run prepares it.
|
|
487
|
+
runs=$(GH_TOKEN="$VALIDATION_TOKEN" gh api \
|
|
488
|
+
"repos/$GITHUB_REPOSITORY/actions/runs" --method GET \
|
|
489
|
+
-f event=pull_request -f head_sha="$PR_HEAD" -f per_page=100)
|
|
490
|
+
validation=$(jq -er --arg head "$PR_HEAD" --arg branch "$PR_BRANCH" \
|
|
491
|
+
--arg repo "$GITHUB_REPOSITORY" '
|
|
492
|
+
def active: .status == "queued" or .status == "in_progress" or
|
|
493
|
+
.status == "waiting" or .status == "pending" or .status == "requested";
|
|
494
|
+
def valid_run:
|
|
495
|
+
type == "object" and
|
|
496
|
+
(.id | type == "number" and . > 0 and floor == .) and
|
|
497
|
+
(.run_attempt | type == "number" and . >= 1 and floor == .) and
|
|
498
|
+
(.head_sha | type == "string" and test("^[a-f0-9]{40}$")) and
|
|
499
|
+
(.head_branch | type == "string" and length > 0) and
|
|
500
|
+
(.event | type == "string" and length > 0) and
|
|
501
|
+
(.repository | type == "object") and
|
|
502
|
+
(.repository.full_name | type == "string" and length > 0) and
|
|
503
|
+
(.path | type == "string" and length > 0) and
|
|
504
|
+
((active and has("conclusion") and .conclusion == null) or
|
|
505
|
+
(.status == "completed" and
|
|
506
|
+
(.conclusion == "success" or .conclusion == "failure" or
|
|
507
|
+
.conclusion == "timed_out" or .conclusion == "action_required" or
|
|
508
|
+
.conclusion == "cancelled" or .conclusion == "skipped" or
|
|
509
|
+
.conclusion == "neutral")));
|
|
510
|
+
if (.workflow_runs | type) != "array" or
|
|
511
|
+
(.total_count | type) != "number" then
|
|
512
|
+
error("Invalid validation owner response")
|
|
513
|
+
elif .total_count != (.workflow_runs | length) then
|
|
514
|
+
error("Incomplete validation owner response")
|
|
515
|
+
elif any(.workflow_runs[]; valid_run | not) then
|
|
516
|
+
error("Invalid validation run identity or state")
|
|
517
|
+
else
|
|
518
|
+
[.workflow_runs[] | (.path | split("@")[0]) as $workflow | select(
|
|
519
|
+
.head_sha == $head and .head_branch == $branch and
|
|
520
|
+
.event == "pull_request" and .repository.full_name == $repo and
|
|
521
|
+
($workflow == ".github/workflows/validation.yml" or
|
|
522
|
+
$workflow == ".github/workflows/validation_self-ci.yml")
|
|
523
|
+
)] | sort_by(.id) | last |
|
|
524
|
+
if . == null then "missing"
|
|
525
|
+
elif active or (.status == "completed" and .conclusion == "success") then "owned"
|
|
526
|
+
elif .status == "completed" and
|
|
527
|
+
(.conclusion == "failure" or .conclusion == "timed_out" or
|
|
528
|
+
.conclusion == "action_required") then "failed"
|
|
529
|
+
elif .status == "completed" and
|
|
530
|
+
(.conclusion == "cancelled" or .conclusion == "skipped" or
|
|
531
|
+
.conclusion == "neutral") then "missing"
|
|
532
|
+
else error("Unconfirmed validation owner state") end
|
|
533
|
+
end' <<< "$runs")
|
|
534
|
+
fresh_state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" \
|
|
535
|
+
--json isDraft,headRefOid,headRefName \
|
|
536
|
+
--jq '[.isDraft, .headRefOid, .headRefName] | @tsv')
|
|
537
|
+
if [ "$fresh_state" != "$pr_state" ]; then
|
|
538
|
+
echo 'Release PR state changed during validation lookup; refusing a stale readiness mutation.' >&2
|
|
539
|
+
exit 1
|
|
540
|
+
fi
|
|
541
|
+
if [ "$validation" = owned ]; then
|
|
542
|
+
echo "Release PR #$pr already has current-head validation; preserving readiness."
|
|
543
|
+
continue
|
|
544
|
+
fi
|
|
545
|
+
if [ "$validation" = failed ]; then
|
|
546
|
+
echo "Current-head validation failed for release PR #$pr; resolve or explicitly retry it before release." >&2
|
|
547
|
+
exit 1
|
|
548
|
+
fi
|
|
549
|
+
# A missing, cancelled or skipped owner still needs the existing
|
|
550
|
+
# explicit ready_for_review transition with the validated token.
|
|
473
551
|
gh pr ready --undo "$pr" --repo "$GITHUB_REPOSITORY"
|
|
474
552
|
fi
|
|
475
553
|
gh pr ready "$pr" --repo "$GITHUB_REPOSITORY"
|
package/.gitignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.46.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.4...v1.46.5) (2026-10-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **sync:** chain machine-level hooks instead of silencing them ([#747](https://github.com/0xPlayerOne/code-foundry/issues/747)) ([7fcad52](https://github.com/0xPlayerOne/code-foundry/commit/7fcad52d400dbb2aaa9ba46c0c76f14e8d63f660)), closes [#690](https://github.com/0xPlayerOne/code-foundry/issues/690)
|
|
9
|
+
|
|
10
|
+
## [1.46.4](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.3...v1.46.4) (2026-10-08)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **release:** preserve current-head validation ownership ([#737](https://github.com/0xPlayerOne/code-foundry/issues/737)) ([aa8bfcf](https://github.com/0xPlayerOne/code-foundry/commit/aa8bfcfbfcd3c213d15ba9e1fbfb5c65ab8ddb6b))
|
|
16
|
+
|
|
3
17
|
## [1.46.3](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.2...v1.46.3) (2026-10-07)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -126,23 +140,9 @@
|
|
|
126
140
|
|
|
127
141
|
* **release:** budget the package without the changelog and ship recent history only ([#708](https://github.com/0xPlayerOne/code-foundry/issues/708)) ([ab365e0](https://github.com/0xPlayerOne/code-foundry/commit/ab365e0910472c3596429adc86a1c8f3e19730f9))
|
|
128
142
|
|
|
129
|
-
## [1.44.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.2...v1.44.0) (2026-10-03)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
### Features
|
|
133
|
-
|
|
134
|
-
* **pre-commit:** make the commit gate change-aware ([#702](https://github.com/0xPlayerOne/code-foundry/issues/702)) ([f5bca91](https://github.com/0xPlayerOne/code-foundry/commit/f5bca91c177b30b99f5d62287255d191617b4788))
|
|
135
|
-
|
|
136
|
-
## [1.43.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.1...v1.43.2) (2026-10-03)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
### Bug Fixes
|
|
140
|
-
|
|
141
|
-
* require candidate asset qualification before stable promotion ([#705](https://github.com/0xPlayerOne/code-foundry/issues/705)) ([f42e4f5](https://github.com/0xPlayerOne/code-foundry/commit/f42e4f5fb063ea22fa25791a13c75c90d41bf17a))
|
|
142
|
-
|
|
143
143
|
<!-- code-foundry: packaged changelog -->
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
-
This packaged changelog lists the 20 most recent releases;
|
|
147
|
+
This packaged changelog lists the 20 most recent releases; 240 older releases are omitted to keep the package small.
|
|
148
148
|
See the [full changelog](https://github.com/0xPlayerOne/code-foundry/blob/main/CHANGELOG.md) for the complete history.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -201,6 +201,27 @@ exists on disk. Task scripts that declare dependencies of their own, such as a
|
|
|
201
201
|
Turborepo `typecheck` task with `dependsOn: ["^build"]`, still run them; drop
|
|
202
202
|
that dependency to keep type checks build-free.
|
|
203
203
|
|
|
204
|
+
### Machine-level hook chaining
|
|
205
|
+
|
|
206
|
+
`core.hooksPath` names a single directory, so enabling `.githooks` would
|
|
207
|
+
silence every hook in the machine's own hooks directory: secret guards,
|
|
208
|
+
git-lfs, signing. Sync chains instead of replacing. The generated
|
|
209
|
+
`.githooks/pre-commit` discovers a machine-level `pre-commit` at run time —
|
|
210
|
+
from the hooks directory sync replaced (recorded as the
|
|
211
|
+
`code-foundry.previousHooksPath` repository config), then the global
|
|
212
|
+
`core.hooksPath`, then the checkout's default hooks directory — and runs it
|
|
213
|
+
before the gate, so a cheap secret guard rejects before the heavier project
|
|
214
|
+
checks. The committed hook stays machine-independent; the target is resolved
|
|
215
|
+
on the machine that runs it.
|
|
216
|
+
|
|
217
|
+
Hook names this repository does not own (`post-checkout`, `pre-push`,
|
|
218
|
+
`commit-msg`, and the other client-side hooks) get delegating stubs in
|
|
219
|
+
`.githooks` when a machine-level counterpart exists, which is what keeps
|
|
220
|
+
git-lfs working after sync. Stubs are machine-local, gitignored, rewritten on
|
|
221
|
+
every sync, and removed once the machine hook they mirrored disappears.
|
|
222
|
+
`code-foundry doctor` warns when a machine-level pre-commit exists but the
|
|
223
|
+
installed hook predates chaining.
|
|
224
|
+
|
|
204
225
|
## Release and branch policy
|
|
205
226
|
|
|
206
227
|
| Key | Values | Purpose |
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -82,6 +82,23 @@ to run generated gates for drafts. Cloudflare callers use their equivalent
|
|
|
82
82
|
`draft-protection: false` input. These opt-outs do not remove Draft Guard or
|
|
83
83
|
draft-PR automation; they only allow the protected gates to run for drafts.
|
|
84
84
|
|
|
85
|
+
Generated release PR preparation preserves a ready head when the canonical
|
|
86
|
+
generated `.github/workflows/validation.yml` (or Code Foundry
|
|
87
|
+
`validation_self-ci.yml`) pull-request run for that exact head, branch, and
|
|
88
|
+
repository is the newest matching run and is active or successful. API workflow
|
|
89
|
+
paths may include a Git ref suffix; ownership compares the canonical filename.
|
|
90
|
+
The workflow credential reads Actions ownership; the validated automation credential still performs readiness writes.
|
|
91
|
+
A newer failed qualification blocks release preparation until it is resolved or
|
|
92
|
+
explicitly retried. Missing, cancelled, or skipped validation still receives the
|
|
93
|
+
explicit draft/ready transition; a new draft receives its first readiness event.
|
|
94
|
+
An unavailable or malformed ownership response, an incomplete Actions page, or a
|
|
95
|
+
PR state change during the lookup fails before mutation. An older live run cannot
|
|
96
|
+
mask a newer failed qualification. Invalid automation credentials retain the
|
|
97
|
+
manual draft fallback. This changes readiness only: release creation, qualification,
|
|
98
|
+
tag publication, and branch-policy-gated exact-head merging still run through
|
|
99
|
+
the existing release pipeline. Consumers must adopt the fixed reusable workflow
|
|
100
|
+
pin before they receive this behavior; older pins still reset ready heads.
|
|
101
|
+
|
|
85
102
|
## Billing pause
|
|
86
103
|
|
|
87
104
|
Code Foundry consumers can stop all generated jobs from allocating GitHub-hosted
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.46.
|
|
3
|
+
"version": "1.46.5",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
4
|
-
import { join, resolve } from 'node:path'
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
4
|
+
import { join, resolve, isAbsolute } from 'node:path'
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { includesValue, readConfig } from '../lib/config.mjs'
|
|
7
7
|
import { recommendRunners, resolveProfile } from '../lib/profile.mjs'
|
|
@@ -53,6 +53,29 @@ export function doctor(root, options = {}) {
|
|
|
53
53
|
warn(
|
|
54
54
|
'.githooks/pre-commit does not delegate to the Code Foundry gate; run `npx code-foundry sync`'
|
|
55
55
|
)
|
|
56
|
+
// core.hooksPath shadows every machine-level hook behind it (secret
|
|
57
|
+
// guards, git-lfs). The generated hook chains to the machine's pre-commit
|
|
58
|
+
// when sync recorded one, so a machine hook without the chain marker in
|
|
59
|
+
// the installed hook is exactly the silent-disable bug chaining exists to
|
|
60
|
+
// prevent.
|
|
61
|
+
const hookText = existsSync(hookFile) ? readFileSync(hookFile, 'utf8') : ''
|
|
62
|
+
const machineDirs = [
|
|
63
|
+
git(target, ['config', '--get', 'code-foundry.previousHooksPath']),
|
|
64
|
+
git(target, ['config', '--global', '--get', 'core.hooksPath']),
|
|
65
|
+
`${git(target, ['rev-parse', '--git-common-dir']) || '.git'}/hooks`,
|
|
66
|
+
].filter((dir) => dir && dir !== '.githooks' && dir !== './.githooks')
|
|
67
|
+
const machinePreCommit = machineDirs.some((dir) => {
|
|
68
|
+
const candidate = isAbsolute(dir) ? join(dir, 'pre-commit') : join(target, dir, 'pre-commit')
|
|
69
|
+
try {
|
|
70
|
+
return statSync(candidate).isFile()
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
if (machinePreCommit && !hookText.includes('machine_hook'))
|
|
76
|
+
warn(
|
|
77
|
+
'A machine-level pre-commit exists but the installed hook does not chain to it; run `npx code-foundry sync`'
|
|
78
|
+
)
|
|
56
79
|
}
|
|
57
80
|
if (
|
|
58
81
|
['rust', 'python', 'solidity'].some((language) =>
|
package/src/commands/sync.mjs
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
readdirSync,
|
|
8
8
|
readFileSync,
|
|
9
9
|
rmSync,
|
|
10
|
+
statSync,
|
|
10
11
|
writeFileSync,
|
|
11
12
|
} from 'node:fs'
|
|
12
|
-
import { dirname, join, resolve } from 'node:path'
|
|
13
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
13
14
|
import { spawnSync } from 'node:child_process'
|
|
14
15
|
import {
|
|
15
16
|
detectLanguages,
|
|
@@ -550,7 +551,17 @@ function synchronize(options) {
|
|
|
550
551
|
existsSync(join(target, '.githooks/pre-commit'))
|
|
551
552
|
) {
|
|
552
553
|
chmodSync(join(target, '.githooks/pre-commit'), 0o755)
|
|
554
|
+
// core.hooksPath names a single directory, so pointing it at .githooks
|
|
555
|
+
// would silence every machine-level hook behind it (secret guards,
|
|
556
|
+
// git-lfs, signing). Record what is being replaced so the generated
|
|
557
|
+
// hooks can chain back to it, then delegate the hook names this
|
|
558
|
+
// repository does not own.
|
|
559
|
+
const previousHooksPath = gitOutput(target, ['config', '--get', 'core.hooksPath'])
|
|
560
|
+
if (previousHooksPath && previousHooksPath !== '.githooks') {
|
|
561
|
+
git(target, ['config', 'code-foundry.previousHooksPath', previousHooksPath])
|
|
562
|
+
}
|
|
553
563
|
git(target, ['config', 'core.hooksPath', '.githooks'])
|
|
564
|
+
syncMachineHookDelegates(target, previousHooksPath)
|
|
554
565
|
}
|
|
555
566
|
console.log(`${changed.length} baseline file(s) differ.`)
|
|
556
567
|
if (overlays.custom_workflows === 'preserve') {
|
|
@@ -1848,6 +1859,138 @@ function git(target, args) {
|
|
|
1848
1859
|
spawnSync('git', args, { cwd: target, stdio: 'ignore' })
|
|
1849
1860
|
}
|
|
1850
1861
|
|
|
1862
|
+
/** @param {string} target @param {string[]} args @returns {string} */
|
|
1863
|
+
function gitOutput(target, args) {
|
|
1864
|
+
const result = spawnSync('git', args, { cwd: target, encoding: 'utf8' })
|
|
1865
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : ''
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* Hook names git runs on the client that Code Foundry does not own. When one
|
|
1870
|
+
* of these exists in a machine-level hooks directory, sync writes a
|
|
1871
|
+
* delegating stub for it so enabling `core.hooksPath=.githooks` keeps the
|
|
1872
|
+
* machine's hooks (git-lfs, secret scanners) firing.
|
|
1873
|
+
*/
|
|
1874
|
+
const MACHINE_DELEGATED_HOOKS = [
|
|
1875
|
+
'applypatch-msg',
|
|
1876
|
+
'commit-msg',
|
|
1877
|
+
'prepare-commit-msg',
|
|
1878
|
+
'post-applypatch',
|
|
1879
|
+
'post-checkout',
|
|
1880
|
+
'post-commit',
|
|
1881
|
+
'post-merge',
|
|
1882
|
+
'post-rewrite',
|
|
1883
|
+
'pre-applypatch',
|
|
1884
|
+
'pre-auto-gc',
|
|
1885
|
+
'pre-rebase',
|
|
1886
|
+
'pre-push',
|
|
1887
|
+
'reference-transaction',
|
|
1888
|
+
'sendemail-validate',
|
|
1889
|
+
'post-index-change',
|
|
1890
|
+
]
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* The hooks directories a machine-level hook may live in, in the order the
|
|
1894
|
+
* generated hooks probe them: the repository hooks path sync just replaced,
|
|
1895
|
+
* then the global one, then the checkout's default directory. Relative values
|
|
1896
|
+
* resolve against the repository root, matching git's own semantics.
|
|
1897
|
+
*
|
|
1898
|
+
* @param {string} target @param {string} previousHooksPath @returns {string[]}
|
|
1899
|
+
*/
|
|
1900
|
+
function machineHooksDirectories(target, previousHooksPath) {
|
|
1901
|
+
return [
|
|
1902
|
+
previousHooksPath,
|
|
1903
|
+
gitOutput(target, ['config', '--global', '--get', 'core.hooksPath']),
|
|
1904
|
+
`${gitOutput(target, ['rev-parse', '--git-common-dir']) || '.git'}/hooks`,
|
|
1905
|
+
].flatMap((dir) => {
|
|
1906
|
+
if (!dir || dir === '.githooks' || dir === './.githooks') return []
|
|
1907
|
+
return [isAbsolute(dir) ? dir : join(target, dir)]
|
|
1908
|
+
})
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* The delegating stub for one machine-level hook name. Self-contained: the
|
|
1913
|
+
* machine's hooks directories are re-probed at run time so the committed
|
|
1914
|
+
* templates never bake in machine-specific paths.
|
|
1915
|
+
*
|
|
1916
|
+
* @param {string} name
|
|
1917
|
+
*/
|
|
1918
|
+
const machineDelegateStub = (name) => `#!/usr/bin/env sh
|
|
1919
|
+
# Generated by Code Foundry — delegates ${name} to the machine-level hook so
|
|
1920
|
+
# enabling .githooks keeps secret guards, git-lfs, and other machine hooks
|
|
1921
|
+
# working. Discovery mirrors .githooks/pre-commit; rerun sync after changing
|
|
1922
|
+
# the machine's hooks.
|
|
1923
|
+
set -eu
|
|
1924
|
+
machine_hook() {
|
|
1925
|
+
hook_name="$1"
|
|
1926
|
+
for dir in \\
|
|
1927
|
+
"$(git config --get code-foundry.previousHooksPath 2>/dev/null || true)" \\
|
|
1928
|
+
"$(git config --global --get core.hooksPath 2>/dev/null || true)" \\
|
|
1929
|
+
"$(git rev-parse --git-common-dir 2>/dev/null || printf '.git')/hooks"
|
|
1930
|
+
do
|
|
1931
|
+
case "$dir" in
|
|
1932
|
+
'' | .githooks | ./.githooks | */.githooks) continue ;;
|
|
1933
|
+
esac
|
|
1934
|
+
candidate="$dir/$hook_name"
|
|
1935
|
+
if [ -f "$candidate" ] && ! [ "$candidate" -ef "$0" ]; then
|
|
1936
|
+
printf '%s' "$candidate"
|
|
1937
|
+
return 0
|
|
1938
|
+
fi
|
|
1939
|
+
done
|
|
1940
|
+
return 1
|
|
1941
|
+
}
|
|
1942
|
+
if [ -n "\${CODE_FOUNDRY_HOOK_CHAIN:-}" ]; then
|
|
1943
|
+
exit 0
|
|
1944
|
+
fi
|
|
1945
|
+
if machine="$(machine_hook ${name})"; then
|
|
1946
|
+
# The marker makes a machine hook that delegates back to .githooks settle
|
|
1947
|
+
# here instead of looping.
|
|
1948
|
+
if [ -x "$machine" ]; then
|
|
1949
|
+
CODE_FOUNDRY_HOOK_CHAIN=1 exec "$machine" "$@"
|
|
1950
|
+
else
|
|
1951
|
+
CODE_FOUNDRY_HOOK_CHAIN=1 exec sh "$machine" "$@"
|
|
1952
|
+
fi
|
|
1953
|
+
fi
|
|
1954
|
+
exit 0
|
|
1955
|
+
`
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Write delegating stubs into .githooks for every machine-level hook this
|
|
1959
|
+
* repository does not own, and remove stubs the machine no longer backs. The
|
|
1960
|
+
* stubs are machine-local (gitignored): they are rewritten on every sync and
|
|
1961
|
+
* only ever removed when their content carries this generator's marker.
|
|
1962
|
+
*
|
|
1963
|
+
* @param {string} target @param {string} previousHooksPath
|
|
1964
|
+
*/
|
|
1965
|
+
function syncMachineHookDelegates(target, previousHooksPath) {
|
|
1966
|
+
const directories = machineHooksDirectories(target, previousHooksPath)
|
|
1967
|
+
/** @param {string} name */
|
|
1968
|
+
const machineHook = (name) => {
|
|
1969
|
+
for (const dir of directories) {
|
|
1970
|
+
const candidate = join(dir, name)
|
|
1971
|
+
try {
|
|
1972
|
+
if (statSync(candidate).isFile()) return candidate
|
|
1973
|
+
} catch {
|
|
1974
|
+
// Absent from this directory; keep probing.
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return null
|
|
1978
|
+
}
|
|
1979
|
+
mkdirSync(join(target, '.githooks'), { recursive: true })
|
|
1980
|
+
for (const name of MACHINE_DELEGATED_HOOKS) {
|
|
1981
|
+
const stubPath = join(target, '.githooks', name)
|
|
1982
|
+
if (machineHook(name)) {
|
|
1983
|
+
writeFileSync(stubPath, machineDelegateStub(name))
|
|
1984
|
+
chmodSync(stubPath, 0o755)
|
|
1985
|
+
} else if (
|
|
1986
|
+
existsSync(stubPath) &&
|
|
1987
|
+
readFileSync(stubPath, 'utf8').includes('Generated by Code Foundry')
|
|
1988
|
+
) {
|
|
1989
|
+
rmSync(stubPath)
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1851
1994
|
/** @param {string} file @param {Buffer|string} content @param {boolean} dryRun */
|
|
1852
1995
|
function writeOrReport(file, content, dryRun) {
|
|
1853
1996
|
if (dryRun) {
|
package/src/templates/gitignore
CHANGED