code-foundry 1.20.2 → 1.25.3

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 (50) 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/eval.yml +116 -0
  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/validation-no-codeql.yml +34 -6
  12. package/.github/workflows/validation.yml +32 -5
  13. package/.github/workflows/validation_audit_self-ci.yml +1 -0
  14. package/.github/workflows/validation_self-ci.yml +1 -0
  15. package/.gitignore +1 -1
  16. package/AGENTS.md +5 -1
  17. package/CHANGELOG.md +95 -0
  18. package/README.md +23 -17
  19. package/docs/CONFIGURATION.md +189 -152
  20. package/docs/EVALS.md +139 -0
  21. package/docs/EXTENSIONS.md +28 -7
  22. package/docs/INITIALIZATION.md +13 -9
  23. package/docs/PERFORMANCE.md +67 -59
  24. package/docs/PUBLISHING.md +39 -14
  25. package/docs/README.md +37 -22
  26. package/docs/RELEASES.md +18 -9
  27. package/docs/WORKFLOWS.md +23 -4
  28. package/docs/agent-validation.md +6 -5
  29. package/docs/cloudflare-delivery.md +20 -8
  30. package/docs/consumer-qualification.md +11 -9
  31. package/docs/fleet-release-eligibility.md +14 -15
  32. package/docs/fleet-rollouts.md +3 -3
  33. package/docs/merge-queues.md +21 -21
  34. package/docs/product-quality.md +9 -10
  35. package/docs/qualified-publication.md +166 -93
  36. package/docs/release-integrity.md +7 -6
  37. package/docs/required-capabilities.md +24 -15
  38. package/package.json +1 -1
  39. package/src/commands/cloudflare-delivery.mjs +6 -1
  40. package/src/commands/qualified-publication.mjs +103 -12
  41. package/src/commands/release-integrity.mjs +46 -14
  42. package/src/commands/sync.mjs +30 -24
  43. package/src/lib/eval-envelope.mjs +234 -0
  44. package/src/lib/merge-queue.mjs +1 -0
  45. package/src/lib/product-quality.mjs +220 -16
  46. package/src/lib/task-policy.mjs +7 -0
  47. package/src/lib/validation-policy.mjs +10 -6
  48. package/src/runtime-core.mjs +166 -8
  49. package/src/runtime.mjs +3 -1
  50. package/src/templates/gitignore +3 -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)
@@ -0,0 +1,116 @@
1
+ name: Code Foundry Eval
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ runtime-repository:
7
+ description: Repository containing the Code Foundry runtime.
8
+ required: false
9
+ type: string
10
+ default: 0xPlayerOne/code-foundry
11
+ runtime-ref:
12
+ description: Code Foundry runtime tag or ref.
13
+ required: false
14
+ type: string
15
+ default: v1.0.5
16
+ runner:
17
+ description: >-
18
+ Runner used by the eval harness. Browser evals need a
19
+ Chrome-capable runner such as ubuntu-latest; ubuntu-slim does not
20
+ ship a browser.
21
+ required: false
22
+ type: string
23
+ default: ubuntu-latest
24
+ artifact-prefix:
25
+ description: Prefix for task receipts; use a unique value when calling this workflow more than once.
26
+ required: false
27
+ type: string
28
+ default: task-result
29
+
30
+ permissions:
31
+ contents: read
32
+
33
+ env:
34
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
35
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
36
+ REPO_FOUNDRY_PROFILE: ${{ vars.REPO_FOUNDRY_PROFILE }}
37
+ REPO_FOUNDRY_LANGUAGES: ${{ vars.REPO_FOUNDRY_LANGUAGES }}
38
+ REPO_FOUNDRY_FEATURES: ${{ vars.REPO_FOUNDRY_FEATURES }}
39
+ REPO_FOUNDRY_PACKAGE_MANAGER: ${{ vars.REPO_FOUNDRY_PACKAGE_MANAGER }}
40
+ REPO_FOUNDRY_RUNNER: ${{ vars.REPO_FOUNDRY_RUNNER }}
41
+ REPO_FOUNDRY_CACHE_PACKAGES: ${{ vars.REPO_FOUNDRY_CACHE_PACKAGES || 'auto' }}
42
+ REPO_FOUNDRY_CACHE_BUILD: ${{ vars.REPO_FOUNDRY_CACHE_BUILD || 'auto' }}
43
+
44
+ concurrency:
45
+ group: code-foundry-eval-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
46
+ cancel-in-progress: true
47
+
48
+ jobs:
49
+ eval:
50
+ name: Eval
51
+ if: vars.CI_BILLING_PAUSED != 'true'
52
+ runs-on: ${{ inputs.runner }}
53
+ steps:
54
+ - name: Checkout
55
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
56
+ with:
57
+ persist-credentials: false
58
+ - name: Runtime
59
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
60
+ with:
61
+ persist-credentials: false
62
+ repository: ${{ inputs.runtime-repository }}
63
+ ref: ${{ inputs.runtime-ref }}
64
+ path: .github/.code-foundry
65
+ sparse-checkout: |
66
+ .github/actions
67
+ src/lib
68
+ src/runtime-core.mjs
69
+ src/runtime.mjs
70
+ - name: Install runtime
71
+ run: |
72
+ mkdir -p .github/actions
73
+ mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
74
+ cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
75
+ - name: Detect
76
+ id: applicability
77
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run eval >> "$GITHUB_OUTPUT"
78
+ - name: Setup
79
+ if: steps.applicability.outputs.applicable == 'true'
80
+ uses: ./.github/actions/setup
81
+ with:
82
+ # Eval harnesses run through the repository's own package scripts and
83
+ # need their dependencies installed before the task executes.
84
+ install: 'true'
85
+ cache-build: 'true'
86
+ task: eval
87
+ - name: Eval
88
+ id: execute
89
+ if: steps.applicability.outputs.applicable == 'true'
90
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci eval
91
+ - name: Retain task receipt
92
+ if: >-
93
+ always() &&
94
+ (steps.execute.outcome == 'success' || steps.execute.outcome == 'failure' ||
95
+ steps.applicability.outputs.applicable == 'false') &&
96
+ hashFiles('.code-foundry/results/eval.json') != ''
97
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
98
+ with:
99
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval
100
+ path: .code-foundry/results/eval.json
101
+ include-hidden-files: true
102
+ if-no-files-found: error
103
+ retention-days: 14
104
+ - name: Retain eval report
105
+ if: >-
106
+ always() &&
107
+ steps.execute.outcome == 'failure' &&
108
+ hashFiles('eval-results/result.json') != ''
109
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
110
+ with:
111
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval-report
112
+ path: |
113
+ eval-results/result.json
114
+ eval-results/summary.json
115
+ if-no-files-found: error
116
+ retention-days: 14
@@ -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: