code-foundry 1.53.5 → 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/eval.yml +10 -1
- package/.github/workflows/security.yml +29 -3
- package/.github/workflows/test.yml +22 -5
- package/.github/workflows/validation-billing.yml +41 -6
- package/.github/workflows/validation-no-codeql.yml +48 -5
- package/.github/workflows/validation.yml +58 -8
- package/.github/workflows/validation_self-ci.yml +105 -3
- package/CHANGELOG.md +15 -15
- package/docs/CONFIGURATION.md +1 -1
- package/docs/WORKFLOWS.md +28 -3
- package/package.json +1 -1
- package/src/commands/sync.mjs +30 -1
- package/src/runtime-core.mjs +169 -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
|
|
@@ -26,6 +26,15 @@ on:
|
|
|
26
26
|
required: false
|
|
27
27
|
type: string
|
|
28
28
|
default: task-result
|
|
29
|
+
idle-lanes:
|
|
30
|
+
description: >-
|
|
31
|
+
Comma-separated lanes the caller's planner found no entrypoint for
|
|
32
|
+
(integration, e2e, smoke, performance, eval). Their jobs are
|
|
33
|
+
skipped instead of allocating a runner for a no-op. Empty runs every
|
|
34
|
+
lane.
|
|
35
|
+
required: false
|
|
36
|
+
type: string
|
|
37
|
+
default: ''
|
|
29
38
|
|
|
30
39
|
permissions:
|
|
31
40
|
contents: read
|
|
@@ -51,7 +60,7 @@ concurrency:
|
|
|
51
60
|
jobs:
|
|
52
61
|
eval:
|
|
53
62
|
name: Eval
|
|
54
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
63
|
+
if: vars.CI_BILLING_PAUSED != 'true' && !contains(format(',{0},', inputs.idle-lanes), ',eval,')
|
|
55
64
|
runs-on: ${{ inputs.runner }}
|
|
56
65
|
timeout-minutes: 30
|
|
57
66
|
steps:
|
|
@@ -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
|
|
@@ -53,6 +53,15 @@ on:
|
|
|
53
53
|
required: false
|
|
54
54
|
type: string
|
|
55
55
|
default: '1'
|
|
56
|
+
idle-lanes:
|
|
57
|
+
description: >-
|
|
58
|
+
Comma-separated lanes the caller's planner found no entrypoint for
|
|
59
|
+
(integration, e2e, smoke, performance, eval). Their jobs are
|
|
60
|
+
skipped instead of allocating a runner for a no-op. Empty runs every
|
|
61
|
+
lane.
|
|
62
|
+
required: false
|
|
63
|
+
type: string
|
|
64
|
+
default: ''
|
|
56
65
|
secrets:
|
|
57
66
|
TURBO_TOKEN:
|
|
58
67
|
required: false
|
|
@@ -171,7 +180,7 @@ jobs:
|
|
|
171
180
|
|
|
172
181
|
performance:
|
|
173
182
|
name: Performance
|
|
174
|
-
if: vars.CI_BILLING_PAUSED != 'true'
|
|
183
|
+
if: vars.CI_BILLING_PAUSED != 'true' && !contains(format(',{0},', inputs.idle-lanes), ',performance,')
|
|
175
184
|
runs-on: ${{ inputs.performance-runner }}
|
|
176
185
|
timeout-minutes: 30
|
|
177
186
|
steps:
|
|
@@ -260,7 +269,9 @@ jobs:
|
|
|
260
269
|
|
|
261
270
|
integration:
|
|
262
271
|
name: Integration
|
|
263
|
-
if:
|
|
272
|
+
if: >-
|
|
273
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true && inputs.integration-total-shards != '2' &&
|
|
274
|
+
!contains(format(',{0},', inputs.idle-lanes), ',integration,')
|
|
264
275
|
runs-on: ${{ inputs.runner }}
|
|
265
276
|
timeout-minutes: 30
|
|
266
277
|
steps:
|
|
@@ -337,7 +348,9 @@ jobs:
|
|
|
337
348
|
|
|
338
349
|
integration-sharded:
|
|
339
350
|
name: Integration shard
|
|
340
|
-
if:
|
|
351
|
+
if: >-
|
|
352
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true && inputs.integration-total-shards == '2' &&
|
|
353
|
+
!contains(format(',{0},', inputs.idle-lanes), ',integration,')
|
|
341
354
|
runs-on: ${{ inputs.runner }}
|
|
342
355
|
timeout-minutes: 30
|
|
343
356
|
strategy:
|
|
@@ -417,7 +430,9 @@ jobs:
|
|
|
417
430
|
|
|
418
431
|
e2e:
|
|
419
432
|
name: E2E
|
|
420
|
-
if:
|
|
433
|
+
if: >-
|
|
434
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true &&
|
|
435
|
+
!contains(format(',{0},', inputs.idle-lanes), ',e2e,')
|
|
421
436
|
runs-on: ${{ inputs.runner }}
|
|
422
437
|
timeout-minutes: 45
|
|
423
438
|
# Shards duplicate the whole job, but each shard owns a runner-local test
|
|
@@ -514,7 +529,9 @@ jobs:
|
|
|
514
529
|
|
|
515
530
|
smoke:
|
|
516
531
|
name: Smoke
|
|
517
|
-
if:
|
|
532
|
+
if: >-
|
|
533
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true &&
|
|
534
|
+
!contains(format(',{0},', inputs.idle-lanes), ',smoke,')
|
|
518
535
|
runs-on: ${{ inputs.runner }}
|
|
519
536
|
timeout-minutes: 15
|
|
520
537
|
steps:
|
|
@@ -115,6 +115,31 @@ on:
|
|
|
115
115
|
required: false
|
|
116
116
|
type: string
|
|
117
117
|
default: '1'
|
|
118
|
+
idle-lanes:
|
|
119
|
+
description: >-
|
|
120
|
+
Comma-separated lanes the caller's planner found no entrypoint for
|
|
121
|
+
(integration, e2e, smoke, performance, eval). Only the separate E2E
|
|
122
|
+
job is skipped here; the merged fast lanes keep their own Detect
|
|
123
|
+
steps. Empty runs every lane.
|
|
124
|
+
required: false
|
|
125
|
+
type: string
|
|
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: ''
|
|
118
143
|
secrets:
|
|
119
144
|
TURBO_TOKEN:
|
|
120
145
|
required: false
|
|
@@ -787,7 +812,11 @@ jobs:
|
|
|
787
812
|
|
|
788
813
|
e2e:
|
|
789
814
|
name: E2E
|
|
790
|
-
|
|
815
|
+
# The gate already accepts a skipped E2E lane; a planner-idle E2E lane
|
|
816
|
+
# skips here instead of allocating a runner for a no-op.
|
|
817
|
+
if: >-
|
|
818
|
+
vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit' &&
|
|
819
|
+
!contains(format(',{0},', inputs.idle-lanes), ',e2e,')
|
|
791
820
|
permissions:
|
|
792
821
|
actions: read
|
|
793
822
|
contents: read
|
|
@@ -1203,7 +1232,9 @@ jobs:
|
|
|
1203
1232
|
name: CodeQL
|
|
1204
1233
|
# Release-please pull requests also analyze with CodeQL so repository
|
|
1205
1234
|
# rulesets requiring code scanning results never deadlock the release.
|
|
1206
|
-
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'
|
|
1207
1238
|
permissions:
|
|
1208
1239
|
actions: read
|
|
1209
1240
|
contents: read
|
|
@@ -1217,6 +1248,7 @@ jobs:
|
|
|
1217
1248
|
rust-shards: ${{ inputs.rust-shards }}
|
|
1218
1249
|
rust-threads: ${{ inputs.rust-threads }}
|
|
1219
1250
|
rust-max-parallel: ${{ inputs.rust-max-parallel }}
|
|
1251
|
+
plan: ${{ inputs.codeql-plan }}
|
|
1220
1252
|
|
|
1221
1253
|
# Aggregate check: "Validation / Gate" (caller job name + this job name).
|
|
1222
1254
|
# Always evaluates; only jobs required for the mode must have succeeded.
|
|
@@ -1243,8 +1275,10 @@ jobs:
|
|
|
1243
1275
|
FOUNDRY_SECURITY: ${{ needs.audit.result }}
|
|
1244
1276
|
# This orchestrator runs CodeQL only when the consumer's canonical
|
|
1245
1277
|
# configuration enables it; otherwise the unavailability decision is
|
|
1246
|
-
# treated as satisfied without registering a skipped PR check.
|
|
1247
|
-
|
|
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' }}
|
|
1248
1282
|
# Eval runs inside the merged fast-lanes job on the audit tier; a
|
|
1249
1283
|
# fast-lanes failure on that tier marks the eval category failed even if
|
|
1250
1284
|
# another merged lane caused it, which fails closed.
|
|
@@ -1293,7 +1327,8 @@ jobs:
|
|
|
1293
1327
|
needs.fast-lanes.result == 'success' && needs.lint.result == 'success' &&
|
|
1294
1328
|
needs.type-check.result == 'success' && needs.unit.result == 'success' &&
|
|
1295
1329
|
needs.e2e.result == 'success' && needs.audit.result == 'success' &&
|
|
1296
|
-
(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')) &&
|
|
1297
1332
|
needs.gate.result == 'success'
|
|
1298
1333
|
permissions:
|
|
1299
1334
|
actions: read
|
|
@@ -1308,5 +1343,5 @@ jobs:
|
|
|
1308
1343
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
1309
1344
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
1310
1345
|
job-results-json: >-
|
|
1311
|
-
{"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 }}"}
|
|
1312
1347
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -75,6 +75,32 @@ on:
|
|
|
75
75
|
required: false
|
|
76
76
|
type: number
|
|
77
77
|
default: 1
|
|
78
|
+
idle-lanes:
|
|
79
|
+
description: >-
|
|
80
|
+
Comma-separated lanes the caller's planner found no entrypoint for
|
|
81
|
+
(integration, e2e, smoke, performance, eval). Their jobs are
|
|
82
|
+
skipped instead of allocating a runner for a no-op. Empty runs every
|
|
83
|
+
lane.
|
|
84
|
+
required: false
|
|
85
|
+
type: string
|
|
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: ''
|
|
78
104
|
artifact-prefix:
|
|
79
105
|
description: Prefix for task receipts; use a unique value when calling this workflow more than once.
|
|
80
106
|
required: false
|
|
@@ -181,12 +207,17 @@ jobs:
|
|
|
181
207
|
integration-total-shards: ${{ inputs.integration-total-shards }}
|
|
182
208
|
e2e-shard-list: ${{ inputs.e2e-shard-list }}
|
|
183
209
|
e2e-total-shards: ${{ inputs.e2e-total-shards }}
|
|
210
|
+
idle-lanes: ${{ inputs.idle-lanes }}
|
|
184
211
|
secrets:
|
|
185
212
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
186
213
|
|
|
187
214
|
security:
|
|
188
215
|
name: Security
|
|
189
|
-
|
|
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'
|
|
190
221
|
permissions:
|
|
191
222
|
contents: read
|
|
192
223
|
uses: ./.github/workflows/security.yml
|
|
@@ -194,6 +225,7 @@ jobs:
|
|
|
194
225
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
195
226
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
196
227
|
runner: ${{ inputs.security-runner }}
|
|
228
|
+
plan: ${{ inputs.security-plan }}
|
|
197
229
|
|
|
198
230
|
eval:
|
|
199
231
|
name: Eval
|
|
@@ -209,6 +241,7 @@ jobs:
|
|
|
209
241
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
210
242
|
runner: ${{ inputs.eval-runner }}
|
|
211
243
|
artifact-prefix: ${{ inputs.artifact-prefix }}
|
|
244
|
+
idle-lanes: ${{ inputs.idle-lanes }}
|
|
212
245
|
|
|
213
246
|
# Aggregate check: "Validation / Gate" (caller job name + this job name).
|
|
214
247
|
# Always evaluates; only jobs required for the mode must have succeeded.
|
|
@@ -228,8 +261,15 @@ jobs:
|
|
|
228
261
|
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
229
262
|
FOUNDRY_CI: ${{ needs.ci.result }}
|
|
230
263
|
FOUNDRY_TEST: ${{ needs.test.result }}
|
|
231
|
-
|
|
232
|
-
|
|
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 }}
|
|
268
|
+
# An eval lane the caller planned as idle (no eval script, no
|
|
269
|
+
# eval_command, or eval: false) never starts, so the whole Eval
|
|
270
|
+
# workflow reports skipped. Count that exactly like the no-op success
|
|
271
|
+
# it reported before; any other skip still fails the gate.
|
|
272
|
+
FOUNDRY_EVAL: ${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}
|
|
233
273
|
FOUNDRY_SOURCE_QUALIFICATION: ${{ needs.qualification.outputs.reuse }}
|
|
234
274
|
FOUNDRY_QUALIFICATION: ${{ needs.qualification.result }}
|
|
235
275
|
# This workflow is selected only for repositories whose canonical
|
|
@@ -276,7 +316,10 @@ jobs:
|
|
|
276
316
|
inputs.release-qualification-receipts == true &&
|
|
277
317
|
github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
|
|
278
318
|
needs.ci.result == 'success' && needs.test.result == 'success' &&
|
|
279
|
-
needs.security.result == 'success'
|
|
319
|
+
(needs.security.result == 'success' ||
|
|
320
|
+
(needs.security.result == 'skipped' && fromJSON(inputs.security-plan || '{}').idle == 'true')) &&
|
|
321
|
+
(needs.eval.result == 'success' ||
|
|
322
|
+
(needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
|
|
280
323
|
needs.gate.result == 'success'
|
|
281
324
|
permissions:
|
|
282
325
|
actions: read
|
|
@@ -291,5 +334,5 @@ jobs:
|
|
|
291
334
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
292
335
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
293
336
|
job-results-json: >-
|
|
294
|
-
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ needs.security.result }}","codeql":"not-configured","eval":"${{ 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 }}"}
|
|
295
338
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -90,6 +90,34 @@ on:
|
|
|
90
90
|
required: false
|
|
91
91
|
type: string
|
|
92
92
|
default: '1'
|
|
93
|
+
idle-lanes:
|
|
94
|
+
description: >-
|
|
95
|
+
Comma-separated lanes the caller's planner found no entrypoint for
|
|
96
|
+
(integration, e2e, smoke, performance, eval). Their jobs are
|
|
97
|
+
skipped instead of allocating a runner for a no-op. Empty runs every
|
|
98
|
+
lane.
|
|
99
|
+
required: false
|
|
100
|
+
type: string
|
|
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: ''
|
|
93
121
|
artifact-prefix:
|
|
94
122
|
description: Prefix for task receipts; use a unique value when calling this workflow more than once.
|
|
95
123
|
required: false
|
|
@@ -181,12 +209,17 @@ jobs:
|
|
|
181
209
|
integration-total-shards: ${{ inputs.integration-total-shards }}
|
|
182
210
|
e2e-shard-list: ${{ inputs.e2e-shard-list }}
|
|
183
211
|
e2e-total-shards: ${{ inputs.e2e-total-shards }}
|
|
212
|
+
idle-lanes: ${{ inputs.idle-lanes }}
|
|
184
213
|
secrets:
|
|
185
214
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
186
215
|
|
|
187
216
|
security:
|
|
188
217
|
name: Security
|
|
189
|
-
|
|
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'
|
|
190
223
|
permissions:
|
|
191
224
|
contents: read
|
|
192
225
|
uses: ./.github/workflows/security.yml
|
|
@@ -194,12 +227,15 @@ jobs:
|
|
|
194
227
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
195
228
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
196
229
|
runner: ${{ inputs.security-runner }}
|
|
230
|
+
plan: ${{ inputs.security-plan }}
|
|
197
231
|
|
|
198
232
|
codeql:
|
|
199
233
|
name: CodeQL
|
|
200
234
|
# Release-please pull requests also analyze with CodeQL so repository
|
|
201
235
|
# rulesets requiring code scanning results never deadlock the release.
|
|
202
|
-
if:
|
|
236
|
+
if: >-
|
|
237
|
+
vars.CI_BILLING_PAUSED != 'true' && (inputs.mode == 'audit' || inputs.mode == 'release') &&
|
|
238
|
+
fromJSON(inputs.codeql-plan || '{}').idle != 'true'
|
|
203
239
|
permissions:
|
|
204
240
|
actions: read
|
|
205
241
|
contents: read
|
|
@@ -213,6 +249,7 @@ jobs:
|
|
|
213
249
|
rust-shards: ${{ inputs.rust-shards }}
|
|
214
250
|
rust-threads: ${{ inputs.rust-threads }}
|
|
215
251
|
rust-max-parallel: ${{ inputs.rust-max-parallel }}
|
|
252
|
+
plan: ${{ inputs.codeql-plan }}
|
|
216
253
|
|
|
217
254
|
eval:
|
|
218
255
|
name: Eval
|
|
@@ -228,6 +265,7 @@ jobs:
|
|
|
228
265
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
229
266
|
runner: ${{ inputs.eval-runner }}
|
|
230
267
|
artifact-prefix: ${{ inputs.artifact-prefix }}
|
|
268
|
+
idle-lanes: ${{ inputs.idle-lanes }}
|
|
231
269
|
|
|
232
270
|
# Aggregate check: "Validation / Gate" (caller job name + this job name).
|
|
233
271
|
# Always evaluates; only jobs required for the mode must have succeeded.
|
|
@@ -247,9 +285,16 @@ jobs:
|
|
|
247
285
|
FOUNDRY_MODE: ${{ inputs.mode }}
|
|
248
286
|
FOUNDRY_CI: ${{ needs.ci.result }}
|
|
249
287
|
FOUNDRY_TEST: ${{ needs.test.result }}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 }}
|
|
293
|
+
# An eval lane the caller planned as idle (no eval script, no
|
|
294
|
+
# eval_command, or eval: false) never starts, so the whole Eval
|
|
295
|
+
# workflow reports skipped. Count that exactly like the no-op success
|
|
296
|
+
# it reported before; any other skip still fails the gate.
|
|
297
|
+
FOUNDRY_EVAL: ${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}
|
|
253
298
|
FOUNDRY_SOURCE_QUALIFICATION: ${{ needs.qualification.outputs.reuse }}
|
|
254
299
|
FOUNDRY_QUALIFICATION: ${{ needs.qualification.result }}
|
|
255
300
|
steps:
|
|
@@ -292,8 +337,13 @@ jobs:
|
|
|
292
337
|
inputs.release-qualification-receipts == true &&
|
|
293
338
|
github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
|
|
294
339
|
needs.ci.result == 'success' && needs.test.result == 'success' &&
|
|
295
|
-
needs.security.result == 'success'
|
|
296
|
-
needs.
|
|
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')) &&
|
|
344
|
+
(needs.eval.result == 'success' ||
|
|
345
|
+
(needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
|
|
346
|
+
needs.gate.result == 'success'
|
|
297
347
|
permissions:
|
|
298
348
|
actions: read
|
|
299
349
|
contents: read
|
|
@@ -307,5 +357,5 @@ jobs:
|
|
|
307
357
|
runtime-repository: ${{ inputs.runtime-repository }}
|
|
308
358
|
runtime-ref: ${{ inputs.runtime-ref }}
|
|
309
359
|
job-results-json: >-
|
|
310
|
-
{"ci":"${{ needs.ci.result }}","test":"${{ needs.test.result }}","security":"${{ needs.security.result }}","codeql":"${{ needs.codeql.result }}","eval":"${{ 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 }}"}
|
|
311
361
|
required-job-names-json: '["Validation / Gate"]'
|
|
@@ -22,18 +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 }}
|
|
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 }}
|
|
37
53
|
steps:
|
|
38
54
|
# The runtime classifies policy from the consumer repository's config.
|
|
39
55
|
# Keep this checkout before the isolated runtime checkout below.
|
|
@@ -61,6 +77,89 @@ jobs:
|
|
|
61
77
|
FOUNDRY_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
62
78
|
FOUNDRY_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
63
79
|
run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation mode >> "$GITHUB_OUTPUT"
|
|
80
|
+
# Lanes without an entrypoint (no package script, configured command,
|
|
81
|
+
# or test files) are skipped by the orchestrator rather than started on
|
|
82
|
+
# a runner only to report a no-op. Detection errors keep the lane.
|
|
83
|
+
- name: Plan lanes
|
|
84
|
+
id: lanes
|
|
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"
|
|
64
163
|
|
|
65
164
|
# The stable aggregate check name is the caller job name plus the
|
|
66
165
|
# orchestrator gate job name: "Validation / Gate". Keep both names fixed.
|
|
@@ -107,6 +206,9 @@ jobs:
|
|
|
107
206
|
integration-total-shards: '1'
|
|
108
207
|
e2e-shard-list: '1'
|
|
109
208
|
e2e-total-shards: '1'
|
|
209
|
+
idle-lanes: ${{ needs.mode.outputs.idle-lanes }}
|
|
210
|
+
security-plan: ${{ needs.mode.outputs.security-plan }}
|
|
211
|
+
codeql-plan: ${{ needs.mode.outputs.codeql-plan }}
|
|
110
212
|
secrets:
|
|
111
213
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
112
214
|
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.53.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.5...v1.53.6) (2026-10-31)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Performance
|
|
14
|
+
|
|
15
|
+
* **validation:** skip lane jobs that have nothing to run ([#825](https://github.com/0xPlayerOne/code-foundry/issues/825)) ([4d04f62](https://github.com/0xPlayerOne/code-foundry/commit/4d04f627bfac572bc056b5a5f448b66c17500c00))
|
|
16
|
+
|
|
3
17
|
## [1.53.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.4...v1.53.5) (2026-10-31)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -136,23 +150,9 @@
|
|
|
136
150
|
|
|
137
151
|
* handle empty security audit matrices ([#785](https://github.com/0xPlayerOne/code-foundry/issues/785)) ([02f0565](https://github.com/0xPlayerOne/code-foundry/commit/02f05652e68267a2aa2df5d5af8f13df9ba1e3aa))
|
|
138
152
|
|
|
139
|
-
## [1.49.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.9...v1.49.0) (2026-10-27)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
### Features
|
|
143
|
-
|
|
144
|
-
* support isolated integration test shards ([#783](https://github.com/0xPlayerOne/code-foundry/issues/783)) ([ad1123f](https://github.com/0xPlayerOne/code-foundry/commit/ad1123ff3bb9654bfbdaef4219490cca3e7e3d20))
|
|
145
|
-
|
|
146
|
-
## [1.48.9](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.8...v1.48.9) (2026-10-26)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
### Bug Fixes
|
|
150
|
-
|
|
151
|
-
* **release:** preserve fallback stdin and paginate existing releases ([#781](https://github.com/0xPlayerOne/code-foundry/issues/781)) ([ede4d2c](https://github.com/0xPlayerOne/code-foundry/commit/ede4d2c7e3bc7d1174580c9a0bc83bdaa160c2da))
|
|
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,6 +310,29 @@ 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 `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
|
|
315
|
+
each lane's Detect step runs and passes the integration, E2E, smoke,
|
|
316
|
+
performance, and eval lanes that have no package script, configured command,
|
|
317
|
+
or test files to the orchestrator as `idle-lanes`. Those jobs are skipped
|
|
318
|
+
instead of queuing a runner only to report a no-op. Unit always keeps its job,
|
|
319
|
+
and a lane whose detection errors keeps its job so the error surfaces there.
|
|
320
|
+
The gate counts a planned idle Eval workflow (which reports `skipped`) as the
|
|
321
|
+
no-op success it reported before; any other skip, and every lane with work,
|
|
322
|
+
is still required. Callers that pass no `idle-lanes` run every lane as before.
|
|
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
|
+
|
|
313
336
|
## Security behavior
|
|
314
337
|
|
|
315
338
|
CodeQL is a separate workflow using GitHub's official actions. The default
|
|
@@ -318,9 +341,11 @@ Security availability for private repositories. When CodeQL is unavailable,
|
|
|
318
341
|
set `codeql: false` and sync: the generated caller selects an orchestrator that
|
|
319
342
|
omits CodeQL entirely, so pull requests do not show a skipped analyzer.
|
|
320
343
|
Dependency Review follows the same capability policy through
|
|
321
|
-
`dependency_review: auto`, but executes as a conditional step
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
324
349
|
Security. If GitHub default setup is enabled, disable its generated CodeQL
|
|
325
350
|
workflow to avoid duplicate analysis.
|
|
326
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
|
@@ -354,6 +354,13 @@ function downgradeDocsOnlyPullRequest(mode) {
|
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Lanes that own a whole runner job and can be skipped when they have no work.
|
|
359
|
+
* Unit stays out: it is the one test lane every tier requires, so it always
|
|
360
|
+
* keeps its job.
|
|
361
|
+
*/
|
|
362
|
+
const PLANNED_LANES = ['integration', 'e2e', 'smoke', 'performance', 'eval']
|
|
363
|
+
|
|
357
364
|
/** @param {string} task */
|
|
358
365
|
function validation(task) {
|
|
359
366
|
if (task === 'mode') {
|
|
@@ -370,6 +377,60 @@ function validation(task) {
|
|
|
370
377
|
)
|
|
371
378
|
return
|
|
372
379
|
}
|
|
380
|
+
if (task === 'lanes') {
|
|
381
|
+
// Lanes with no entrypoint would only allocate a runner to report a
|
|
382
|
+
// no-op. The caller's mode job already holds the checkout, so it names
|
|
383
|
+
// them once and the orchestrator skips those jobs. This mirrors each
|
|
384
|
+
// lane's own Detect step (`relevant`) and fails open: a lane whose
|
|
385
|
+
// detection throws keeps its job so the job reports the error itself.
|
|
386
|
+
const idle = PLANNED_LANES.filter((lane) => {
|
|
387
|
+
try {
|
|
388
|
+
return !relevant(lane)
|
|
389
|
+
} catch {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
writeOutput('idle_lanes', idle.join(','))
|
|
394
|
+
return
|
|
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
|
+
}
|
|
373
434
|
if (task === 'gate') {
|
|
374
435
|
const mode = process.env.FOUNDRY_MODE ?? ''
|
|
375
436
|
const gate = evaluateValidationGate({
|
|
@@ -917,24 +978,56 @@ function readFileSafe(file) {
|
|
|
917
978
|
}
|
|
918
979
|
}
|
|
919
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
|
+
|
|
920
1027
|
/** @param {string} task @param {string} ecosystem */
|
|
921
1028
|
function security(task, ecosystem) {
|
|
922
1029
|
if (task === 'profile') {
|
|
923
|
-
|
|
924
|
-
writeOutput('rust', hasDependencyManifest('rust') ? 'true' : 'false')
|
|
925
|
-
writeOutput('python', hasDependencyManifest('python') ? 'true' : 'false')
|
|
926
|
-
const requirements = capture('git', ['ls-files', '*requirements*.txt'])
|
|
927
|
-
.split('\n')
|
|
928
|
-
.filter(Boolean)
|
|
929
|
-
writeOutput(
|
|
930
|
-
'python_requirements',
|
|
931
|
-
hasDependencyManifest('python')
|
|
932
|
-
? requirements.length
|
|
933
|
-
? requirements
|
|
934
|
-
: ['project']
|
|
935
|
-
: ['none']
|
|
936
|
-
)
|
|
937
|
-
writeOutput('dependency_review', featureEnabled('dependency_review') ? 'true' : 'false')
|
|
1030
|
+
for (const [key, value] of Object.entries(securityProfile())) writeOutput(key, value)
|
|
938
1031
|
return
|
|
939
1032
|
}
|
|
940
1033
|
if (task === 'should_run') {
|
|
@@ -1059,18 +1152,28 @@ function assertRustShardsAligned() {
|
|
|
1059
1152
|
}
|
|
1060
1153
|
|
|
1061
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() {
|
|
1062
1164
|
const enabled =
|
|
1063
1165
|
featureEnabled('codeql') &&
|
|
1064
1166
|
(isPublicRepository() || process.env.REPO_FOUNDRY_CODE_SECURITY === 'enabled')
|
|
1065
|
-
|
|
1167
|
+
/** @type {Record<string, string>} */
|
|
1168
|
+
const outputs = { enabled: enabled ? 'true' : 'false' }
|
|
1066
1169
|
if (!enabled) {
|
|
1067
|
-
|
|
1170
|
+
outputs.languages = '[]'
|
|
1068
1171
|
for (const language of ['actions', 'javascript', 'python', 'rust']) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1172
|
+
outputs[`${language}_available`] = 'false'
|
|
1173
|
+
outputs[`${language}_changed`] = 'false'
|
|
1174
|
+
outputs[`${language}_build_mode`] = 'none'
|
|
1072
1175
|
}
|
|
1073
|
-
return
|
|
1176
|
+
return outputs
|
|
1074
1177
|
}
|
|
1075
1178
|
// Detect lane drift before any analysis uploads a category the other lanes
|
|
1076
1179
|
// never produce; a stall surfaces only later as a blocked merge gate.
|
|
@@ -1106,14 +1209,55 @@ function codeql() {
|
|
|
1106
1209
|
rustEntry.shards = shards
|
|
1107
1210
|
rustEntry.changed = shards.some((entry) => entry.changed)
|
|
1108
1211
|
}
|
|
1109
|
-
|
|
1212
|
+
outputs.languages = JSON.stringify(languagesJson)
|
|
1110
1213
|
for (const language of ['actions', 'javascript-typescript', 'python', 'rust']) {
|
|
1111
1214
|
const entry = languagesJson.find((item) => item.language === language)
|
|
1112
1215
|
const prefix = language === 'javascript-typescript' ? 'javascript' : language
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
+
})
|
|
1116
1258
|
}
|
|
1259
|
+
if (!matrix.length) throw new Error('CodeQL analysis matrix is empty while analysis is enabled.')
|
|
1260
|
+
return matrix
|
|
1117
1261
|
}
|
|
1118
1262
|
|
|
1119
1263
|
/**
|