code-foundry 0.34.10 → 0.34.12

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.
@@ -18,9 +18,6 @@ permissions:
18
18
  contents: read
19
19
  pull-requests: write
20
20
 
21
- env:
22
- HAS_AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN != '' || secrets.RELEASE_PLEASE_TOKEN != '' }}
23
-
24
21
  jobs:
25
22
  create-draft-pr:
26
23
  name: Create
@@ -34,7 +31,7 @@ jobs:
34
31
  id: check-pr
35
32
  env:
36
33
  HEAD_REF: ${{ github.ref_name }}
37
- GH_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN || github.token }}
34
+ GH_TOKEN: ${{ github.token }}
38
35
  run: |
39
36
  BRANCH="$HEAD_REF"
40
37
  EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --limit 1 --base staging --head "$BRANCH" --state open --json number,title 2>/dev/null | node -e 'let d=""; process.stdin.on("data", c => d += c).on("end", () => console.log(JSON.parse(d || "[]").length))')
@@ -43,9 +40,11 @@ jobs:
43
40
  - name: Create
44
41
  if: steps.check-pr.outputs.existing == '0'
45
42
  env:
43
+ AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN }}
44
+ GITHUB_TOKEN: ${{ github.token }}
46
45
  HEAD_REF: ${{ github.ref_name }}
47
- GH_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN || github.token }}
48
46
  run: |
47
+ set -euo pipefail
49
48
  BRANCH="$HEAD_REF"
50
49
  PREFIX="${BRANCH%%/*}"
51
50
  SUFFIX="${BRANCH#*/}"
@@ -71,19 +70,42 @@ jobs:
71
70
  PR_TITLE="$BRANCH"
72
71
  fi
73
72
 
74
- DRAFT_ARGS=()
75
- if [ "$HAS_AUTOMATION_TOKEN" = true ]; then
76
- BODY_NOTICE="This PR is ready for review and merging once required validation passes."
77
- else
78
- DRAFT_ARGS+=(--draft)
79
- BODY_NOTICE="This PR was created automatically and is currently in draft mode."
80
- echo "::notice title=Manual PR readiness required::No automation token is configured; this PR may remain draft until manually marked ready for validation and merge."
73
+ BODY_FILE=$(mktemp)
74
+ ERROR_FILE=$(mktemp)
75
+ trap 'rm -f -- "$BODY_FILE" "$ERROR_FILE"' EXIT
76
+ cat > "$BODY_FILE" <<EOF
77
+ CI PR created automatically by this workflow.
78
+
79
+ This PR is ready for review once required validation passes. If the
80
+ configured automation token is rejected, it is intentionally opened
81
+ as a draft for manual readiness.
82
+
83
+ **CI Status:** Pending — this workflow triggered on push to $BRANCH.
84
+ EOF
85
+
86
+ CREATE_ARGS=(
87
+ "repos/${GITHUB_REPOSITORY}/pulls"
88
+ --method POST
89
+ --field base=staging
90
+ --field head="$BRANCH"
91
+ --field title="$PR_TITLE"
92
+ --field body="@$BODY_FILE"
93
+ )
94
+
95
+ CREATED=false
96
+ if [ -n "$AUTOMATION_TOKEN" ] && GH_TOKEN="$AUTOMATION_TOKEN" gh api "${CREATE_ARGS[@]}" > /dev/null 2> "$ERROR_FILE"; then
97
+ CREATED=true
98
+ fi
99
+
100
+ if [ "$CREATED" != true ]; then
101
+ DRAFT_ARGS=("${CREATE_ARGS[@]}" --field draft=true)
102
+ if GH_TOKEN="$GITHUB_TOKEN" gh api "${DRAFT_ARGS[@]}" > /dev/null 2> "$ERROR_FILE"; then
103
+ echo "::notice title=Manual PR readiness required::The configured automation token was absent or rejected; this PR was opened as a draft with the short-lived workflow token."
104
+ CREATED=true
105
+ fi
81
106
  fi
82
107
 
83
- gh pr create \
84
- --repo "$GITHUB_REPOSITORY" \
85
- --base staging \
86
- --head "$BRANCH" \
87
- "${DRAFT_ARGS[@]}" \
88
- --title "$PR_TITLE" \
89
- --body "CI PR created automatically by this workflow.\n\n $BODY_NOTICE\n\n **CI Status:** Pending \u2014 this workflow triggered on push to \`$BRANCH\`."
108
+ if [ "$CREATED" != true ]; then
109
+ echo "::error title=Draft PR creation failed::Both the configured automation token and the workflow token failed to create the pull request."
110
+ exit 1
111
+ fi
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.12](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.11...v0.34.12) (2026-08-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **reconcile:** classify final tree deltas ([#353](https://github.com/0xPlayerOne/code-foundry/issues/353)) ([18d23e4](https://github.com/0xPlayerOne/code-foundry/commit/18d23e4847ffbcfc274bc11c80244ecac64395ed))
9
+
10
+ ## [0.34.11](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.10...v0.34.11) (2026-08-02)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **draft-pr:** fall back to workflow token ([#350](https://github.com/0xPlayerOne/code-foundry/issues/350)) ([aef07db](https://github.com/0xPlayerOne/code-foundry/commit/aef07db6945badd7d7b3526b5490b1512ac1a468))
16
+
3
17
  ## [0.34.10](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.9...v0.34.10) (2026-08-02)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.34.10",
3
+ "version": "0.34.12",
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",
@@ -83,14 +83,16 @@ function resolveReconciliationState(target, base, head, allowed, requireRemote)
83
83
  try {
84
84
  const mainOnlyCommits = divergentCommits(target, stagingSha, mainSha)
85
85
  const stagingOnlyCommits = divergentCommits(target, mainSha, stagingSha)
86
+ const directChangedPaths = treeChangedPaths(target, stagingSha, mainSha)
86
87
  const plan = classifyReconciliation({
87
88
  mainSha,
88
89
  stagingSha,
89
90
  mainOnlyCommits,
90
91
  stagingOnlyCommits,
92
+ directChangedPaths,
91
93
  allowed,
92
94
  })
93
- return { plan, mainSha, stagingSha, mainOnlyCommits, stagingOnlyCommits }
95
+ return { plan, mainSha, stagingSha, mainOnlyCommits, stagingOnlyCommits, directChangedPaths }
94
96
  } catch (error) {
95
97
  const message = error instanceof Error ? error.message : String(error)
96
98
  return {
@@ -102,6 +104,7 @@ function resolveReconciliationState(target, base, head, allowed, requireRemote)
102
104
  stagingSha,
103
105
  mainOnlyCommits: [],
104
106
  stagingOnlyCommits: [],
107
+ directChangedPaths: [],
105
108
  }
106
109
  }
107
110
  }
@@ -570,6 +573,15 @@ function commitChangedPaths(root, commitSha) {
570
573
  return result.stdout.split(/\r?\n/).filter(Boolean)
571
574
  }
572
575
 
576
+ /** @param {string} root @param {string} from @param {string} to @returns {string[]} */
577
+ function treeChangedPaths(root, from, to) {
578
+ const result = spawnSync('git', ['diff', '--name-only', from, to], { cwd: root, encoding: 'utf8' })
579
+ if (result.status !== 0) {
580
+ throw new Error(`Failed to compare branch trees between ${from} and ${to}. ${result.stderr?.trim() || result.stdout?.trim()}`)
581
+ }
582
+ return result.stdout.split(/\r?\n/).filter(Boolean)
583
+ }
584
+
573
585
  /** @param {string} root @param {string[]} args @returns {{ status: number | null, stdout: string, stderr: string }} */
574
586
  function ghSpawn(root, args) {
575
587
  const token = process.env.RELEASE_PLEASE_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN
@@ -217,12 +217,16 @@ function compareVersions(a, b) {
217
217
  }
218
218
 
219
219
  /**
220
- * Classify the relationship between main and staging using commit-level divergence.
221
- * Any non-release main-only commit fails. Any staging-only commit triggers a replay
222
- * path so no staging-only work can be silently discarded.
220
+ * Classify the relationship between main and staging using the final tree delta
221
+ * when available. Historical commit paths can include equivalent workflow or
222
+ * configuration changes that are no longer present in the branch delta, so the
223
+ * final trees are the source of truth for release-only reconciliation. Any
224
+ * staging-only commit still triggers a replay path so no staging-only work can
225
+ * be silently discarded.
223
226
  * @param {{
224
227
  * mainSha: string,
225
228
  * stagingSha: string,
229
+ * directChangedPaths?: string[],
226
230
  * mainOnlyCommits?: Array<{ sha?: string, changedPaths?: string[] }>,
227
231
  * stagingOnlyCommits?: Array<{ sha?: string, changedPaths?: string[] }>,
228
232
  * allowed?: Set<string>,
@@ -232,6 +236,7 @@ export function classifyReconciliation(input) {
232
236
  const {
233
237
  mainSha,
234
238
  stagingSha,
239
+ directChangedPaths,
235
240
  mainOnlyCommits = [],
236
241
  stagingOnlyCommits = [],
237
242
  allowed = approvedReleaseFiles(),
@@ -248,6 +253,29 @@ export function classifyReconciliation(input) {
248
253
  action: 'fail',
249
254
  reason: 'Unable to inspect staging-only commit metadata.',
250
255
  }
256
+ if (Array.isArray(directChangedPaths)) {
257
+ const paths = [...new Set(directChangedPaths.map((path) => path.trim()).filter(Boolean))]
258
+ const unexpected = unexpectedReleasePaths(paths, allowed)
259
+ const stagingOnlyReleaseOnly = stagingOnlyCommits.length > 0 && stagingOnlyCommits.every((commit) =>
260
+ Array.isArray(commit.changedPaths) && commit.changedPaths.length > 0 && unexpectedReleasePaths(commit.changedPaths, allowed).length === 0,
261
+ )
262
+ if (!unexpected.length && !stagingOnlyReleaseOnly) {
263
+ return {
264
+ action: 'fast-forward',
265
+ targetSha: mainSha,
266
+ reason: paths.length
267
+ ? 'Branches differ only by approved release metadata.'
268
+ : 'Branches have different history but identical content.',
269
+ }
270
+ }
271
+ if (!stagingOnlyCommits.length) {
272
+ return {
273
+ action: 'fail',
274
+ reason: 'branch content differs outside release metadata.',
275
+ unexpected,
276
+ }
277
+ }
278
+ }
251
279
  const unexpectedMain = unexpectedReleasePaths(
252
280
  [...new Set(mainOnlyCommits.flatMap((commit) => commit.changedPaths || []))],
253
281
  allowed,