code-foundry 1.46.2 → 1.46.4

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.
@@ -447,6 +447,9 @@ jobs:
447
447
  # when it validated, github.token after fallback. The token value
448
448
  # lives only in this masked env expression, never in step outputs.
449
449
  AUTO_MERGE: ${{ steps.credentials.outputs.auto_merge }}
450
+ # Read Actions ownership with the workflow credential, not an
451
+ # automation token that may only have release/PR write permissions.
452
+ VALIDATION_TOKEN: ${{ github.token }}
450
453
  GH_TOKEN: ${{ steps.credentials.outputs.token_source == 'configured' && (secrets.CODE_FOUNDRY_TOKEN) || github.token }}
451
454
  run: |
452
455
  set -euo pipefail
@@ -464,12 +467,87 @@ jobs:
464
467
  fi
465
468
 
466
469
  for pr in "${release_prs[@]}"; do
467
- PR_DRAFT=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')
470
+ pr_state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" \
471
+ --json isDraft,headRefOid,headRefName \
472
+ --jq '[.isDraft, .headRefOid, .headRefName] | @tsv')
473
+ read -r PR_DRAFT PR_HEAD PR_BRANCH <<< "$pr_state"
474
+ if [[ ! "$PR_DRAFT" =~ ^(true|false)$ || ! "$PR_HEAD" =~ ^[a-f0-9]{40}$ ]]; then
475
+ echo 'Unable to verify generated release PR state.' >&2
476
+ exit 1
477
+ fi
478
+ case "$PR_BRANCH" in
479
+ release-please--branches--main|release-please--branches--main--*) ;;
480
+ *) echo 'Release PR head left the generated namespace; refusing readiness mutation.' >&2; exit 1 ;;
481
+ esac
468
482
  if [ "$AUTO_MERGE" = true ]; then
469
483
  if [ "$PR_DRAFT" = false ]; then
470
- # A Release Please PR can be updated while already ready. Reset
471
- # it so ready_for_review always starts validation for the exact
472
- # current release head.
484
+ # A ready head already receives synchronize validation. Preserve
485
+ # its live/completed owner instead of cancelling and reallocating
486
+ # CI every time another queued main release run prepares it.
487
+ runs=$(GH_TOKEN="$VALIDATION_TOKEN" gh api \
488
+ "repos/$GITHUB_REPOSITORY/actions/runs" --method GET \
489
+ -f event=pull_request -f head_sha="$PR_HEAD" -f per_page=100)
490
+ validation=$(jq -er --arg head "$PR_HEAD" --arg branch "$PR_BRANCH" \
491
+ --arg repo "$GITHUB_REPOSITORY" '
492
+ def active: .status == "queued" or .status == "in_progress" or
493
+ .status == "waiting" or .status == "pending" or .status == "requested";
494
+ def valid_run:
495
+ type == "object" and
496
+ (.id | type == "number" and . > 0 and floor == .) and
497
+ (.run_attempt | type == "number" and . >= 1 and floor == .) and
498
+ (.head_sha | type == "string" and test("^[a-f0-9]{40}$")) and
499
+ (.head_branch | type == "string" and length > 0) and
500
+ (.event | type == "string" and length > 0) and
501
+ (.repository | type == "object") and
502
+ (.repository.full_name | type == "string" and length > 0) and
503
+ (.path | type == "string" and length > 0) and
504
+ ((active and has("conclusion") and .conclusion == null) or
505
+ (.status == "completed" and
506
+ (.conclusion == "success" or .conclusion == "failure" or
507
+ .conclusion == "timed_out" or .conclusion == "action_required" or
508
+ .conclusion == "cancelled" or .conclusion == "skipped" or
509
+ .conclusion == "neutral")));
510
+ if (.workflow_runs | type) != "array" or
511
+ (.total_count | type) != "number" then
512
+ error("Invalid validation owner response")
513
+ elif .total_count != (.workflow_runs | length) then
514
+ error("Incomplete validation owner response")
515
+ elif any(.workflow_runs[]; valid_run | not) then
516
+ error("Invalid validation run identity or state")
517
+ else
518
+ [.workflow_runs[] | (.path | split("@")[0]) as $workflow | select(
519
+ .head_sha == $head and .head_branch == $branch and
520
+ .event == "pull_request" and .repository.full_name == $repo and
521
+ ($workflow == ".github/workflows/validation.yml" or
522
+ $workflow == ".github/workflows/validation_self-ci.yml")
523
+ )] | sort_by(.id) | last |
524
+ if . == null then "missing"
525
+ elif active or (.status == "completed" and .conclusion == "success") then "owned"
526
+ elif .status == "completed" and
527
+ (.conclusion == "failure" or .conclusion == "timed_out" or
528
+ .conclusion == "action_required") then "failed"
529
+ elif .status == "completed" and
530
+ (.conclusion == "cancelled" or .conclusion == "skipped" or
531
+ .conclusion == "neutral") then "missing"
532
+ else error("Unconfirmed validation owner state") end
533
+ end' <<< "$runs")
534
+ fresh_state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" \
535
+ --json isDraft,headRefOid,headRefName \
536
+ --jq '[.isDraft, .headRefOid, .headRefName] | @tsv')
537
+ if [ "$fresh_state" != "$pr_state" ]; then
538
+ echo 'Release PR state changed during validation lookup; refusing a stale readiness mutation.' >&2
539
+ exit 1
540
+ fi
541
+ if [ "$validation" = owned ]; then
542
+ echo "Release PR #$pr already has current-head validation; preserving readiness."
543
+ continue
544
+ fi
545
+ if [ "$validation" = failed ]; then
546
+ echo "Current-head validation failed for release PR #$pr; resolve or explicitly retry it before release." >&2
547
+ exit 1
548
+ fi
549
+ # A missing, cancelled or skipped owner still needs the existing
550
+ # explicit ready_for_review transition with the validated token.
473
551
  gh pr ready --undo "$pr" --repo "$GITHUB_REPOSITORY"
474
552
  fi
475
553
  gh pr ready "$pr" --repo "$GITHUB_REPOSITORY"
@@ -227,17 +227,6 @@ jobs:
227
227
  HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
228
228
  BEFORE_SHA: ${{ github.event.before }}
229
229
  run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run eval >> "$GITHUB_OUTPUT"
230
- # --- Lint lane (mirror of ci.yml lint) ---
231
- - name: Detect lint
232
- id: lint_applicability
233
- if: ${{ !cancelled() }}
234
- env:
235
- EVENT_NAME: ${{ github.event_name }}
236
- BASE_REF: ${{ github.event.pull_request.base.ref }}
237
- BASE_SHA: ${{ github.event.pull_request.base.sha }}
238
- HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
239
- BEFORE_SHA: ${{ github.event.before }}
240
- run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run lint >> "$GITHUB_OUTPUT"
241
230
  # --- Integration lane (mirror of test.yml integration; audit tier only) ---
242
231
  - name: Detect integration
243
232
  id: integration_applicability
@@ -314,13 +303,6 @@ jobs:
314
303
  install: 'true'
315
304
  cache-build: 'true'
316
305
  task: eval
317
- - name: Setup lint
318
- if: ${{ !cancelled() && steps.lint_applicability.outputs.applicable == 'true' && steps.lint_applicability.outputs.affected != 'false' }}
319
- uses: ./.github/actions/setup
320
- with:
321
- install: 'true'
322
- cache-lint: 'true'
323
- task: lint
324
306
  - name: Setup integration
325
307
  if: ${{ !cancelled() && inputs.mode == 'audit' && steps.integration_applicability.outputs.applicable == 'true' && steps.integration_applicability.outputs.affected != 'false' }}
326
308
  uses: ./.github/actions/setup
@@ -365,11 +347,6 @@ jobs:
365
347
  id: eval_execute
366
348
  if: ${{ !cancelled() && inputs.mode == 'audit' && steps.eval_applicability.outputs.applicable == 'true' && steps.eval_applicability.outputs.affected != 'false' }}
367
349
  run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci eval
368
- # --- Lint execute ---
369
- - name: Lint
370
- id: lint_execute
371
- if: ${{ !cancelled() && steps.lint_applicability.outputs.applicable == 'true' && steps.lint_applicability.outputs.affected != 'false' }}
372
- run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci lint
373
350
  # --- Integration execute ---
374
351
  - name: Integration
375
352
  id: integration_execute
@@ -477,19 +454,6 @@ jobs:
477
454
  eval-results/summary.json
478
455
  if-no-files-found: error
479
456
  retention-days: 14
480
- - name: Retain lint task receipt
481
- if: >-
482
- always() &&
483
- (steps.lint_execute.outcome == 'success' || steps.lint_execute.outcome == 'failure' ||
484
- steps.lint_applicability.outputs.applicable == 'false' || steps.lint_applicability.outputs.affected == 'false') &&
485
- hashFiles('.code-foundry/results/lint.json') != ''
486
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
487
- with:
488
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-lint
489
- path: .code-foundry/results/lint.json
490
- include-hidden-files: true
491
- if-no-files-found: error
492
- retention-days: 14
493
457
  - name: Retain integration task receipt
494
458
  if: >-
495
459
  always() &&
@@ -505,6 +469,67 @@ jobs:
505
469
  if-no-files-found: error
506
470
  retention-days: 14
507
471
 
472
+ lint:
473
+ name: Lint
474
+ if: vars.CI_BILLING_PAUSED != 'true'
475
+ runs-on: ${{ inputs.ci-runner }}
476
+ timeout-minutes: 30
477
+ steps:
478
+ - name: Checkout
479
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
480
+ with:
481
+ persist-credentials: false
482
+ - name: Runtime
483
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
484
+ with:
485
+ persist-credentials: false
486
+ repository: ${{ inputs.runtime-repository }}
487
+ ref: ${{ inputs.runtime-ref }}
488
+ path: .github/.code-foundry
489
+ sparse-checkout: |
490
+ .github/actions
491
+ src/lib
492
+ src/runtime-core.mjs
493
+ src/runtime.mjs
494
+ - name: Install runtime
495
+ run: |
496
+ mkdir -p .github/actions
497
+ mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
498
+ cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
499
+ - name: Detect
500
+ id: applicability
501
+ env:
502
+ EVENT_NAME: ${{ github.event_name }}
503
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
504
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
505
+ HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
506
+ BEFORE_SHA: ${{ github.event.before }}
507
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run lint >> "$GITHUB_OUTPUT"
508
+ - name: Setup
509
+ if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.affected != 'false'
510
+ uses: ./.github/actions/setup
511
+ with:
512
+ install: 'true'
513
+ cache-lint: 'true'
514
+ task: lint
515
+ - name: Lint
516
+ id: execute
517
+ if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.affected != 'false'
518
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci lint
519
+ - name: Retain task receipt
520
+ if: >-
521
+ always() &&
522
+ (steps.execute.outcome == 'success' || steps.execute.outcome == 'failure' ||
523
+ steps.applicability.outputs.applicable == 'false' || steps.applicability.outputs.affected == 'false') &&
524
+ hashFiles('.code-foundry/results/lint.json') != ''
525
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
526
+ with:
527
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-lint
528
+ path: .code-foundry/results/lint.json
529
+ include-hidden-files: true
530
+ if-no-files-found: error
531
+ retention-days: 14
532
+
508
533
  type-check:
509
534
  name: Type-Check
510
535
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -1074,7 +1099,7 @@ jobs:
1074
1099
  # evaluateValidationGate's mode truth table is reused unchanged.
1075
1100
  gate:
1076
1101
  name: Gate
1077
- needs: [fast-lanes, type-check, unit, e2e, audit, codeql]
1102
+ needs: [fast-lanes, lint, type-check, unit, e2e, audit, codeql]
1078
1103
  if: vars.CI_BILLING_PAUSED != 'true' && always()
1079
1104
  runs-on: ubuntu-slim
1080
1105
  timeout-minutes: 10
@@ -1082,7 +1107,7 @@ jobs:
1082
1107
  FOUNDRY_MODE: ${{ inputs.mode }}
1083
1108
  # CI category: lint and type-check always run; the merged fast-lanes job
1084
1109
  # carries the format and build lanes.
1085
- FOUNDRY_CI: ${{ (needs.type-check.result != 'success' && needs.type-check.result) || (needs.fast-lanes.result != 'success' && needs.fast-lanes.result) || 'success' }}
1110
+ FOUNDRY_CI: ${{ (needs.lint.result != 'success' && needs.lint.result) || (needs.type-check.result != 'success' && needs.type-check.result) || (needs.fast-lanes.result != 'success' && needs.fast-lanes.result) || 'success' }}
1086
1111
  # Test category: unit always runs and must succeed; integration and e2e
1087
1112
  # are audit-tier jobs whose expected skip on fast/release is not a
1088
1113
  # failure; the merged fast-lanes job carries performance, smoke, and
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.46.4](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.3...v1.46.4) (2026-10-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **release:** preserve current-head validation ownership ([#737](https://github.com/0xPlayerOne/code-foundry/issues/737)) ([aa8bfcf](https://github.com/0xPlayerOne/code-foundry/commit/aa8bfcfbfcd3c213d15ba9e1fbfb5c65ab8ddb6b))
9
+
10
+ ## [1.46.3](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.2...v1.46.3) (2026-10-07)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **billing:** keep lint standalone; join fast-lane receipts in parallel ([#744](https://github.com/0xPlayerOne/code-foundry/issues/744)) ([574690b](https://github.com/0xPlayerOne/code-foundry/commit/574690b707abffe9350ccc67589540afb82323ee))
16
+
3
17
  ## [1.46.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.46.1...v1.46.2) (2026-10-07)
4
18
 
5
19
 
@@ -126,23 +140,9 @@
126
140
 
127
141
  * **pre-commit:** make the commit gate change-aware ([#702](https://github.com/0xPlayerOne/code-foundry/issues/702)) ([f5bca91](https://github.com/0xPlayerOne/code-foundry/commit/f5bca91c177b30b99f5d62287255d191617b4788))
128
142
 
129
- ## [1.43.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.1...v1.43.2) (2026-10-03)
130
-
131
-
132
- ### Bug Fixes
133
-
134
- * require candidate asset qualification before stable promotion ([#705](https://github.com/0xPlayerOne/code-foundry/issues/705)) ([f42e4f5](https://github.com/0xPlayerOne/code-foundry/commit/f42e4f5fb063ea22fa25791a13c75c90d41bf17a))
135
-
136
- ## [1.43.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.0...v1.43.1) (2026-10-03)
137
-
138
-
139
- ### Bug Fixes
140
-
141
- * **release:** make batched publication and stable promotion reliable ([#703](https://github.com/0xPlayerOne/code-foundry/issues/703)) ([271731d](https://github.com/0xPlayerOne/code-foundry/commit/271731d8fac3bff42a99b3d5f4a774eccc0a7788))
142
-
143
143
  <!-- code-foundry: packaged changelog -->
144
144
 
145
145
  ---
146
146
 
147
- This packaged changelog lists the 20 most recent releases; 237 older releases are omitted to keep the package small.
147
+ This packaged changelog lists the 20 most recent releases; 239 older releases are omitted to keep the package small.
148
148
  See the [full changelog](https://github.com/0xPlayerOne/code-foundry/blob/main/CHANGELOG.md) for the complete history.
package/docs/WORKFLOWS.md CHANGED
@@ -82,6 +82,23 @@ to run generated gates for drafts. Cloudflare callers use their equivalent
82
82
  `draft-protection: false` input. These opt-outs do not remove Draft Guard or
83
83
  draft-PR automation; they only allow the protected gates to run for drafts.
84
84
 
85
+ Generated release PR preparation preserves a ready head when the canonical
86
+ generated `.github/workflows/validation.yml` (or Code Foundry
87
+ `validation_self-ci.yml`) pull-request run for that exact head, branch, and
88
+ repository is the newest matching run and is active or successful. API workflow
89
+ paths may include a Git ref suffix; ownership compares the canonical filename.
90
+ The workflow credential reads Actions ownership; the validated automation credential still performs readiness writes.
91
+ A newer failed qualification blocks release preparation until it is resolved or
92
+ explicitly retried. Missing, cancelled, or skipped validation still receives the
93
+ explicit draft/ready transition; a new draft receives its first readiness event.
94
+ An unavailable or malformed ownership response, an incomplete Actions page, or a
95
+ PR state change during the lookup fails before mutation. An older live run cannot
96
+ mask a newer failed qualification. Invalid automation credentials retain the
97
+ manual draft fallback. This changes readiness only: release creation, qualification,
98
+ tag publication, and branch-policy-gated exact-head merging still run through
99
+ the existing release pipeline. Consumers must adopt the fixed reusable workflow
100
+ pin before they receive this behavior; older pins still reset ready heads.
101
+
85
102
  ## Billing pause
86
103
 
87
104
  Code Foundry consumers can stop all generated jobs from allocating GitHub-hosted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.46.2",
3
+ "version": "1.46.4",
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": {