code-foundry 1.20.2 → 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 (40) 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/cloudflare-delivery.yml +9 -0
  5. package/.github/workflows/cloudflare-deploy.yml +8 -1
  6. package/.github/workflows/opencode-security_self-ci.yml +1 -1
  7. package/.github/workflows/qualified-foundry-publish.yml +6 -7
  8. package/.github/workflows/release.yml +58 -10
  9. package/.github/workflows/release_self-ci.yml +211 -8
  10. package/.gitignore +1 -1
  11. package/AGENTS.md +4 -1
  12. package/CHANGELOG.md +53 -0
  13. package/README.md +20 -17
  14. package/docs/CONFIGURATION.md +182 -152
  15. package/docs/EXTENSIONS.md +28 -7
  16. package/docs/INITIALIZATION.md +13 -9
  17. package/docs/PERFORMANCE.md +67 -59
  18. package/docs/PUBLISHING.md +39 -14
  19. package/docs/README.md +37 -22
  20. package/docs/RELEASES.md +18 -9
  21. package/docs/WORKFLOWS.md +11 -0
  22. package/docs/agent-validation.md +6 -5
  23. package/docs/cloudflare-delivery.md +20 -8
  24. package/docs/consumer-qualification.md +11 -9
  25. package/docs/fleet-release-eligibility.md +14 -15
  26. package/docs/fleet-rollouts.md +3 -3
  27. package/docs/merge-queues.md +21 -21
  28. package/docs/product-quality.md +9 -10
  29. package/docs/qualified-publication.md +166 -93
  30. package/docs/release-integrity.md +7 -6
  31. package/docs/required-capabilities.md +19 -12
  32. package/package.json +1 -1
  33. package/src/commands/cloudflare-delivery.mjs +6 -1
  34. package/src/commands/qualified-publication.mjs +23 -10
  35. package/src/commands/release-integrity.mjs +9 -0
  36. package/src/commands/sync.mjs +29 -24
  37. package/src/lib/product-quality.mjs +220 -16
  38. package/src/runtime-core.mjs +12 -8
  39. package/src/runtime.mjs +1 -1
  40. package/src/templates/gitignore +1 -1
@@ -44,7 +44,10 @@ This repository uses the `direct` workflow. Topic pull requests target `main`.
44
44
  - Keep ordinary pull requests in draft while preparing them. The generated
45
45
  Draft Guard converts ready ordinary pull requests to draft when they are
46
46
  opened or reopened, and runner-heavy validation starts only after an
47
- explicit `ready_for_review` transition.
47
+ explicit `ready_for_review` transition unless `draft_protection: false` is
48
+ configured for generated callers. That opt-out does not disable Draft Guard
49
+ or draft-PR automation. Cloudflare reusable callers use
50
+ `draft-protection: false`.
48
51
  - Run local validation and finish review preparation before marking an ordinary
49
52
  pull request ready. Ready pull requests stay ready when new commits arrive,
50
53
  and validation reruns for the current head; draft updates allocate no
@@ -184,7 +187,7 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
184
187
  | Push to a working branch | Draft PR workflow |
185
188
  | Push to `main` | Release workflow plus default-branch CodeQL scan; validation ran on the merged PR |
186
189
 
187
- 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.
190
+ 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.
188
191
 
189
192
  Pull-request validation keys concurrency by event and pull-request head, so a newer update cancels its superseded run. Scheduled and manual audits use a separate caller pinned to the protected default branch; this prevents caller-selected runtime code from executing with default-branch cache access. Pull-request and scheduled/manual callers use the mode-aware orchestrator, which fans out only the required jobs and concludes with the stable aggregate gate. Main pushes run the default-branch CodeQL lane separately.
190
193
 
@@ -3,6 +3,7 @@ version: 1
3
3
  profile: auto
4
4
  languages: typescript
5
5
  features: all
6
+ draft_protection: true
6
7
  codeql: auto
7
8
  codeql_rust_shards: '["all"]'
8
9
  codeql_rust_threads: 1
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "bump-minor-pre-major": true,
4
+ "pull-request-title-pattern": "chore(main): release ${version}",
5
+ "group-pull-request-title-pattern": "chore(main): release ${version}",
6
+ "changelog-sections": [
7
+ {
8
+ "type": "feat",
9
+ "section": "Features"
10
+ },
11
+ {
12
+ "type": "fix",
13
+ "section": "Bug Fixes"
14
+ },
15
+ {
16
+ "type": "perf",
17
+ "section": "Performance"
18
+ },
19
+ {
20
+ "type": "revert",
21
+ "section": "Reverts"
22
+ },
23
+ {
24
+ "type": "deps",
25
+ "section": "Dependencies"
26
+ },
27
+ {
28
+ "type": "docs",
29
+ "section": "Documentation"
30
+ },
31
+ {
32
+ "type": "test",
33
+ "section": "Tests"
34
+ },
35
+ {
36
+ "type": "ci",
37
+ "section": "CI"
38
+ },
39
+ {
40
+ "type": "refactor",
41
+ "section": "Maintenance"
42
+ },
43
+ {
44
+ "type": "chore",
45
+ "section": "Maintenance"
46
+ }
47
+ ],
48
+ "packages": {
49
+ ".": {
50
+ "release-type": "node",
51
+ "include-component-in-tag": false,
52
+ "draft": true,
53
+ "force-tag-creation": true
54
+ }
55
+ }
56
+ }
@@ -23,6 +23,11 @@ on:
23
23
  required: false
24
24
  type: string
25
25
  default: preview
26
+ draft-protection:
27
+ description: 'Skip draft pull requests by default; set false to run candidate gates for drafts.'
28
+ required: false
29
+ type: boolean
30
+ default: true
26
31
  working-directory:
27
32
  required: false
28
33
  type: string
@@ -113,6 +118,7 @@ jobs:
113
118
  candidate:
114
119
  if: >-
115
120
  vars.CI_BILLING_PAUSED != 'true' &&
121
+ (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
116
122
  github.event_name != 'pull_request_target' &&
117
123
  (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
118
124
  runs-on: ubuntu-latest
@@ -187,6 +193,7 @@ jobs:
187
193
  id: start
188
194
  env:
189
195
  GH_TOKEN: ${{ github.token }}
196
+ GITHUB_DEPLOYMENT_REF: ${{ github.event.pull_request.head.sha || github.sha }}
190
197
  run: node "$FOUNDRY_EXECUTOR" record-start
191
198
  - name: Set up Bun
192
199
  uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
@@ -238,6 +245,7 @@ jobs:
238
245
  needs: candidate
239
246
  if: >-
240
247
  vars.CI_BILLING_PAUSED != 'true' &&
248
+ (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
241
249
  inputs.mode == 'production' && needs.candidate.outputs.verified == 'true'
242
250
  runs-on: ubuntu-latest
243
251
  timeout-minutes: 30
@@ -291,6 +299,7 @@ jobs:
291
299
  id: start
292
300
  env:
293
301
  GH_TOKEN: ${{ github.token }}
302
+ GITHUB_DEPLOYMENT_REF: ${{ github.event.pull_request.head.sha || github.sha }}
294
303
  run: node "$FOUNDRY_EXECUTOR" record-start
295
304
  - name: Set up Bun for verification only
296
305
  uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
@@ -8,6 +8,11 @@ on:
8
8
  required: false
9
9
  type: string
10
10
  default: preview
11
+ draft-protection:
12
+ description: 'Skip draft pull requests by default; set false to run deployments for drafts.'
13
+ required: false
14
+ type: boolean
15
+ default: true
11
16
  working-directory:
12
17
  required: false
13
18
  type: string
@@ -68,6 +73,7 @@ jobs:
68
73
  name: Cloudflare Deploy / ${{ inputs.mode }}
69
74
  if: >-
70
75
  vars.CI_BILLING_PAUSED != 'true' &&
76
+ (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
71
77
  github.event_name != 'pull_request_target' &&
72
78
  (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
73
79
  runs-on: ${{ inputs.runner }}
@@ -98,9 +104,10 @@ jobs:
98
104
  GH_TOKEN: ${{ github.token }}
99
105
  MODE: ${{ inputs.mode }}
100
106
  ENVIRONMENT: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
107
+ GITHUB_DEPLOYMENT_REF: ${{ github.event.pull_request.head.sha || github.sha }}
101
108
  run: |
102
109
  set -euo pipefail
103
- body=$(jq -n --arg ref "$GITHUB_SHA" --arg environment "$ENVIRONMENT" \
110
+ body=$(jq -n --arg ref "$GITHUB_DEPLOYMENT_REF" --arg environment "$ENVIRONMENT" \
104
111
  --argjson production "$([ "$MODE" = production ] && echo true || echo false)" \
105
112
  '{ref: $ref, environment: $environment, auto_merge: false, production_environment: $production, required_contexts: []}')
106
113
  id=$(gh api "repos/$GITHUB_REPOSITORY/deployments" --input - <<< "$body" --jq .id)
@@ -14,7 +14,7 @@ permissions:
14
14
  jobs:
15
15
  detect:
16
16
  name: OpenCode Security / Detect
17
- if: vars.CI_BILLING_PAUSED != 'true' && (github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false)
17
+ if: vars.CI_BILLING_PAUSED != 'true' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false))
18
18
  runs-on: ubuntu-slim
19
19
  outputs:
20
20
  enabled: ${{ steps.detect.outputs.enabled }}
@@ -11,11 +11,11 @@ on:
11
11
  description: Exact pre-attached Code Foundry npm tarball name.
12
12
  required: true
13
13
  type: string
14
- environment:
15
- description: Protected GitHub environment for npm publication.
14
+ billing-pause-bypass:
15
+ description: Allow only an explicitly dispatched release while shared CI remains paused.
16
16
  required: false
17
- default: npm
18
- type: string
17
+ type: boolean
18
+ default: false
19
19
  secrets:
20
20
  NPM_TOKEN:
21
21
  required: false
@@ -27,7 +27,7 @@ permissions:
27
27
  jobs:
28
28
  qualify:
29
29
  name: Qualify Node ${{ matrix.node }}
30
- if: vars.CI_BILLING_PAUSED != 'true' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
30
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['billing-pause-bypass'] == true)) && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
31
31
  runs-on: ubuntu-24.04
32
32
  timeout-minutes: 15
33
33
  strategy:
@@ -76,10 +76,9 @@ jobs:
76
76
  publish:
77
77
  name: Publish qualified archive
78
78
  needs: qualify
79
- if: vars.CI_BILLING_PAUSED != 'true' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
79
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['billing-pause-bypass'] == true)) && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
80
80
  runs-on: ubuntu-24.04
81
81
  timeout-minutes: 15
82
- environment: ${{ inputs.environment }}
83
82
  concurrency:
84
83
  group: qualified-foundry-npm-${{ github.repository }}-${{ inputs.tag }}
85
84
  cancel-in-progress: false
@@ -23,6 +23,29 @@ on:
23
23
  required: false
24
24
  type: boolean
25
25
  default: false
26
+ config-file:
27
+ description: Repository-relative Release Please configuration file.
28
+ required: false
29
+ type: string
30
+ default: release-please-config.json
31
+ defer-publication:
32
+ description: Create a root draft release for an external qualified publisher; skip legacy publication and post-release jobs.
33
+ required: false
34
+ type: boolean
35
+ default: false
36
+ outputs:
37
+ release_created:
38
+ description: Whether Release Please created a release (possibly a draft).
39
+ value: ${{ jobs.release.outputs.release_created }}
40
+ tag_name:
41
+ description: Exact tag returned by Release Please.
42
+ value: ${{ jobs.release.outputs.tag_name }}
43
+ sha:
44
+ description: Exact source commit returned by Release Please.
45
+ value: ${{ jobs.release.outputs.sha }}
46
+ token_source:
47
+ description: Credential selected for release operations.
48
+ value: ${{ jobs.release.outputs.token_source }}
26
49
  secrets:
27
50
  CODE_FOUNDRY_TOKEN:
28
51
  required: false
@@ -57,6 +80,7 @@ jobs:
57
80
  outputs:
58
81
  release_created: ${{ steps.release.outputs.release_created || 'false' }}
59
82
  tag_name: ${{ steps.release.outputs.tag_name }}
83
+ sha: ${{ steps.release.outputs.sha }}
60
84
  npm_publish: ${{ steps.profile.outputs.npm_publish }}
61
85
  token_source: ${{ steps.credentials.outputs.token_source }}
62
86
  steps:
@@ -77,6 +101,9 @@ jobs:
77
101
  run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
78
102
  - name: Detect release profile
79
103
  id: profile
104
+ env:
105
+ RELEASE_CONFIG_FILE: ${{ inputs.config-file }}
106
+ DEFER_PUBLICATION: ${{ inputs.defer-publication }}
80
107
  run: |
81
108
  node <<'NODE'
82
109
  const fs = require('node:fs')
@@ -108,9 +135,25 @@ jobs:
108
135
  if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
109
136
  let legacyReleaseType = ''
110
137
  if (releaseType !== 'none') {
111
- const sourcePath = 'release-please-config.json'
138
+ const sourcePath = process.env.RELEASE_CONFIG_FILE || 'release-please-config.json'
139
+ if (!/^(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.json$/.test(sourcePath) || sourcePath.split('/').includes('..')) {
140
+ throw new Error('config-file must be a repository-relative JSON path')
141
+ }
112
142
  if (!fs.existsSync(sourcePath)) throw new Error(`Missing ${sourcePath}`)
143
+ const path = require('node:path')
144
+ const relative = path.relative(fs.realpathSync('.'), fs.realpathSync(sourcePath))
145
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || !fs.statSync(sourcePath).isFile()) {
146
+ throw new Error('config-file must be a file inside the repository')
147
+ }
113
148
  const releaseConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'))
149
+ if (process.env.DEFER_PUBLICATION === 'true') {
150
+ const rootPackage = releaseConfig.packages?.['.']
151
+ if (!rootPackage || Object.keys(releaseConfig.packages).length !== 1 ||
152
+ (rootPackage.draft ?? releaseConfig.draft) !== true ||
153
+ (rootPackage['force-tag-creation'] ?? releaseConfig['force-tag-creation']) !== true) {
154
+ throw new Error('defer-publication requires one root package with draft and force-tag-creation enabled')
155
+ }
156
+ }
114
157
  if (!releaseConfig.packages && !releaseConfig['release-type']) {
115
158
  legacyReleaseType = releaseType
116
159
  } else if (!fs.existsSync('.release-please-manifest.json')) {
@@ -152,7 +195,7 @@ jobs:
152
195
  uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
153
196
  with:
154
197
  token: ${{ secrets.CODE_FOUNDRY_TOKEN }}
155
- config-file: release-please-config.json
198
+ config-file: ${{ inputs.config-file }}
156
199
  release-type: ${{ steps.profile.outputs.legacy_release_type }}
157
200
  - name: Release Please (workflow token)
158
201
  id: release_workflow
@@ -160,7 +203,7 @@ jobs:
160
203
  uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
161
204
  with:
162
205
  token: ${{ github.token }}
163
- config-file: release-please-config.json
206
+ config-file: ${{ inputs.config-file }}
164
207
  release-type: ${{ steps.profile.outputs.legacy_release_type }}
165
208
  - name: Normalize Release Please outputs
166
209
  id: release
@@ -168,9 +211,11 @@ jobs:
168
211
  env:
169
212
  AUTOMATION_RELEASE_CREATED: ${{ steps.release_automation.outputs.release_created }}
170
213
  AUTOMATION_TAG_NAME: ${{ steps.release_automation.outputs.tag_name }}
214
+ AUTOMATION_SHA: ${{ steps.release_automation.outputs.sha }}
171
215
  AUTOMATION_PRS_CREATED: ${{ steps.release_automation.outputs.prs_created }}
172
216
  WORKFLOW_RELEASE_CREATED: ${{ steps.release_workflow.outputs.release_created }}
173
217
  WORKFLOW_TAG_NAME: ${{ steps.release_workflow.outputs.tag_name }}
218
+ WORKFLOW_SHA: ${{ steps.release_workflow.outputs.sha }}
174
219
  WORKFLOW_PRS_CREATED: ${{ steps.release_workflow.outputs.prs_created }}
175
220
  run: |
176
221
  set -euo pipefail
@@ -182,12 +227,14 @@ jobs:
182
227
  {
183
228
  echo "release_created=$AUTOMATION_RELEASE_CREATED"
184
229
  echo "tag_name=$AUTOMATION_TAG_NAME"
230
+ echo "sha=$AUTOMATION_SHA"
185
231
  echo "prs_created=$AUTOMATION_PRS_CREATED"
186
232
  } >> "$GITHUB_OUTPUT"
187
233
  else
188
234
  {
189
235
  echo "release_created=$WORKFLOW_RELEASE_CREATED"
190
236
  echo "tag_name=$WORKFLOW_TAG_NAME"
237
+ echo "sha=$WORKFLOW_SHA"
191
238
  echo "prs_created=$WORKFLOW_PRS_CREATED"
192
239
  } >> "$GITHUB_OUTPUT"
193
240
  fi
@@ -325,7 +372,7 @@ jobs:
325
372
  # creating a release. Reconciliation is only meaningful after a release
326
373
  # commit/tag is actually created; otherwise the promotion's source changes
327
374
  # are incorrectly rejected as non-metadata commits.
328
- if: (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.result == 'success' && needs.release.outputs.release_created == 'true'
375
+ if: inputs['defer-publication'] != true && (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.result == 'success' && needs.release.outputs.release_created == 'true'
329
376
  runs-on: ${{ inputs.runner }}
330
377
  timeout-minutes: 15
331
378
  permissions:
@@ -413,7 +460,7 @@ jobs:
413
460
  post-release:
414
461
  name: Post Hook
415
462
  needs: release
416
- if: (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.outputs.release_created == 'true'
463
+ if: inputs['defer-publication'] != true && (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.outputs.release_created == 'true'
417
464
  runs-on: ${{ inputs.runner }}
418
465
  timeout-minutes: 15
419
466
  concurrency:
@@ -423,9 +470,6 @@ jobs:
423
470
  actions: write
424
471
  contents: read
425
472
  env:
426
- # The release job validates the configured automation token and exposes
427
- # the selection; the post hook reuses the same selected credential so a
428
- # rejected long-lived token is never used after fallback.
429
473
  CODE_FOUNDRY_TOKEN: ${{ needs.release.outputs.token_source == 'configured' && (secrets.CODE_FOUNDRY_TOKEN) || github.token }}
430
474
  GH_TOKEN: ${{ needs.release.outputs.token_source == 'configured' && (secrets.CODE_FOUNDRY_TOKEN) || github.token }}
431
475
  steps:
@@ -464,7 +508,11 @@ jobs:
464
508
  NODE
465
509
  - name: Dispatch post-release hook once
466
510
  if: steps.hook.outputs.enabled == 'true' && steps.hook.outputs.workflow != ''
467
- run: node "$RUNNER_TEMP/code-foundry/src/cli.mjs" release hook --tag '${{ needs.release.outputs.tag_name }}' --workflow '${{ steps.hook.outputs.workflow }}' --mode '${{ steps.hook.outputs.mode }}'
511
+ env:
512
+ RELEASE_TAG: ${{ needs.release.outputs.tag_name }}
513
+ POST_RELEASE_WORKFLOW: ${{ steps.hook.outputs.workflow }}
514
+ POST_RELEASE_MODE: ${{ steps.hook.outputs.mode }}
515
+ run: node "$RUNNER_TEMP/code-foundry/src/cli.mjs" release hook --tag "$RELEASE_TAG" --workflow "$POST_RELEASE_WORKFLOW" --mode "$POST_RELEASE_MODE"
468
516
  - name: Explain disabled post-release hook
469
517
  if: steps.hook.outputs.enabled != 'true' || steps.hook.outputs.workflow == ''
470
518
  run: echo 'No post-release workflow configured; artifact delivery is intentionally skipped.'
@@ -472,7 +520,7 @@ jobs:
472
520
  npm:
473
521
  name: Publish npm
474
522
  needs: release
475
- if: (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.outputs.release_created == 'true' && needs.release.outputs.npm_publish == 'true'
523
+ if: inputs['defer-publication'] != true && (vars.CI_BILLING_PAUSED != 'true' || inputs['billing-pause-bypass'] == true) && needs.release.outputs.release_created == 'true' && needs.release.outputs.npm_publish == 'true'
476
524
  runs-on: ${{ inputs.runner }}
477
525
  timeout-minutes: 15
478
526
  permissions:
@@ -12,32 +12,235 @@ on:
12
12
  default: false
13
13
 
14
14
  permissions:
15
- actions: write
16
- contents: write
17
- issues: write
18
- pull-requests: write
19
- id-token: write
15
+ contents: read
16
+
17
+ # One owner for draft staging and npm publication. Never cancel an active release.
18
+ concurrency:
19
+ group: code-foundry-qualified-release-${{ github.repository }}-${{ github.ref }}
20
+ cancel-in-progress: false
20
21
 
21
22
  jobs:
22
23
  qualification:
23
24
  name: Qualify consumers
24
- if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)
25
+ if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true))
25
26
  uses: ./.github/workflows/consumer-qualification.yml
26
27
  with:
27
28
  billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
28
29
  permissions:
29
30
  actions: read
30
31
  contents: read
32
+
33
+ preflight:
34
+ name: Verify release prerequisites
35
+ if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true))
36
+ runs-on: ubuntu-24.04
37
+ timeout-minutes: 5
38
+ steps:
39
+ - name: Checkout trusted verifier
40
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
41
+ with:
42
+ persist-credentials: false
43
+ ref: ${{ github.sha }}
44
+ - name: Setup Node
45
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
46
+ with:
47
+ node-version: '24'
48
+ - name: Require activation and verify immutability without changing it
49
+ env:
50
+ REQUIRE_IMMUTABLE_RELEASES: ${{ vars.REQUIRE_IMMUTABLE_RELEASES }}
51
+ CODE_FOUNDRY_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN }}
52
+ GH_TOKEN: ${{ github.token }}
53
+ run: |
54
+ set -euo pipefail
55
+ test "$REQUIRE_IMMUTABLE_RELEASES" = true || { echo 'Enable and review immutable release prerequisites before this cutover.' >&2; exit 1; }
56
+ gh release verify --help >/dev/null
57
+ gh release verify-asset --help >/dev/null
58
+ # Match the reusable producer's credential selection. A rejected
59
+ # configured token must not make the later staging job use it.
60
+ if [ -n "$CODE_FOUNDRY_TOKEN" ] && GH_TOKEN="$CODE_FOUNDRY_TOKEN" gh api "repos/${GITHUB_REPOSITORY}" --jq '.full_name' >/dev/null 2>&1; then
61
+ GH_TOKEN="$CODE_FOUNDRY_TOKEN" node src/commands/release-integrity.mjs settings --repo "$GITHUB_REPOSITORY"
62
+ else
63
+ node src/commands/release-integrity.mjs settings --repo "$GITHUB_REPOSITORY"
64
+ fi
65
+
31
66
  release:
32
67
  name: Release
33
- needs: qualification
34
- if: vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)
68
+ needs: [qualification, preflight]
69
+ if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true))
35
70
  uses: ./.github/workflows/release.yml
36
71
  with:
37
72
  billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
38
73
  runner: ubuntu-slim
39
74
  runtime-repository: 0xPlayerOne/code-foundry
40
75
  runtime-ref: ${{ github.sha }}
76
+ config-file: .github/release-please-foundry.json
77
+ defer-publication: true
78
+ permissions:
79
+ actions: write
80
+ contents: write
81
+ issues: write
82
+ pull-requests: write
83
+ # Required by the reusable legacy workflow's declared npm job, which is disabled here.
84
+ id-token: write
41
85
  secrets:
42
86
  CODE_FOUNDRY_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN }}
87
+
88
+ recovery:
89
+ name: Recover qualified draft identity
90
+ needs: [qualification, release]
91
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && needs.qualification.result == 'success' && needs.release.result == 'success'
92
+ runs-on: ubuntu-24.04
93
+ timeout-minutes: 5
94
+ permissions:
95
+ contents: read
96
+ outputs:
97
+ found: ${{ steps.recover.outputs.found }}
98
+ tag: ${{ steps.recover.outputs.tag }}
99
+ sha: ${{ steps.recover.outputs.sha }}
100
+ env:
101
+ RELEASE_CREATED: ${{ needs.release.outputs.release_created }}
102
+ SOURCE_SHA: ${{ needs.qualification.outputs.source-sha }}
103
+ steps:
104
+ - name: Checkout trusted recovery runtime
105
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
106
+ with:
107
+ persist-credentials: false
108
+ ref: ${{ github.sha }}
109
+ - name: Setup Node
110
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
111
+ with:
112
+ node-version: '24'
113
+ - name: Locate an exact existing draft
114
+ id: recover
115
+ env:
116
+ GH_TOKEN: ${{ github.token }}
117
+ GITHUB_REPOSITORY: ${{ github.repository }}
118
+ run: |
119
+ set -euo pipefail
120
+ node --input-type=module <<'NODE'
121
+ import { appendFileSync, readFileSync } from "node:fs"
122
+ import { resolveTagCommit, runGh } from "./src/commands/release-integrity.mjs"
123
+ const output = process.env.GITHUB_OUTPUT
124
+ const write = (found, tag = "", sha = "") => appendFileSync(output, `found=${found}\ntag=${tag}\nsha=${sha}\n`)
125
+ if (process.env.RELEASE_CREATED === "true") {
126
+ write("false")
127
+ process.exit(0)
128
+ }
129
+ const version = JSON.parse(readFileSync("package.json", "utf8")).version
130
+ const tag = `v${version}`
131
+ // GitHub's get-by-tag endpoint does not return draft releases. Use
132
+ // the authenticated release list and inspect every page instead.
133
+ const endpoint = `repos/${process.env.GITHUB_REPOSITORY}/releases?per_page=100`
134
+ const response = runGh(["api", "--paginate", "--slurp", endpoint])
135
+ if (response.status !== 0) throw new Error("Could not inspect the expected draft release; recovery remains blocked")
136
+ let pages
137
+ try {
138
+ pages = JSON.parse(response.stdout)
139
+ } catch {
140
+ throw new Error("GitHub returned an invalid release response; recovery remains blocked")
141
+ }
142
+ if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page))) {
143
+ throw new Error("GitHub returned an invalid release list; recovery remains blocked")
144
+ }
145
+ const release = pages.flat().find((entry) => entry?.tag_name === tag)
146
+ if (!release || release.draft !== true) {
147
+ write("false")
148
+ process.exit(0)
149
+ }
150
+ if (release.tag_name !== tag) throw new Error("Existing draft tag does not match the expected package version")
151
+ const sourceSha = resolveTagCommit(process.env.GITHUB_REPOSITORY, tag, runGh)
152
+ if (sourceSha.toLowerCase() !== process.env.SOURCE_SHA.toLowerCase()) throw new Error("Existing draft tag does not match the qualified source SHA")
153
+ write("true", tag, sourceSha)
154
+ NODE
155
+
156
+ stage:
157
+ name: Stage qualified release
158
+ needs: [qualification, release, recovery]
159
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && github.ref == 'refs/heads/main' && (needs.release.outputs.release_created == 'true' || needs.recovery.outputs.found == 'true')
160
+ runs-on: ubuntu-24.04
161
+ timeout-minutes: 15
162
+ permissions:
163
+ actions: read
164
+ attestations: read
165
+ contents: write
166
+ env:
167
+ RELEASE_TAG: ${{ needs.release.outputs.tag_name || needs.recovery.outputs.tag }}
168
+ RELEASE_SHA: ${{ needs.release.outputs.sha || needs.recovery.outputs.sha }}
169
+ SOURCE_SHA: ${{ needs.qualification.outputs.source-sha }}
170
+ PACKAGE: ${{ needs.qualification.outputs.filename }}
171
+ CANDIDATE_SHA256: ${{ needs.qualification.outputs.sha256 }}
172
+ CANDIDATE_ARTIFACT: ${{ needs.qualification.outputs.candidate-artifact }}
173
+ steps:
174
+ - name: Checkout trusted staging runtime
175
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
176
+ with:
177
+ persist-credentials: false
178
+ ref: ${{ github.sha }}
179
+ - name: Setup Node
180
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
181
+ with:
182
+ node-version: '24'
183
+ - name: Require the release to identify this qualified candidate
184
+ run: |
185
+ node --input-type=module <<'NODE'
186
+ import assert from 'node:assert/strict'
187
+ import { readFileSync } from 'node:fs'
188
+ const env = process.env
189
+ const version = JSON.parse(readFileSync('package.json', 'utf8')).version
190
+ assert.match(env.SOURCE_SHA, /^[a-f0-9]{40}$/)
191
+ assert.equal(env.SOURCE_SHA, env.GITHUB_SHA)
192
+ assert.equal(env.RELEASE_SHA, env.SOURCE_SHA)
193
+ assert.equal(env.RELEASE_TAG, `v${version}`)
194
+ assert.equal(env.PACKAGE, `code-foundry-${version}.tgz`)
195
+ assert.match(env.CANDIDATE_SHA256, /^[a-f0-9]{64}$/)
196
+ assert.equal(env.CANDIDATE_ARTIFACT, `qualification-candidate-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`)
197
+ NODE
198
+ - name: Retrieve only this attempt's qualified bytes and reports
199
+ env:
200
+ GH_TOKEN: ${{ github.token }}
201
+ run: |
202
+ set -euo pipefail
203
+ gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
204
+ --name "$CANDIDATE_ARTIFACT" --dir "$RUNNER_TEMP/candidate"
205
+ test "$(sha256sum "$RUNNER_TEMP/candidate/$PACKAGE" | cut -d' ' -f1)" = "$CANDIDATE_SHA256"
206
+ for node in 20 22 24; do
207
+ gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
208
+ --name "consumer-qualification-$GITHUB_RUN_ATTEMPT-node-$node" \
209
+ --dir "$RUNNER_TEMP/reports/$node"
210
+ done
211
+ - name: Stage identical assets then publish and verify the immutable release
212
+ env:
213
+ GH_TOKEN: ${{ needs.release.outputs.token_source == 'configured' && secrets.CODE_FOUNDRY_TOKEN || github.token }}
214
+ run: |
215
+ set -euo pipefail
216
+ node src/commands/qualified-publication.mjs stage \
217
+ "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$SOURCE_SHA" "$PACKAGE" \
218
+ "$RUNNER_TEMP/candidate" \
219
+ "$RUNNER_TEMP/reports/20/report.json" \
220
+ "$RUNNER_TEMP/reports/22/report.json" \
221
+ "$RUNNER_TEMP/reports/24/report.json" > "$RUNNER_TEMP/staging.json"
222
+ - name: Retain staging identity
223
+ if: always()
224
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
225
+ with:
226
+ name: release-staging-${{ github.run_id }}-${{ github.run_attempt }}
227
+ path: ${{ runner.temp }}/staging.json
228
+ if-no-files-found: warn
229
+ retention-days: 14
230
+
231
+ publish:
232
+ name: Publish verified package
233
+ needs: [qualification, release, recovery, stage]
234
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && github.ref == 'refs/heads/main' && needs.stage.result == 'success' && (needs.release.outputs.release_created == 'true' || needs.recovery.outputs.found == 'true')
235
+ uses: ./.github/workflows/qualified-foundry-publish.yml
236
+ permissions:
237
+ actions: read
238
+ attestations: read
239
+ contents: read
240
+ id-token: write
241
+ with:
242
+ tag: ${{ needs.release.outputs.tag_name || needs.recovery.outputs.tag }}
243
+ asset: ${{ needs.qualification.outputs.filename }}
244
+ billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
245
+ secrets:
43
246
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.gitignore CHANGED
@@ -46,7 +46,7 @@ cache/
46
46
  !.github/actions/cache/action.yml
47
47
  typechain/
48
48
  coverage.json
49
- performance-results/
49
+ .code-foundry/
50
50
  tests/.bin/
51
51
  **/.app/
52
52
 
package/AGENTS.md CHANGED
@@ -106,7 +106,10 @@ This repository uses the `direct` workflow. Topic pull requests target `main`.
106
106
  - Keep ordinary pull requests in draft while preparing them. The generated
107
107
  Draft Guard converts ready ordinary pull requests to draft when they are
108
108
  opened or reopened, and runner-heavy validation starts only after an
109
- explicit `ready_for_review` transition.
109
+ explicit `ready_for_review` transition unless `draft_protection: false` is
110
+ configured for generated callers. That opt-out does not disable Draft Guard
111
+ or draft-PR automation. Cloudflare reusable callers use
112
+ `draft-protection: false`.
110
113
  - Run local validation and finish review preparation before marking an ordinary
111
114
  pull request ready. Ready pull requests stay ready when new commits arrive,
112
115
  and validation reruns for the current head; draft updates allocate no