code-foundry 1.53.6 → 1.53.7
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/workflows/codeql.yml +18 -4
- package/.github/workflows/security.yml +29 -3
- package/.github/workflows/validation-billing.yml +27 -5
- package/.github/workflows/validation-no-codeql.yml +30 -4
- package/.github/workflows/validation.yml +39 -6
- package/.github/workflows/validation_self-ci.yml +97 -3
- package/CHANGELOG.md +8 -8
- package/docs/CONFIGURATION.md +1 -1
- package/docs/WORKFLOWS.md +19 -4
- package/package.json +1 -1
- package/src/commands/sync.mjs +30 -1
- package/src/runtime-core.mjs +146 -25
|
@@ -35,6 +35,15 @@ on:
|
|
|
35
35
|
default: 1
|
|
36
36
|
# Retained for caller compatibility. Rust shards share the single
|
|
37
37
|
# analysis matrix, so no separate throttle applies.
|
|
38
|
+
plan:
|
|
39
|
+
description: >-
|
|
40
|
+
JSON object of the Detect job's outputs, computed by the caller's
|
|
41
|
+
planning job (`validation plan`). When set, Detect is skipped and
|
|
42
|
+
analysis reads the plan instead, saving one runner hop. Empty runs
|
|
43
|
+
Detect here as before.
|
|
44
|
+
required: false
|
|
45
|
+
type: string
|
|
46
|
+
default: ''
|
|
38
47
|
permissions:
|
|
39
48
|
actions: read
|
|
40
49
|
contents: read
|
|
@@ -60,7 +69,7 @@ concurrency:
|
|
|
60
69
|
jobs:
|
|
61
70
|
detect:
|
|
62
71
|
name: Detect
|
|
63
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
72
|
+
if: vars.CI_BILLING_PAUSED != 'true' && inputs.plan == ''
|
|
64
73
|
runs-on: ${{ inputs.runner }}
|
|
65
74
|
timeout-minutes: 10
|
|
66
75
|
outputs:
|
|
@@ -238,11 +247,16 @@ jobs:
|
|
|
238
247
|
analyze:
|
|
239
248
|
name: Analyze (${{ matrix.entry.display }})
|
|
240
249
|
needs: detect
|
|
241
|
-
|
|
250
|
+
# A planned run skips Detect and reads the same outputs from the plan;
|
|
251
|
+
# an unplanned run still requires Detect to have succeeded.
|
|
252
|
+
if: >-
|
|
253
|
+
!cancelled() && vars.CI_BILLING_PAUSED != 'true' &&
|
|
254
|
+
((inputs.plan == '' && needs.detect.result == 'success' && needs.detect.outputs.enabled == 'true') ||
|
|
255
|
+
(inputs.plan != '' && needs.detect.result == 'skipped' && fromJSON(inputs.plan || '{}').enabled == 'true'))
|
|
242
256
|
strategy:
|
|
243
257
|
fail-fast: false
|
|
244
258
|
matrix:
|
|
245
|
-
entry: ${{ fromJson(needs.detect.outputs.matrix || '[]') }}
|
|
259
|
+
entry: ${{ fromJson((inputs.plan != '' && fromJSON(inputs.plan || '{}').matrix) || needs.detect.outputs.matrix || '[]') }}
|
|
246
260
|
runs-on: ${{ inputs.runner }}
|
|
247
261
|
timeout-minutes: 30
|
|
248
262
|
steps:
|
|
@@ -336,7 +350,7 @@ jobs:
|
|
|
336
350
|
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
|
|
337
351
|
with:
|
|
338
352
|
languages: rust
|
|
339
|
-
build-mode: ${{ needs.detect.outputs.rust_build_mode }}
|
|
353
|
+
build-mode: ${{ (inputs.plan != '' && fromJSON(inputs.plan || '{}').rust_build_mode) || needs.detect.outputs.rust_build_mode }}
|
|
340
354
|
config-file: ${{ steps.scope.outputs.config_file }}
|
|
341
355
|
threads: ${{ inputs.rust-threads }}
|
|
342
356
|
- name: Analyze Rust
|
|
@@ -18,6 +18,16 @@ on:
|
|
|
18
18
|
required: false
|
|
19
19
|
type: string
|
|
20
20
|
default: ubuntu-slim
|
|
21
|
+
plan:
|
|
22
|
+
description: >-
|
|
23
|
+
JSON object of the Profile job's outputs, computed by the caller's
|
|
24
|
+
planning job (`validation plan`). When set, Profile is skipped,
|
|
25
|
+
audits read the plan instead, and Dependency Review runs as its own
|
|
26
|
+
job beside them, saving one runner hop. Empty runs Profile here as
|
|
27
|
+
before.
|
|
28
|
+
required: false
|
|
29
|
+
type: string
|
|
30
|
+
default: ''
|
|
21
31
|
permissions:
|
|
22
32
|
contents: read
|
|
23
33
|
|
|
@@ -39,7 +49,7 @@ concurrency:
|
|
|
39
49
|
jobs:
|
|
40
50
|
profile:
|
|
41
51
|
name: Profile
|
|
42
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
52
|
+
if: vars.CI_BILLING_PAUSED != 'true' && inputs.plan == ''
|
|
43
53
|
runs-on: ${{ inputs.runner }}
|
|
44
54
|
timeout-minutes: 10
|
|
45
55
|
outputs:
|
|
@@ -142,12 +152,16 @@ jobs:
|
|
|
142
152
|
needs: profile
|
|
143
153
|
# Avoid expanding an empty matrix; skip only the explicitly empty result.
|
|
144
154
|
# Missing or malformed output still fails closed during matrix expansion.
|
|
145
|
-
|
|
155
|
+
# A planned run skips Profile and reads the same matrix from the plan.
|
|
156
|
+
if: >-
|
|
157
|
+
!cancelled() && vars.CI_BILLING_PAUSED != 'true' &&
|
|
158
|
+
((inputs.plan == '' && needs.profile.result == 'success' && needs.profile.outputs.audit_matrix != '[]') ||
|
|
159
|
+
(inputs.plan != '' && needs.profile.result == 'skipped' && fromJSON(inputs.plan || '{}').audit_matrix != '[]'))
|
|
146
160
|
strategy:
|
|
147
161
|
fail-fast: false
|
|
148
162
|
max-parallel: 8
|
|
149
163
|
matrix:
|
|
150
|
-
entry: ${{ fromJson(needs.profile.outputs.audit_matrix || '[]') }}
|
|
164
|
+
entry: ${{ fromJson((inputs.plan != '' && fromJSON(inputs.plan || '{}').audit_matrix) || needs.profile.outputs.audit_matrix || '[]') }}
|
|
151
165
|
runs-on: ${{ inputs.runner }}
|
|
152
166
|
timeout-minutes: 20
|
|
153
167
|
env:
|
|
@@ -386,3 +400,15 @@ jobs:
|
|
|
386
400
|
- name: Audit Python dependencies
|
|
387
401
|
if: matrix.entry.language == 'python'
|
|
388
402
|
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" security audit python
|
|
403
|
+
# Planned runs only: with no Profile job, the first audit job runs
|
|
404
|
+
# Dependency Review, whatever the audit's own outcome, so the review
|
|
405
|
+
# costs no runner hop. The planner leaves a run with a review but no
|
|
406
|
+
# audit unplanned, so Profile still runs it there.
|
|
407
|
+
- name: Dependency Review
|
|
408
|
+
if: >-
|
|
409
|
+
!cancelled() && inputs.plan != '' && strategy.job-index == 0 &&
|
|
410
|
+
github.event_name == 'pull_request' && fromJSON(inputs.plan || '{}').dependency_review == 'true'
|
|
411
|
+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5
|
|
412
|
+
with:
|
|
413
|
+
fail-on-severity: high
|
|
414
|
+
license-check: true
|
|
@@ -124,6 +124,22 @@ on:
|
|
|
124
124
|
required: false
|
|
125
125
|
type: string
|
|
126
126
|
default: ''
|
|
127
|
+
security-plan:
|
|
128
|
+
description: >-
|
|
129
|
+
Accepted so every orchestrator takes the same caller inputs. The
|
|
130
|
+
merged audit job profiles in place, so the plan is unused.
|
|
131
|
+
required: false
|
|
132
|
+
type: string
|
|
133
|
+
default: ''
|
|
134
|
+
codeql-plan:
|
|
135
|
+
description: >-
|
|
136
|
+
JSON CodeQL Detect outputs from the caller's planning job. When set,
|
|
137
|
+
the CodeQL workflow skips its Detect job and reads the plan, and a
|
|
138
|
+
plan marked idle (CodeQL unavailable) skips the workflow. Empty
|
|
139
|
+
detects inside the workflow as before.
|
|
140
|
+
required: false
|
|
141
|
+
type: string
|
|
142
|
+
default: ''
|
|
127
143
|
secrets:
|
|
128
144
|
TURBO_TOKEN:
|
|
129
145
|
required: false
|
|
@@ -1216,7 +1232,9 @@ jobs:
|
|
|
1216
1232
|
name: CodeQL
|
|
1217
1233
|
# Release-please pull requests also analyze with CodeQL so repository
|
|
1218
1234
|
# rulesets requiring code scanning results never deadlock the release.
|
|
1219
|
-
if:
|
|
1235
|
+
if: >-
|
|
1236
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.codeql == true && (inputs.mode == 'audit' || inputs.mode == 'release') &&
|
|
1237
|
+
fromJSON(inputs.codeql-plan || '{}').idle != 'true'
|
|
1220
1238
|
permissions:
|
|
1221
1239
|
actions: read
|
|
1222
1240
|
contents: read
|
|
@@ -1230,6 +1248,7 @@ jobs:
|
|
|
1230
1248
|
rust-shards: ${{ inputs.rust-shards }}
|
|
1231
1249
|
rust-threads: ${{ inputs.rust-threads }}
|
|
1232
1250
|
rust-max-parallel: ${{ inputs.rust-max-parallel }}
|
|
1251
|
+
plan: ${{ inputs.codeql-plan }}
|
|
1233
1252
|
|
|
1234
1253
|
# Aggregate check: "Validation / Gate" (caller job name + this job name).
|
|
1235
1254
|
# Always evaluates; only jobs required for the mode must have succeeded.
|
|
@@ -1256,8 +1275,10 @@ jobs:
|
|
|
1256
1275
|
FOUNDRY_SECURITY: ${{ needs.audit.result }}
|
|
1257
1276
|
# This orchestrator runs CodeQL only when the consumer's canonical
|
|
1258
1277
|
# configuration enables it; otherwise the unavailability decision is
|
|
1259
|
-
# treated as satisfied without registering a skipped PR check.
|
|
1260
|
-
|
|
1278
|
+
# treated as satisfied without registering a skipped PR check. A CodeQL
|
|
1279
|
+
# workflow the caller planned as idle (unavailable) reports skipped;
|
|
1280
|
+
# only that planned skip counts as success.
|
|
1281
|
+
FOUNDRY_CODEQL: ${{ inputs.codeql == true && ((needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true' && 'success') || needs.codeql.result) || 'success' }}
|
|
1261
1282
|
# Eval runs inside the merged fast-lanes job on the audit tier; a
|
|
1262
1283
|
# fast-lanes failure on that tier marks the eval category failed even if
|
|
1263
1284
|
# another merged lane caused it, which fails closed.
|
|
@@ -1306,7 +1327,8 @@ jobs:
|
|
|
1306
1327
|
needs.fast-lanes.result == 'success' && needs.lint.result == 'success' &&
|
|
1307
1328
|
needs.type-check.result == 'success' && needs.unit.result == 'success' &&
|
|
1308
1329
|
needs.e2e.result == 'success' && needs.audit.result == 'success' &&
|
|
1309
|
-
(inputs.codeql != true || needs.codeql.result == 'success'
|
|
1330
|
+
(inputs.codeql != true || needs.codeql.result == 'success' ||
|
|
1331
|
+
(needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true')) &&
|
|
1310
1332
|
needs.gate.result == 'success'
|
|
1311
1333
|
permissions:
|
|
1312
1334
|
actions: read
|
|
@@ -1321,5 +1343,5 @@ jobs:
|
|
|
1321
1343
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
1322
1344
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
1323
1345
|
job-results-json: >-
|
|
1324
|
-
{"ci":"${{ needs.lint.result == 'success' && needs.type-check.result == 'success' && needs.fast-lanes.result == 'success' && 'success' || 'failure' }}","test":"${{ needs.unit.result == 'success' && needs.fast-lanes.result == 'success' && needs.e2e.result == 'success' && 'success' || 'failure' }}","security":"${{ needs.audit.result }}","codeql":"${{ inputs.codeql == true && needs.codeql.result || 'not-configured' }}","eval":"${{ needs.fast-lanes.result }}","gate":"${{ needs.gate.result }}"}
|
|
1346
|
+
{"ci":"${{ needs.lint.result == 'success' && needs.type-check.result == 'success' && needs.fast-lanes.result == 'success' && 'success' || 'failure' }}","test":"${{ needs.unit.result == 'success' && needs.fast-lanes.result == 'success' && needs.e2e.result == 'success' && 'success' || 'failure' }}","security":"${{ needs.audit.result }}","codeql":"${{ inputs.codeql == true && ((needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true' && 'success') || needs.codeql.result) || 'not-configured' }}","eval":"${{ needs.fast-lanes.result }}","gate":"${{ needs.gate.result }}"}
|
|
1325
1347
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -84,6 +84,23 @@ on:
|
|
|
84
84
|
required: false
|
|
85
85
|
type: string
|
|
86
86
|
default: ''
|
|
87
|
+
security-plan:
|
|
88
|
+
description: >-
|
|
89
|
+
JSON Security Profile outputs from the caller's planning job. When
|
|
90
|
+
set, the Security workflow skips its Profile job and reads the plan,
|
|
91
|
+
and a plan marked idle (no dependency audit and no dependency
|
|
92
|
+
review to run) skips the workflow. Empty profiles inside the
|
|
93
|
+
workflow as before.
|
|
94
|
+
required: false
|
|
95
|
+
type: string
|
|
96
|
+
default: ''
|
|
97
|
+
codeql-plan:
|
|
98
|
+
description: >-
|
|
99
|
+
Accepted so every orchestrator takes the same caller inputs. This
|
|
100
|
+
variant runs no CodeQL, so the plan is unused.
|
|
101
|
+
required: false
|
|
102
|
+
type: string
|
|
103
|
+
default: ''
|
|
87
104
|
artifact-prefix:
|
|
88
105
|
description: Prefix for task receipts; use a unique value when calling this workflow more than once.
|
|
89
106
|
required: false
|
|
@@ -196,7 +213,11 @@ jobs:
|
|
|
196
213
|
|
|
197
214
|
security:
|
|
198
215
|
name: Security
|
|
199
|
-
|
|
216
|
+
# A plan marked idle has no audit or review to run; the gate counts that
|
|
217
|
+
# planned skip as the success the empty workflow used to report.
|
|
218
|
+
if: >-
|
|
219
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit' &&
|
|
220
|
+
fromJSON(inputs.security-plan || '{}').idle != 'true'
|
|
200
221
|
permissions:
|
|
201
222
|
contents: read
|
|
202
223
|
uses: ./.github/workflows/security.yml
|
|
@@ -204,6 +225,7 @@ jobs:
|
|
|
204
225
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
205
226
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
206
227
|
runner: ${{ inputs.security-runner }}
|
|
228
|
+
plan: ${{ inputs.security-plan }}
|
|
207
229
|
|
|
208
230
|
eval:
|
|
209
231
|
name: Eval
|
|
@@ -239,7 +261,10 @@ jobs:
|
|
|
239
261
|
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
240
262
|
FOUNDRY_CI: ${{ needs.ci.result }}
|
|
241
263
|
FOUNDRY_TEST: ${{ needs.test.result }}
|
|
242
|
-
|
|
264
|
+
# A Security or CodeQL workflow the caller planned as idle never
|
|
265
|
+
# starts and reports skipped. Count only that planned skip as the
|
|
266
|
+
# success its empty run reported before.
|
|
267
|
+
FOUNDRY_SECURITY: ${{ (needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true' && 'success') || needs.security.result }}
|
|
243
268
|
# An eval lane the caller planned as idle (no eval script, no
|
|
244
269
|
# eval_command, or eval: false) never starts, so the whole Eval
|
|
245
270
|
# workflow reports skipped. Count that exactly like the no-op success
|
|
@@ -291,7 +316,8 @@ jobs:
|
|
|
291
316
|
inputs.release-qualification-receipts == true &&
|
|
292
317
|
github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
|
|
293
318
|
needs.ci.result == 'success' && needs.test.result == 'success' &&
|
|
294
|
-
needs.security.result == 'success'
|
|
319
|
+
(needs.security.result == 'success' ||
|
|
320
|
+
(needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true')) &&
|
|
295
321
|
(needs.eval.result == 'success' ||
|
|
296
322
|
(needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
|
|
297
323
|
needs.gate.result == 'success'
|
|
@@ -308,5 +334,5 @@ jobs:
|
|
|
308
334
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
309
335
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
310
336
|
job-results-json: >-
|
|
311
|
-
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ needs.security.result }}","codeql":"not-configured","eval":"${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}","gate":"${{ needs.gate.result }}"}
|
|
337
|
+
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ (needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true' && 'success') || needs.security.result }}","codeql":"not-configured","eval":"${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}","gate":"${{ needs.gate.result }}"}
|
|
312
338
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -99,6 +99,25 @@ on:
|
|
|
99
99
|
required: false
|
|
100
100
|
type: string
|
|
101
101
|
default: ''
|
|
102
|
+
security-plan:
|
|
103
|
+
description: >-
|
|
104
|
+
JSON Security Profile outputs from the caller's planning job. When
|
|
105
|
+
set, the Security workflow skips its Profile job and reads the plan,
|
|
106
|
+
and a plan marked idle (no dependency audit and no dependency
|
|
107
|
+
review to run) skips the workflow. Empty profiles inside the
|
|
108
|
+
workflow as before.
|
|
109
|
+
required: false
|
|
110
|
+
type: string
|
|
111
|
+
default: ''
|
|
112
|
+
codeql-plan:
|
|
113
|
+
description: >-
|
|
114
|
+
JSON CodeQL Detect outputs from the caller's planning job. When set,
|
|
115
|
+
the CodeQL workflow skips its Detect job and reads the plan, and a
|
|
116
|
+
plan marked idle (CodeQL unavailable) skips the workflow. Empty
|
|
117
|
+
detects inside the workflow as before.
|
|
118
|
+
required: false
|
|
119
|
+
type: string
|
|
120
|
+
default: ''
|
|
102
121
|
artifact-prefix:
|
|
103
122
|
description: Prefix for task receipts; use a unique value when calling this workflow more than once.
|
|
104
123
|
required: false
|
|
@@ -196,7 +215,11 @@ jobs:
|
|
|
196
215
|
|
|
197
216
|
security:
|
|
198
217
|
name: Security
|
|
199
|
-
|
|
218
|
+
# A plan marked idle has no audit or review to run; the gate counts that
|
|
219
|
+
# planned skip as the success the empty workflow used to report.
|
|
220
|
+
if: >-
|
|
221
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit' &&
|
|
222
|
+
fromJSON(inputs.security-plan || '{}').idle != 'true'
|
|
200
223
|
permissions:
|
|
201
224
|
contents: read
|
|
202
225
|
uses: ./.github/workflows/security.yml
|
|
@@ -204,12 +227,15 @@ jobs:
|
|
|
204
227
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
205
228
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
206
229
|
runner: ${{ inputs.security-runner }}
|
|
230
|
+
plan: ${{ inputs.security-plan }}
|
|
207
231
|
|
|
208
232
|
codeql:
|
|
209
233
|
name: CodeQL
|
|
210
234
|
# Release-please pull requests also analyze with CodeQL so repository
|
|
211
235
|
# rulesets requiring code scanning results never deadlock the release.
|
|
212
|
-
if:
|
|
236
|
+
if: >-
|
|
237
|
+
vars.CI_BILLING_PAUSED != 'true' && (inputs.mode == 'audit' || inputs.mode == 'release') &&
|
|
238
|
+
fromJSON(inputs.codeql-plan || '{}').idle != 'true'
|
|
213
239
|
permissions:
|
|
214
240
|
actions: read
|
|
215
241
|
contents: read
|
|
@@ -223,6 +249,7 @@ jobs:
|
|
|
223
249
|
rust-shards: ${{ inputs.rust-shards }}
|
|
224
250
|
rust-threads: ${{ inputs.rust-threads }}
|
|
225
251
|
rust-max-parallel: ${{ inputs.rust-max-parallel }}
|
|
252
|
+
plan: ${{ inputs.codeql-plan }}
|
|
226
253
|
|
|
227
254
|
eval:
|
|
228
255
|
name: Eval
|
|
@@ -258,8 +285,11 @@ jobs:
|
|
|
258
285
|
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
259
286
|
FOUNDRY_CI: ${{ needs.ci.result }}
|
|
260
287
|
FOUNDRY_TEST: ${{ needs.test.result }}
|
|
261
|
-
|
|
262
|
-
|
|
288
|
+
# A Security or CodeQL workflow the caller planned as idle never
|
|
289
|
+
# starts and reports skipped. Count only that planned skip as the
|
|
290
|
+
# success its empty run reported before.
|
|
291
|
+
FOUNDRY_SECURITY: ${{ (needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true' && 'success') || needs.security.result }}
|
|
292
|
+
FOUNDRY_CODEQL: ${{ (needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true' && 'success') || needs.codeql.result }}
|
|
263
293
|
# An eval lane the caller planned as idle (no eval script, no
|
|
264
294
|
# eval_command, or eval: false) never starts, so the whole Eval
|
|
265
295
|
# workflow reports skipped. Count that exactly like the no-op success
|
|
@@ -307,7 +337,10 @@ jobs:
|
|
|
307
337
|
inputs.release-qualification-receipts == true &&
|
|
308
338
|
github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
|
|
309
339
|
needs.ci.result == 'success' && needs.test.result == 'success' &&
|
|
310
|
-
needs.security.result == 'success'
|
|
340
|
+
(needs.security.result == 'success' ||
|
|
341
|
+
(needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true')) &&
|
|
342
|
+
(needs.codeql.result == 'success' ||
|
|
343
|
+
(needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true')) &&
|
|
311
344
|
(needs.eval.result == 'success' ||
|
|
312
345
|
(needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
|
|
313
346
|
needs.gate.result == 'success'
|
|
@@ -324,5 +357,5 @@ jobs:
|
|
|
324
357
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
325
358
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
326
359
|
job-results-json: >-
|
|
327
|
-
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ needs.security.result }}","codeql":"${{ needs.codeql.result }}","eval":"${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}","gate":"${{ needs.gate.result }}"}
|
|
360
|
+
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ (needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true' && 'success') || needs.security.result }}","codeql":"${{ (needs.codeql.result == 'skipped' && fromJSON(inputs.codeql-plan || '{}').idle == 'true' && 'success') || needs.codeql.result }}","eval":"${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}","gate":"${{ needs.gate.result }}"}
|
|
328
361
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -22,19 +22,34 @@ concurrency:
|
|
|
22
22
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
23
23
|
|
|
24
24
|
jobs:
|
|
25
|
+
# One planning job decides everything the orchestrator needs before any
|
|
26
|
+
# lane starts: the validation tier (with the docs-only downgrade), the idle
|
|
27
|
+
# lanes, the Security Profile, and the CodeQL language detection. Each used
|
|
28
|
+
# to queue its own runner in sequence ahead of the real work; on a saturated
|
|
29
|
+
# runner pool every extra hop costs minutes of queueing for seconds of work.
|
|
25
30
|
mode:
|
|
26
|
-
name:
|
|
27
|
-
# The
|
|
28
|
-
#
|
|
31
|
+
name: Plan
|
|
32
|
+
# The planner is only needed for ready pull requests. Main pushes use the
|
|
33
|
+
# dedicated default-branch CodeQL lane below; draft updates do not
|
|
29
34
|
# allocate a validation runner.
|
|
30
35
|
if: vars.CI_BILLING_PAUSED != 'true' && github.event_name == 'pull_request' && github.event.pull_request.draft == false
|
|
31
36
|
runs-on: ubuntu-slim
|
|
32
37
|
timeout-minutes: 10
|
|
38
|
+
# The scopes of the Mode, Security Profile, and CodeQL Detect jobs this
|
|
39
|
+
# job replaces: the CodeQL plan reads Code Security status, the pull
|
|
40
|
+
# request's changed files, and the repository rulesets.
|
|
33
41
|
permissions:
|
|
42
|
+
actions: read
|
|
34
43
|
contents: read
|
|
44
|
+
packages: read
|
|
45
|
+
security-events: write
|
|
35
46
|
outputs:
|
|
36
47
|
mode: ${{ steps.classify.outputs.mode }}
|
|
37
48
|
idle-lanes: ${{ steps.lanes.outputs.idle_lanes }}
|
|
49
|
+
# Empty unless the tier runs Security or CodeQL and planning succeeded;
|
|
50
|
+
# an empty plan makes each workflow profile and detect for itself.
|
|
51
|
+
security-plan: ${{ steps.plan.outputs.security_plan }}
|
|
52
|
+
codeql-plan: ${{ steps.plan.outputs.codeql_plan }}
|
|
38
53
|
steps:
|
|
39
54
|
# The runtime classifies policy from the consumer repository's config.
|
|
40
55
|
# Keep this checkout before the isolated runtime checkout below.
|
|
@@ -68,6 +83,83 @@ jobs:
|
|
|
68
83
|
- name: Plan lanes
|
|
69
84
|
id: lanes
|
|
70
85
|
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation lanes >> "$GITHUB_OUTPUT"
|
|
86
|
+
# The steps below replace the Security Profile and CodeQL Detect jobs
|
|
87
|
+
# on the tiers that run those workflows. They mirror those jobs' steps
|
|
88
|
+
# and fail open: if any of them fails, the plans stay empty and each
|
|
89
|
+
# workflow runs its own Profile or Detect job, which reports the error.
|
|
90
|
+
- name: Detect Code Security
|
|
91
|
+
id: security
|
|
92
|
+
if: steps.classify.outputs.mode == 'audit' || steps.classify.outputs.mode == 'release'
|
|
93
|
+
continue-on-error: true
|
|
94
|
+
env:
|
|
95
|
+
GH_TOKEN: ${{ github.token }}
|
|
96
|
+
REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}
|
|
97
|
+
run: |
|
|
98
|
+
status="disabled"
|
|
99
|
+
if [ "$REPOSITORY_VISIBILITY" = "public" ]; then
|
|
100
|
+
status="enabled"
|
|
101
|
+
else
|
|
102
|
+
status="$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.security_and_analysis.code_security.status // "disabled"' 2>/dev/null || printf 'disabled')"
|
|
103
|
+
fi
|
|
104
|
+
printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT"
|
|
105
|
+
- name: List pull request files
|
|
106
|
+
id: changed
|
|
107
|
+
if: steps.classify.outputs.mode == 'audit' || steps.classify.outputs.mode == 'release'
|
|
108
|
+
continue-on-error: true
|
|
109
|
+
env:
|
|
110
|
+
GH_TOKEN: ${{ github.token }}
|
|
111
|
+
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
|
|
112
|
+
run: |
|
|
113
|
+
set -euo pipefail
|
|
114
|
+
# Newline-separated changed paths for the runtime's change
|
|
115
|
+
# detection. A failed listing leaves the file absent and the
|
|
116
|
+
# runtime fails open toward analyzing every language and shard.
|
|
117
|
+
gh api "repos/$GITHUB_REPOSITORY/pulls/$PULL_REQUEST_NUMBER/files?per_page=100" \
|
|
118
|
+
--paginate --jq '.[].filename' > "$RUNNER_TEMP/changed-files.txt" || true
|
|
119
|
+
echo "file=$RUNNER_TEMP/changed-files.txt" >> "$GITHUB_OUTPUT"
|
|
120
|
+
- name: Detect code scanning merge gate
|
|
121
|
+
id: gate
|
|
122
|
+
if: steps.classify.outputs.mode == 'audit' || steps.classify.outputs.mode == 'release'
|
|
123
|
+
continue-on-error: true
|
|
124
|
+
env:
|
|
125
|
+
GH_TOKEN: ${{ github.token }}
|
|
126
|
+
run: |
|
|
127
|
+
set -euo pipefail
|
|
128
|
+
# The code-scanning merge gate waits for results in every tracked
|
|
129
|
+
# category, so change detection must never skip an upload while it
|
|
130
|
+
# is active. Any ruleset API failure also disables change
|
|
131
|
+
# detection: full analysis is the safe default.
|
|
132
|
+
gated=false
|
|
133
|
+
for id in $(gh api "repos/$GITHUB_REPOSITORY/rulesets" --paginate \
|
|
134
|
+
-q '.[] | select(.enforcement == "active") | .id' 2>/dev/null); do
|
|
135
|
+
if gh api "repos/$GITHUB_REPOSITORY/rulesets/$id" \
|
|
136
|
+
-q 'any(.rules[]?; .type == "code_scanning")' 2>/dev/null | grep -q true; then
|
|
137
|
+
gated=true
|
|
138
|
+
break
|
|
139
|
+
fi
|
|
140
|
+
done
|
|
141
|
+
echo "gated=$gated" >> "$GITHUB_OUTPUT"
|
|
142
|
+
- name: Plan security and CodeQL
|
|
143
|
+
id: plan
|
|
144
|
+
if: >-
|
|
145
|
+
(steps.classify.outputs.mode == 'audit' || steps.classify.outputs.mode == 'release') &&
|
|
146
|
+
steps.security.outcome == 'success' && steps.changed.outcome == 'success' &&
|
|
147
|
+
steps.gate.outcome == 'success'
|
|
148
|
+
continue-on-error: true
|
|
149
|
+
env:
|
|
150
|
+
REPO_FOUNDRY_PROFILE: ${{ vars.REPO_FOUNDRY_PROFILE }}
|
|
151
|
+
REPO_FOUNDRY_LANGUAGES: ${{ vars.REPO_FOUNDRY_LANGUAGES }}
|
|
152
|
+
REPO_FOUNDRY_FEATURES: ${{ vars.REPO_FOUNDRY_FEATURES }}
|
|
153
|
+
REPO_FOUNDRY_PACKAGE_MANAGER: ${{ vars.REPO_FOUNDRY_PACKAGE_MANAGER }}
|
|
154
|
+
REPO_FOUNDRY_PRIVATE: ${{ github.event.repository.private }}
|
|
155
|
+
REPO_FOUNDRY_VISIBILITY: ${{ github.event.repository.visibility }}
|
|
156
|
+
REPO_FOUNDRY_CODE_SECURITY: ${{ steps.security.outputs.status }}
|
|
157
|
+
FOUNDRY_EVENT_NAME: ${{ github.event_name }}
|
|
158
|
+
FOUNDRY_CODEQL_CHANGED_FILES_FILE: ${{ steps.changed.outputs.file }}
|
|
159
|
+
FOUNDRY_CODEQL_MERGE_GATE: ${{ steps.gate.outputs.gated }}
|
|
160
|
+
# The runtime computes both plans before writing either, so a failure
|
|
161
|
+
# never hands the orchestrator half a plan.
|
|
162
|
+
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation plan >> "$GITHUB_OUTPUT"
|
|
71
163
|
|
|
72
164
|
# The stable aggregate check name is the caller job name plus the
|
|
73
165
|
# orchestrator gate job name: "Validation / Gate". Keep both names fixed.
|
|
@@ -115,6 +207,8 @@ jobs:
|
|
|
115
207
|
e2e-shard-list: '1'
|
|
116
208
|
e2e-total-shards: '1'
|
|
117
209
|
idle-lanes: ${{ needs.mode.outputs.idle-lanes }}
|
|
210
|
+
security-plan: ${{ needs.mode.outputs.security-plan }}
|
|
211
|
+
codeql-plan: ${{ needs.mode.outputs.codeql-plan }}
|
|
118
212
|
secrets:
|
|
119
213
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
120
214
|
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.53.7](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.6...v1.53.7) (2026-10-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance
|
|
7
|
+
|
|
8
|
+
* **validation:** fold Security Profile and CodeQL Detect into the plan job ([#827](https://github.com/0xPlayerOne/code-foundry/issues/827)) ([75ca83c](https://github.com/0xPlayerOne/code-foundry/commit/75ca83c042f01e23a1f3c62b26f92e1bc6641a80))
|
|
9
|
+
|
|
3
10
|
## [1.53.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.5...v1.53.6) (2026-10-31)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -143,16 +150,9 @@
|
|
|
143
150
|
|
|
144
151
|
* handle empty security audit matrices ([#785](https://github.com/0xPlayerOne/code-foundry/issues/785)) ([02f0565](https://github.com/0xPlayerOne/code-foundry/commit/02f05652e68267a2aa2df5d5af8f13df9ba1e3aa))
|
|
145
152
|
|
|
146
|
-
## [1.49.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.9...v1.49.0) (2026-10-27)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
### Features
|
|
150
|
-
|
|
151
|
-
* support isolated integration test shards ([#783](https://github.com/0xPlayerOne/code-foundry/issues/783)) ([ad1123f](https://github.com/0xPlayerOne/code-foundry/commit/ad1123ff3bb9654bfbdaef4219490cca3e7e3d20))
|
|
152
|
-
|
|
153
153
|
<!-- code-foundry: packaged changelog -->
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
-
This packaged changelog lists the 20 most recent releases;
|
|
157
|
+
This packaged changelog lists the 20 most recent releases; 278 older releases are omitted to keep the package small.
|
|
158
158
|
See the [full changelog](https://github.com/0xPlayerOne/code-foundry/blob/main/CHANGELOG.md) for the complete history.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -409,7 +409,7 @@ duplicates, empty scopes, and more than eight shards are rejected. Use
|
|
|
409
409
|
|
|
410
410
|
Every lane rendered from this configuration (pull-request validation, the
|
|
411
411
|
default-branch scan, the scheduled audit, and merge queues) receives the same
|
|
412
|
-
shard list, and the CodeQL Detect job fails closed when caller workflows drift
|
|
412
|
+
shard list, and CodeQL detection (the caller's Plan job or the CodeQL Detect job) fails closed when caller workflows drift
|
|
413
413
|
apart. Keep them aligned: each shard uploads a SARIF category keyed by a hash
|
|
414
414
|
of the shard scope, GitHub code scanning baselines every category it has seen
|
|
415
415
|
on the default branch, and the code-scanning merge gate waits for results in
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -310,7 +310,8 @@ repositories by failing closed unless every generated check succeeds. An
|
|
|
310
310
|
empty analysis matrix while analysis is enabled fails the build instead of
|
|
311
311
|
silently skipping every analyzer.
|
|
312
312
|
|
|
313
|
-
The caller's `
|
|
313
|
+
The caller's `Plan` job (job id `mode`, formerly named `Mode`) classifies the
|
|
314
|
+
validation tier, including the docs-only downgrade, and also plans lanes: it runs the same applicability check
|
|
314
315
|
each lane's Detect step runs and passes the integration, E2E, smoke,
|
|
315
316
|
performance, and eval lanes that have no package script, configured command,
|
|
316
317
|
or test files to the orchestrator as `idle-lanes`. Those jobs are skipped
|
|
@@ -320,6 +321,18 @@ The gate counts a planned idle Eval workflow (which reports `skipped`) as the
|
|
|
320
321
|
no-op success it reported before; any other skip, and every lane with work,
|
|
321
322
|
is still required. Callers that pass no `idle-lanes` run every lane as before.
|
|
322
323
|
|
|
324
|
+
On the tiers that run Security or CodeQL, the same `Plan` job also runs the
|
|
325
|
+
Security Profile and CodeQL language detection (Code Security status, changed
|
|
326
|
+
files, code-scanning merge gate) and passes their outputs to the orchestrator
|
|
327
|
+
as `security-plan` and `codeql-plan`. The Security workflow then skips its
|
|
328
|
+
`Profile` job and the CodeQL workflow skips its `Detect` job, so audits and
|
|
329
|
+
analyzers start one runner hop earlier. A plan marked idle (no audit or review
|
|
330
|
+
to run, or CodeQL unavailable) skips that workflow, and the gate counts only
|
|
331
|
+
that planned skip as success. Planning fails open: if any planning step fails,
|
|
332
|
+
the plans stay empty and each workflow profiles and detects for itself, as it
|
|
333
|
+
does for callers that pass no plans (the scheduled audit and default-branch
|
|
334
|
+
CodeQL lanes).
|
|
335
|
+
|
|
323
336
|
## Security behavior
|
|
324
337
|
|
|
325
338
|
CodeQL is a separate workflow using GitHub's official actions. The default
|
|
@@ -328,9 +341,11 @@ Security availability for private repositories. When CodeQL is unavailable,
|
|
|
328
341
|
set `codeql: false` and sync: the generated caller selects an orchestrator that
|
|
329
342
|
omits CodeQL entirely, so pull requests do not show a skipped analyzer.
|
|
330
343
|
Dependency Review follows the same capability policy through
|
|
331
|
-
`dependency_review: auto`, but executes as a conditional step
|
|
332
|
-
|
|
333
|
-
|
|
344
|
+
`dependency_review: auto`, but executes as a conditional step and never
|
|
345
|
+
registers a separate skipped pull-request check: inside the Security profile
|
|
346
|
+
job, or, when the caller planned the profile, as the last step of the first
|
|
347
|
+
dependency audit job. A run with a review but no audit stays unplanned, so the
|
|
348
|
+
profile job still runs the review. JavaScript, Python, and Rust audits remain available without Advanced
|
|
334
349
|
Security. If GitHub default setup is enabled, disable its generated CodeQL
|
|
335
350
|
workflow to avoid duplicate analysis.
|
|
336
351
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.53.
|
|
3
|
+
"version": "1.53.7",
|
|
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": {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -433,7 +433,10 @@ function synchronize(options) {
|
|
|
433
433
|
if (!entry.endsWith('.yml') || standard.has(`.github/workflows/${entry}`)) continue
|
|
434
434
|
const destination = join(workflowsDir, entry)
|
|
435
435
|
const original = readFileSync(destination, 'utf8')
|
|
436
|
-
const updated =
|
|
436
|
+
const updated = alignRuntimeRefInputs(
|
|
437
|
+
original.replace(runtimePin, `$1@${runtimeRef}`),
|
|
438
|
+
runtimeRepository
|
|
439
|
+
)
|
|
437
440
|
if (updated !== original) {
|
|
438
441
|
changed.push(`.github/workflows/${entry}`)
|
|
439
442
|
if (dryRun)
|
|
@@ -1099,6 +1102,32 @@ export function renderDevChannel(config) {
|
|
|
1099
1102
|
)
|
|
1100
1103
|
}
|
|
1101
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Keep each preserved job's `runtime-ref:` input on the same plain semver
|
|
1107
|
+
* version as its `uses:` pin of the runtime repository. The reusable
|
|
1108
|
+
* workflows check out the runtime at `runtime-ref`, so a pin refreshed without
|
|
1109
|
+
* its input would run the new workflow against the old runtime. Only plain
|
|
1110
|
+
* semver values move: expressions, branch names, and SHAs are intentional.
|
|
1111
|
+
* @param {string} content @param {string} runtimeRepository
|
|
1112
|
+
*/
|
|
1113
|
+
export function alignRuntimeRefInputs(content, runtimeRepository) {
|
|
1114
|
+
const pin = new RegExp(
|
|
1115
|
+
`uses:\\s*${escapeRegExp(runtimeRepository)}/\\.github/(?:workflows|actions)/[^\\s@]+@(v\\d+\\.\\d+\\.\\d+)\\s*$`,
|
|
1116
|
+
'm'
|
|
1117
|
+
)
|
|
1118
|
+
return content
|
|
1119
|
+
.split(/(?=^ {2}[A-Za-z0-9_-]+:[ \t]*\r?$)/m)
|
|
1120
|
+
.map((block) => {
|
|
1121
|
+
const version = block.match(pin)?.[1]
|
|
1122
|
+
if (!version) return block
|
|
1123
|
+
return block.replace(
|
|
1124
|
+
/^([ \t]+runtime-ref:[ \t]*)(['"]?)v\d+\.\d+\.\d+\2([ \t]*)$/gm,
|
|
1125
|
+
(_, key, quote, trailing) => `${key}${quote}${version}${quote}${trailing}`
|
|
1126
|
+
)
|
|
1127
|
+
})
|
|
1128
|
+
.join('')
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1102
1131
|
/**
|
|
1103
1132
|
* Pin consumer runtime refs without retaining the self-CI expression used by
|
|
1104
1133
|
* the Code Foundry repository. Self callers keep their event-relative refs.
|
package/src/runtime-core.mjs
CHANGED
|
@@ -393,6 +393,44 @@ function validation(task) {
|
|
|
393
393
|
writeOutput('idle_lanes', idle.join(','))
|
|
394
394
|
return
|
|
395
395
|
}
|
|
396
|
+
if (task === 'plan') {
|
|
397
|
+
// The caller's planning job already holds the consumer checkout and the
|
|
398
|
+
// runtime, so it also runs the Security Profile and CodeQL Detect steps
|
|
399
|
+
// that would otherwise each queue a runner before any audit or analysis
|
|
400
|
+
// could start. Each plan carries exactly the outputs that job emits, plus
|
|
401
|
+
// `idle` when the workflow would have nothing to run.
|
|
402
|
+
const profile = securityProfile()
|
|
403
|
+
const auditMatrix = JSON.stringify(securityAuditMatrix(profile))
|
|
404
|
+
const dependencyReview =
|
|
405
|
+
(process.env.FOUNDRY_EVENT_NAME ?? '') === 'pull_request' &&
|
|
406
|
+
profile.dependency_review === 'true'
|
|
407
|
+
const securityPlan = {
|
|
408
|
+
...profile,
|
|
409
|
+
audit_matrix: auditMatrix,
|
|
410
|
+
idle: auditMatrix === '[]' && !dependencyReview ? 'true' : 'false',
|
|
411
|
+
}
|
|
412
|
+
const detection = codeqlDetection()
|
|
413
|
+
const enabled = detection.enabled === 'true'
|
|
414
|
+
const codeqlPlan = {
|
|
415
|
+
...detection,
|
|
416
|
+
matrix: enabled
|
|
417
|
+
? JSON.stringify(
|
|
418
|
+
codeqlAnalysisMatrix(JSON.parse(detection.languages), configuredRustShards())
|
|
419
|
+
)
|
|
420
|
+
: '[]',
|
|
421
|
+
code_security: process.env.REPO_FOUNDRY_CODE_SECURITY ?? '',
|
|
422
|
+
idle: enabled ? 'false' : 'true',
|
|
423
|
+
}
|
|
424
|
+
// Dependency Review rides on the first audit job of a planned run. With
|
|
425
|
+
// no audit to ride on, leave Security unplanned so its Profile job runs
|
|
426
|
+
// the review as it always has, rather than adding a job of its own.
|
|
427
|
+
writeOutput(
|
|
428
|
+
'security_plan',
|
|
429
|
+
auditMatrix === '[]' && dependencyReview ? '' : JSON.stringify(securityPlan)
|
|
430
|
+
)
|
|
431
|
+
writeOutput('codeql_plan', JSON.stringify(codeqlPlan))
|
|
432
|
+
return
|
|
433
|
+
}
|
|
396
434
|
if (task === 'gate') {
|
|
397
435
|
const mode = process.env.FOUNDRY_MODE ?? ''
|
|
398
436
|
const gate = evaluateValidationGate({
|
|
@@ -940,24 +978,56 @@ function readFileSafe(file) {
|
|
|
940
978
|
}
|
|
941
979
|
}
|
|
942
980
|
|
|
981
|
+
/**
|
|
982
|
+
* The Security workflow's Profile outputs, as the strings the job emits.
|
|
983
|
+
* @returns {Record<string, string>}
|
|
984
|
+
*/
|
|
985
|
+
function securityProfile() {
|
|
986
|
+
const python = hasDependencyManifest('python')
|
|
987
|
+
const requirements = capture('git', ['ls-files', '*requirements*.txt'])
|
|
988
|
+
.split('\n')
|
|
989
|
+
.filter(Boolean)
|
|
990
|
+
return {
|
|
991
|
+
javascript: hasDependencyManifest('javascript') ? 'true' : 'false',
|
|
992
|
+
rust: hasDependencyManifest('rust') ? 'true' : 'false',
|
|
993
|
+
python: python ? 'true' : 'false',
|
|
994
|
+
python_requirements: JSON.stringify(
|
|
995
|
+
python ? (requirements.length ? requirements : ['project']) : ['none']
|
|
996
|
+
),
|
|
997
|
+
dependency_review: featureEnabled('dependency_review') ? 'true' : 'false',
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* The dependency audit matrix built from a profile: the same entries the
|
|
1003
|
+
* Security workflow's "Build audit matrix" step emits.
|
|
1004
|
+
* @param {Record<string, string>} profile
|
|
1005
|
+
*/
|
|
1006
|
+
function securityAuditMatrix(profile) {
|
|
1007
|
+
/** @type {Array<Record<string, string>>} */
|
|
1008
|
+
const entries = []
|
|
1009
|
+
if (profile.javascript === 'true') entries.push({ display: 'JavaScript', language: 'javascript' })
|
|
1010
|
+
if (profile.rust === 'true') entries.push({ display: 'Rust', language: 'rust' })
|
|
1011
|
+
if (profile.python === 'true') {
|
|
1012
|
+
/** @type {unknown} */
|
|
1013
|
+
let requirements = []
|
|
1014
|
+
try {
|
|
1015
|
+
requirements = JSON.parse(profile.python_requirements || '[]')
|
|
1016
|
+
} catch {
|
|
1017
|
+
requirements = []
|
|
1018
|
+
}
|
|
1019
|
+
for (const requirement of Array.isArray(requirements) ? requirements : []) {
|
|
1020
|
+
if (!requirement || requirement === 'none') continue
|
|
1021
|
+
entries.push({ display: `Python / ${requirement}`, language: 'python', requirement })
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return entries
|
|
1025
|
+
}
|
|
1026
|
+
|
|
943
1027
|
/** @param {string} task @param {string} ecosystem */
|
|
944
1028
|
function security(task, ecosystem) {
|
|
945
1029
|
if (task === 'profile') {
|
|
946
|
-
|
|
947
|
-
writeOutput('rust', hasDependencyManifest('rust') ? 'true' : 'false')
|
|
948
|
-
writeOutput('python', hasDependencyManifest('python') ? 'true' : 'false')
|
|
949
|
-
const requirements = capture('git', ['ls-files', '*requirements*.txt'])
|
|
950
|
-
.split('\n')
|
|
951
|
-
.filter(Boolean)
|
|
952
|
-
writeOutput(
|
|
953
|
-
'python_requirements',
|
|
954
|
-
hasDependencyManifest('python')
|
|
955
|
-
? requirements.length
|
|
956
|
-
? requirements
|
|
957
|
-
: ['project']
|
|
958
|
-
: ['none']
|
|
959
|
-
)
|
|
960
|
-
writeOutput('dependency_review', featureEnabled('dependency_review') ? 'true' : 'false')
|
|
1030
|
+
for (const [key, value] of Object.entries(securityProfile())) writeOutput(key, value)
|
|
961
1031
|
return
|
|
962
1032
|
}
|
|
963
1033
|
if (task === 'should_run') {
|
|
@@ -1082,18 +1152,28 @@ function assertRustShardsAligned() {
|
|
|
1082
1152
|
}
|
|
1083
1153
|
|
|
1084
1154
|
function codeql() {
|
|
1155
|
+
for (const [key, value] of Object.entries(codeqlDetection())) writeOutput(key, value)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* The CodeQL workflow's "Detect languages" outputs, as the strings the step
|
|
1160
|
+
* emits, in the order it emits them.
|
|
1161
|
+
* @returns {Record<string, string>}
|
|
1162
|
+
*/
|
|
1163
|
+
function codeqlDetection() {
|
|
1085
1164
|
const enabled =
|
|
1086
1165
|
featureEnabled('codeql') &&
|
|
1087
1166
|
(isPublicRepository() || process.env.REPO_FOUNDRY_CODE_SECURITY === 'enabled')
|
|
1088
|
-
|
|
1167
|
+
/** @type {Record<string, string>} */
|
|
1168
|
+
const outputs = { enabled: enabled ? 'true' : 'false' }
|
|
1089
1169
|
if (!enabled) {
|
|
1090
|
-
|
|
1170
|
+
outputs.languages = '[]'
|
|
1091
1171
|
for (const language of ['actions', 'javascript', 'python', 'rust']) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1172
|
+
outputs[`${language}_available`] = 'false'
|
|
1173
|
+
outputs[`${language}_changed`] = 'false'
|
|
1174
|
+
outputs[`${language}_build_mode`] = 'none'
|
|
1095
1175
|
}
|
|
1096
|
-
return
|
|
1176
|
+
return outputs
|
|
1097
1177
|
}
|
|
1098
1178
|
// Detect lane drift before any analysis uploads a category the other lanes
|
|
1099
1179
|
// never produce; a stall surfaces only later as a blocked merge gate.
|
|
@@ -1129,14 +1209,55 @@ function codeql() {
|
|
|
1129
1209
|
rustEntry.shards = shards
|
|
1130
1210
|
rustEntry.changed = shards.some((entry) => entry.changed)
|
|
1131
1211
|
}
|
|
1132
|
-
|
|
1212
|
+
outputs.languages = JSON.stringify(languagesJson)
|
|
1133
1213
|
for (const language of ['actions', 'javascript-typescript', 'python', 'rust']) {
|
|
1134
1214
|
const entry = languagesJson.find((item) => item.language === language)
|
|
1135
1215
|
const prefix = language === 'javascript-typescript' ? 'javascript' : language
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1216
|
+
outputs[`${prefix}_available`] = entry ? 'true' : 'false'
|
|
1217
|
+
outputs[`${prefix}_changed`] = entry?.changed ? 'true' : 'false'
|
|
1218
|
+
outputs[`${prefix}_build_mode`] = entry?.['build-mode'] ?? 'none'
|
|
1219
|
+
}
|
|
1220
|
+
return outputs
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The CodeQL analysis matrix: the same entries the CodeQL workflow's "Build
|
|
1225
|
+
* analysis matrix" step emits from detection. SARIF categories are the
|
|
1226
|
+
* code-scanning baseline keys and must stay byte-identical to that step's.
|
|
1227
|
+
* @param {CodeqlLanguageEntry[]} detected @param {string[]} shards
|
|
1228
|
+
*/
|
|
1229
|
+
function codeqlAnalysisMatrix(detected, shards) {
|
|
1230
|
+
/** @type {Record<string, string>} */
|
|
1231
|
+
const display = { actions: 'Actions', 'javascript-typescript': 'TypeScript', python: 'Python' }
|
|
1232
|
+
/** @type {Array<Record<string, string | boolean>>} */
|
|
1233
|
+
const matrix = detected
|
|
1234
|
+
.filter((entry) => entry.language !== 'rust')
|
|
1235
|
+
.map((entry) => ({
|
|
1236
|
+
display: display[entry.language] || entry.language,
|
|
1237
|
+
language: entry.language,
|
|
1238
|
+
category: `/language:${entry.language}`,
|
|
1239
|
+
build_mode: entry['build-mode'] || 'none',
|
|
1240
|
+
changed: entry.changed === true,
|
|
1241
|
+
}))
|
|
1242
|
+
const rust = detected.find((entry) => entry.language === 'rust')
|
|
1243
|
+
const rustShards =
|
|
1244
|
+
rust && Array.isArray(rust.shards) && rust.shards.length
|
|
1245
|
+
? rust.shards
|
|
1246
|
+
: rust
|
|
1247
|
+
? shards.map((shard) => ({ shard: String(shard), changed: rust.changed === true }))
|
|
1248
|
+
: []
|
|
1249
|
+
for (const entry of rustShards) {
|
|
1250
|
+
matrix.push({
|
|
1251
|
+
display: entry.shard === 'all' ? 'Rust' : `Rust (${entry.shard})`,
|
|
1252
|
+
language: 'rust',
|
|
1253
|
+
category: '/language:rust',
|
|
1254
|
+
build_mode: 'none',
|
|
1255
|
+
changed: entry.changed === true,
|
|
1256
|
+
shard: String(entry.shard),
|
|
1257
|
+
})
|
|
1139
1258
|
}
|
|
1259
|
+
if (!matrix.length) throw new Error('CodeQL analysis matrix is empty while analysis is enabled.')
|
|
1260
|
+
return matrix
|
|
1140
1261
|
}
|
|
1141
1262
|
|
|
1142
1263
|
/**
|