code-foundry 0.38.0 → 0.38.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.
@@ -473,7 +473,7 @@ runs:
473
473
  steps.profile.outputs.javascript_package_manager == 'bun'
474
474
  uses: oven-sh/setup-bun@v2
475
475
  with:
476
- bun-version: 1.3.14
476
+ bun-version: 1.4.0
477
477
 
478
478
  - name: Enable Corepack
479
479
  if: >-
@@ -15,11 +15,12 @@ on:
15
15
  required: false
16
16
 
17
17
  permissions:
18
- contents: read
18
+ contents: write
19
19
  pull-requests: write
20
20
 
21
21
  env:
22
22
  HAS_AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN != '' || secrets.RELEASE_PLEASE_TOKEN != '' }}
23
+ PROMOTION_BRANCH: code-foundry/promote/staging-to-main
23
24
 
24
25
  jobs:
25
26
  create-release-pr:
@@ -44,8 +45,11 @@ jobs:
44
45
  # persist-credentials disabled, git has no auth for that fetch; configure
45
46
  # the credential helper with the short-lived workflow token. NiftyLeague
46
47
  # rejects long-lived fine-grained automation tokens with HTTP 403 even
47
- # on REST, so all read-only operations use github.token; it is scoped
48
- # to contents: read, so this never enables write credentials.
48
+ # on REST, so all read-only operations use github.token. It is used
49
+ # only for GitHub operations owned by this workflow. The job's
50
+ # contents: write permission is required later for the isolated,
51
+ # deterministic promotion ref; protected main and staging are never
52
+ # written by this workflow.
49
53
  env:
50
54
  GH_TOKEN: ${{ github.token }}
51
55
  run: gh auth setup-git
@@ -65,7 +69,7 @@ jobs:
65
69
  # REST query keeps the exact base/head/state filter and the per_page=1
66
70
  # cap (existing is 0 or 1) and fails closed on any API error.
67
71
  EXISTING=$(gh api \
68
- "repos/${GITHUB_REPOSITORY}/pulls?base=main&head=staging&state=open&per_page=1" \
72
+ "repos/${GITHUB_REPOSITORY}/pulls?base=main&head=${PROMOTION_BRANCH}&state=open&per_page=1" \
69
73
  --jq 'length')
70
74
  MAIN_TREE=$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq '.commit.tree.sha')
71
75
  STAGING_TREE=$(gh api "repos/${GITHUB_REPOSITORY}/commits/staging" --jq '.commit.tree.sha')
@@ -144,6 +148,51 @@ jobs:
144
148
  echo 'staging is already aligned with main; no promotion PR is needed.'
145
149
  fi
146
150
 
151
+ - name: Prepare exact-tree promotion head
152
+ if: steps.check-pr.outputs.commits != '0' && steps.check-pr.outputs.tree_equal != 'true' && steps.check-pr.outputs.release_only != 'true'
153
+ env:
154
+ GH_TOKEN: ${{ github.token }}
155
+ AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN }}
156
+ run: |
157
+ set -euo pipefail
158
+ MAIN_SHA=$(git rev-parse origin/main)
159
+ STAGING_SHA=$(git rev-parse origin/staging)
160
+ STAGING_TREE=$(git rev-parse 'origin/staging^{tree}')
161
+ STAGING_SUBJECT=$(git log -1 --format=%s origin/staging)
162
+ COMMIT_FILE="$RUNNER_TEMP/promotion-commit.json"
163
+ node - "$MAIN_SHA" "$STAGING_SHA" "$STAGING_TREE" "$STAGING_SUBJECT" <<'NODE' > "$COMMIT_FILE"
164
+ const [main, staging, tree, subject] = process.argv.slice(2)
165
+ process.stdout.write(JSON.stringify({
166
+ message: `${subject}\n\nPromoted from staging snapshot ${staging.slice(0, 12)}.`,
167
+ tree,
168
+ parents: [main],
169
+ }))
170
+ NODE
171
+ PROMOTION_SHA=$(gh api \
172
+ --method POST \
173
+ "repos/${GITHUB_REPOSITORY}/git/commits" \
174
+ --input "$COMMIT_FILE" \
175
+ --jq '.sha')
176
+ REF_ENDPOINT="repos/${GITHUB_REPOSITORY}/git/refs/heads/${PROMOTION_BRANCH}"
177
+ if gh api "$REF_ENDPOINT" >/dev/null 2>&1; then
178
+ UPDATED=false
179
+ if [ "$HAS_AUTOMATION_TOKEN" = true ] && GH_TOKEN="$AUTOMATION_TOKEN" gh api --method PATCH "$REF_ENDPOINT" --field sha="$PROMOTION_SHA" --field force=true >/dev/null; then
180
+ UPDATED=true
181
+ fi
182
+ if [ "$UPDATED" != true ]; then
183
+ if [ "$HAS_AUTOMATION_TOKEN" = true ]; then
184
+ echo "::warning title=Promotion token fallback::Automation token could not refresh the promotion ref; using the workflow token. GitHub may require a manual check rerun for an already-open pull request."
185
+ fi
186
+ gh api --method PATCH "$REF_ENDPOINT" --field sha="$PROMOTION_SHA" --field force=true >/dev/null
187
+ fi
188
+ else
189
+ gh api --method POST \
190
+ "repos/${GITHUB_REPOSITORY}/git/refs" \
191
+ --field ref="refs/heads/${PROMOTION_BRANCH}" \
192
+ --field sha="$PROMOTION_SHA" >/dev/null
193
+ fi
194
+ echo "Prepared ${PROMOTION_BRANCH} at ${PROMOTION_SHA} with parent ${MAIN_SHA} and the exact staging tree ${STAGING_TREE}."
195
+
147
196
  - name: Create
148
197
  if: steps.check-pr.outputs.existing == '0' && steps.check-pr.outputs.commits != '0' && steps.check-pr.outputs.tree_equal != 'true' && steps.check-pr.outputs.release_only != 'true'
149
198
  env:
@@ -203,7 +252,7 @@ jobs:
203
252
  "repos/${GITHUB_REPOSITORY}/pulls"
204
253
  --method POST
205
254
  --field base=main
206
- --field head=staging
255
+ --field head="$PROMOTION_BRANCH"
207
256
  --field title="Promote staging -> main ($DATE)"
208
257
  --field body=@"$BODY_FILE"
209
258
  )
@@ -237,7 +286,7 @@ jobs:
237
286
  # the short-lived workflow token; gh pr list issues a GraphQL query
238
287
  # that NiftyLeague's long-lived fine-grained token policy rejects.
239
288
  EXISTING_NUMBER=$(GH_TOKEN="${{ github.token }}" gh api \
240
- "repos/${GITHUB_REPOSITORY}/pulls?base=main&head=staging&state=open&per_page=1" \
289
+ "repos/${GITHUB_REPOSITORY}/pulls?base=main&head=${PROMOTION_BRANCH}&state=open&per_page=1" \
241
290
  --jq '.[0].number // empty')
242
291
  if [ -z "$EXISTING_NUMBER" ]; then
243
292
  echo 'Pull request creation failed and no existing promotion PR was found.' >&2
@@ -6,7 +6,7 @@ on:
6
6
  workflow_dispatch:
7
7
 
8
8
  permissions:
9
- contents: read
9
+ contents: write
10
10
  pull-requests: write
11
11
 
12
12
  jobs:
package/.mise.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tools]
2
2
  node = "24.18.0"
3
- bun = "1.3.14"
3
+ bun = "1.4.0"
4
4
 
5
5
  [settings]
6
6
  experimental = true
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.38.2](https://github.com/0xPlayerOne/code-foundry/compare/v0.38.1...v0.38.2) (2026-08-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **release:** preserve promotion release semantics ([#418](https://github.com/0xPlayerOne/code-foundry/issues/418)) ([b1702a7](https://github.com/0xPlayerOne/code-foundry/commit/b1702a756cc6d7297f98b529b0bca178a1cdc3d1))
9
+
10
+ ## [0.38.1](https://github.com/0xPlayerOne/code-foundry/compare/v0.38.0...v0.38.1) (2026-08-29)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **release:** avoid unsafe history-only reconciliation PRs ([#412](https://github.com/0xPlayerOne/code-foundry/issues/412)) ([941f08b](https://github.com/0xPlayerOne/code-foundry/commit/941f08ba7e830ec3ab0ae9c6b937e488c621b95e))
16
+
3
17
  ## [0.38.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.37.5...v0.38.0) (2026-08-24)
4
18
 
5
19
 
package/docs/RELEASES.md CHANGED
@@ -61,6 +61,14 @@ possible. The final branch trees are inspected before mutation; validated
61
61
  main-only changes are inherited by `staging`, while unpromoted staging work is
62
62
  replayed on top of `main`.
63
63
 
64
+ Promotion never opens a pull request directly from a divergent `staging`
65
+ history. The workflow creates or refreshes a deterministic promotion branch
66
+ whose single commit has the current `main` tip as its parent and the exact
67
+ validated `staging` tree. This keeps the review diff limited to the actual
68
+ environment delta even after squash or rebase merges have changed ancestry.
69
+ The generated commit preserves the staged tip's conventional subject so
70
+ Release Please still detects the appropriate release type after promotion.
71
+
64
72
  The `staging` → `main` reconciliation exists only in the `staging-release`
65
73
  topology. Patch-equivalent divergence between `main` and `staging` is treated
66
74
  as aligned. When `staging` has pending commits that are not yet represented on
package/docs/WORKFLOWS.md CHANGED
@@ -95,7 +95,7 @@ workflow refuses to run unless its strategy is exactly `rebase`.
95
95
  | --- | --- | --- |
96
96
  | Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
97
97
  | Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
98
- | `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | `merge_strategy` must be `rebase` when `git_workflow: staging-release`; merge commits are rejected |
98
+ | `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | Code Foundry creates a one-commit head with `main` as its parent and the exact validated `staging` tree; `merge_strategy` must be `rebase`, and merge commits are rejected |
99
99
  | Release Please version PR into `main` | Rebase (`release_merge_strategy: rebase`) | Release automation fails closed unless `rebase`; never defaults to `merge`, never uses `--admin` |
100
100
 
101
101
  The promotion rows above apply only to `staging-release`; `direct`
@@ -136,7 +136,9 @@ so repositories without a Deploy Key ruleset bypass still reconcile successfully
136
136
 
137
137
  When the protected `staging` branch rejects the exact-lease reconcile push with
138
138
  a branch-policy/ruleset/required-PR error, the reconcile job does not fail:
139
- it opens (or reuses) a generated `code-foundry/reconcile/main-to-staging`
139
+ if the branch trees are already identical, it reports a content-aligned no-op
140
+ because a normal pull request cannot safely represent a history-only ref move.
141
+ Otherwise, it opens (or reuses) a generated `code-foundry/reconcile/main-to-staging`
140
142
  pull request whose head contains the exact target tip (the `main` tip for a
141
143
  fast-forward, the replay tip when staging-only commits are replayed) and whose
142
144
  body documents the reconciliation. Reruns reuse the open pull request instead
@@ -146,6 +148,11 @@ history, replay conflicts, authentication errors, and ambiguous or stale pull
146
148
  request state still fail the job closed, and exact-lease protection stays in
147
149
  place for every direct push.
148
150
 
151
+ Repositories using the generated pull request fallback must enable
152
+ **Settings > Actions > General > Workflow permissions > Allow GitHub Actions
153
+ to create and approve pull requests**. Code Foundry reports this exact setting
154
+ when GitHub rejects pull request creation through the Actions integration.
155
+
149
156
  For this reconciliation path, maintainer PATs and administrator roles are not
150
157
  authorized bypasses; the job deliberately authenticates with `github.token`,
151
158
  not `CODE_FOUNDRY_TOKEN` or `RELEASE_PLEASE_TOKEN`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.38.0",
3
+ "version": "0.38.2",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/0xPlayerOne/code-foundry/issues"
14
14
  },
15
- "packageManager": "bun@1.3.14",
15
+ "packageManager": "bun@1.4.0",
16
16
  "bin": {
17
17
  "code-foundry": "src/cli.mjs"
18
18
  },
@@ -197,6 +197,14 @@ function executeReconciliationMutation(target, base, head, state) {
197
197
  }
198
198
  }
199
199
  if (classification.category === 'policy') {
200
+ // A normal pull request cannot faithfully represent a history-only ref
201
+ // alignment. GitHub compares the divergent ancestry and can render a
202
+ // large, misleading content diff even though both branch trees are
203
+ // already identical. Preserve the protected branch and report the
204
+ // content-aligned state instead of opening an unsafe synchronization PR.
205
+ if (state.plan.action === 'aligned') {
206
+ return { success: true, result: { synchronization: 'content-aligned' } }
207
+ }
200
208
  return deliverReconciliationPullRequest(target, base, head, state, targetSha, {}, classification.message)
201
209
  }
202
210
  return { success: false, retry: true, error: `${head} synchronization failed with lease: ${classification.message}` }
@@ -282,7 +290,7 @@ function deliverReconciliationPullRequest(target, base, head, state, targetSha,
282
290
  selection = selectReconciliationPullRequest(prs, expected)
283
291
  if (selection.error) return failResult(selection.error)
284
292
  if (selection.create) {
285
- return failResult(`gh pr create failed for ${branch}: ${sanitizeReconcileOutput(created.stderr) || 'unknown error'}`)
293
+ return failResult(reconciliationPullRequestCreateError(branch, created.stderr))
286
294
  }
287
295
  if (!selection.reuse) return failResult(`No reusable reconciliation pull request for ${branch} after gh pr create failed.`)
288
296
  return reuseReconciliationPullRequest(target, repository, selection.reuse, targetSha, body)
@@ -300,6 +308,15 @@ function deliverReconciliationPullRequest(target, base, head, state, targetSha,
300
308
  }
301
309
  }
302
310
 
311
+ /** @param {string} branch @param {string} stderr */
312
+ function reconciliationPullRequestCreateError(branch, stderr) {
313
+ const detail = sanitizeReconcileOutput(stderr) || 'unknown error'
314
+ const permissionHint = /github actions is not permitted to create or approve pull requests|resource not accessible by integration/i.test(detail)
315
+ ? ' Enable Settings > Actions > General > Workflow permissions > Allow GitHub Actions to create and approve pull requests, then rerun the workflow.'
316
+ : ''
317
+ return `gh pr create failed for ${branch}: ${detail}${permissionHint}`
318
+ }
319
+
303
320
  /**
304
321
  * Refresh an existing reconciliation pull request to the exact target tip and
305
322
  * body. The head branch is pushed under an exact lease when its tip is stale,