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.
- package/.github/actions/setup/action.yml +1 -1
- package/.github/workflows/release-pr.yml +55 -6
- package/.github/workflows/release-pr_self-ci.yml +1 -1
- package/.mise.toml +1 -1
- package/CHANGELOG.md +14 -0
- package/docs/RELEASES.md +8 -0
- package/docs/WORKFLOWS.md +9 -2
- package/package.json +2 -2
- package/src/commands/release.mjs +18 -1
|
@@ -15,11 +15,12 @@ on:
|
|
|
15
15
|
required: false
|
|
16
16
|
|
|
17
17
|
permissions:
|
|
18
|
-
contents:
|
|
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
|
|
48
|
-
#
|
|
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
|
|
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=
|
|
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
|
|
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
|
package/.mise.toml
CHANGED
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
15
|
+
"packageManager": "bun@1.4.0",
|
|
16
16
|
"bin": {
|
|
17
17
|
"code-foundry": "src/cli.mjs"
|
|
18
18
|
},
|
package/src/commands/release.mjs
CHANGED
|
@@ -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(
|
|
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,
|