code-foundry 1.18.0 → 1.20.1
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/.github/workflows/consumer-qualification.yml +113 -11
- package/CHANGELOG.md +21 -0
- package/docs/PERFORMANCE.md +11 -15
- package/docs/consumer-qualification.md +18 -2
- package/docs/fleet-release-eligibility.md +80 -0
- package/docs/fleet-rollouts.md +4 -3
- package/docs/merge-queues.md +89 -0
- package/package.json +1 -1
- package/src/cli.mjs +19 -6
- package/src/commands/fleet-core.mjs +567 -0
- package/src/commands/fleet.mjs +9 -562
- package/src/commands/sync.mjs +44 -1
- package/src/lib/fleet-eligibility.mjs +245 -0
- package/src/lib/merge-queue.mjs +271 -0
|
@@ -8,6 +8,19 @@ on:
|
|
|
8
8
|
required: false
|
|
9
9
|
type: boolean
|
|
10
10
|
default: false
|
|
11
|
+
outputs:
|
|
12
|
+
filename:
|
|
13
|
+
description: Exact qualified npm archive name.
|
|
14
|
+
value: ${{ jobs.pack.outputs.filename }}
|
|
15
|
+
source-sha:
|
|
16
|
+
description: Commit whose archive passed the complete qualification matrix.
|
|
17
|
+
value: ${{ jobs.gate.outputs.source-sha }}
|
|
18
|
+
sha256:
|
|
19
|
+
description: SHA-256 of the qualified archive, without an algorithm prefix.
|
|
20
|
+
value: ${{ jobs.gate.outputs.sha256 }}
|
|
21
|
+
candidate-artifact:
|
|
22
|
+
description: Candidate artifact scoped to this workflow run and attempt.
|
|
23
|
+
value: ${{ jobs.gate.outputs.candidate-artifact }}
|
|
11
24
|
|
|
12
25
|
permissions:
|
|
13
26
|
actions: read
|
|
@@ -21,6 +34,9 @@ jobs:
|
|
|
21
34
|
timeout-minutes: 10
|
|
22
35
|
outputs:
|
|
23
36
|
filename: ${{ steps.pack.outputs.filename }}
|
|
37
|
+
source-sha: ${{ steps.pack.outputs.source-sha }}
|
|
38
|
+
sha256: ${{ steps.pack.outputs.sha256 }}
|
|
39
|
+
run-attempt: ${{ steps.pack.outputs.run-attempt }}
|
|
24
40
|
steps:
|
|
25
41
|
- name: Checkout candidate
|
|
26
42
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
@@ -37,14 +53,18 @@ jobs:
|
|
|
37
53
|
mkdir -p "$RUNNER_TEMP/candidate"
|
|
38
54
|
npm pack --ignore-scripts --json --pack-destination "$RUNNER_TEMP/candidate" > "$RUNNER_TEMP/candidate/pack.json"
|
|
39
55
|
PACKAGE=$(node -e 'const p=require(process.argv[1]); if(p.length!==1 || !/^[A-Za-z0-9._-]+\.tgz$/.test(p[0].filename)) process.exit(1); console.log(p[0].filename)' "$RUNNER_TEMP/candidate/pack.json")
|
|
40
|
-
|
|
56
|
+
{
|
|
57
|
+
echo "filename=$PACKAGE"
|
|
58
|
+
echo "source-sha=$(git rev-parse HEAD)"
|
|
59
|
+
echo "sha256=$(sha256sum "$RUNNER_TEMP/candidate/$PACKAGE" | cut -d' ' -f1)"
|
|
60
|
+
echo "run-attempt=$GITHUB_RUN_ATTEMPT"
|
|
61
|
+
} >> "$GITHUB_OUTPUT"
|
|
41
62
|
- name: Retain the shared candidate
|
|
42
63
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
43
64
|
with:
|
|
44
|
-
name: qualification-candidate-${{ github.run_id }}
|
|
65
|
+
name: qualification-candidate-${{ github.run_id }}-${{ github.run_attempt }}
|
|
45
66
|
path: ${{ runner.temp }}/candidate/*
|
|
46
67
|
if-no-files-found: error
|
|
47
|
-
overwrite: true
|
|
48
68
|
retention-days: 14
|
|
49
69
|
|
|
50
70
|
qualify:
|
|
@@ -57,6 +77,11 @@ jobs:
|
|
|
57
77
|
fail-fast: false
|
|
58
78
|
matrix:
|
|
59
79
|
node: ['20', '22', '24']
|
|
80
|
+
env:
|
|
81
|
+
PACKAGE: ${{ needs.pack.outputs.filename }}
|
|
82
|
+
SOURCE_SHA: ${{ needs.pack.outputs.source-sha }}
|
|
83
|
+
CANDIDATE_SHA256: ${{ needs.pack.outputs.sha256 }}
|
|
84
|
+
PACK_ATTEMPT: ${{ needs.pack.outputs.run-attempt }}
|
|
60
85
|
steps:
|
|
61
86
|
- name: Checkout qualification runtime
|
|
62
87
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
@@ -70,26 +95,103 @@ jobs:
|
|
|
70
95
|
env:
|
|
71
96
|
GH_TOKEN: ${{ github.token }}
|
|
72
97
|
run: |
|
|
98
|
+
set -euo pipefail
|
|
99
|
+
test "$PACK_ATTEMPT" = "$GITHUB_RUN_ATTEMPT" || { echo 'Rerun all jobs: qualification cannot mix run attempts.' >&2; exit 1; }
|
|
100
|
+
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
|
73
101
|
gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
|
74
|
-
--name "qualification-candidate-$GITHUB_RUN_ID" \
|
|
102
|
+
--name "qualification-candidate-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
|
|
75
103
|
--dir "$RUNNER_TEMP/candidate"
|
|
104
|
+
test "$(sha256sum "$RUNNER_TEMP/candidate/$PACKAGE" | cut -d' ' -f1)" = "$CANDIDATE_SHA256"
|
|
76
105
|
- name: Install pinned workflow validator
|
|
77
106
|
run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
|
|
78
107
|
- name: Qualify the exact shared distributable
|
|
79
|
-
env:
|
|
80
|
-
PACKAGE: ${{ needs.pack.outputs.filename }}
|
|
81
108
|
run: |
|
|
82
109
|
set -euo pipefail
|
|
83
110
|
node src/lib/consumer-qualification.mjs \
|
|
84
|
-
"$RUNNER_TEMP/candidate/$PACKAGE" \
|
|
85
|
-
"$(
|
|
86
|
-
"$RUNNER_TEMP/report.json" \
|
|
87
|
-
"$(go env GOPATH)/bin/actionlint"
|
|
111
|
+
"$RUNNER_TEMP/candidate/$PACKAGE" "$SOURCE_SHA" \
|
|
112
|
+
"$RUNNER_TEMP/report.json" "$(go env GOPATH)/bin/actionlint"
|
|
88
113
|
- name: Retain qualification evidence
|
|
89
114
|
if: always()
|
|
90
115
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
91
116
|
with:
|
|
92
117
|
name: consumer-qualification-${{ github.run_attempt }}-node-${{ matrix.node }}
|
|
93
118
|
path: ${{ runner.temp }}/report.json
|
|
94
|
-
if-no-files-found:
|
|
119
|
+
if-no-files-found: error
|
|
120
|
+
retention-days: 14
|
|
121
|
+
|
|
122
|
+
gate:
|
|
123
|
+
name: Gate
|
|
124
|
+
needs: [pack, qualify]
|
|
125
|
+
if: >-
|
|
126
|
+
always() && !cancelled() &&
|
|
127
|
+
(vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true)
|
|
128
|
+
runs-on: ubuntu-24.04
|
|
129
|
+
timeout-minutes: 10
|
|
130
|
+
outputs:
|
|
131
|
+
source-sha: ${{ steps.verify.outputs.source-sha }}
|
|
132
|
+
sha256: ${{ steps.verify.outputs.sha256 }}
|
|
133
|
+
candidate-artifact: ${{ steps.verify.outputs.candidate-artifact }}
|
|
134
|
+
env:
|
|
135
|
+
PACKAGE: ${{ needs.pack.outputs.filename }}
|
|
136
|
+
SOURCE_SHA: ${{ needs.pack.outputs.source-sha }}
|
|
137
|
+
CANDIDATE_SHA256: ${{ needs.pack.outputs.sha256 }}
|
|
138
|
+
PACK_ATTEMPT: ${{ needs.pack.outputs.run-attempt }}
|
|
139
|
+
PACK_RESULT: ${{ needs.pack.result }}
|
|
140
|
+
QUALIFY_RESULT: ${{ needs.qualify.result }}
|
|
141
|
+
steps:
|
|
142
|
+
- name: Require the complete successful matrix
|
|
143
|
+
run: |
|
|
144
|
+
set -euo pipefail
|
|
145
|
+
test "$PACK_RESULT" = success
|
|
146
|
+
test "$QUALIFY_RESULT" = success
|
|
147
|
+
test "$PACK_ATTEMPT" = "$GITHUB_RUN_ATTEMPT" || { echo 'Rerun all jobs: qualification cannot mix run attempts.' >&2; exit 1; }
|
|
148
|
+
- name: Checkout qualification policy
|
|
149
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
150
|
+
with:
|
|
151
|
+
persist-credentials: false
|
|
152
|
+
- name: Setup Node
|
|
153
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
154
|
+
with:
|
|
155
|
+
node-version: '24'
|
|
156
|
+
- name: Retrieve this attempt's candidate and reports
|
|
157
|
+
env:
|
|
158
|
+
GH_TOKEN: ${{ github.token }}
|
|
159
|
+
run: |
|
|
160
|
+
set -euo pipefail
|
|
161
|
+
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
|
162
|
+
gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
|
163
|
+
--name "qualification-candidate-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
|
|
164
|
+
--dir "$RUNNER_TEMP/candidate"
|
|
165
|
+
for node in 20 22 24; do
|
|
166
|
+
gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
|
167
|
+
--name "consumer-qualification-$GITHUB_RUN_ATTEMPT-node-$node" \
|
|
168
|
+
--dir "$RUNNER_TEMP/reports/$node"
|
|
169
|
+
done
|
|
170
|
+
- name: Verify source, archive and every fixture before handing off
|
|
171
|
+
id: verify
|
|
172
|
+
run: |
|
|
173
|
+
node --input-type=module <<'NODE'
|
|
174
|
+
import assert from 'node:assert/strict'
|
|
175
|
+
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
|
|
176
|
+
import { join } from 'node:path'
|
|
177
|
+
import { qualifyArchive } from './src/commands/qualified-publication.mjs'
|
|
178
|
+
const env = process.env
|
|
179
|
+
const version = JSON.parse(readFileSync('package.json', 'utf8')).version
|
|
180
|
+
const candidate = {
|
|
181
|
+
repository: env.GITHUB_REPOSITORY, tag: `v${version}`,
|
|
182
|
+
sourceSha: env.SOURCE_SHA, asset: env.PACKAGE,
|
|
183
|
+
directory: join(env.RUNNER_TEMP, 'candidate'),
|
|
184
|
+
}
|
|
185
|
+
const result = qualifyArchive(candidate, ['20', '22', '24'].map(node => join(env.RUNNER_TEMP, 'reports', node, 'report.json')))
|
|
186
|
+
assert.equal(result.digest, `sha256:${env.CANDIDATE_SHA256}`)
|
|
187
|
+
const artifact = `qualification-candidate-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
|
|
188
|
+
writeFileSync(join(env.RUNNER_TEMP, 'qualification.json'), JSON.stringify({ ...result, run_id: env.GITHUB_RUN_ID, run_attempt: env.GITHUB_RUN_ATTEMPT }, null, 2) + '\n')
|
|
189
|
+
appendFileSync(env.GITHUB_OUTPUT, `source-sha=${result.sourceSha}\nsha256=${env.CANDIDATE_SHA256}\ncandidate-artifact=${artifact}\n`)
|
|
190
|
+
NODE
|
|
191
|
+
- name: Retain the qualified handoff
|
|
192
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
193
|
+
with:
|
|
194
|
+
name: qualification-handoff-${{ github.run_id }}-${{ github.run_attempt }}
|
|
195
|
+
path: ${{ runner.temp }}/qualification.json
|
|
196
|
+
if-no-files-found: error
|
|
95
197
|
retention-days: 14
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.20.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.20.0...v1.20.1) (2026-09-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **qualification:** isolate attempts and verify release handoff ([#556](https://github.com/0xPlayerOne/code-foundry/issues/556)) ([5fe7f42](https://github.com/0xPlayerOne/code-foundry/commit/5fe7f4203b8018fd3eb88bd68e2286c6feb3d3dc))
|
|
9
|
+
|
|
10
|
+
## [1.20.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.19.0...v1.20.0) (2026-09-09)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **ci:** add opt-in merge-group audit validation ([#549](https://github.com/0xPlayerOne/code-foundry/issues/549)) ([ae418a7](https://github.com/0xPlayerOne/code-foundry/commit/ae418a779ada2927bd391cc6cf54db16e342d921))
|
|
16
|
+
|
|
17
|
+
## [1.19.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.18.0...v1.19.0) (2026-09-09)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **fleet:** gate upgrades on verified release qualification ([#548](https://github.com/0xPlayerOne/code-foundry/issues/548)) ([3247cc6](https://github.com/0xPlayerOne/code-foundry/commit/3247cc6f2295c65c42bb43e4ec8f87a08f3dd02d))
|
|
23
|
+
|
|
3
24
|
## [1.18.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.17.0...v1.18.0) (2026-09-09)
|
|
4
25
|
|
|
5
26
|
|
package/docs/PERFORMANCE.md
CHANGED
|
@@ -15,25 +15,19 @@ belong to the run that produced them, not the source tree.
|
|
|
15
15
|
| Format, lint, type-check, and build | 15 s | Bound the local CI feedback loop |
|
|
16
16
|
| Runtime dependencies | 0 | Keep the installed CLI dependency-free |
|
|
17
17
|
| Development dependencies | 4 | Prevent unreviewed toolchain growth |
|
|
18
|
-
| Packed artifact |
|
|
19
|
-
| Unpacked artifact |
|
|
20
|
-
| Packed files |
|
|
18
|
+
| Packed artifact | 255 kB | Bound registry transfer and install cost |
|
|
19
|
+
| Unpacked artifact | 965 kB | Bound installed footprint |
|
|
20
|
+
| Packed files | 115 | Detect accidental release contents |
|
|
21
21
|
|
|
22
22
|
The performance workflow disables build-cache reads and writes for this task.
|
|
23
23
|
That makes timing comparisons independent of a warm protected-branch cache and
|
|
24
24
|
prevents benchmark code from populating shared cache entries.
|
|
25
25
|
|
|
26
|
-
The
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
packed bytes, 890,418 unpacked bytes, and 103 files on Node 24.18.0, so the
|
|
32
|
-
limits remain intentionally bounded while covering both opt-in feature sets.
|
|
33
|
-
The qualified-publication workflow adds the release staging and publication
|
|
34
|
-
contract to the distributable. This candidate measured 238,816 packed bytes,
|
|
35
|
-
915,957 unpacked bytes, and 106 files on Node 24.18.0; the 250 kB and 930 kB
|
|
36
|
-
limits retain a measured margin while covering the required release path.
|
|
26
|
+
The merged candidate includes the release-integrity verifier, fleet eligibility,
|
|
27
|
+
consumer qualification harness, product-quality profiles, qualified publication
|
|
28
|
+
workflow, and opt-in merge-queue verifier. It measured 248,464 packed bytes,
|
|
29
|
+
947,310 unpacked bytes, and 111 files on Node 24.18.0; the 255 kB, 965 kB, and
|
|
30
|
+
115-file limits retain a small margin while continuing to bound package growth.
|
|
37
31
|
|
|
38
32
|
## v1.6.1 baseline
|
|
39
33
|
|
|
@@ -67,4 +61,6 @@ pull request, run the complete validation gate, and merge only when the new
|
|
|
67
61
|
result artifact is available. A Release Please PR then carries the change into
|
|
68
62
|
the next version. Never raise a budget solely to clear CI; include before/after
|
|
69
63
|
measurements and the expected effect on local feedback, hosted runner time,
|
|
70
|
-
package transfer, or installed footprint.
|
|
64
|
+
package transfer, or installed footprint. The current budget covers the measured
|
|
65
|
+
combined release-integrity, fleet, consumer-qualification, and product-quality
|
|
66
|
+
package footprint described above.
|
|
@@ -6,7 +6,7 @@ Release-candidate compatibility checks against the distributable package.
|
|
|
6
6
|
**Scope:** Package installation, CLI initialization/synchronization, generated workflow contracts.
|
|
7
7
|
|
|
8
8
|
`consumer-qualification.yml` packs the checked-out candidate once, shares that archive across all supported
|
|
9
|
-
Node majors using an artifact scoped to the same workflow run
|
|
9
|
+
Node majors using an artifact scoped to the same workflow run **and attempt**, installs it offline with lifecycle scripts disabled,
|
|
10
10
|
and executes its public CLI from the installed package. The harness does not
|
|
11
11
|
import the source checkout's initializer as a substitute for package testing.
|
|
12
12
|
|
|
@@ -39,10 +39,26 @@ member. Reports are evidence, not signatures or a substitute for release identit
|
|
|
39
39
|
verification.
|
|
40
40
|
|
|
41
41
|
The self release caller cannot start Release Please or npm publication until all
|
|
42
|
-
qualification jobs succeed. An explicit `release-while-paused` dispatch is passed
|
|
42
|
+
qualification jobs and the aggregate `Gate` succeed. An explicit `release-while-paused` dispatch is passed
|
|
43
43
|
through to the reusable workflow; ordinary calls remain blocked by the billing pause.
|
|
44
44
|
Consumer-generated release callers do not inherit this Code Foundry-specific
|
|
45
45
|
qualification job. This does not yet guarantee that its legacy publishing job uses
|
|
46
46
|
this same archive; the verified-publication workflow handles that separate
|
|
47
47
|
requirement. No branch protections, repository settings, credentials, or consumer
|
|
48
48
|
runtimes are changed by this feature.
|
|
49
|
+
|
|
50
|
+
## Verified handoff and reruns
|
|
51
|
+
|
|
52
|
+
The reusable workflow exports `filename`, `source-sha`, `sha256`, and
|
|
53
|
+
`candidate-artifact`. Only the gate exports the verified source, digest, and
|
|
54
|
+
artifact identity. It requires a successful pack and complete Node matrix,
|
|
55
|
+
retrieves every report from the current attempt, and checks all fixture results
|
|
56
|
+
against the same source and archive through the publication eligibility policy.
|
|
57
|
+
Its retained `qualification-handoff-RUN_ID-ATTEMPT` receipt records that identity.
|
|
58
|
+
A green matrix without valid matching reports cannot authorize a release.
|
|
59
|
+
|
|
60
|
+
Artifacts are never overwritten. On a failed or cancelled qualification, use
|
|
61
|
+
**Re-run all jobs**, not a failed-job-only rerun: a later attempt cannot reuse
|
|
62
|
+
an earlier pack or silently combine old and new reports. Missing/expired artifacts
|
|
63
|
+
fail closed; start a fresh complete run. This stricter retry policy also applies
|
|
64
|
+
when qualification is nested in a release workflow. No release settings change.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Fleet Release Eligibility
|
|
2
|
+
|
|
3
|
+
Require a verified, qualified runtime before any fleet upgrade mutates repositories.
|
|
4
|
+
|
|
5
|
+
**Dependency:** Installed release-integrity verifier (#537).
|
|
6
|
+
**Activation:** Consumer-owned `.code-foundry-release-policy.json` at the fleet root.
|
|
7
|
+
|
|
8
|
+
The public `upgradeFleet` entrypoint now applies an optional release eligibility
|
|
9
|
+
guard before delegating to the unchanged legacy/manifest rollout implementation.
|
|
10
|
+
The implementation was moved verbatim to `fleet-core.mjs`; discovery exports and
|
|
11
|
+
existing callers retain their interface. No policy file means existing behavior.
|
|
12
|
+
A malformed or symlinked policy is an error, not an opt-out. Dry runs use the same
|
|
13
|
+
guard, and `--force` cannot bypass source identity or qualification.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"schema_version": 1,
|
|
18
|
+
"repository": "owner/code-foundry",
|
|
19
|
+
"workflow": ".github/workflows/release_self-ci.yml",
|
|
20
|
+
"branch": "main",
|
|
21
|
+
"required_jobs": [
|
|
22
|
+
"Qualify consumers / Node 20",
|
|
23
|
+
"Qualify consumers / Node 22",
|
|
24
|
+
"Qualify consumers / Node 24"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is a shape example, not a completed fleet policy. Populate canonical repository
|
|
30
|
+
identity, actual workflow path, and exact job names from the qualified release
|
|
31
|
+
caller's Actions API results. Include the publication job when successful registry
|
|
32
|
+
publication is required before adoption. Require every relevant job: a workflow's
|
|
33
|
+
aggregate success alone can conceal skipped jobs. The qualification gate in #544
|
|
34
|
+
and verified publisher in #547 provide the producer side of this policy.
|
|
35
|
+
|
|
36
|
+
Keep the policy alongside the **consumer workspace's** fleet inventory. Do not put
|
|
37
|
+
private repository names, local layouts, or deployment credentials into this
|
|
38
|
+
reusable baseline. Commit policy changes for review; the guard does not secretly
|
|
39
|
+
change branch protection, environments, rollout cohorts, or required capabilities.
|
|
40
|
+
|
|
41
|
+
## What is verified
|
|
42
|
+
|
|
43
|
+
The proposed runtime must be a clean Git source checkout. Its exact HEAD must
|
|
44
|
+
match the cryptographically verified immutable release tag. An installed npm
|
|
45
|
+
package without Git metadata cannot substitute for a verifiable source checkout;
|
|
46
|
+
use the existing `--source` option with the checked-out release. The verifier comes
|
|
47
|
+
from the trusted running Foundry installation, not the unverified candidate.
|
|
48
|
+
|
|
49
|
+
The guard resolves the configured workflow's identity, retrieves every page of
|
|
50
|
+
runs for the exact source, and selects the latest matching protected-branch push
|
|
51
|
+
or manual run. It rejects pending/failed/cancelled runs, fork provenance, disabled
|
|
52
|
+
workflows, missing/ambiguous/skipped jobs, and incomplete pagination. An older
|
|
53
|
+
success does not override a newer failed run. Job evidence comes from the precise
|
|
54
|
+
run attempt; that attempt is rechecked before proceeding. Source HEAD, cleanliness,
|
|
55
|
+
and the policy's bytes are rechecked before the upgrade callback executes.
|
|
56
|
+
|
|
57
|
+
Successful eligibility emits JSON to stderr with source SHA, policy digest,
|
|
58
|
+
workflow/run/attempt identifiers, and required job names. It does not write an
|
|
59
|
+
approval into the source or silently make PRs ready. Existing resumable rollouts,
|
|
60
|
+
canary validation, draft creation, and provenance markers stay in the original
|
|
61
|
+
fleet engine. The guard is policy enforcement in the supported public entrypoint,
|
|
62
|
+
not a sandbox against someone intentionally importing private implementation files
|
|
63
|
+
or using Git directly. Normal credentials still determine what remote mutations
|
|
64
|
+
are possible after eligibility passes.
|
|
65
|
+
|
|
66
|
+
## Activation and validation
|
|
67
|
+
|
|
68
|
+
First release and validate the qualification/verification producer and confirm
|
|
69
|
+
real workflow/job identities. Then opt a consumer workspace into this policy and
|
|
70
|
+
exercise `fleet upgrade --dry-run --root <fleet-root> --source <clean-release-checkout>`.
|
|
71
|
+
When `--source` is omitted, the installed package is used and an enabled policy
|
|
72
|
+
will reject it unless that installation is itself a clean Git checkout. Older mutable
|
|
73
|
+
releases or unavailable permissions should fail; do not weaken the policy just to
|
|
74
|
+
make an old release eligible. No real fleet inventory is fabricated by this PR.
|
|
75
|
+
|
|
76
|
+
The focused suite verifies guard ordering and failure propagation with GitHub/CLI
|
|
77
|
+
fixtures. Live authenticated verification, exact production job naming, and the
|
|
78
|
+
full existing fleet-engine suite must pass before activation. The original fleet
|
|
79
|
+
engine's Git blob is retained byte-for-byte; the source move still needs full
|
|
80
|
+
repository import/type-check/packaging validation in CI.
|
package/docs/fleet-rollouts.md
CHANGED
|
@@ -55,11 +55,12 @@ Adopt those requirements in consumer repositories before requiring them here.
|
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
57
|
code-foundry fleet status --root /path/to/fleet
|
|
58
|
-
code-foundry fleet upgrade --root /path/to/fleet --dry-run
|
|
59
|
-
code-foundry fleet upgrade --root /path/to/fleet --create-pr
|
|
58
|
+
code-foundry fleet upgrade --root /path/to/fleet --source /path/to/release-checkout --dry-run
|
|
59
|
+
code-foundry fleet upgrade --root /path/to/fleet --source /path/to/release-checkout --create-pr
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Use `--source` to provide the clean Code Foundry release checkout used for the
|
|
63
|
+
upgrade; when omitted, the installed Code Foundry package is used. The existing
|
|
63
64
|
source-version guard still rejects mismatched `--version` requests. Manifest mode
|
|
64
65
|
requires `--create-pr` or `--dry-run` and never syncs original checkouts in place.
|
|
65
66
|
`--force` does not bypass dirty-tree safety in manifest mode. Dry-run reports
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Merge Queue Validation
|
|
2
|
+
|
|
3
|
+
Opt-in validation of the combined commit produced by GitHub's merge queue.
|
|
4
|
+
|
|
5
|
+
**Activation:** `merge_queue: true` in the consumer's `.github/code-foundry.yml`, followed by normal sync.
|
|
6
|
+
**Required check:** Existing canonical `Validation / Gate`.
|
|
7
|
+
|
|
8
|
+
The synchronizer adds `.github/workflows/validation-merge-queue.yml` only when
|
|
9
|
+
explicitly enabled. The ordinary PR readiness workflow is unchanged: this feature
|
|
10
|
+
does not turn draft pushes into CI, mark PRs ready, enqueue PRs, or change release
|
|
11
|
+
behavior. Queue events receive the full audit even when staging PRs normally use
|
|
12
|
+
fast validation. Release Please branch detection is deliberately not reused for
|
|
13
|
+
a merge group containing several PRs; the group's combined tree is audited, not
|
|
14
|
+
treated as a generated version-only diff.
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
merge_queue: true
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Normal `code-foundry sync` updates the generated queue caller alongside the other
|
|
21
|
+
runtime pins. Explicit fleet runtime overrides are honored. The installed runtime
|
|
22
|
+
must contain this feature, and queues require an exact commit or released version
|
|
23
|
+
pin rather than a moving branch. Direct topology enables main; staging-release
|
|
24
|
+
enables main and staging. Existing runner choices, Rust CodeQL sharding, and an
|
|
25
|
+
explicit `codeql: false` policy select the same appropriate validation orchestrator
|
|
26
|
+
as ordinary PRs. No CodeQL policy or runtime default is relaxed.
|
|
27
|
+
|
|
28
|
+
## Event identity and checks
|
|
29
|
+
|
|
30
|
+
The generated caller accepts only `merge_group` / `checks_requested`. A pinned,
|
|
31
|
+
trusted verifier checks the payload's repository, allowed base branch, queue ref,
|
|
32
|
+
base SHA, and head SHA against the actual workflow context before validation can
|
|
33
|
+
start. The existing reusable orchestrator then checks out the merge-group commit
|
|
34
|
+
through its normal GitHub context and runs CI, tests (including non-unit lanes),
|
|
35
|
+
security, and the configured CodeQL policy with `mode: audit`.
|
|
36
|
+
|
|
37
|
+
The caller job is named `Validation`, so the existing orchestrator still emits
|
|
38
|
+
`Validation / Gate`. That gate's audit truth table requires successful results;
|
|
39
|
+
skipped or failed required jobs cannot satisfy it. A failed identity job prevents
|
|
40
|
+
validation from starting and cannot manufacture a successful aggregate check.
|
|
41
|
+
The standard PR classifier remains PR/scheduled/manual-specific; merge groups
|
|
42
|
+
have their own strict classifier rather than weakening its event whitelist.
|
|
43
|
+
|
|
44
|
+
Concurrency is scoped to the actual queue ref and has a different prefix from PR,
|
|
45
|
+
release, and scheduled workflows. New groups do not cancel unrelated groups or
|
|
46
|
+
PR readiness runs. Replaced attempts for the same queue ref can supersede each
|
|
47
|
+
other. The billing-pause switch is preserved; paused validation does not produce
|
|
48
|
+
a success that authorizes a merge.
|
|
49
|
+
|
|
50
|
+
The caller forwards no secrets and grants no deployment or OIDC publication
|
|
51
|
+
permission. Its permission union matches the existing audit chain (including
|
|
52
|
+
CodeQL security-event uploads). Applications requiring secret-backed tests must
|
|
53
|
+
provide safe queue-compatible fixtures or a separately reviewed explicit caller;
|
|
54
|
+
this feature does not introduce `secrets: inherit`. Native test commands remain
|
|
55
|
+
repository-owned code, not a sandbox.
|
|
56
|
+
|
|
57
|
+
## Preservation and disabling
|
|
58
|
+
|
|
59
|
+
Setting `merge_queue: false` or removing the key removes only a caller bearing the
|
|
60
|
+
exact Foundry management marker. A custom file at that path is preserved when
|
|
61
|
+
disabled; enabling over it fails before synchronization writes, even with force.
|
|
62
|
+
Symlinked workflow paths are rejected. Dry runs report changes without creating,
|
|
63
|
+
updating, or deleting the caller. The original synchronizer is retained verbatim
|
|
64
|
+
in `sync-core.mjs`; its public exports are preserved through the thin wrapper.
|
|
65
|
+
|
|
66
|
+
Disable the repository's queue rule before removing its required queue workflow.
|
|
67
|
+
Otherwise queued PRs will correctly wait for checks that no longer run.
|
|
68
|
+
|
|
69
|
+
## Repository activation remains separate
|
|
70
|
+
|
|
71
|
+
A repository administrator must verify GitHub plan/repository eligibility,
|
|
72
|
+
review queue merge-method compatibility with the chosen branch topology, and
|
|
73
|
+
configure required checks. Register the aggregate check, not PR-only mode or
|
|
74
|
+
readiness checks that have no merge-group equivalent. An existing rule requiring
|
|
75
|
+
individual checks needs those exact contexts reviewed as well. Do not enable the
|
|
76
|
+
queue rule before the workflow is merged and a disposable queue exercise passes.
|
|
77
|
+
No repository rule, branch protection, secret, merge setting, or queue is changed
|
|
78
|
+
by this PR.
|
|
79
|
+
|
|
80
|
+
Focused tests cover event identity and rejection, generated audit wiring,
|
|
81
|
+
configured CodeQL policy, pin evolution, idempotence, dry runs, and ownership
|
|
82
|
+
preservation. They do not establish a live merge-group run or full sync integration.
|
|
83
|
+
Before readiness, run the existing complete sync/fleet tests, locked formatter,
|
|
84
|
+
linter, TypeScript and package budgets, and Actionlint against rendered callers
|
|
85
|
+
and their pinned/local reusable workflow contracts. Exercise two queued PRs and
|
|
86
|
+
one deliberately failing check in an eligible disposable repository.
|
|
87
|
+
|
|
88
|
+
References: [GitHub merge-queue CI configuration](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue),
|
|
89
|
+
[merge-group event](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#merge_group).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
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/cli.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
|
|
8
8
|
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
9
9
|
|
|
10
|
-
/** @typedef {{ target: string, root: string, dryRun: boolean, force: boolean, github: boolean, createPr: boolean, exclude: string[], base: string, head: string, tag: string, workflow: string, mode: string, version: string, repository: string, expectedSha: string, assets: string[], rootProvided: boolean, releaseSubcommand?: string, fleetSubcommand?: string, integritySubcommand?: 'settings'|'release'|'manifest', ciSubcommand?: 'pause'|'resume'|'status' }} Options */
|
|
10
|
+
/** @typedef {{ target: string, root: string, source: string, dryRun: boolean, force: boolean, github: boolean, createPr: boolean, exclude: string[], base: string, head: string, tag: string, workflow: string, mode: string, version: string, versionExplicit: boolean, repository: string, expectedSha: string, assets: string[], rootProvided: boolean, releaseSubcommand?: string, fleetSubcommand?: string, integritySubcommand?: 'settings'|'release'|'manifest', ciSubcommand?: 'pause'|'resume'|'status' }} Options */
|
|
11
11
|
/** @typedef {{ command: string, options: Options }} ParsedArgs */
|
|
12
12
|
|
|
13
13
|
const usage = `code-foundry — initialize and maintain agent-ready repositories
|
|
@@ -25,7 +25,7 @@ Usage:
|
|
|
25
25
|
npx code-foundry release-integrity release --repo OWNER/REPO --tag TAG
|
|
26
26
|
npx code-foundry release-integrity manifest --asset PATH [--root PATH]
|
|
27
27
|
npx code-foundry fleet status [--root PATH]
|
|
28
|
-
npx code-foundry fleet upgrade [--root PATH] [--dry-run] [--create-pr]
|
|
28
|
+
npx code-foundry fleet upgrade [--root PATH] [--source PATH] [--dry-run] [--create-pr]
|
|
29
29
|
|
|
30
30
|
The repository configuration lives in .github/code-foundry.yml.
|
|
31
31
|
init detects the repository, creates that file, and renders the baseline.
|
|
@@ -45,6 +45,7 @@ Options:
|
|
|
45
45
|
--expected-sha SHA Expected source commit for release-integrity verification
|
|
46
46
|
--asset PATH Selected release-integrity artifact (repeatable)
|
|
47
47
|
--root PATH Fleet root containing repositories (default: current directory)
|
|
48
|
+
--source PATH Clean Code Foundry release checkout used for fleet upgrades
|
|
48
49
|
--create-pr Create isolated upgrade branches and pull requests
|
|
49
50
|
--version TAG Runtime tag to report in fleet upgrade branches
|
|
50
51
|
--exclude NAME Skip a repository path or owner/name in fleet operations
|
|
@@ -66,6 +67,7 @@ function parseArgs(argv) {
|
|
|
66
67
|
const options = {
|
|
67
68
|
target: process.cwd(),
|
|
68
69
|
root: process.cwd(),
|
|
70
|
+
source: packageRoot,
|
|
69
71
|
dryRun: false,
|
|
70
72
|
force: false,
|
|
71
73
|
github: false,
|
|
@@ -77,6 +79,7 @@ function parseArgs(argv) {
|
|
|
77
79
|
workflow: '',
|
|
78
80
|
mode: 'auto',
|
|
79
81
|
version: `v${readPackageVersion(packageRoot)}`,
|
|
82
|
+
versionExplicit: false,
|
|
80
83
|
repository: '',
|
|
81
84
|
expectedSha: '',
|
|
82
85
|
assets: [],
|
|
@@ -146,9 +149,17 @@ function parseArgs(argv) {
|
|
|
146
149
|
else if (arg === '--root') {
|
|
147
150
|
options.root = argv.shift() ?? fail('--root requires a path')
|
|
148
151
|
options.rootProvided = true
|
|
152
|
+
} else if (arg === '--source') {
|
|
153
|
+
if (command !== 'fleet' || options.fleetSubcommand !== 'upgrade')
|
|
154
|
+
fail('--source is only supported by fleet upgrade')
|
|
155
|
+
const value = argv.shift()
|
|
156
|
+
if (!value || value.startsWith('-')) fail('--source requires a path')
|
|
157
|
+
options.source = value
|
|
149
158
|
} else if (arg === '--create-pr') options.createPr = true
|
|
150
|
-
else if (arg === '--version')
|
|
151
|
-
|
|
159
|
+
else if (arg === '--version') {
|
|
160
|
+
options.version = argv.shift() ?? fail('--version requires a tag')
|
|
161
|
+
options.versionExplicit = true
|
|
162
|
+
} else if (arg === '--exclude')
|
|
152
163
|
options.exclude.push(argv.shift() ?? fail('--exclude requires a name'))
|
|
153
164
|
else fail(`unknown option: ${arg}; run --help for the supported options`)
|
|
154
165
|
}
|
|
@@ -241,11 +252,13 @@ async function main() {
|
|
|
241
252
|
if (options.fleetSubcommand === 'status')
|
|
242
253
|
console.log(JSON.stringify(discoverRepositories(root), null, 2))
|
|
243
254
|
else
|
|
244
|
-
upgradeFleet(root,
|
|
255
|
+
upgradeFleet(root, resolve(options.source), {
|
|
245
256
|
createPr: options.createPr,
|
|
246
257
|
dryRun: options.dryRun,
|
|
247
258
|
force: options.force,
|
|
248
|
-
version: options.
|
|
259
|
+
version: options.versionExplicit
|
|
260
|
+
? options.version
|
|
261
|
+
: `v${readPackageVersion(resolve(options.source))}`,
|
|
249
262
|
exclude: options.exclude,
|
|
250
263
|
})
|
|
251
264
|
} catch (error) {
|