code-foundry 1.48.8 → 1.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CONTRIBUTING.md +8 -8
- package/.github/code-foundry.schema.json +10 -0
- package/.github/code-foundry.yml +2 -0
- package/.github/workflows/ci.yml +41 -5
- package/.github/workflows/draft-enforcement_self-ci.yml +12 -8
- package/.github/workflows/qualification-receipt.yml +201 -0
- package/.github/workflows/qualified-foundry-publish.yml +0 -12
- package/.github/workflows/release.yml +58 -20
- package/.github/workflows/release_self-ci.yml +0 -2
- package/.github/workflows/security.yml +3 -1
- package/.github/workflows/test.yml +90 -1
- package/.github/workflows/validation-billing.yml +159 -23
- package/.github/workflows/validation-no-codeql.yml +91 -15
- package/.github/workflows/validation.yml +96 -15
- package/.github/workflows/validation_self-ci.yml +7 -4
- package/CHANGELOG.md +50 -50
- package/docs/CONFIGURATION.md +11 -0
- package/docs/PUBLISHING.md +13 -5
- package/docs/WORKFLOWS.md +25 -12
- package/docs/qualified-publication.md +22 -14
- package/package.json +4 -1
- package/src/commands/qualified-publication.mjs +27 -0
- package/src/commands/sync.mjs +76 -9
- package/src/lib/github-doctor.mjs +23 -2
- package/src/lib/node-package-performance.mjs +21 -1
- package/src/lib/validation-policy.mjs +11 -11
- package/src/runtime-core.mjs +2 -0
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -220,14 +220,14 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
|
|
|
220
220
|
|
|
221
221
|
## Workflow and check behavior
|
|
222
222
|
|
|
223
|
-
| Event
|
|
224
|
-
|
|
|
225
|
-
| Draft pull request targeting `main`
|
|
226
|
-
| Ready pull request targeting `main`
|
|
227
|
-
| Exact Release Please
|
|
228
|
-
| Scheduled or manual validation
|
|
229
|
-
| Push to a working branch
|
|
230
|
-
| Push to `main`
|
|
223
|
+
| Event | Expected automation |
|
|
224
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
225
|
+
| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |
|
|
226
|
+
| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |
|
|
227
|
+
| Exact Release Please PR targeting `main` | Release diff, configured CodeQL, build/package check, and either a verified receipt or full CI/tests |
|
|
228
|
+
| Scheduled or manual validation | Full audit tier |
|
|
229
|
+
| Push to a working branch | Draft PR workflow |
|
|
230
|
+
| Push to `main` | Release workflow; CodeQL skips release-only merges (audit covers drift) |
|
|
231
231
|
|
|
232
232
|
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.
|
|
233
233
|
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
"description": "Explicit JavaScript formatter. Omit to preserve repository scripts and the Oxfmt fallback.",
|
|
8
8
|
"type": "string",
|
|
9
9
|
"enum": ["oxfmt", "prettier", "bun"]
|
|
10
|
+
},
|
|
11
|
+
"release_qualification_receipts": {
|
|
12
|
+
"description": "Opt in to seven-day exact-tree qualification receipts for Release Please validation.",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": ["true", "false"]
|
|
15
|
+
},
|
|
16
|
+
"npm_publish_auth": {
|
|
17
|
+
"description": "npm publisher authentication: auto preserves token users and uses OIDC when no publish token is present; oidc forces trusted publishing; token requires NPM_TOKEN.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["auto", "oidc", "token"]
|
|
10
20
|
}
|
|
11
21
|
},
|
|
12
22
|
"additionalProperties": true
|
package/.github/code-foundry.yml
CHANGED
|
@@ -5,6 +5,7 @@ languages: typescript
|
|
|
5
5
|
features: all
|
|
6
6
|
draft_protection: true
|
|
7
7
|
codeql: auto
|
|
8
|
+
release_qualification_receipts: false
|
|
8
9
|
codeql_rust_shards: '["all"]'
|
|
9
10
|
codeql_rust_threads: 1
|
|
10
11
|
codeql_rust_max_parallel: 1
|
|
@@ -15,6 +16,7 @@ runtime_repository: 0xPlayerOne/code-foundry
|
|
|
15
16
|
runtime_ref: v1.30.0
|
|
16
17
|
release_type: node
|
|
17
18
|
npm_publish: true
|
|
19
|
+
npm_publish_auth: oidc
|
|
18
20
|
license: agpl-3.0-or-later
|
|
19
21
|
git_workflow: direct
|
|
20
22
|
merge_strategy: squash
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -23,6 +23,16 @@ on:
|
|
|
23
23
|
required: false
|
|
24
24
|
type: string
|
|
25
25
|
default: task-result
|
|
26
|
+
release-build-only:
|
|
27
|
+
description: Run only the build lane after an exact successful source qualification receipt.
|
|
28
|
+
required: false
|
|
29
|
+
type: boolean
|
|
30
|
+
default: false
|
|
31
|
+
release-package-check:
|
|
32
|
+
description: Inspect the versioned npm package contents on release pull requests.
|
|
33
|
+
required: false
|
|
34
|
+
type: boolean
|
|
35
|
+
default: false
|
|
26
36
|
secrets:
|
|
27
37
|
TURBO_TOKEN:
|
|
28
38
|
required: false
|
|
@@ -55,7 +65,7 @@ concurrency:
|
|
|
55
65
|
jobs:
|
|
56
66
|
format:
|
|
57
67
|
name: Format
|
|
58
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
68
|
+
if: vars.CI_BILLING_PAUSED != 'true' && inputs.release-build-only != true
|
|
59
69
|
runs-on: ${{ inputs.runner }}
|
|
60
70
|
timeout-minutes: 30
|
|
61
71
|
steps:
|
|
@@ -124,7 +134,7 @@ jobs:
|
|
|
124
134
|
|
|
125
135
|
lint:
|
|
126
136
|
name: Lint
|
|
127
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
137
|
+
if: vars.CI_BILLING_PAUSED != 'true' && inputs.release-build-only != true
|
|
128
138
|
runs-on: ${{ inputs.runner }}
|
|
129
139
|
timeout-minutes: 30
|
|
130
140
|
steps:
|
|
@@ -188,7 +198,7 @@ jobs:
|
|
|
188
198
|
|
|
189
199
|
type-check:
|
|
190
200
|
name: Type-Check
|
|
191
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
201
|
+
if: vars.CI_BILLING_PAUSED != 'true' && inputs.release-build-only != true
|
|
192
202
|
permissions:
|
|
193
203
|
contents: read
|
|
194
204
|
issues: read
|
|
@@ -279,6 +289,7 @@ jobs:
|
|
|
279
289
|
path: .github/.code-foundry
|
|
280
290
|
sparse-checkout: |
|
|
281
291
|
.github/actions
|
|
292
|
+
src/commands/qualification-receipt.mjs
|
|
282
293
|
src/lib
|
|
283
294
|
src/runtime-core.mjs
|
|
284
295
|
src/runtime.mjs
|
|
@@ -287,6 +298,11 @@ jobs:
|
|
|
287
298
|
mkdir -p .github/actions
|
|
288
299
|
mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
|
|
289
300
|
cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
|
|
301
|
+
- name: Set up Node.js 24.18.0 for package inspection
|
|
302
|
+
if: inputs.release-package-check == true
|
|
303
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
304
|
+
with:
|
|
305
|
+
node-version: 24.18.0
|
|
290
306
|
- name: Detect
|
|
291
307
|
id: applicability
|
|
292
308
|
env:
|
|
@@ -296,8 +312,13 @@ jobs:
|
|
|
296
312
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
297
313
|
BEFORE_SHA: ${{ github.event.before }}
|
|
298
314
|
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run build >> "$GITHUB_OUTPUT"
|
|
315
|
+
- name: Verify versioned package build contract
|
|
316
|
+
if: inputs.release-package-check == true
|
|
317
|
+
env:
|
|
318
|
+
FOUNDRY_BUILD_TASK_APPLICABLE: ${{ steps.applicability.outputs.applicable }}
|
|
319
|
+
run: node "$RUNNER_TEMP/code-foundry/src/commands/qualification-receipt.mjs" package-contract
|
|
299
320
|
- name: Setup
|
|
300
|
-
if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.affected != 'false'
|
|
321
|
+
if: steps.applicability.outputs.applicable == 'true' && (inputs.release-build-only == true || steps.applicability.outputs.affected != 'false')
|
|
301
322
|
uses: ./.github/actions/setup
|
|
302
323
|
with:
|
|
303
324
|
install: 'true'
|
|
@@ -305,8 +326,23 @@ jobs:
|
|
|
305
326
|
task: build
|
|
306
327
|
- name: Build
|
|
307
328
|
id: execute
|
|
308
|
-
if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.affected != 'false'
|
|
329
|
+
if: steps.applicability.outputs.applicable == 'true' && (inputs.release-build-only == true || steps.applicability.outputs.affected != 'false')
|
|
309
330
|
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci build
|
|
331
|
+
- name: Inspect versioned npm package contents
|
|
332
|
+
if: inputs.release-package-check == true && hashFiles('package.json') != ''
|
|
333
|
+
run: |
|
|
334
|
+
npm pack --dry-run --ignore-scripts --json > "$RUNNER_TEMP/foundry-package-inspection.json"
|
|
335
|
+
node --input-type=module -e '
|
|
336
|
+
import { readFileSync } from "node:fs";
|
|
337
|
+
import { join } from "node:path";
|
|
338
|
+
import { pathToFileURL } from "node:url";
|
|
339
|
+
|
|
340
|
+
const runtime = join(process.env.RUNNER_TEMP, "code-foundry", "src/lib/node-package-performance.mjs");
|
|
341
|
+
const { verifyNpmPackPackage } = await import(pathToFileURL(runtime).href);
|
|
342
|
+
const manifest = JSON.parse(readFileSync("package.json", "utf8"));
|
|
343
|
+
const report = JSON.parse(readFileSync(process.argv[1], "utf8"));
|
|
344
|
+
verifyNpmPackPackage(report, manifest);
|
|
345
|
+
' "$RUNNER_TEMP/foundry-package-inspection.json"
|
|
310
346
|
- name: Retain task receipt
|
|
311
347
|
if: >-
|
|
312
348
|
always() &&
|
|
@@ -36,11 +36,11 @@ jobs:
|
|
|
36
36
|
exit 1
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
|
-
read -r draft head_ref head_sha updated_at < <(
|
|
39
|
+
read -r draft head_ref head_sha updated_at node_id < <(
|
|
40
40
|
gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
|
|
41
|
-
--jq '[.draft, .head.ref, .head.sha, .updated_at] | @tsv'
|
|
41
|
+
--jq '[.draft, .head.ref, .head.sha, .updated_at, .node_id] | @tsv'
|
|
42
42
|
)
|
|
43
|
-
if [ -z "$draft" ] || [ -z "$head_ref" ] || [ -z "$head_sha" ] || [ -z "$updated_at" ]; then
|
|
43
|
+
if [ -z "$draft" ] || [ -z "$head_ref" ] || [ -z "$head_sha" ] || [ -z "$updated_at" ] || [ -z "$node_id" ]; then
|
|
44
44
|
echo '::error title=Draft guard cannot verify current state::The GitHub API response did not include the required pull-request metadata.'
|
|
45
45
|
exit 1
|
|
46
46
|
fi
|
|
@@ -70,9 +70,13 @@ jobs:
|
|
|
70
70
|
exit 1
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
|
-
gh api \
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
result=$(gh api graphql \
|
|
74
|
+
-f query='mutation($pullRequestId: ID!) { convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) { pullRequest { id isDraft } } }' \
|
|
75
|
+
-f pullRequestId="$node_id" \
|
|
76
|
+
--jq '.data.convertPullRequestToDraft.pullRequest | [.id, .isDraft] | @tsv')
|
|
77
|
+
read -r returned_id is_draft <<< "$result"
|
|
78
|
+
if [ "$returned_id" != "$node_id" ] || [ "$is_draft" != true ]; then
|
|
79
|
+
echo '::error title=Draft guard conversion failed::GraphQL did not confirm the expected pull request is a draft.'
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
78
82
|
echo "Converted ordinary pull request #$PR_NUMBER to draft; validation waits for ready_for_review."
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
name: Code Foundry Qualification Receipt
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
phase:
|
|
7
|
+
description: Produce evidence after a full audit or verify it for a release PR.
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
mode:
|
|
11
|
+
description: Validation tier associated with this receipt operation.
|
|
12
|
+
required: true
|
|
13
|
+
type: string
|
|
14
|
+
enabled:
|
|
15
|
+
description: Consumer opt-in for qualification receipt reuse.
|
|
16
|
+
required: false
|
|
17
|
+
type: boolean
|
|
18
|
+
default: false
|
|
19
|
+
codeql-enabled:
|
|
20
|
+
description: Whether the consumer requires CodeQL in its audit and release lanes.
|
|
21
|
+
required: false
|
|
22
|
+
type: boolean
|
|
23
|
+
default: true
|
|
24
|
+
runtime-repository:
|
|
25
|
+
description: Repository containing the Code Foundry runtime.
|
|
26
|
+
required: true
|
|
27
|
+
type: string
|
|
28
|
+
runtime-ref:
|
|
29
|
+
description: Runtime commit, tag, or ref used by source validation.
|
|
30
|
+
required: true
|
|
31
|
+
type: string
|
|
32
|
+
job-results-json:
|
|
33
|
+
description: Normalized component results for the successful full audit.
|
|
34
|
+
required: false
|
|
35
|
+
type: string
|
|
36
|
+
default: '{}'
|
|
37
|
+
required-job-names-json:
|
|
38
|
+
description: Exact aggregate gate job names to verify from the source run's complete attempt-job listing.
|
|
39
|
+
required: false
|
|
40
|
+
type: string
|
|
41
|
+
default: '[]'
|
|
42
|
+
outputs:
|
|
43
|
+
reuse:
|
|
44
|
+
description: True only when a matching recent source receipt was verified.
|
|
45
|
+
value: ${{ jobs.verify.outputs.reuse }}
|
|
46
|
+
|
|
47
|
+
permissions:
|
|
48
|
+
actions: read
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: read
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
verify:
|
|
54
|
+
name: Verify source receipt
|
|
55
|
+
if: >-
|
|
56
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.enabled == true &&
|
|
57
|
+
inputs.phase == 'verify' && inputs.mode == 'release'
|
|
58
|
+
runs-on: ubuntu-slim
|
|
59
|
+
timeout-minutes: 15
|
|
60
|
+
outputs:
|
|
61
|
+
reuse: ${{ steps.verify.outputs.reuse }}
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout release PR source
|
|
64
|
+
id: source
|
|
65
|
+
continue-on-error: true
|
|
66
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
67
|
+
with:
|
|
68
|
+
persist-credentials: false
|
|
69
|
+
fetch-depth: 0
|
|
70
|
+
- name: Checkout runtime
|
|
71
|
+
id: runtime
|
|
72
|
+
if: steps.source.outcome == 'success'
|
|
73
|
+
continue-on-error: true
|
|
74
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
75
|
+
with:
|
|
76
|
+
persist-credentials: false
|
|
77
|
+
repository: ${{ inputs.runtime-repository }}
|
|
78
|
+
ref: ${{ inputs.runtime-ref }}
|
|
79
|
+
path: .github/.code-foundry
|
|
80
|
+
sparse-checkout: |
|
|
81
|
+
src/commands/qualification-receipt.mjs
|
|
82
|
+
src/lib
|
|
83
|
+
src/runtime-core.mjs
|
|
84
|
+
src/runtime.mjs
|
|
85
|
+
- name: Install runtime
|
|
86
|
+
id: install
|
|
87
|
+
if: steps.runtime.outcome == 'success'
|
|
88
|
+
continue-on-error: true
|
|
89
|
+
run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
|
|
90
|
+
- name: Set up Node.js 24.18.0
|
|
91
|
+
id: node
|
|
92
|
+
if: steps.install.outcome == 'success'
|
|
93
|
+
continue-on-error: true
|
|
94
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
95
|
+
with:
|
|
96
|
+
node-version: 24.18.0
|
|
97
|
+
- name: Check version-only release diff
|
|
98
|
+
id: release-policy
|
|
99
|
+
if: steps.node.outcome == 'success'
|
|
100
|
+
continue-on-error: true
|
|
101
|
+
env:
|
|
102
|
+
FOUNDRY_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
103
|
+
FOUNDRY_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
104
|
+
FOUNDRY_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
|
105
|
+
FOUNDRY_REPOSITORY: ${{ github.repository }}
|
|
106
|
+
RUNTIME_PATH: ${{ runner.temp }}/code-foundry
|
|
107
|
+
run: node "$RUNTIME_PATH/src/runtime.mjs" validation release_diff
|
|
108
|
+
- name: Verify source qualification receipt
|
|
109
|
+
id: verify
|
|
110
|
+
if: always()
|
|
111
|
+
continue-on-error: true
|
|
112
|
+
env:
|
|
113
|
+
SOURCE_READY: ${{ steps.source.outcome }}
|
|
114
|
+
RUNTIME_READY: ${{ steps.install.outcome }}
|
|
115
|
+
NODE_READY: ${{ steps.node.outcome }}
|
|
116
|
+
POLICY_READY: ${{ steps.release-policy.outcome }}
|
|
117
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
118
|
+
RUNTIME_PATH: ${{ runner.temp }}/code-foundry
|
|
119
|
+
RUNTIME_REPOSITORY: ${{ inputs.runtime-repository }}
|
|
120
|
+
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
121
|
+
FOUNDRY_CODEQL_ENABLED: ${{ inputs.codeql-enabled }}
|
|
122
|
+
FOUNDRY_REQUIRED_JOB_NAMES_JSON: ${{ inputs.required-job-names-json }}
|
|
123
|
+
run: |
|
|
124
|
+
if [ "$SOURCE_READY" = success ] && [ "$RUNTIME_READY" = success ] && [ "$NODE_READY" = success ] && [ "$POLICY_READY" = success ] && [ -f "$RUNTIME_PATH/src/commands/qualification-receipt.mjs" ]; then
|
|
125
|
+
node "$RUNTIME_PATH/src/commands/qualification-receipt.mjs" verify >> "$GITHUB_OUTPUT"
|
|
126
|
+
else
|
|
127
|
+
echo 'reuse=false' >> "$GITHUB_OUTPUT"
|
|
128
|
+
echo 'reason=qualification or version-only release policy unavailable; using full validation' >> "$GITHUB_OUTPUT"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
produce:
|
|
132
|
+
name: Produce source receipt
|
|
133
|
+
if: >-
|
|
134
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.enabled == true &&
|
|
135
|
+
inputs.phase == 'produce' && inputs.mode == 'audit'
|
|
136
|
+
runs-on: ubuntu-slim
|
|
137
|
+
timeout-minutes: 10
|
|
138
|
+
steps:
|
|
139
|
+
- name: Checkout tested source tree
|
|
140
|
+
id: source
|
|
141
|
+
continue-on-error: true
|
|
142
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
143
|
+
with:
|
|
144
|
+
persist-credentials: false
|
|
145
|
+
ref: ${{ github.sha }}
|
|
146
|
+
fetch-depth: 2
|
|
147
|
+
- name: Checkout runtime
|
|
148
|
+
id: runtime
|
|
149
|
+
if: steps.source.outcome == 'success'
|
|
150
|
+
continue-on-error: true
|
|
151
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
152
|
+
with:
|
|
153
|
+
persist-credentials: false
|
|
154
|
+
repository: ${{ inputs.runtime-repository }}
|
|
155
|
+
ref: ${{ inputs.runtime-ref }}
|
|
156
|
+
path: .github/.code-foundry
|
|
157
|
+
sparse-checkout: |
|
|
158
|
+
src/commands/qualification-receipt.mjs
|
|
159
|
+
src/lib
|
|
160
|
+
- name: Install runtime
|
|
161
|
+
id: install
|
|
162
|
+
if: steps.runtime.outcome == 'success'
|
|
163
|
+
continue-on-error: true
|
|
164
|
+
run: mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
|
|
165
|
+
- name: Set up Node.js 24.18.0
|
|
166
|
+
id: node
|
|
167
|
+
if: steps.install.outcome == 'success'
|
|
168
|
+
continue-on-error: true
|
|
169
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
170
|
+
with:
|
|
171
|
+
node-version: 24.18.0
|
|
172
|
+
- name: Create qualification receipt
|
|
173
|
+
id: produce
|
|
174
|
+
if: always()
|
|
175
|
+
continue-on-error: true
|
|
176
|
+
env:
|
|
177
|
+
SOURCE_READY: ${{ steps.source.outcome }}
|
|
178
|
+
RUNTIME_READY: ${{ steps.install.outcome }}
|
|
179
|
+
NODE_READY: ${{ steps.node.outcome }}
|
|
180
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
181
|
+
RUNTIME_PATH: ${{ runner.temp }}/code-foundry
|
|
182
|
+
RUNTIME_REPOSITORY: ${{ inputs.runtime-repository }}
|
|
183
|
+
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
184
|
+
FOUNDRY_JOB_RESULTS_JSON: ${{ inputs.job-results-json }}
|
|
185
|
+
FOUNDRY_REQUIRED_JOB_NAMES_JSON: ${{ inputs.required-job-names-json }}
|
|
186
|
+
run: |
|
|
187
|
+
if [ "$SOURCE_READY" = success ] && [ "$RUNTIME_READY" = success ] && [ "$NODE_READY" = success ] && [ -f "$RUNTIME_PATH/src/commands/qualification-receipt.mjs" ]; then
|
|
188
|
+
node "$RUNTIME_PATH/src/commands/qualification-receipt.mjs" produce >> "$GITHUB_OUTPUT"
|
|
189
|
+
else
|
|
190
|
+
echo 'reuse=false' >> "$GITHUB_OUTPUT"
|
|
191
|
+
echo 'reason=receipt setup unavailable' >> "$GITHUB_OUTPUT"
|
|
192
|
+
fi
|
|
193
|
+
- name: Upload qualification receipt
|
|
194
|
+
if: steps.produce.outputs.reuse == 'true'
|
|
195
|
+
continue-on-error: true
|
|
196
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
197
|
+
with:
|
|
198
|
+
name: code-foundry-qualification-v2-${{ github.run_id }}-${{ github.run_attempt }}
|
|
199
|
+
path: qualification-receipt.json
|
|
200
|
+
if-no-files-found: error
|
|
201
|
+
retention-days: 7
|
|
@@ -16,9 +16,6 @@ on:
|
|
|
16
16
|
required: false
|
|
17
17
|
type: boolean
|
|
18
18
|
default: false
|
|
19
|
-
secrets:
|
|
20
|
-
NPM_TOKEN:
|
|
21
|
-
required: false
|
|
22
19
|
|
|
23
20
|
permissions:
|
|
24
21
|
attestations: read
|
|
@@ -88,7 +85,6 @@ jobs:
|
|
|
88
85
|
contents: read
|
|
89
86
|
id-token: write
|
|
90
87
|
env:
|
|
91
|
-
HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }}
|
|
92
88
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
93
89
|
RELEASE_ASSET: ${{ inputs.asset }}
|
|
94
90
|
steps:
|
|
@@ -98,16 +94,9 @@ jobs:
|
|
|
98
94
|
persist-credentials: false
|
|
99
95
|
ref: ${{ github.sha }}
|
|
100
96
|
- name: Setup Node for trusted publishing
|
|
101
|
-
if: env.HAS_NPM_TOKEN != 'true'
|
|
102
|
-
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
103
|
-
with:
|
|
104
|
-
node-version: '24'
|
|
105
|
-
- name: Setup Node for token publishing
|
|
106
|
-
if: env.HAS_NPM_TOKEN == 'true'
|
|
107
97
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
108
98
|
with:
|
|
109
99
|
node-version: '24'
|
|
110
|
-
registry-url: https://registry.npmjs.org
|
|
111
100
|
- name: Retrieve only this run attempt's evidence
|
|
112
101
|
env:
|
|
113
102
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -124,7 +113,6 @@ jobs:
|
|
|
124
113
|
- name: Reverify and publish the tested tarball without lifecycle hooks
|
|
125
114
|
env:
|
|
126
115
|
GH_TOKEN: ${{ github.token }}
|
|
127
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
128
116
|
run: |
|
|
129
117
|
set -euo pipefail
|
|
130
118
|
node src/commands/qualified-publication.mjs publish \
|
|
@@ -43,6 +43,11 @@ on:
|
|
|
43
43
|
required: false
|
|
44
44
|
type: boolean
|
|
45
45
|
default: false
|
|
46
|
+
npm-publish-auth:
|
|
47
|
+
description: Select automatic token compatibility or force trusted OIDC publication.
|
|
48
|
+
required: false
|
|
49
|
+
type: string
|
|
50
|
+
default: auto
|
|
46
51
|
outputs:
|
|
47
52
|
release_created:
|
|
48
53
|
description: Whether Release Please created a release (possibly a draft).
|
|
@@ -63,6 +68,8 @@ on:
|
|
|
63
68
|
required: false
|
|
64
69
|
NPM_TOKEN:
|
|
65
70
|
required: false
|
|
71
|
+
NPM_READ_TOKEN:
|
|
72
|
+
required: false
|
|
66
73
|
|
|
67
74
|
permissions:
|
|
68
75
|
actions: write
|
|
@@ -271,11 +278,12 @@ jobs:
|
|
|
271
278
|
// Writes intermittently hit GitHub's secondary rate limiting from
|
|
272
279
|
// busy automation accounts (403 with the generic not-accessible
|
|
273
280
|
// message); back off and retry before giving up.
|
|
274
|
-
const gh = (args) => {
|
|
281
|
+
const gh = (args, options = {}) => {
|
|
275
282
|
for (const wait of [0, 10, 30]) {
|
|
276
283
|
try {
|
|
277
284
|
if (wait) sleep(wait);
|
|
278
285
|
return execFileSync('gh', args, {
|
|
286
|
+
...options,
|
|
279
287
|
encoding: 'utf8',
|
|
280
288
|
maxBuffer: 32 * 1024 * 1024,
|
|
281
289
|
});
|
|
@@ -316,6 +324,18 @@ jobs:
|
|
|
316
324
|
const latestTag = versionedTags[0] ?? '';
|
|
317
325
|
const prefix = latestTag.replace(/v?\d+\.\d+\.\d+.*$/, '') || 'v';
|
|
318
326
|
|
|
327
|
+
// Include drafts and every page before deciding an object is absent.
|
|
328
|
+
const releasePages = JSON.parse(gh([
|
|
329
|
+
'api', `repos/${repo}/releases`, '-X', 'GET', '-f', 'per_page=100',
|
|
330
|
+
'--paginate', '--slurp',
|
|
331
|
+
]));
|
|
332
|
+
if (!Array.isArray(releasePages) || releasePages.some((page) =>
|
|
333
|
+
!Array.isArray(page) || page.some((release) =>
|
|
334
|
+
!release || typeof release.tag_name !== 'string' || !release.tag_name))) {
|
|
335
|
+
throw new Error('Invalid existing release listing; refusing release creation');
|
|
336
|
+
}
|
|
337
|
+
const existingReleaseTags = new Set(releasePages.flat().map((release) => release.tag_name));
|
|
338
|
+
|
|
319
339
|
let ensured = 0;
|
|
320
340
|
let failures = 0;
|
|
321
341
|
let lastTag = '';
|
|
@@ -324,12 +344,7 @@ jobs:
|
|
|
324
344
|
const version = /^chore\(main\): release (\S+)/.exec(pull.title)?.[1];
|
|
325
345
|
if (!version) continue;
|
|
326
346
|
const tag = `${prefix}v${version}`;
|
|
327
|
-
|
|
328
|
-
// the list endpoint (which returns drafts) for the tag.
|
|
329
|
-
const releasesForCheck = JSON.parse(gh([
|
|
330
|
-
'api', `repos/${repo}/releases`, '-X', 'GET', '-f', 'per_page=100',
|
|
331
|
-
]));
|
|
332
|
-
if (releasesForCheck.some((release) => release.tag_name === tag)) {
|
|
347
|
+
if (existingReleaseTags.has(tag)) {
|
|
333
348
|
console.log(`${tag}: release object already exists`);
|
|
334
349
|
} else {
|
|
335
350
|
// No release object for this tag: create it. The tag may also
|
|
@@ -354,6 +369,7 @@ jobs:
|
|
|
354
369
|
prerelease: version.includes('-'),
|
|
355
370
|
});
|
|
356
371
|
gh(['api', `repos/${repo}/releases`, '-X', 'POST', '--input', '-'], { input: body });
|
|
372
|
+
existingReleaseTags.add(tag);
|
|
357
373
|
console.log(`${tag}: release object created`);
|
|
358
374
|
} catch (error) {
|
|
359
375
|
failures += 1;
|
|
@@ -834,24 +850,30 @@ jobs:
|
|
|
834
850
|
contents: read
|
|
835
851
|
id-token: write
|
|
836
852
|
env:
|
|
837
|
-
|
|
853
|
+
NPM_PUBLISH_AUTH: ${{ inputs['npm-publish-auth'] }}
|
|
854
|
+
HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }}
|
|
855
|
+
HAS_NPM_READ_TOKEN: ${{ secrets.NPM_READ_TOKEN != '' }}
|
|
838
856
|
steps:
|
|
839
857
|
- name: Checkout release
|
|
840
858
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
841
859
|
with:
|
|
842
860
|
persist-credentials: false
|
|
843
861
|
ref: ${{ needs.release.outputs.tag_name }}
|
|
844
|
-
- name:
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
862
|
+
- name: Validate npm publication authentication
|
|
863
|
+
run: |
|
|
864
|
+
set -euo pipefail
|
|
865
|
+
case "$NPM_PUBLISH_AUTH" in
|
|
866
|
+
auto|oidc|token) ;;
|
|
867
|
+
*) echo "Unsupported npm-publish-auth: $NPM_PUBLISH_AUTH" >&2; exit 1 ;;
|
|
868
|
+
esac
|
|
869
|
+
if [ "$NPM_PUBLISH_AUTH" = token ] && [ "$HAS_NPM_TOKEN" != true ]; then
|
|
870
|
+
echo 'npm-publish-auth token requires the NPM_TOKEN secret.' >&2
|
|
871
|
+
exit 1
|
|
872
|
+
fi
|
|
873
|
+
- name: Setup Node
|
|
851
874
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
852
875
|
with:
|
|
853
876
|
node-version: 24
|
|
854
|
-
registry-url: https://registry.npmjs.org
|
|
855
877
|
- name: Setup Bun (lockfile managers)
|
|
856
878
|
# The install step below dispatches on lockfile: bun, pnpm, yarn, or
|
|
857
879
|
# npm. Stock runners only provide Node, so provision Bun (oven-sh)
|
|
@@ -865,8 +887,18 @@ jobs:
|
|
|
865
887
|
- name: Install dependencies
|
|
866
888
|
# npm publish runs prepublishOnly (usually a build), which needs
|
|
867
889
|
# devDependencies present. Install with the project's own manager.
|
|
890
|
+
env:
|
|
891
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_READ_TOKEN || (inputs['npm-publish-auth'] != 'oidc' && secrets.NPM_TOKEN) || '' }}
|
|
868
892
|
run: |
|
|
869
893
|
set -euo pipefail
|
|
894
|
+
npmrc=''
|
|
895
|
+
if [ -n "${NODE_AUTH_TOKEN:-}" ]; then
|
|
896
|
+
npmrc="$(mktemp)"
|
|
897
|
+
chmod 600 "$npmrc"
|
|
898
|
+
printf '//registry.npmjs.org/:_authToken=%s\n' "$NODE_AUTH_TOKEN" > "$npmrc"
|
|
899
|
+
export NPM_CONFIG_USERCONFIG="$npmrc"
|
|
900
|
+
fi
|
|
901
|
+
trap '[ -z "$npmrc" ] || rm -f "$npmrc"' EXIT
|
|
870
902
|
if [ -f bun.lock ] || [ -f bun.lockb ]; then bun install --frozen-lockfile
|
|
871
903
|
elif [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile
|
|
872
904
|
elif [ -f yarn.lock ]; then yarn install --immutable
|
|
@@ -874,10 +906,16 @@ jobs:
|
|
|
874
906
|
elif [ -f package.json ]; then npm install --no-audit --fund=false --package-lock=false
|
|
875
907
|
fi
|
|
876
908
|
- name: Publish npm with token
|
|
877
|
-
if: env.
|
|
909
|
+
if: inputs['npm-publish-auth'] == 'token' || (inputs['npm-publish-auth'] == 'auto' && env.HAS_NPM_TOKEN == 'true')
|
|
878
910
|
env:
|
|
879
|
-
NODE_AUTH_TOKEN: ${{
|
|
880
|
-
run:
|
|
911
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
912
|
+
run: |
|
|
913
|
+
set -euo pipefail
|
|
914
|
+
npmrc="$(mktemp)"
|
|
915
|
+
chmod 600 "$npmrc"
|
|
916
|
+
printf '//registry.npmjs.org/:_authToken=%s\n' "$NODE_AUTH_TOKEN" > "$npmrc"
|
|
917
|
+
trap 'rm -f "$npmrc"' EXIT
|
|
918
|
+
NPM_CONFIG_USERCONFIG="$npmrc" npm publish --provenance --access public
|
|
881
919
|
- name: Publish npm with trusted publishing
|
|
882
|
-
if: env.
|
|
920
|
+
if: inputs['npm-publish-auth'] == 'oidc' || (inputs['npm-publish-auth'] == 'auto' && env.HAS_NPM_TOKEN != 'true')
|
|
883
921
|
run: npm publish --provenance --access public
|
|
@@ -256,5 +256,3 @@ jobs:
|
|
|
256
256
|
tag: ${{ needs.release.outputs.tag_name || needs.recovery.outputs.tag }}
|
|
257
257
|
asset: ${{ needs.qualification.outputs.filename }}
|
|
258
258
|
billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
|
|
259
|
-
secrets:
|
|
260
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -140,7 +140,9 @@ jobs:
|
|
|
140
140
|
dependency-audit:
|
|
141
141
|
name: Dependency Audit (${{ matrix.entry.display }})
|
|
142
142
|
needs: profile
|
|
143
|
-
|
|
143
|
+
# Avoid expanding an empty matrix; skip only the explicitly empty result.
|
|
144
|
+
# Missing or malformed output still fails closed during matrix expansion.
|
|
145
|
+
if: vars.CI_BILLING_PAUSED != 'true' && needs.profile.result == 'success' && needs.profile.outputs.audit_matrix != '[]'
|
|
144
146
|
strategy:
|
|
145
147
|
fail-fast: false
|
|
146
148
|
max-parallel: 8
|