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.
Files changed (44) hide show
  1. package/.github/CONTRIBUTING.md +5 -2
  2. package/.github/code-foundry.yml +1 -0
  3. package/.github/release-please-foundry.json +56 -0
  4. package/.github/workflows/ci.yml +61 -0
  5. package/.github/workflows/cloudflare-delivery.yml +9 -0
  6. package/.github/workflows/cloudflare-deploy.yml +8 -1
  7. package/.github/workflows/opencode-security_self-ci.yml +1 -1
  8. package/.github/workflows/qualified-foundry-publish.yml +6 -7
  9. package/.github/workflows/release.yml +58 -10
  10. package/.github/workflows/release_self-ci.yml +211 -8
  11. package/.github/workflows/test.yml +75 -0
  12. package/.github/workflows/validation-no-codeql.yml +7 -0
  13. package/.github/workflows/validation.yml +7 -0
  14. package/.gitignore +1 -1
  15. package/AGENTS.md +4 -1
  16. package/CHANGELOG.md +60 -0
  17. package/README.md +20 -17
  18. package/docs/CONFIGURATION.md +182 -152
  19. package/docs/EXTENSIONS.md +28 -7
  20. package/docs/INITIALIZATION.md +13 -9
  21. package/docs/PERFORMANCE.md +67 -59
  22. package/docs/PUBLISHING.md +39 -14
  23. package/docs/README.md +37 -22
  24. package/docs/RELEASES.md +18 -9
  25. package/docs/WORKFLOWS.md +11 -0
  26. package/docs/agent-validation.md +6 -5
  27. package/docs/cloudflare-delivery.md +20 -8
  28. package/docs/consumer-qualification.md +11 -9
  29. package/docs/fleet-release-eligibility.md +14 -15
  30. package/docs/fleet-rollouts.md +3 -3
  31. package/docs/merge-queues.md +21 -21
  32. package/docs/product-quality.md +9 -10
  33. package/docs/qualified-publication.md +166 -93
  34. package/docs/release-integrity.md +7 -6
  35. package/docs/required-capabilities.md +42 -15
  36. package/package.json +1 -1
  37. package/src/commands/cloudflare-delivery.mjs +6 -1
  38. package/src/commands/qualified-publication.mjs +23 -10
  39. package/src/commands/release-integrity.mjs +9 -0
  40. package/src/commands/sync.mjs +29 -24
  41. package/src/lib/product-quality.mjs +220 -16
  42. package/src/runtime-core.mjs +12 -8
  43. package/src/runtime.mjs +1 -1
  44. package/src/templates/gitignore +1 -1
@@ -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`; 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 their current
25
- commands and compose the quality command after them rather than replacing the
26
- existing performance or E2E checks. The standalone entrypoint deliberately avoids
27
- changing the public CLI dispatch being introduced in the separate agent-validation
28
- PR. Install the reviewed Foundry version in the consumer's existing dependency
29
- manager and lockfile, not an unpinned network invocation. Add `.code-foundry/` to
30
- Git/package ignores and retain selected evidence through the consumer workflow's
31
- artifact uploader. Reports and browser traces may contain application data; review
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 Publication
2
-
3
- Publish only the same immutable Code Foundry archive that passed consumer qualification.
4
-
5
- **Dependencies:** Consumer qualification (#544) and release integrity (#537).
6
- **Activation:** Opt-in replacement publisher; no existing publisher is silently changed.
7
-
8
- The reusable `qualified-foundry-publish.yml` downloads an explicitly named npm
9
- archive from an already-published immutable release, verifies the release and
10
- asset attestations against the caller's exact commit, qualifies that same archive
11
- on Node 20/22/24, and only then admits a protected npm publication job. The final
12
- job downloads only reports from its own workflow run **and run attempt**, rechecks
13
- all required fixtures and archive/source identities, repeats cryptographic asset
14
- verification, validates the package name/version, and publishes the tarball with
15
- lifecycle scripts disabled. It never publishes a directory or rebuilds the package.
16
-
17
- The publishing job uses a GitHub environment, serializes publication for a tag,
18
- and has no dependency installation/build step. Qualification has no npm credential.
19
- Normal npm trusted publishing is preferred; an explicit optional token supports
20
- existing consumers. Configure the trusted publisher for the **actual caller and
21
- reusable-workflow relationship** before enabling this route. Existing version
22
- publication is not overwritten: retrying an already-published version fails rather
23
- than treating a registry conflict or network error as proof of identity.
24
-
25
- ## Release producer contract
26
-
27
- Build and test a package once, attach its tarball to a **draft** release, then
28
- publish that release with immutability enabled. The archive must contain the
29
- Code Foundry CLI/templates; this is intentionally not a generic package harness.
30
- The candidate's `package.json` name must be `code-foundry` and its version must
31
- match the explicit tag. All qualification modules must exist in the caller commit.
32
- A recent GitHub CLI with `release verify` and `release verify-asset` is required;
33
- missing support or authentication errors fail closed.
34
-
35
- This workflow blocks npm publication, not a GitHub Release that was already
36
- published. It does **not** add assets after an immutable release is published.
37
- The `stage` command implements the draft-asset producer: it verifies qualification,
38
- checks immutability without changing settings, resolves the existing tag to the
39
- qualified commit, uploads the archive and digest-bound qualification receipt,
40
- rechecks uploaded digests, then publishes and verifies the release. It requires
41
- an existing draft and existing tag; it never creates/moves tags or overwrites
42
- conflicting assets. Settings permission failures block writes. Matching existing
43
- assets can resume staging; conflicting assets require manual reconciliation.
44
- The pre-release gate in #544 supplies the required matrix reports.
45
- The legacy Release Please workflow creates releases without
46
- this archive staging step. Wire the new `stage` command into a draft-producing
47
- caller instead of pointing the old direct-release caller at the new publisher:
48
-
49
- ```sh
50
- node src/commands/qualified-publication.mjs stage \
51
- "$GITHUB_REPOSITORY" "$TAG" "$SOURCE_SHA" "$ASSET" "$CANDIDATE_DIRECTORY" \
52
- "$REPORT_NODE_20" "$REPORT_NODE_22" "$REPORT_NODE_24"
53
- ```
54
-
55
- The stage command needs a credential with immutable-setting read, release
56
- write, and attestation-read access; a normal Actions token may lack the
57
- administration-read permission.
58
- No elevated credential is installed or requested automatically. The producer must
59
- also prevent concurrent tag mutation (for example with protected release-tag
60
- rules and a single tag-scoped producer); GitHub does not offer an atomic
61
- "publish this draft only if the tag still resolves to SHA" operation. The final
62
- verification blocks npm if that invariant is violated, but cannot undo an
63
- already-published immutable release.
64
-
65
- Disable the old npm path before
66
- activating the replacement to prevent racing publishers. None of those production
67
- settings or existing release workflows are changed in this PR.
68
-
69
- Example caller job after its release producer (illustrative job IDs):
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
- Only main-branch `push` and `workflow_dispatch` callers are admitted. Pull-request
90
- events (including fork PRs), release-event shortcuts, and billing-paused runs
91
- cannot publish through this workflow. The event guard does not itself verify
92
- branch protection or prohibit a fork's independent main-branch workflow; configure
93
- branch/environment protections and registry publisher identity separately.
94
- The tag must resolve to `github.sha`, not a caller-selected old commit. To retry
95
- an older release after main moves, use a separately reviewed recovery procedure;
96
- do not weaken the identity gate ad hoc.
97
-
98
- ## Local policy tests and trust
99
-
100
- `node --test test/qualified-publication.test.mjs` uses CLI/verifier fixtures to
101
- exercise missing matrix members, skipped checks, absent Actionlint, changed
102
- archives, invalid asset names, wrong package identity, and prevention of npm
103
- execution before verification. These tests do not establish live GitHub signing,
104
- OIDC permissions, registry publication, or runner tool availability.
105
-
106
- Qualification reports are not standalone signatures: they are trusted only after
107
- selection from this workflow's successful jobs in the same run attempt. Supplying
108
- arbitrary local JSON to the library is not a security boundary. The reusable
109
- workflow and its caller must be reviewed/trusted and protected; repository-owned
110
- code executes with the permissions of its job. No credentials or production
111
- resources were configured by adding this feature.
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 now provides a read-only immutable-setting preflight, cryptographic
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 their
6
- origin, and GitHub's immutable-release setting prevents replacement after publish.
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. The existing Release
20
- Please/npm workflow is not rewritten by this change; repositories adding release
21
- assets must review their attachment ordering before enabling immutability.
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
- Existing optional task discovery remains available. Configure scalar values in
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` now means required, not merely enabled when a
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
- unchanged private `src/runtime-core.mjs`. Keep both files and `src/lib` when
24
- vendoring the runtime. Published packages and the reusable workflows' existing
25
- cone-mode sparse checkout include both files. Do not call the private executor
26
- from consumer CI: it intentionally does not enforce the public policy contract.
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. Upload `.code-foundry/results/*.json` with `if: always()` and
64
- `include-hidden-files: true` to retain downloadable reports; only upload this
65
- specific directory, not arbitrary hidden files or the entire checkout.
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` prints a JSON discovery plan without executing
68
- checks. Discovery validates every required task before reusable workflows select
69
- jobs. A fast/unit-only tier is still a subset: discovery proves that E2E exists,
70
- not that E2E ran. Require the existing audit validation gate before merging.
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 unchanged ecosystem executor remains
82
- covered by `test/runtime.test.mjs` in the full suite.
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.20.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: state.sourceSha,
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 endpoint = `${prefix}/releases/tags/${encodeURIComponent(candidate.tag)}`
256
- const release = api(endpoint)
257
- ensure(
258
- Number.isSafeInteger(release.id) &&
259
- release.draft === true &&
260
- release.tag_name === candidate.tag &&
261
- Array.isArray(release.assets),
262
- 'An existing draft release with the exact tag is required'
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 = api(endpoint)
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]
@@ -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
- rendered = removeWorkflowBlock(rendered, 'qualification')
641
- rendered = removeWorkflowNeed(rendered, 'release', 'qualification')
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',