code-foundry 1.53.5 → 1.53.6

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.
@@ -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:
@@ -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: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true && inputs.integration-total-shards != '2'
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: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true && inputs.integration-total-shards == '2'
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: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true
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: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true
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,15 @@ 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: ''
118
127
  secrets:
119
128
  TURBO_TOKEN:
120
129
  required: false
@@ -787,7 +796,11 @@ jobs:
787
796
 
788
797
  e2e:
789
798
  name: E2E
790
- if: vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit'
799
+ # The gate already accepts a skipped E2E lane; a planner-idle E2E lane
800
+ # skips here instead of allocating a runner for a no-op.
801
+ if: >-
802
+ vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit' &&
803
+ !contains(format(',{0},', inputs.idle-lanes), ',e2e,')
791
804
  permissions:
792
805
  actions: read
793
806
  contents: read
@@ -75,6 +75,15 @@ 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: ''
78
87
  artifact-prefix:
79
88
  description: Prefix for task receipts; use a unique value when calling this workflow more than once.
80
89
  required: false
@@ -181,6 +190,7 @@ jobs:
181
190
  integration-total-shards: ${{ inputs.integration-total-shards }}
182
191
  e2e-shard-list: ${{ inputs.e2e-shard-list }}
183
192
  e2e-total-shards: ${{ inputs.e2e-total-shards }}
193
+ idle-lanes: ${{ inputs.idle-lanes }}
184
194
  secrets:
185
195
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
186
196
 
@@ -209,6 +219,7 @@ jobs:
209
219
  runtime-ref: ${{ inputs.runtime-ref }}
210
220
  runner: ${{ inputs.eval-runner }}
211
221
  artifact-prefix: ${{ inputs.artifact-prefix }}
222
+ idle-lanes: ${{ inputs.idle-lanes }}
212
223
 
213
224
  # Aggregate check: "Validation / Gate" (caller job name + this job name).
214
225
  # Always evaluates; only jobs required for the mode must have succeeded.
@@ -229,7 +240,11 @@ jobs:
229
240
  FOUNDRY_CI: ${{ needs.ci.result }}
230
241
  FOUNDRY_TEST: ${{ needs.test.result }}
231
242
  FOUNDRY_SECURITY: ${{ needs.security.result }}
232
- FOUNDRY_EVAL: ${{ needs.eval.result }}
243
+ # An eval lane the caller planned as idle (no eval script, no
244
+ # eval_command, or eval: false) never starts, so the whole Eval
245
+ # workflow reports skipped. Count that exactly like the no-op success
246
+ # it reported before; any other skip still fails the gate.
247
+ FOUNDRY_EVAL: ${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}
233
248
  FOUNDRY_SOURCE_QUALIFICATION: ${{ needs.qualification.outputs.reuse }}
234
249
  FOUNDRY_QUALIFICATION: ${{ needs.qualification.result }}
235
250
  # This workflow is selected only for repositories whose canonical
@@ -276,7 +291,9 @@ jobs:
276
291
  inputs.release-qualification-receipts == true &&
277
292
  github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
278
293
  needs.ci.result == 'success' && needs.test.result == 'success' &&
279
- needs.security.result == 'success' && needs.eval.result == 'success' &&
294
+ needs.security.result == 'success' &&
295
+ (needs.eval.result == 'success' ||
296
+ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
280
297
  needs.gate.result == 'success'
281
298
  permissions:
282
299
  actions: read
@@ -291,5 +308,5 @@ jobs:
291
308
  runtime-repository: ${{ inputs.runtime-repository }}
292
309
  runtime-ref: ${{ inputs.runtime-ref }}
293
310
  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 }}"}
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 }}"}
295
312
  required-job-names-json: '["Validation / Gate"]'
@@ -90,6 +90,15 @@ 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: ''
93
102
  artifact-prefix:
94
103
  description: Prefix for task receipts; use a unique value when calling this workflow more than once.
95
104
  required: false
@@ -181,6 +190,7 @@ jobs:
181
190
  integration-total-shards: ${{ inputs.integration-total-shards }}
182
191
  e2e-shard-list: ${{ inputs.e2e-shard-list }}
183
192
  e2e-total-shards: ${{ inputs.e2e-total-shards }}
193
+ idle-lanes: ${{ inputs.idle-lanes }}
184
194
  secrets:
185
195
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
186
196
 
@@ -228,6 +238,7 @@ jobs:
228
238
  runtime-ref: ${{ inputs.runtime-ref }}
229
239
  runner: ${{ inputs.eval-runner }}
230
240
  artifact-prefix: ${{ inputs.artifact-prefix }}
241
+ idle-lanes: ${{ inputs.idle-lanes }}
231
242
 
232
243
  # Aggregate check: "Validation / Gate" (caller job name + this job name).
233
244
  # Always evaluates; only jobs required for the mode must have succeeded.
@@ -249,7 +260,11 @@ jobs:
249
260
  FOUNDRY_TEST: ${{ needs.test.result }}
250
261
  FOUNDRY_SECURITY: ${{ needs.security.result }}
251
262
  FOUNDRY_CODEQL: ${{ needs.codeql.result }}
252
- FOUNDRY_EVAL: ${{ needs.eval.result }}
263
+ # An eval lane the caller planned as idle (no eval script, no
264
+ # eval_command, or eval: false) never starts, so the whole Eval
265
+ # workflow reports skipped. Count that exactly like the no-op success
266
+ # it reported before; any other skip still fails the gate.
267
+ FOUNDRY_EVAL: ${{ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,') && 'success') || needs.eval.result }}
253
268
  FOUNDRY_SOURCE_QUALIFICATION: ${{ needs.qualification.outputs.reuse }}
254
269
  FOUNDRY_QUALIFICATION: ${{ needs.qualification.result }}
255
270
  steps:
@@ -293,7 +308,9 @@ jobs:
293
308
  github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' &&
294
309
  needs.ci.result == 'success' && needs.test.result == 'success' &&
295
310
  needs.security.result == 'success' && needs.codeql.result == 'success' &&
296
- needs.eval.result == 'success' && needs.gate.result == 'success'
311
+ (needs.eval.result == 'success' ||
312
+ (needs.eval.result == 'skipped' && contains(format(',{0},', inputs.idle-lanes), ',eval,'))) &&
313
+ needs.gate.result == 'success'
297
314
  permissions:
298
315
  actions: read
299
316
  contents: read
@@ -307,5 +324,5 @@ jobs:
307
324
  runtime-repository: ${{ inputs.runtime-repository }}
308
325
  runtime-ref: ${{ inputs.runtime-ref }}
309
326
  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 }}"}
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 }}"}
311
328
  required-job-names-json: '["Validation / Gate"]'
@@ -34,6 +34,7 @@ jobs:
34
34
  contents: read
35
35
  outputs:
36
36
  mode: ${{ steps.classify.outputs.mode }}
37
+ idle-lanes: ${{ steps.lanes.outputs.idle_lanes }}
37
38
  steps:
38
39
  # The runtime classifies policy from the consumer repository's config.
39
40
  # Keep this checkout before the isolated runtime checkout below.
@@ -61,6 +62,12 @@ jobs:
61
62
  FOUNDRY_BASE_REF: ${{ github.event.pull_request.base.ref }}
62
63
  FOUNDRY_HEAD_REF: ${{ github.event.pull_request.head.ref }}
63
64
  run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation mode >> "$GITHUB_OUTPUT"
65
+ # Lanes without an entrypoint (no package script, configured command,
66
+ # or test files) are skipped by the orchestrator rather than started on
67
+ # a runner only to report a no-op. Detection errors keep the lane.
68
+ - name: Plan lanes
69
+ id: lanes
70
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" validation lanes >> "$GITHUB_OUTPUT"
64
71
 
65
72
  # The stable aggregate check name is the caller job name plus the
66
73
  # orchestrator gate job name: "Validation / Gate". Keep both names fixed.
@@ -107,6 +114,7 @@ jobs:
107
114
  integration-total-shards: '1'
108
115
  e2e-shard-list: '1'
109
116
  e2e-total-shards: '1'
117
+ idle-lanes: ${{ needs.mode.outputs.idle-lanes }}
110
118
  secrets:
111
119
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
112
120
  NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.53.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.5...v1.53.6) (2026-10-31)
4
+
5
+
6
+ ### Performance
7
+
8
+ * **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))
9
+
3
10
  ## [1.53.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.53.4...v1.53.5) (2026-10-31)
4
11
 
5
12
 
@@ -143,16 +150,9 @@
143
150
 
144
151
  * support isolated integration test shards ([#783](https://github.com/0xPlayerOne/code-foundry/issues/783)) ([ad1123f](https://github.com/0xPlayerOne/code-foundry/commit/ad1123ff3bb9654bfbdaef4219490cca3e7e3d20))
145
152
 
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; 276 older releases are omitted to keep the package small.
157
+ This packaged changelog lists the 20 most recent releases; 277 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/WORKFLOWS.md CHANGED
@@ -310,6 +310,16 @@ 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 `Mode` job also plans lanes: it runs the same applicability check
314
+ each lane's Detect step runs and passes the integration, E2E, smoke,
315
+ performance, and eval lanes that have no package script, configured command,
316
+ or test files to the orchestrator as `idle-lanes`. Those jobs are skipped
317
+ instead of queuing a runner only to report a no-op. Unit always keeps its job,
318
+ and a lane whose detection errors keeps its job so the error surfaces there.
319
+ The gate counts a planned idle Eval workflow (which reports `skipped`) as the
320
+ no-op success it reported before; any other skip, and every lane with work,
321
+ is still required. Callers that pass no `idle-lanes` run every lane as before.
322
+
313
323
  ## Security behavior
314
324
 
315
325
  CodeQL is a separate workflow using GitHub's official actions. The default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.53.5",
3
+ "version": "1.53.6",
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": {
@@ -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,22 @@ 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
+ }
373
396
  if (task === 'gate') {
374
397
  const mode = process.env.FOUNDRY_MODE ?? ''
375
398
  const gate = evaluateValidationGate({