code-foundry 1.48.6 → 1.48.7
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/release.yml +47 -0
- package/CHANGELOG.md +8 -8
- package/docs/RELEASES.md +21 -0
- package/package.json +1 -1
- package/src/cli.mjs +5 -2
- package/src/commands/release.mjs +22 -0
- package/src/lib/bun-lock.mjs +149 -0
- package/src/lib/release-policy.mjs +2 -0
|
@@ -439,6 +439,53 @@ jobs:
|
|
|
439
439
|
latest=false
|
|
440
440
|
if [ "$PRERELEASE" != true ]; then latest=true; fi
|
|
441
441
|
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease="$PRERELEASE" --latest="$latest"
|
|
442
|
+
- name: Sync lockfile workspace versions on generated release PRs
|
|
443
|
+
# Release Please bumps every workspace package.json but has no updater
|
|
444
|
+
# for bun.lock, which records each workspace's version, so a generated
|
|
445
|
+
# release PR leaves the lock naming the previous version of every
|
|
446
|
+
# workspace. Commit the corrected lock onto the release branch before
|
|
447
|
+
# the draft-state and merge steps read its head. The commit is made with
|
|
448
|
+
# the validated automation token so the PR's required checks run on it;
|
|
449
|
+
# a push made with the workflow token starts no workflows, so that
|
|
450
|
+
# credential skips this step and the release gate reports the stale
|
|
451
|
+
# lock instead. A rejected push (Release Please moved the branch) is
|
|
452
|
+
# not retried: the next release run starts again from the new head.
|
|
453
|
+
if: steps.release.outcome == 'success' && steps.credentials.outputs.token_source == 'configured'
|
|
454
|
+
continue-on-error: true
|
|
455
|
+
env:
|
|
456
|
+
GH_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN }}
|
|
457
|
+
run: |
|
|
458
|
+
set -euo pipefail
|
|
459
|
+
mapfile -t release_branches < <(
|
|
460
|
+
gh pr list \
|
|
461
|
+
--repo "$GITHUB_REPOSITORY" \
|
|
462
|
+
--state open \
|
|
463
|
+
--base main \
|
|
464
|
+
--json headRefName \
|
|
465
|
+
--jq '.[] | select(.headRefName == "release-please--branches--main" or (.headRefName | startswith("release-please--branches--main--"))) | .headRefName'
|
|
466
|
+
)
|
|
467
|
+
if [ ${#release_branches[@]} -eq 0 ]; then
|
|
468
|
+
exit 0
|
|
469
|
+
fi
|
|
470
|
+
gh auth setup-git
|
|
471
|
+
index=0
|
|
472
|
+
for branch in "${release_branches[@]}"; do
|
|
473
|
+
index=$((index + 1))
|
|
474
|
+
worktree="$RUNNER_TEMP/release-lock-$index"
|
|
475
|
+
git fetch --no-tags --depth=1 origin "refs/heads/$branch"
|
|
476
|
+
git worktree add --detach "$worktree" FETCH_HEAD
|
|
477
|
+
node "$RUNNER_TEMP/code-foundry/src/cli.mjs" release sync-lock --target "$worktree"
|
|
478
|
+
if git -C "$worktree" diff --quiet -- bun.lock; then
|
|
479
|
+
continue
|
|
480
|
+
fi
|
|
481
|
+
git -C "$worktree" \
|
|
482
|
+
-c user.name='github-actions[bot]' \
|
|
483
|
+
-c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
|
|
484
|
+
commit --quiet -m 'chore(release): sync bun.lock workspace versions' -- bun.lock
|
|
485
|
+
if ! git -C "$worktree" push origin "HEAD:refs/heads/$branch"; then
|
|
486
|
+
echo "::warning title=Release lock sync::$branch moved before the lockfile sync could be pushed; the next release run will sync it."
|
|
487
|
+
fi
|
|
488
|
+
done
|
|
442
489
|
- name: Normalize generated release PR draft state
|
|
443
490
|
if: steps.release.outcome == 'success'
|
|
444
491
|
env:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.48.7](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.6...v1.48.7) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **release:** sync bun.lock workspace versions on generated release PRs ([#777](https://github.com/0xPlayerOne/code-foundry/issues/777)) ([b24bfae](https://github.com/0xPlayerOne/code-foundry/commit/b24bfae309ac7003fcc4cd57f79ddc418c27b039))
|
|
9
|
+
|
|
3
10
|
## [1.48.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.5...v1.48.6) (2026-10-23)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -133,16 +140,9 @@
|
|
|
133
140
|
|
|
134
141
|
* **qualification:** skip parallel-syntax files until actionlint supports them ([#740](https://github.com/0xPlayerOne/code-foundry/issues/740)) ([db98c7f](https://github.com/0xPlayerOne/code-foundry/commit/db98c7f40cae080cdc9dcab7baac4976e5e0656e))
|
|
135
142
|
|
|
136
|
-
## [1.46.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.45.2...v1.46.0) (2026-10-07)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
### Features
|
|
140
|
-
|
|
141
|
-
* **billing:** run fast-lane executes concurrently ([#738](https://github.com/0xPlayerOne/code-foundry/issues/738)) ([b40e82a](https://github.com/0xPlayerOne/code-foundry/commit/b40e82a0709b288a8be46998cfdc8b909f92c23c))
|
|
142
|
-
|
|
143
143
|
<!-- code-foundry: packaged changelog -->
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
-
This packaged changelog lists the 20 most recent releases;
|
|
147
|
+
This packaged changelog lists the 20 most recent releases; 255 older releases are omitted to keep the package small.
|
|
148
148
|
See the [full changelog](https://github.com/0xPlayerOne/code-foundry/blob/main/CHANGELOG.md) for the complete history.
|
package/docs/RELEASES.md
CHANGED
|
@@ -137,6 +137,27 @@ A failed audit leaves the version PR unmerged. Re-run the Release workflow
|
|
|
137
137
|
(`workflow_dispatch`) so Release Please regenerates the branch from the current
|
|
138
138
|
`main`; do not edit the generated branch or release manually.
|
|
139
139
|
|
|
140
|
+
## Lockfile workspace versions
|
|
141
|
+
|
|
142
|
+
Release Please's Node updater bumps every workspace `package.json` but has no
|
|
143
|
+
updater for `bun.lock`, which records each workspace's `version`. A version PR
|
|
144
|
+
therefore leaves the lock naming the previous version of every workspace (the
|
|
145
|
+
agent-sim 0.15.14 release left all 14 workspaces at 0.15.10).
|
|
146
|
+
|
|
147
|
+
- `validation release_diff` fails a generated release whose `bun.lock` records a
|
|
148
|
+
workspace version that differs from its `package.json`, and names the first
|
|
149
|
+
workspaces and the fix.
|
|
150
|
+
- `code-foundry release sync-lock [--target PATH] [--dry-run]` rewrites only
|
|
151
|
+
those drifted `version` values; it never touches dependencies, the `packages`
|
|
152
|
+
section or line endings, and does nothing when the lock already matches.
|
|
153
|
+
- The Release workflow runs it on each open generated release branch before the
|
|
154
|
+
draft-state and merge steps read the PR head, then pushes one
|
|
155
|
+
`chore(release): sync bun.lock workspace versions` commit with the validated
|
|
156
|
+
automation token so the required checks run on it. With the fallback workflow
|
|
157
|
+
token (whose pushes start no workflows) the step is skipped and the gate
|
|
158
|
+
reports the stale lock instead. A push rejected because Release Please moved
|
|
159
|
+
the branch is left for the next run.
|
|
160
|
+
|
|
140
161
|
## Pull request permissions
|
|
141
162
|
|
|
142
163
|
The release job validates the configured automation token
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.7",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
package/src/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ Usage:
|
|
|
23
23
|
npx code-foundry ci pause|resume|status [--target PATH]
|
|
24
24
|
npx code-foundry release reconcile [--github] [--base BRANCH] [--head BRANCH]
|
|
25
25
|
npx code-foundry release hook --tag TAG --workflow WORKFLOW
|
|
26
|
+
npx code-foundry release sync-lock [--target PATH] [--dry-run]
|
|
26
27
|
npx code-foundry release-integrity settings --repo OWNER/REPO
|
|
27
28
|
npx code-foundry release-integrity release --repo OWNER/REPO --tag TAG
|
|
28
29
|
npx code-foundry release-integrity manifest --asset PATH [--root PATH]
|
|
@@ -93,9 +94,9 @@ function parseArgs(argv) {
|
|
|
93
94
|
|
|
94
95
|
if (command === 'release') {
|
|
95
96
|
const subcommand = argv.shift() ?? ''
|
|
96
|
-
if (!['reconcile', 'hook', 'validate-prs', 'recovery-plan'].includes(subcommand))
|
|
97
|
+
if (!['reconcile', 'hook', 'validate-prs', 'recovery-plan', 'sync-lock'].includes(subcommand))
|
|
97
98
|
fail(
|
|
98
|
-
`unknown release command: ${subcommand ?? '(missing)'}; use release reconcile, release hook, release validate-prs, or release
|
|
99
|
+
`unknown release command: ${subcommand ?? '(missing)'}; use release reconcile, release hook, release validate-prs, release recovery-plan, or release sync-lock`
|
|
99
100
|
)
|
|
100
101
|
options.releaseSubcommand = subcommand
|
|
101
102
|
} else if (command === 'release-integrity') {
|
|
@@ -247,11 +248,13 @@ async function main() {
|
|
|
247
248
|
dispatchPostReleaseHook,
|
|
248
249
|
reconcileRelease,
|
|
249
250
|
releaseRecoveryPlan,
|
|
251
|
+
syncReleaseLock,
|
|
250
252
|
validateReleasePullRequestDiffs,
|
|
251
253
|
} = await import('./commands/release.mjs')
|
|
252
254
|
if (options.releaseSubcommand === 'hook') dispatchPostReleaseHook(target, options)
|
|
253
255
|
else if (options.releaseSubcommand === 'validate-prs') validateReleasePullRequestDiffs(target)
|
|
254
256
|
else if (options.releaseSubcommand === 'recovery-plan') releaseRecoveryPlan(target)
|
|
257
|
+
else if (options.releaseSubcommand === 'sync-lock') syncReleaseLock(target, options)
|
|
255
258
|
else reconcileRelease(target, options)
|
|
256
259
|
} catch (error) {
|
|
257
260
|
fail(error instanceof Error ? error.message : String(error))
|
package/src/commands/release.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
selectReconciliationPullRequest,
|
|
18
18
|
validateReleasePullRequests,
|
|
19
19
|
} from '../lib/release-policy.mjs'
|
|
20
|
+
import { syncBunLockWorkspaceVersions } from '../lib/bun-lock.mjs'
|
|
20
21
|
import { hasDeliveredHook, releaseDeliveryKey, selectHookDelivery } from '../lib/release-hook.mjs'
|
|
21
22
|
|
|
22
23
|
/** @typedef {{ target: string, dryRun: boolean, github: boolean, base: string, head: string }} ReleaseOptions */
|
|
@@ -768,6 +769,27 @@ export function dispatchPostReleaseHook(root, options) {
|
|
|
768
769
|
return { ...decision, deliveryKey: key, dispatched: true }
|
|
769
770
|
}
|
|
770
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Rewrite the workspace versions `bun.lock` records to match the manifests a
|
|
774
|
+
* Release Please run just bumped.
|
|
775
|
+
* @param {string} root
|
|
776
|
+
* @param {{ dryRun: boolean }} options
|
|
777
|
+
*/
|
|
778
|
+
export function syncReleaseLock(root, options) {
|
|
779
|
+
const changes = syncBunLockWorkspaceVersions(root, { dryRun: options.dryRun })
|
|
780
|
+
if (!changes.length) {
|
|
781
|
+
console.log('bun.lock workspace versions already match package.json; nothing to sync.')
|
|
782
|
+
return changes
|
|
783
|
+
}
|
|
784
|
+
const verb = options.dryRun ? 'Would update' : 'Updated'
|
|
785
|
+
for (const change of changes) {
|
|
786
|
+
console.log(
|
|
787
|
+
`${verb} bun.lock ${change.path || '.'} (${change.name}): ${change.from} -> ${change.to}`
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
return changes
|
|
791
|
+
}
|
|
792
|
+
|
|
771
793
|
/** @param {string} root */
|
|
772
794
|
export function validateReleasePullRequestDiffs(root) {
|
|
773
795
|
const repository = process.env.GITHUB_REPOSITORY
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Release Please's Node updater bumps every workspace `package.json` but has no
|
|
8
|
+
* updater for `bun.lock`, which records each workspace's version. A release
|
|
9
|
+
* commit therefore leaves the lock naming the previous version of every
|
|
10
|
+
* workspace. These helpers read and rewrite only those recorded versions; the
|
|
11
|
+
* rest of the lock is never touched.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const WORKSPACES_START = /^ {2}"workspaces": \{\s*$/
|
|
15
|
+
const WORKSPACES_END = /^ {2}\},?\s*$/
|
|
16
|
+
const ENTRY_START = /^ {4}"([^"]*)": \{\s*$/
|
|
17
|
+
const ENTRY_END = /^ {4}\},?\s*$/
|
|
18
|
+
const NAME_LINE = /^ {6}"name": "([^"]*)",?\s*$/
|
|
19
|
+
const VERSION_LINE = /^( {6}"version": ")([^"]*)(".*)$/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{ path: string, name: string, version: string, line: number }} LockedWorkspace
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The workspace versions a `bun.lock` records, with the line each sits on.
|
|
27
|
+
* Workspaces without a `version` line (the root, private packages without a
|
|
28
|
+
* version) are not listed: bun omits the key when the manifest has none.
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @returns {LockedWorkspace[]}
|
|
31
|
+
*/
|
|
32
|
+
export function lockedWorkspaces(text) {
|
|
33
|
+
const lines = text.split(/\r?\n/)
|
|
34
|
+
/** @type {LockedWorkspace[]} */
|
|
35
|
+
const found = []
|
|
36
|
+
let inWorkspaces = false
|
|
37
|
+
let entry = ''
|
|
38
|
+
let name = ''
|
|
39
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
40
|
+
const line = lines[index]
|
|
41
|
+
if (!inWorkspaces) {
|
|
42
|
+
if (WORKSPACES_START.test(line)) inWorkspaces = true
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (!entry) {
|
|
46
|
+
if (WORKSPACES_END.test(line)) break
|
|
47
|
+
const start = ENTRY_START.exec(line)
|
|
48
|
+
if (start) {
|
|
49
|
+
entry = start[1]
|
|
50
|
+
name = ''
|
|
51
|
+
}
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (ENTRY_END.test(line)) {
|
|
55
|
+
entry = ''
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const nameLine = NAME_LINE.exec(line)
|
|
59
|
+
if (nameLine) {
|
|
60
|
+
name = nameLine[1]
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const versionLine = VERSION_LINE.exec(line)
|
|
64
|
+
if (versionLine) found.push({ path: entry, name, version: versionLine[2], line: index })
|
|
65
|
+
}
|
|
66
|
+
return found
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} root
|
|
71
|
+
* @param {string} workspacePath
|
|
72
|
+
* @returns {string | undefined}
|
|
73
|
+
*/
|
|
74
|
+
function manifestVersion(root, workspacePath) {
|
|
75
|
+
const file = join(root, workspacePath, 'package.json')
|
|
76
|
+
if (!existsSync(file)) return undefined
|
|
77
|
+
try {
|
|
78
|
+
const version = JSON.parse(readFileSync(file, 'utf8')).version
|
|
79
|
+
return typeof version === 'string' && version ? version : undefined
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Locked workspace versions that differ from their manifest.
|
|
87
|
+
* @param {string} root
|
|
88
|
+
* @returns {Array<LockedWorkspace & { manifest: string }>}
|
|
89
|
+
*/
|
|
90
|
+
export function bunLockVersionDrift(root) {
|
|
91
|
+
const lockPath = join(root, 'bun.lock')
|
|
92
|
+
if (!existsSync(lockPath)) return []
|
|
93
|
+
/** @type {Array<LockedWorkspace & { manifest: string }>} */
|
|
94
|
+
const drift = []
|
|
95
|
+
for (const workspace of lockedWorkspaces(readFileSync(lockPath, 'utf8'))) {
|
|
96
|
+
const manifest = manifestVersion(root, workspace.path)
|
|
97
|
+
if (manifest !== undefined && manifest !== workspace.version)
|
|
98
|
+
drift.push({ ...workspace, manifest })
|
|
99
|
+
}
|
|
100
|
+
return drift
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Release-gate errors for a stale `bun.lock`.
|
|
105
|
+
* @param {string} root
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
export function validateBunLockVersions(root) {
|
|
109
|
+
const drift = bunLockVersionDrift(root)
|
|
110
|
+
if (!drift.length) return []
|
|
111
|
+
const shown = drift
|
|
112
|
+
.slice(0, 3)
|
|
113
|
+
.map(
|
|
114
|
+
(entry) =>
|
|
115
|
+
`${entry.path || '.'} (${entry.name || 'unnamed'}) is ${entry.version} in bun.lock but ${entry.manifest} in package.json`
|
|
116
|
+
)
|
|
117
|
+
const more = drift.length > shown.length ? ` (+${drift.length - shown.length} more)` : ''
|
|
118
|
+
return [
|
|
119
|
+
`bun.lock records ${drift.length} workspace version(s) that differ from package.json: ${shown.join('; ')}${more}. Run code-foundry release sync-lock.`,
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Rewrite stale workspace versions in `bun.lock` to match their manifests.
|
|
125
|
+
* Only the `version` value of a drifted workspace changes.
|
|
126
|
+
* @param {string} root
|
|
127
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
128
|
+
* @returns {Array<{ path: string, name: string, from: string, to: string }>}
|
|
129
|
+
*/
|
|
130
|
+
export function syncBunLockWorkspaceVersions(root, options = {}) {
|
|
131
|
+
const drift = bunLockVersionDrift(root)
|
|
132
|
+
if (!drift.length) return []
|
|
133
|
+
const lockPath = join(root, 'bun.lock')
|
|
134
|
+
const text = readFileSync(lockPath, 'utf8')
|
|
135
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n'
|
|
136
|
+
const lines = text.split(/\r?\n/)
|
|
137
|
+
for (const entry of drift) {
|
|
138
|
+
const match = VERSION_LINE.exec(lines[entry.line])
|
|
139
|
+
if (!match) continue
|
|
140
|
+
lines[entry.line] = `${match[1]}${entry.manifest}${match[3]}`
|
|
141
|
+
}
|
|
142
|
+
if (!options.dryRun) writeFileSync(lockPath, lines.join(newline))
|
|
143
|
+
return drift.map((entry) => ({
|
|
144
|
+
path: entry.path,
|
|
145
|
+
name: entry.name,
|
|
146
|
+
from: entry.version,
|
|
147
|
+
to: entry.manifest,
|
|
148
|
+
}))
|
|
149
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
|
+
import { validateBunLockVersions } from './bun-lock.mjs'
|
|
5
6
|
import { isReleasePleaseHead } from './validation-policy.mjs'
|
|
6
7
|
|
|
7
8
|
const DEFAULT_RELEASE_FILES = new Set([
|
|
@@ -453,6 +454,7 @@ export function validateGeneratedReleaseDiff(input) {
|
|
|
453
454
|
errors.push('Generated release pull request changes no version metadata.')
|
|
454
455
|
}
|
|
455
456
|
errors.push(...validateCargoLockVersions(root, config))
|
|
457
|
+
errors.push(...validateBunLockVersions(root))
|
|
456
458
|
// Callers that can read the unified diff opt into the content audit. A null
|
|
457
459
|
// patch means the diff was requested but unreadable, which must not pass.
|
|
458
460
|
if (typeof patch === 'string') errors.push(...auditReleaseDiffContent(patch, { config }))
|