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.
- package/.github/workflows/draft-pr.yml +41 -19
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/commands/release.mjs +13 -1
- package/src/lib/release-policy.mjs +31 -3
|
@@ -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: ${{
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
package/src/commands/release.mjs
CHANGED
|
@@ -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
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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,
|