code-foundry 0.38.1 → 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.
@@ -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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.38.1](https://github.com/0xPlayerOne/code-foundry/compare/v0.38.0...v0.38.1) (2026-08-29)
4
11
 
5
12
 
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.38.1",
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",