code-foundry 1.33.1 → 1.33.2

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.
@@ -260,7 +260,7 @@ jobs:
260
260
  --state open \
261
261
  --base main \
262
262
  --json number,headRefName \
263
- --jq '.[] | select(.headRefName | startswith("release-please--branches--main")) | .number'
263
+ --jq '.[] | select(.headRefName == "release-please--branches--main" or (.headRefName | startswith("release-please--branches--main--"))) | .number'
264
264
  )
265
265
 
266
266
  if [ ${#release_prs[@]} -eq 0 ]; then
@@ -301,71 +301,40 @@ jobs:
301
301
  --state open \
302
302
  --base main \
303
303
  --json number,title,headRefName \
304
- --jq '.[] | select(.title | startswith("chore(main): release ")) | select(.headRefName | startswith("release-please--branches--main")) | .number'
304
+ --jq '.[] | select(.title | startswith("chore(main): release ")) | select(.headRefName == "release-please--branches--main" or (.headRefName | startswith("release-please--branches--main--"))) | .number'
305
305
  )
306
306
  if [ "${#release_prs[@]}" -eq 0 ]; then
307
307
  echo 'No generated release pull request is open; nothing to merge.'
308
308
  exit 0
309
309
  fi
310
- node "$RUNNER_TEMP/code-foundry/src/cli.mjs" release validate-prs
310
+ release_validation=$(node "$RUNNER_TEMP/code-foundry/src/cli.mjs" release validate-prs)
311
+ printf '%s\n' "$release_validation"
311
312
  for pr in "${release_prs[@]}"; do
312
- # GitHub's merge state is the authoritative required-check gate.
313
- # `gh pr checks --required` is not reliable for ruleset-backed
314
- # required contexts in private repositories: it can return an
315
- # empty result even while the required aggregate check is present
316
- # and green. Polling mergeStateStatus avoids that discovery race;
317
- # GitHub reports MERGEABLE only after branch policy permits the
318
- # requested merge strategy. Non-required checks may leave the PR
319
- # UNSTABLE, so accept that state only with mergeable MERGEABLE.
320
- echo "Waiting for release PR #$pr to become mergeable under branch policy"
321
- merge_state=UNKNOWN
322
- mergeable=UNKNOWN
323
- for attempt in $(seq 1 90); do
324
- : "$attempt"
325
- if read -r merge_state mergeable < <(
326
- gh pr view "$pr" \
327
- --repo "$GITHUB_REPOSITORY" \
328
- --json mergeStateStatus,mergeable \
329
- --jq '[.mergeStateStatus, .mergeable] | @tsv' 2>/dev/null
330
- ); then
331
- case "$merge_state" in
332
- CLEAN|UNSTABLE)
333
- if [ "$mergeable" = MERGEABLE ]; then
334
- break
335
- fi
336
- ;;
337
- DIRTY)
338
- echo "Release PR #$pr is not mergeable (mergeStateStatus=$merge_state); resolve conflicts before retrying the release." >&2
339
- exit 1
340
- ;;
341
- esac
342
- if [ "$mergeable" = CONFLICTING ]; then
343
- echo "Release PR #$pr is not mergeable (mergeable=$mergeable); resolve conflicts before retrying the release." >&2
344
- exit 1
345
- fi
346
- else
347
- # gh API failure: read leaves the variables empty on EOF;
348
- # treat the state as unknown and keep polling.
349
- merge_state=UNKNOWN
350
- mergeable=UNKNOWN
351
- fi
352
- sleep 10
353
- done
354
- if { [ "$merge_state" != CLEAN ] && [ "$merge_state" != UNSTABLE ]; } || [ "$mergeable" != MERGEABLE ]; then
355
- echo "Release PR #$pr did not become mergeable within the mergeability window (last mergeStateStatus=$merge_state, mergeable=$mergeable). Required checks may still be pending or branch policy blocks the merge; review the pull request before retrying." >&2
356
- exit 1
357
- fi
358
- release_head=$(gh pr view "$pr" \
313
+ # Keep the generated-PR audit above, then let GitHub wait for the
314
+ # actual branch policy. A synchronous poll here made a healthy
315
+ # release fail when required workflows outlasted the runner's
316
+ # mergeability window. --auto does not bypass checks or reviews;
317
+ # GitHub merges only after every required condition is satisfied.
318
+ echo "Requesting branch-policy-gated auto-merge for release PR #$pr"
319
+ release_head=$(jq -er --arg number "$pr" \
320
+ '.generated[] | select((.number | tostring) == $number) | .headRefOid | strings | select(length > 0)' \
321
+ <<< "$release_validation")
322
+ current_release_head=$(gh pr view "$pr" \
359
323
  --repo "$GITHUB_REPOSITORY" \
360
324
  --json headRefOid \
361
325
  --jq '.headRefOid')
362
- if [ -z "$release_head" ]; then
363
- echo "Could not resolve head commit SHA for release PR #$pr" >&2
326
+ if [ -z "$release_head" ] || [ "$current_release_head" != "$release_head" ]; then
327
+ echo "Release PR #$pr changed after its path diff was validated; refusing to enqueue a stale validation result." >&2
364
328
  exit 1
365
329
  fi
330
+ # --match-head-commit binds this queue request. GitHub may keep
331
+ # auto-merge enabled if Release Please later updates the branch;
332
+ # the latest head must still pass the required Validation / Gate,
333
+ # including its release-diff policy, before GitHub can merge it.
366
334
  gh pr merge "$pr" \
367
335
  --repo "$GITHUB_REPOSITORY" \
368
336
  --${{ steps.profile.outputs.release_merge_strategy }} \
337
+ --auto \
369
338
  --match-head-commit "$release_head" \
370
339
  --delete-branch
371
340
  done
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.33.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.33.1...v1.33.2) (2026-09-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **release:** queue version PR merges on branch policy ([#646](https://github.com/0xPlayerOne/code-foundry/issues/646)) ([ca492c8](https://github.com/0xPlayerOne/code-foundry/commit/ca492c8402bfc43ef718c0a6443ee06fcb33e957))
9
+
3
10
  ## [1.33.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.33.0...v1.33.1) (2026-09-25)
4
11
 
5
12
 
package/docs/RELEASES.md CHANGED
@@ -128,15 +128,29 @@ repository or organization secret to enable guarded
128
128
  automatic merging and downstream workflows triggered by the resulting
129
129
  release. The token needs `contents`, `issues`, and `pull-requests` write
130
130
  permissions. Code Foundry validates every changed path in the generated
131
- version pull request before using the token to merge it, waits for the
132
- required checks to finish, and then polls the pull request's
133
- `mergeStateStatus` until it is `CLEAN`, or `UNSTABLE` with `mergeable`
134
- `MERGEABLE`, before merging. Non-required checks that branch policy does not
135
- require (for example external code review still running after every required
136
- check passed) do not block the merge; conflicts fail immediately and a bounded
137
- polling window fails closed if branch or ruleset policy still blocks the merge
138
- (a ruleset may register additional required checks after earlier ones have
139
- already passed).
131
+ version pull request before requesting its merge with GitHub's auto-merge
132
+ feature and the configured canonical method (`squash` for `direct`, `rebase`
133
+ for `staging-release`). GitHub performs the merge only after the branch's
134
+ required checks and reviews pass; the release runner exits after submitting
135
+ that request rather than waiting for every workflow to finish. This keeps a
136
+ healthy release from failing just because required checks outlast a bounded
137
+ runner-side wait. No checks, reviews, or branch rules are bypassed. The
138
+ request uses the exact head SHA returned by Code Foundry's path audit and is
139
+ refused if the branch changed during that audit.
140
+
141
+ Repositories using a valid `CODE_FOUNDRY_TOKEN` must allow auto-merge in their
142
+ GitHub settings when release checks may still be pending. GitHub rejects the
143
+ request if auto-merge is unavailable. If the PR is already eligible, the same
144
+ command merges it immediately under branch policy. A head change detected
145
+ between the path audit and enqueueing fails the release job. Conflicts remain
146
+ subject to GitHub's branch policy and keep the merge from completing until
147
+ resolved. GitHub's
148
+ automatic head-branch deletion setting removes branches after deferred merges;
149
+ the CLI's `--delete-branch` option also removes a branch when the merge completes
150
+ immediately. GitHub can keep auto-merge enabled if a later Release Please run
151
+ updates the branch. With the documented required `Validation / Gate`, each
152
+ synchronized head reruns the release-diff policy, and GitHub waits for that
153
+ head's required checks before merging.
140
154
 
141
155
  ## Operational checklist
142
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.33.1",
3
+ "version": "1.33.2",
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": {
@@ -783,7 +783,7 @@ export function validateReleasePullRequestDiffs(root) {
783
783
  '--base',
784
784
  'main',
785
785
  '--json',
786
- 'number,title,headRefName',
786
+ 'number,title,headRefName,headRefOid',
787
787
  ])
788
788
  prs = Array.isArray(result) ? result : []
789
789
  if (selectGeneratedReleasePrs(prs).length) break
@@ -792,6 +792,8 @@ export function validateReleasePullRequestDiffs(root) {
792
792
  const generated = selectGeneratedReleasePrs(Array.isArray(prs) ? prs : [])
793
793
  /** @type {Map<number, string[]>} */
794
794
  const paths = new Map()
795
+ /** @type {string[]} */
796
+ const headErrors = []
795
797
  for (const pr of generated) {
796
798
  const number = Number(pr.number)
797
799
  const result = spawnSync(
@@ -800,12 +802,36 @@ export function validateReleasePullRequestDiffs(root) {
800
802
  { cwd: resolve(root), encoding: 'utf8' }
801
803
  )
802
804
  paths.set(number, result.status === 0 ? result.stdout.split(/\r?\n/).filter(Boolean) : [])
805
+
806
+ const auditedHead = typeof pr.headRefOid === 'string' ? pr.headRefOid : ''
807
+ const currentPr = ghJson(root, [
808
+ 'pr',
809
+ 'view',
810
+ String(number),
811
+ '--repo',
812
+ repository,
813
+ '--json',
814
+ 'headRefOid',
815
+ ])
816
+ const currentHead =
817
+ currentPr && typeof currentPr === 'object' && !Array.isArray(currentPr)
818
+ ? /** @type {{ headRefOid?: unknown }} */ (currentPr).headRefOid
819
+ : undefined
820
+ if (!auditedHead || currentHead !== auditedHead) {
821
+ headErrors.push(
822
+ `Generated release PR #${number} changed head while its path diff was being validated; rerun release validation before merging.`
823
+ )
824
+ }
803
825
  }
804
826
  const validation = validateReleasePullRequests(
805
827
  Array.isArray(prs) ? prs : [],
806
828
  paths,
807
829
  approvedReleaseFiles(readReleaseConfig(root))
808
830
  )
831
+ if (headErrors.length) {
832
+ validation.valid = false
833
+ validation.errors.push(...headErrors)
834
+ }
809
835
  console.log(JSON.stringify(validation, null, 2))
810
836
  if (!validation.valid) throw new Error(validation.errors.join(' '))
811
837
  return validation
@@ -173,18 +173,18 @@ export function selectReleaseCredential(input) {
173
173
  return { token: '', source: 'missing', autoMerge: false }
174
174
  }
175
175
 
176
- /** @param {Array<{number?: number, title?: string, headRefName?: string}>} prs */
176
+ /** @param {Array<{number?: number, title?: string, headRefName?: string, headRefOid?: string}>} prs */
177
177
  export function selectGeneratedReleasePrs(prs) {
178
178
  return prs.filter(
179
179
  (pr) =>
180
180
  String(pr.title ?? '').startsWith('chore(main): release ') &&
181
- String(pr.headRefName ?? '').startsWith('release-please--branches--main')
181
+ isReleasePleaseHead(pr.headRefName)
182
182
  )
183
183
  }
184
184
 
185
185
  /**
186
186
  * Validate all generated release PR diffs before any merge is attempted.
187
- * @param {Array<{number?: number, title?: string, headRefName?: string}>} prs
187
+ * @param {Array<{number?: number, title?: string, headRefName?: string, headRefOid?: string}>} prs
188
188
  * @param {Map<number, string[]>} changedPathsByPr
189
189
  * @param {Set<string>} allowed
190
190
  */