code-foundry 1.20.1 → 1.22.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/CONTRIBUTING.md +5 -2
- package/.github/code-foundry.yml +1 -0
- package/.github/release-please-foundry.json +56 -0
- package/.github/workflows/ci.yml +61 -0
- package/.github/workflows/cloudflare-delivery.yml +9 -0
- package/.github/workflows/cloudflare-deploy.yml +8 -1
- package/.github/workflows/opencode-security_self-ci.yml +1 -1
- package/.github/workflows/qualified-foundry-publish.yml +6 -7
- package/.github/workflows/release.yml +58 -10
- package/.github/workflows/release_self-ci.yml +211 -8
- package/.github/workflows/test.yml +75 -0
- package/.github/workflows/validation-no-codeql.yml +7 -0
- package/.github/workflows/validation.yml +7 -0
- package/.gitignore +1 -1
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +60 -0
- package/README.md +20 -17
- package/docs/CONFIGURATION.md +182 -152
- package/docs/EXTENSIONS.md +28 -7
- package/docs/INITIALIZATION.md +13 -9
- package/docs/PERFORMANCE.md +67 -59
- package/docs/PUBLISHING.md +39 -14
- package/docs/README.md +37 -22
- package/docs/RELEASES.md +18 -9
- package/docs/WORKFLOWS.md +11 -0
- package/docs/agent-validation.md +6 -5
- package/docs/cloudflare-delivery.md +20 -8
- package/docs/consumer-qualification.md +11 -9
- package/docs/fleet-release-eligibility.md +14 -15
- package/docs/fleet-rollouts.md +3 -3
- package/docs/merge-queues.md +21 -21
- package/docs/product-quality.md +9 -10
- package/docs/qualified-publication.md +166 -93
- package/docs/release-integrity.md +7 -6
- package/docs/required-capabilities.md +42 -15
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +6 -1
- package/src/commands/qualified-publication.mjs +23 -10
- package/src/commands/release-integrity.mjs +9 -0
- package/src/commands/sync.mjs +29 -24
- package/src/lib/product-quality.mjs +220 -16
- package/src/runtime-core.mjs +12 -8
- package/src/runtime.mjs +1 -1
- package/src/templates/gitignore +1 -1
package/docs/product-quality.md
CHANGED
|
@@ -20,16 +20,15 @@ Commit a version-1 JSON manifest and use the installed, pinned Foundry runtime:
|
|
|
20
20
|
|
|
21
21
|
Wire these scripts into the consumer's existing validation entrypoints. The
|
|
22
22
|
Foundry runtime discovers only `performance:check`/`perf:check` and
|
|
23
|
-
`test:e2e`/`e2e
|
|
24
|
-
`bun run quality:build` and `bun run quality:browser`. If they do exist, preserve
|
|
25
|
-
commands and compose the quality command after them rather than
|
|
26
|
-
existing performance or E2E checks.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
retention and never upload authenticated traces publicly without sanitization.
|
|
23
|
+
`test:e2e`/`e2e`. If those names do not already exist, they can run
|
|
24
|
+
`bun run quality:build` and `bun run quality:browser`. If they do exist, preserve
|
|
25
|
+
their current commands and compose the quality command after them rather than
|
|
26
|
+
replacing the existing performance or E2E checks. Install the reviewed Foundry
|
|
27
|
+
version in the consumer's existing dependency manager and lockfile, not through
|
|
28
|
+
an unpinned network invocation. Add `.code-foundry/` to Git/package ignores and
|
|
29
|
+
retain selected evidence through the consumer workflow's artifact uploader.
|
|
30
|
+
Reports and browser traces may contain application data; review retention and
|
|
31
|
+
never upload authenticated traces publicly without sanitization.
|
|
33
32
|
|
|
34
33
|
```json
|
|
35
34
|
{
|
|
@@ -1,72 +1,77 @@
|
|
|
1
|
-
# Qualified
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
The
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
# Qualified publication
|
|
2
|
+
|
|
3
|
+
Code Foundry publishes only the same immutable archive that passed consumer
|
|
4
|
+
qualification. This path is used by Code Foundry's own release caller; generated
|
|
5
|
+
consumer release callers keep the ordinary `release.yml` behavior.
|
|
6
|
+
|
|
7
|
+
## Publication contract
|
|
8
|
+
|
|
9
|
+
The self-release pipeline is intentionally staged:
|
|
10
|
+
|
|
11
|
+
1. `consumer-qualification.yml` packs the candidate once and qualifies it across
|
|
12
|
+
Node 20, 22, and 24.
|
|
13
|
+
2. Release Please creates a draft release for the qualified source.
|
|
14
|
+
3. The staging job attaches the exact archive and a digest-bound qualification
|
|
15
|
+
receipt, publishes the immutable GitHub Release, and verifies its identity.
|
|
16
|
+
4. `qualified-foundry-publish.yml` downloads that archive and the reports from
|
|
17
|
+
its own workflow run and attempt, requalifies all three Node versions, and
|
|
18
|
+
publishes the tarball.
|
|
19
|
+
|
|
20
|
+
The reusable publisher downloads an explicitly named npm archive from the
|
|
21
|
+
already-published immutable release, verifies the release and asset attestations
|
|
22
|
+
against the caller's exact commit, and only then admits the npm publication job.
|
|
23
|
+
The final job downloads only reports from its own workflow run and attempt,
|
|
24
|
+
rechecks all required fixtures and archive/source identities, repeats cryptographic
|
|
25
|
+
asset verification, validates the package name/version, and publishes the tarball
|
|
26
|
+
with lifecycle scripts disabled. It never publishes a directory or rebuilds the
|
|
27
|
+
package.
|
|
28
|
+
|
|
29
|
+
The publishing job serializes publication for a tag and has no dependency
|
|
30
|
+
installation/build step. Qualification has no npm credential. This self-only
|
|
31
|
+
publisher has no environment approval gate; publication is automatic after its
|
|
32
|
+
completed gates. npm trusted publishing is preferred; an explicit optional token
|
|
33
|
+
supports environments that cannot use it. Configure the trusted publisher for the
|
|
34
|
+
**actual caller and reusable-workflow relationship** before enabling this route.
|
|
35
|
+
Existing version publication is not overwritten: retrying an already-published
|
|
36
|
+
version fails rather than treating a registry conflict or network error as proof
|
|
37
|
+
of identity.
|
|
38
|
+
|
|
39
|
+
The publisher never rebuilds the package, publishes a directory, or treats an
|
|
40
|
+
already-published version conflict as proof of success. The workflow uses npm
|
|
41
|
+
trusted publishing when no `NPM_TOKEN` is supplied; the optional token is a
|
|
42
|
+
fallback for environments that cannot use trusted publishing.
|
|
43
|
+
|
|
44
|
+
Qualification and publication are gated on `main` push or an explicitly
|
|
45
|
+
requested `workflow_dispatch`. The shared billing pause blocks normal runs. A
|
|
46
|
+
manual release-only dispatch can pass `billing-pause-bypass: true` through the
|
|
47
|
+
publisher, but it does not bypass environment approvals, branch protection, or
|
|
48
|
+
identity checks.
|
|
49
|
+
|
|
50
|
+
## Release producer requirements
|
|
51
|
+
|
|
52
|
+
The producer must:
|
|
53
|
+
|
|
54
|
+
- pack the candidate with lifecycle scripts disabled;
|
|
55
|
+
- create or reuse a draft release for the exact package version;
|
|
56
|
+
- attach the package archive before the release is published;
|
|
57
|
+
- keep the tag at the qualified source commit;
|
|
58
|
+
- enable and verify GitHub release immutability before publication; and
|
|
59
|
+
- serialize release-tag mutation so two producers cannot race.
|
|
60
|
+
|
|
61
|
+
The candidate package must be named `code-foundry`, and its version must match
|
|
62
|
+
the explicit release tag. A recent GitHub CLI with `release verify` and
|
|
63
|
+
`release verify-asset` support is required. Missing support, ambiguous release
|
|
64
|
+
identity, unavailable permissions, changed assets, or conflicting assets fail
|
|
65
|
+
closed.
|
|
66
|
+
|
|
67
|
+
The generic reusable `release.yml` supports `defer-publication: true` for this
|
|
68
|
+
producer pattern. That mode suppresses its legacy npm, reconciliation, and
|
|
69
|
+
post-release jobs while exposing the release outputs needed by the self caller.
|
|
70
|
+
Consumer callers do not inherit the self-only qualification and staging jobs.
|
|
71
|
+
|
|
72
|
+
## Reusable publisher interface
|
|
73
|
+
|
|
74
|
+
The caller supplies the tag and exact pre-attached archive name:
|
|
70
75
|
|
|
71
76
|
```yaml
|
|
72
77
|
permissions:
|
|
@@ -74,6 +79,7 @@ permissions:
|
|
|
74
79
|
attestations: read
|
|
75
80
|
contents: read
|
|
76
81
|
id-token: write
|
|
82
|
+
|
|
77
83
|
jobs:
|
|
78
84
|
publish:
|
|
79
85
|
needs: release-producer
|
|
@@ -81,31 +87,98 @@ jobs:
|
|
|
81
87
|
with:
|
|
82
88
|
tag: ${{ needs.release-producer.outputs.tag }}
|
|
83
89
|
asset: ${{ needs.release-producer.outputs.npm-asset }}
|
|
84
|
-
environment: npm
|
|
85
90
|
secrets:
|
|
86
91
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
branch
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
For the self repository, `release_self-ci.yml` supplies the release output and
|
|
95
|
+
uses the protected `npm` environment. Configure trusted publishing for the
|
|
96
|
+
actual caller/reusable-workflow relationship, not only for a similarly named
|
|
97
|
+
workflow. Protect both the `release` and `npm` environments with the intended
|
|
98
|
+
branch restrictions and approvals.
|
|
99
|
+
|
|
100
|
+
## Identity and retry rules
|
|
101
|
+
|
|
102
|
+
Every qualification report is bound to the source SHA, archive digest, Node
|
|
103
|
+
version, and workflow attempt. The staging and publish jobs verify:
|
|
104
|
+
|
|
105
|
+
- the tag resolves to the current `github.sha`;
|
|
106
|
+
- the archive name and package identity are expected;
|
|
107
|
+
- all required Node reports belong to the same run attempt;
|
|
108
|
+
- the downloaded bytes match the qualified digest; and
|
|
109
|
+
- the release and assets have not been replaced.
|
|
110
|
+
|
|
111
|
+
On a failed or cancelled qualification, use **Re-run all jobs**. A failed-job-only
|
|
112
|
+
rerun cannot safely reuse an earlier pack or combine reports from different
|
|
113
|
+
attempts. Missing or expired artifacts require a fresh run.
|
|
114
|
+
|
|
115
|
+
If Release Please reports that no new release was created, the recovery path may
|
|
116
|
+
reuse an exact draft release only when its tag, version, source SHA, and draft
|
|
117
|
+
state still match the qualified candidate. An already-published release is never
|
|
118
|
+
restaged or overwritten. Inspect the retained staging and publication identity
|
|
119
|
+
receipts when recovery is needed.
|
|
120
|
+
|
|
121
|
+
## Publication prerequisites
|
|
122
|
+
|
|
123
|
+
Enable immutable releases and verify the setting with the actual
|
|
124
|
+
`CODE_FOUNDRY_TOKEN` (or workflow credential), then set
|
|
125
|
+
`REQUIRE_IMMUTABLE_RELEASES=true`. Missing permissions, a disabled or unknown
|
|
126
|
+
setting, or a CLI without release verification support fail before Release Please
|
|
127
|
+
writes. Protect release tags against concurrent moves. The staging token needs
|
|
128
|
+
administration-read, contents-write, and verification access; it is never exposed
|
|
129
|
+
to qualification jobs. Preflight and staging reuse the producer's validated
|
|
130
|
+
credential selection, falling back to the workflow token only when the configured
|
|
131
|
+
token is rejected. Configure npm trusted-publisher identity for the actual
|
|
132
|
+
caller/reusable-workflow relationship, or explicitly retain the optional npm
|
|
133
|
+
token in the final publisher.
|
|
134
|
+
|
|
135
|
+
## Trust boundaries
|
|
136
|
+
|
|
137
|
+
Qualification reports are evidence selected from successful jobs in the same
|
|
138
|
+
workflow attempt; they are not standalone signatures or authorization outside
|
|
139
|
+
the protected workflow. The workflow executes repository-owned code with the
|
|
140
|
+
permissions of its job. Keep credentials out of qualification jobs and review
|
|
141
|
+
release/environment protections separately. A YAML environment reference does
|
|
142
|
+
not prove that approval protections exist.
|
|
143
|
+
|
|
144
|
+
Run the full locked-toolchain suite, Actionlint contracts, and a disposable-repo
|
|
145
|
+
release/signing/registry rehearsal before production approval. The producer
|
|
146
|
+
serializes the full release workflow and never cancels an active publish. The
|
|
147
|
+
self caller auto-publishes after its gates without bypassing branch or review
|
|
148
|
+
requirements for source changes.
|
|
149
|
+
|
|
150
|
+
The staging command requires release-write, attestation-verification, and
|
|
151
|
+
immutable-setting read access. No elevated credential is installed automatically.
|
|
152
|
+
If the configured automation token is rejected, the producer's documented
|
|
153
|
+
fallback is used only where the workflow permits it; missing permissions fail
|
|
154
|
+
closed.
|
|
155
|
+
|
|
156
|
+
Use **Re-run all jobs** for qualification failures; attempts cannot reuse earlier
|
|
157
|
+
reports. If Release Please returns `release_created: false`, the recovery job
|
|
158
|
+
looks up only the package version's draft release through the authenticated,
|
|
159
|
+
paginated release list, resolves its tag, and resumes only when that tag still
|
|
160
|
+
points to the newly qualified source. Staging uses the same list because GitHub's
|
|
161
|
+
get-by-tag endpoint does not return draft releases. Missing or already published
|
|
162
|
+
releases are a safe no-op; malformed, inaccessible, or source-mismatched drafts
|
|
163
|
+
fail closed. Inspect the retained identity receipts. Once a release is
|
|
164
|
+
published, do not attempt to re-stage or overwrite it: rerun the verified
|
|
165
|
+
publisher from the same source-bound workflow after confirming npm has not already
|
|
166
|
+
accepted that version. Never weaken the SHA guard, move an immutable tag, or treat
|
|
167
|
+
npm's version-conflict response as success.
|
|
168
|
+
|
|
169
|
+
## Validation
|
|
170
|
+
|
|
171
|
+
Run the focused local suites before changing this path:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
node --test test/consumer-qualification.test.mjs
|
|
175
|
+
node --test test/consumer-qualification-workflow.test.mjs
|
|
176
|
+
node --test test/qualification-handoff.test.mjs
|
|
177
|
+
node --test test/qualified-publication.test.mjs
|
|
178
|
+
node --test test/release-cutover.test.mjs
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
These tests use fixtures and do not establish live GitHub signing, OIDC
|
|
182
|
+
permissions, registry publication, environment approvals, or runner tool
|
|
183
|
+
availability. Exercise those controls in a disposable repository before changing
|
|
184
|
+
production release settings.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Release integrity and build provenance
|
|
2
2
|
|
|
3
|
-
Code Foundry
|
|
3
|
+
Code Foundry provides a read-only immutable-setting preflight, cryptographic
|
|
4
4
|
release/asset verification, and an optional build-provenance action. These are
|
|
5
|
-
separate guarantees: a checksum identifies bytes, an attestation identifies
|
|
6
|
-
origin, and GitHub's immutable-release setting prevents replacement after
|
|
5
|
+
separate guarantees: a checksum identifies bytes, an attestation identifies
|
|
6
|
+
their origin, and GitHub's immutable-release setting prevents replacement after
|
|
7
|
+
publication.
|
|
7
8
|
|
|
8
9
|
## Enable immutable releases explicitly
|
|
9
10
|
|
|
@@ -16,9 +17,9 @@ can verify an explicitly selected tag without the variable. Billing pause is
|
|
|
16
17
|
honored by both workflows. A skipped workflow is not verification evidence.
|
|
17
18
|
|
|
18
19
|
Publish all assets to a draft release **before** publishing it. Do not attach or
|
|
19
|
-
replace assets after an immutable release is published.
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
replace assets after an immutable release is published. If a repository adds
|
|
21
|
+
release assets, its release workflow must stage them before the release is
|
|
22
|
+
published; the verifier does not repair an already-published release.
|
|
22
23
|
|
|
23
24
|
A release workflow can run this preflight before publication:
|
|
24
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Required capabilities and task evidence
|
|
2
2
|
|
|
3
3
|
Declared requirements fail closed when discovery cannot find an executable task.
|
|
4
|
-
|
|
4
|
+
Optional task discovery remains available. Configure scalar values in
|
|
5
5
|
`.github/code-foundry.yml`:
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
@@ -16,14 +16,14 @@ coverage_report: coverage/coverage-summary.json
|
|
|
16
16
|
Supported task capabilities are `format`, `lint`, `type_check`, `build`, `unit`,
|
|
17
17
|
`integration`, `e2e`, `smoke`, and `performance`. `coverage` additionally requires
|
|
18
18
|
unit tests. Unknown names, contradictory requirements, and invalid thresholds
|
|
19
|
-
are errors. `performance: true`
|
|
19
|
+
are errors. `performance: true` means required, not merely enabled when a
|
|
20
20
|
script happens to exist. Use `performance: auto` to retain optional discovery.
|
|
21
21
|
|
|
22
22
|
The public `src/runtime.mjs` entrypoint delegates ecosystem execution to the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
private `src/runtime-core.mjs`. Keep both files and `src/lib` when vendoring the
|
|
24
|
+
runtime. Published packages and reusable workflows must include those paths. Do
|
|
25
|
+
not call the private executor from consumer CI: it does not enforce the public
|
|
26
|
+
policy contract.
|
|
27
27
|
|
|
28
28
|
## Coverage migration
|
|
29
29
|
|
|
@@ -60,14 +60,41 @@ appear in the GitHub job summary when `GITHUB_STEP_SUMMARY` is available.
|
|
|
60
60
|
The recorded command is the delegated executor invocation, not a transcript of
|
|
61
61
|
all nested package scripts. No environment variables or captured command output
|
|
62
62
|
are copied into the report. Repository scripts remain responsible for sanitizing
|
|
63
|
-
their own logs.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
their own logs and command arguments. Receipts are diagnostics, not signatures,
|
|
64
|
+
attestations, or authorization to publish or merge.
|
|
65
|
+
|
|
66
|
+
The shared CI and Test workflows retain each executed task's receipt, including
|
|
67
|
+
failure, and discovery receipts for explicitly skipped optional tasks. Each upload
|
|
68
|
+
selects exactly `.code-foundry/results/<task>.json`, not the broader hidden
|
|
69
|
+
directory. The artifact name is `task-result-RUN_ID-ATTEMPT-TASK` and retention is
|
|
70
|
+
14 days. The optional `artifact-prefix` input is exposed by CI, Test, and both
|
|
71
|
+
validation orchestrators; it disambiguates multiple invocations in the same run
|
|
72
|
+
when forwarded to the leaf workflows. Use a different prefix for each such
|
|
73
|
+
invocation. Existing coverage/performance artifact uploads are unchanged.
|
|
74
|
+
|
|
75
|
+
Missing receipts from an older runtime, a discovery/setup failure, or termination
|
|
76
|
+
before the runtime writes its report do not create an artifact. Missing evidence
|
|
77
|
+
is not success: inspect the task's actual outcome and the validation gate. Uploads
|
|
78
|
+
run after success or failure and do not suppress a failing task exit. When an
|
|
79
|
+
existing receipt disappears during upload, the upload itself fails. A job skipped
|
|
80
|
+
by the workflow's tier or billing policy cannot create a receipt.
|
|
81
|
+
|
|
82
|
+
These files describe repository-controlled execution and can include paths and
|
|
83
|
+
script-derived reasons. Do not put credentials in command arguments. Artifact
|
|
84
|
+
access follows repository/Actions visibility; receipt retention does not provide
|
|
85
|
+
a sandbox or independently validate repository-authored evidence.
|
|
86
|
+
|
|
87
|
+
Use the public CLI for a repository-facing discovery plan:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npx code-foundry plan --tier audit --json
|
|
91
|
+
```
|
|
66
92
|
|
|
67
|
-
`node src/runtime.mjs ci plan`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
The lower-level `node src/runtime.mjs ci plan` form remains useful to reusable
|
|
94
|
+
workflows and runtime tests. Both forms discover tasks without executing checks.
|
|
95
|
+
Discovery validates every required task before workflows select jobs. A
|
|
96
|
+
fast/unit-only tier is still a subset: discovery proves that E2E exists, not that
|
|
97
|
+
E2E ran. Require the audit validation gate before merging.
|
|
71
98
|
|
|
72
99
|
Native task detection rejects known no-ops such as a JavaScript project with no
|
|
73
100
|
build script or a Python project with no supported type-check command. Add a
|
|
@@ -78,5 +105,5 @@ an installed package manager or discovered project proves task execution.
|
|
|
78
105
|
|
|
79
106
|
`node --test test/task-policy.test.mjs` exercises policy parsing, discovery,
|
|
80
107
|
coverage parsing/thresholds/freshness/path safety, exit propagation, and reports
|
|
81
|
-
with a deterministic executor fixture. The
|
|
82
|
-
|
|
108
|
+
with a deterministic executor fixture. The ecosystem executor is covered by
|
|
109
|
+
`test/runtime.test.mjs` in the full suite.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.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": {
|
|
@@ -300,8 +300,13 @@ export async function deliveryCommand(command, env = process.env) {
|
|
|
300
300
|
else if (command === 'rollback') await rollback(env, state)
|
|
301
301
|
else if (command === 'record-start') {
|
|
302
302
|
const production = env.FOUNDRY_PHASE === 'production'
|
|
303
|
+
// pull_request workflows build the merge ref, while GitHub associates
|
|
304
|
+
// PR deployments with the head commit. The reusable workflow supplies
|
|
305
|
+
// that head SHA; direct pushes fall back to the checked-out source SHA.
|
|
306
|
+
const deploymentRef = env.GITHUB_DEPLOYMENT_REF || state.sourceSha
|
|
307
|
+
if (!/^[0-9a-f]{40}$/i.test(deploymentRef)) throw new Error('Invalid GitHub deployment ref')
|
|
303
308
|
const deployment = await github(env, 'deployments', {
|
|
304
|
-
ref:
|
|
309
|
+
ref: deploymentRef,
|
|
305
310
|
environment: required(env, 'DEPLOYMENT_ENVIRONMENT'),
|
|
306
311
|
auto_merge: false,
|
|
307
312
|
required_contexts: [],
|
|
@@ -252,15 +252,28 @@ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
|
|
|
252
252
|
)
|
|
253
253
|
}
|
|
254
254
|
checkTag()
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Array.isArray(
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
const listEndpoint = `${prefix}/releases?per_page=100`
|
|
256
|
+
const findDraftRelease = () => {
|
|
257
|
+
const pages = JSON.parse(
|
|
258
|
+
run('gh', ['api', '--hostname', 'github.com', '--paginate', '--slurp', listEndpoint])
|
|
259
|
+
)
|
|
260
|
+
ensure(
|
|
261
|
+
Array.isArray(pages) && pages.every((page) => Array.isArray(page)),
|
|
262
|
+
'GitHub returned an invalid release list'
|
|
263
|
+
)
|
|
264
|
+
const release = pages.flat().find((entry) => entry?.tag_name === candidate.tag)
|
|
265
|
+
ensure(
|
|
266
|
+
Number.isSafeInteger(release?.id) &&
|
|
267
|
+
release.draft === true &&
|
|
268
|
+
release.tag_name === candidate.tag &&
|
|
269
|
+
Array.isArray(release.assets),
|
|
270
|
+
'An existing draft release with the exact tag is required'
|
|
271
|
+
)
|
|
272
|
+
return release
|
|
273
|
+
}
|
|
274
|
+
// GitHub's get-by-tag endpoint does not return draft releases. Enumerate the
|
|
275
|
+
// authenticated release list instead, including all pages, before any write.
|
|
276
|
+
const release = findDraftRelease()
|
|
264
277
|
const temporary = mkdtempSync(join(tmpdir(), 'foundry-release-receipt-'))
|
|
265
278
|
try {
|
|
266
279
|
const receipt = join(temporary, 'qualification.json')
|
|
@@ -288,7 +301,7 @@ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
|
|
|
288
301
|
candidate.repository,
|
|
289
302
|
])
|
|
290
303
|
}
|
|
291
|
-
const uploaded =
|
|
304
|
+
const uploaded = findDraftRelease()
|
|
292
305
|
ensure(
|
|
293
306
|
uploaded.id === release.id && uploaded.draft === true && uploaded.tag_name === candidate.tag,
|
|
294
307
|
'Draft changed during staging'
|
|
@@ -140,6 +140,15 @@ function tagCommit(run, repository, tag) {
|
|
|
140
140
|
return object.sha
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** Resolve a release tag without requiring the release itself to be published.
|
|
144
|
+
* @param {string} repository @param {string} tag @param {Runner} [run]
|
|
145
|
+
*/
|
|
146
|
+
export function resolveTagCommit(repository, tag, run = runGh) {
|
|
147
|
+
validateRepository(repository)
|
|
148
|
+
validateTag(tag)
|
|
149
|
+
return tagCommit(run, repository, tag)
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
/** GitHub CLI performs signature/attestation verification, not merely a metadata check.
|
|
144
153
|
* @param {{repository: string, tag: string, root?: string, assets?: string[], expectedSha?: string}} options
|
|
145
154
|
* @param {Runner} [run]
|
package/src/commands/sync.mjs
CHANGED
|
@@ -132,6 +132,10 @@ function synchronize(options) {
|
|
|
132
132
|
if (!['direct', 'staging-release'].includes(workflow)) {
|
|
133
133
|
throw new Error(`Unsupported git_workflow: ${workflow}; use direct or staging-release.`)
|
|
134
134
|
}
|
|
135
|
+
const draftProtection = configured(config.draft_protection, 'true')
|
|
136
|
+
if (!['true', 'false'].includes(draftProtection)) {
|
|
137
|
+
throw new Error(`Unsupported draft_protection: ${draftProtection}; use true or false.`)
|
|
138
|
+
}
|
|
135
139
|
const obsoleteConfigKeys = [
|
|
136
140
|
'opencode_security',
|
|
137
141
|
...(workflow === 'direct' ? ['staging_validation_mode'] : []),
|
|
@@ -632,13 +636,33 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
632
636
|
release: config.release_runner ?? config.runner,
|
|
633
637
|
}
|
|
634
638
|
const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
|
|
639
|
+
// Generated PR callers protect drafts by default. Consumers that intentionally
|
|
640
|
+
// run gates while a PR is still draft can opt out in code-foundry.yml.
|
|
641
|
+
if (configured(config.draft_protection, 'true') === 'false') {
|
|
642
|
+
rendered = rendered.replaceAll(' && github.event.pull_request.draft == false', '')
|
|
643
|
+
}
|
|
635
644
|
// Consumer release callers are generic package release workflows. The
|
|
636
645
|
// installed-consumer qualification harness is specific to Code Foundry's
|
|
637
646
|
// own package and must remain in the self workflow rather than being
|
|
638
647
|
// rendered into every consumer's release caller.
|
|
639
648
|
if (workflow === 'release' && !selfRepository) {
|
|
640
|
-
|
|
641
|
-
|
|
649
|
+
// The self caller owns qualification, immutability preflight, draft
|
|
650
|
+
// recovery, staging, and qualified publication. Consumer callers must
|
|
651
|
+
// retain the ordinary reusable Release Please path instead of inheriting
|
|
652
|
+
// self-only jobs whose outputs and environments do not exist for them.
|
|
653
|
+
for (const job of ['qualification', 'preflight', 'recovery', 'stage', 'publish'])
|
|
654
|
+
rendered = removeWorkflowBlock(rendered, job)
|
|
655
|
+
rendered = rendered.replace(/^ needs: \[qualification, preflight\]\n/m, '')
|
|
656
|
+
rendered = rendered.replace(/^ config-file: \.github\/release-please-foundry\.json\n/m, '')
|
|
657
|
+
rendered = rendered.replace(/^ defer-publication: true\n/m, '')
|
|
658
|
+
rendered = rendered.replace(
|
|
659
|
+
/^ if: github\.ref == 'refs\/heads\/main' && \(vars\.CI_BILLING_PAUSED != 'true' \|\| \(github\.event_name == 'workflow_dispatch' && inputs\['release-while-paused'\] == true\)\)\n/m,
|
|
660
|
+
" if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)\n"
|
|
661
|
+
)
|
|
662
|
+
rendered = rendered.replace(
|
|
663
|
+
/(\n secrets:\n CODE_FOUNDRY_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \}\}\n)/,
|
|
664
|
+
'$1 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n'
|
|
665
|
+
)
|
|
642
666
|
}
|
|
643
667
|
// The staging-release topology validates and scans pull requests against
|
|
644
668
|
// both main and the integration branch; direct repositories only ever
|
|
@@ -730,26 +754,6 @@ function removeWorkflowBlock(content, blockId) {
|
|
|
730
754
|
return lines.join('\n')
|
|
731
755
|
}
|
|
732
756
|
|
|
733
|
-
/**
|
|
734
|
-
* Remove a single root-job dependency while preserving the rest of the job.
|
|
735
|
-
* @param {string} content
|
|
736
|
-
* @param {string} jobId
|
|
737
|
-
* @param {string} dependency
|
|
738
|
-
* @returns {string}
|
|
739
|
-
*/
|
|
740
|
-
function removeWorkflowNeed(content, jobId, dependency) {
|
|
741
|
-
const lines = content.split('\n')
|
|
742
|
-
const start = lines.findIndex((line) => line === ` ${jobId}:`)
|
|
743
|
-
if (start === -1) return content
|
|
744
|
-
let end = start + 1
|
|
745
|
-
while (end < lines.length && !/^ [A-Za-z0-9_-]+:\s*$/.test(lines[end])) end += 1
|
|
746
|
-
const need = lines.findIndex(
|
|
747
|
-
(line, index) => index > start && index < end && line === ` needs: ${dependency}`
|
|
748
|
-
)
|
|
749
|
-
if (need >= 0) lines.splice(need, 1)
|
|
750
|
-
return lines.join('\n')
|
|
751
|
-
}
|
|
752
|
-
|
|
753
757
|
/**
|
|
754
758
|
* Dependabot updates land on the repository's integration branch. Direct
|
|
755
759
|
* repositories have no staging branch, so every update targets main. Cargo,
|
|
@@ -897,8 +901,8 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
897
901
|
'| Change | Target | Merge method | Merge gate |\n| ------------------------- | ------ | --------------------------------- | --------------------------------------- |\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Squash (`release_merge_strategy`) | Validation gate and release policy pass |\n',
|
|
898
902
|
],
|
|
899
903
|
[
|
|
900
|
-
'Draft pull requests do not start runner-heavy validation. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner. Converting a pull request to draft cancels in-flight validation through the lightweight cancellation control.',
|
|
901
|
-
'Draft pull requests do not start validation. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner. Converting a pull request to draft runs only the lightweight cancellation control.',
|
|
904
|
+
'Draft pull requests do not start runner-heavy validation unless `draft_protection: false` is configured for generated callers. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner while protection is enabled. Converting a pull request to draft cancels in-flight validation through the lightweight cancellation control.',
|
|
905
|
+
'Draft pull requests do not start validation unless `draft_protection: false` is configured. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner while protection is enabled. Converting a pull request to draft runs only the lightweight cancellation control.',
|
|
902
906
|
],
|
|
903
907
|
['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
|
|
904
908
|
],
|
|
@@ -1370,6 +1374,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
|
|
|
1370
1374
|
toolchain: 'auto',
|
|
1371
1375
|
...(stagingRelease ? { staging_validation_mode: 'fast' } : {}),
|
|
1372
1376
|
performance: 'auto',
|
|
1377
|
+
draft_protection: 'true',
|
|
1373
1378
|
performance_command: '',
|
|
1374
1379
|
performance_profile: '',
|
|
1375
1380
|
performance_budget_file: 'performance-package-budgets.json',
|