code-foundry 0.32.0 → 0.32.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/code-foundry.yml +2 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release-pr.yml +1 -1
- package/.github/workflows/release.yml +4 -2
- package/.github/workflows/security.yml +1 -1
- package/.github/workflows/test.yml +1 -1
- package/CHANGELOG.md +18 -0
- package/docs/RELEASES.md +24 -1
- package/package.json +1 -1
- package/src/commands/doctor.mjs +12 -0
- package/src/commands/release.mjs +41 -46
- package/src/commands/sync.mjs +31 -6
- package/src/lib/release-manifest.mjs +39 -0
- package/src/lib/release-policy.mjs +1 -1
package/.github/code-foundry.yml
CHANGED
|
@@ -16,7 +16,8 @@ release_type: node
|
|
|
16
16
|
npm_publish: true
|
|
17
17
|
license: agpl-3.0-or-later
|
|
18
18
|
git_workflow: staging-release
|
|
19
|
-
merge_strategy:
|
|
19
|
+
merge_strategy: rebase
|
|
20
|
+
release_merge_strategy: rebase
|
|
20
21
|
runner: ubuntu-latest
|
|
21
22
|
unit_runner: ubuntu-slim
|
|
22
23
|
cache_packages: auto
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -35,7 +35,7 @@ env:
|
|
|
35
35
|
REPO_FOUNDRY_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }}
|
|
36
36
|
|
|
37
37
|
concurrency:
|
|
38
|
-
group: code-foundry-ci-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
38
|
+
group: code-foundry-ci-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
39
39
|
cancel-in-progress: true
|
|
40
40
|
|
|
41
41
|
jobs:
|
|
@@ -47,7 +47,7 @@ env:
|
|
|
47
47
|
REPO_FOUNDRY_FEATURES: ${{ vars.REPO_FOUNDRY_FEATURES }}
|
|
48
48
|
|
|
49
49
|
concurrency:
|
|
50
|
-
group: code-foundry-codeql-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
50
|
+
group: code-foundry-codeql-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
51
51
|
cancel-in-progress: true
|
|
52
52
|
|
|
53
53
|
jobs:
|
|
@@ -157,7 +157,7 @@ jobs:
|
|
|
157
157
|
### Validation
|
|
158
158
|
|
|
159
159
|
- Required CI, test, security, and CodeQL checks must pass before merge.
|
|
160
|
-
- Merge using the repository's configured \`merge_strategy\` (default: **
|
|
160
|
+
- Merge using the repository's configured \`merge_strategy\` (default: **rebase**).
|
|
161
161
|
- After merge, Release Please opens a separate versioned release PR with the changelog.
|
|
162
162
|
EOF
|
|
163
163
|
|
|
@@ -72,7 +72,7 @@ jobs:
|
|
|
72
72
|
: {}
|
|
73
73
|
let releaseType = config.release_type || 'auto'
|
|
74
74
|
let npmPublish = config.npm_publish || 'false'
|
|
75
|
-
let releaseMergeStrategy = String(config.merge_strategy || 'merge').trim().toLowerCase()
|
|
75
|
+
let releaseMergeStrategy = String(config.release_merge_strategy || config.merge_strategy || 'merge').trim().toLowerCase()
|
|
76
76
|
if (releaseType === 'auto') {
|
|
77
77
|
releaseType = fs.existsSync('package.json') ? 'node' : fs.existsSync('pyproject.toml') ? 'python' : fs.existsSync('Cargo.toml') ? 'rust' : fs.existsSync('version.txt') ? 'simple' : 'none'
|
|
78
78
|
}
|
|
@@ -86,6 +86,8 @@ jobs:
|
|
|
86
86
|
const releaseConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'))
|
|
87
87
|
if (!releaseConfig.packages && !releaseConfig['release-type']) {
|
|
88
88
|
legacyReleaseType = releaseType
|
|
89
|
+
} else if (!fs.existsSync('.release-please-manifest.json')) {
|
|
90
|
+
throw new Error('Missing .release-please-manifest.json: run code-foundry sync to bootstrap the release manifest.')
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
fs.appendFileSync(
|
|
@@ -170,7 +172,7 @@ jobs:
|
|
|
170
172
|
reconcile:
|
|
171
173
|
name: Release / Reconcile
|
|
172
174
|
needs: release
|
|
173
|
-
if: needs.release.
|
|
175
|
+
if: needs.release.result == 'success'
|
|
174
176
|
runs-on: ${{ inputs.runner }}
|
|
175
177
|
timeout-minutes: 15
|
|
176
178
|
permissions:
|
|
@@ -31,7 +31,7 @@ env:
|
|
|
31
31
|
REPO_FOUNDRY_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }}
|
|
32
32
|
|
|
33
33
|
concurrency:
|
|
34
|
-
group: code-foundry-security-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
34
|
+
group: code-foundry-security-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
35
35
|
cancel-in-progress: true
|
|
36
36
|
|
|
37
37
|
jobs:
|
|
@@ -40,7 +40,7 @@ env:
|
|
|
40
40
|
REPO_FOUNDRY_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }}
|
|
41
41
|
|
|
42
42
|
concurrency:
|
|
43
|
-
group: code-foundry-test-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
43
|
+
group: code-foundry-test-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
|
|
44
44
|
cancel-in-progress: true
|
|
45
45
|
|
|
46
46
|
jobs:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.32.2](https://github.com/0xPlayerOne/code-foundry/compare/v0.32.1...v0.32.2) (2026-07-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **release:** mirror staging onto main and require rebase merges on main ([31ab9b1](https://github.com/0xPlayerOne/code-foundry/commit/31ab9b125e66ae35f7d2a2c6893d0a3aec5d4560))
|
|
9
|
+
* **release:** synchronize staging with a linear commit instead of a blocked fast-forward ([b808dff](https://github.com/0xPlayerOne/code-foundry/commit/b808dffeae38ec8211f8f0dee79e19d60f70b708))
|
|
10
|
+
|
|
11
|
+
## [0.32.1](https://github.com/0xPlayerOne/code-foundry/compare/v0.32.0...v0.32.1) (2026-07-31)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **ci:** key runtime concurrency by event so promotion PRs do not cancel push checks ([5a8d8fd](https://github.com/0xPlayerOne/code-foundry/commit/5a8d8fd80e62feabc887869e07219a77f2c46c1a))
|
|
17
|
+
* **release:** bootstrap release-please manifest during sync ([98c7e5d](https://github.com/0xPlayerOne/code-foundry/commit/98c7e5d9341eedf22d49611f19825a5ef8d6d596))
|
|
18
|
+
* **release:** satisfy JSDoc type-check for manifest bootstrap ([02ddb45](https://github.com/0xPlayerOne/code-foundry/commit/02ddb459405ee9d54c91afdc3cf5602790a4c2b4))
|
|
19
|
+
* **release:** squash release PRs so staging can fast-forward after releases ([5d9a788](https://github.com/0xPlayerOne/code-foundry/commit/5d9a78843fde4ce764237eacfde9b1720d15696e))
|
|
20
|
+
|
|
3
21
|
## [0.32.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.18...v0.32.0) (2026-07-31)
|
|
4
22
|
|
|
5
23
|
|
package/docs/RELEASES.md
CHANGED
|
@@ -30,12 +30,35 @@ Set these values in `.github/code-foundry.yml`:
|
|
|
30
30
|
```yaml
|
|
31
31
|
release_type: auto # auto, node, python, rust, simple, or none
|
|
32
32
|
npm_publish: false # true only for an npm package
|
|
33
|
+
merge_strategy: rebase # merge, squash, or rebase; used for promotion PRs
|
|
34
|
+
release_merge_strategy: rebase # merge, squash, or rebase; used for Release Please PRs
|
|
33
35
|
```
|
|
34
36
|
|
|
37
|
+
`merge_strategy` applies to promotion PRs (`staging` into `main`) and
|
|
38
|
+
`release_merge_strategy` to Release Please version PRs; both default to
|
|
39
|
+
`merge` when unset. Use **rebase** for both. Rebase merges keep `main` fully
|
|
40
|
+
linear, which is what makes the post-release reconciliation possible:
|
|
41
|
+
`staging` requires linear history, and any merge commit on `main` becomes an
|
|
42
|
+
uncrossable barrier for `staging` (the linear-history rule rejects pushes
|
|
43
|
+
whose new commits contain merge commits). With a linear `main`, the release
|
|
44
|
+
workflow mirrors `staging` onto `main`'s tip after every release — a verified
|
|
45
|
+
fast-forward when possible, otherwise a forced linear update, otherwise a
|
|
46
|
+
single-parent synchronization commit — so `staging` is never left behind and
|
|
47
|
+
never needs a manual rebase.
|
|
48
|
+
|
|
35
49
|
`auto` selects a supported manifest. Use `simple` with `version.txt` for a
|
|
36
50
|
repository without a package manifest and `none` for a repository that should
|
|
37
51
|
not release automatically.
|
|
38
52
|
|
|
53
|
+
Release Please runs in manifest mode whenever the release config declares
|
|
54
|
+
`packages` or a top-level `release-type`; `code-foundry sync` bootstraps
|
|
55
|
+
`.release-please-manifest.json` from the current package versions the first
|
|
56
|
+
time it is needed. Release Please owns the manifest after the first release
|
|
57
|
+
and sync never overwrites existing manifest versions. Legacy configs without
|
|
58
|
+
`packages` or `release-type` continue to run in simple mode and need no
|
|
59
|
+
manifest; `code-foundry doctor` fails when a manifest-mode config is missing
|
|
60
|
+
its manifest.
|
|
61
|
+
|
|
39
62
|
## Pull request permissions
|
|
40
63
|
|
|
41
64
|
Release Please falls back to the repository's `GITHUB_TOKEN` when a
|
|
@@ -53,6 +76,6 @@ the generated version pull request before using the token to merge it.
|
|
|
53
76
|
|
|
54
77
|
1. Merge tested changes from `staging` into `main`.
|
|
55
78
|
2. Review the generated Release Please PR and changelog.
|
|
56
|
-
3. Merge the release PR with the repository's configured `
|
|
79
|
+
3. Merge the release PR with the repository's configured `release_merge_strategy` (default `merge`, recommended `rebase`).
|
|
57
80
|
4. Confirm the GitHub Release and any package publication.
|
|
58
81
|
5. Synchronize `staging` with the new `main` release commit.
|
package/package.json
CHANGED
package/src/commands/doctor.mjs
CHANGED
|
@@ -56,6 +56,18 @@ export function doctor(root, options = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
if (profile.languages.split(',').includes('python') && !commandExists('python') && !existsSync(join(target, '.venv/bin/python'))) error('Python is required for this repository')
|
|
58
58
|
|
|
59
|
+
const releaseConfigPath = join(target, 'release-please-config.json')
|
|
60
|
+
if (existsSync(releaseConfigPath)) {
|
|
61
|
+
/** @type {Record<string, any>} */
|
|
62
|
+
let releaseConfig = {}
|
|
63
|
+
try { releaseConfig = JSON.parse(readFileSync(releaseConfigPath, 'utf8')) }
|
|
64
|
+
catch { error('release-please-config.json is not valid JSON') }
|
|
65
|
+
const manifestMode = Boolean(releaseConfig.packages || releaseConfig['release-type'])
|
|
66
|
+
if (manifestMode && !existsSync(join(target, '.release-please-manifest.json'))) {
|
|
67
|
+
error('release-please-config.json requires .release-please-manifest.json; run code-foundry sync to bootstrap it')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
for (const workflow of ['ci', 'codeql', 'security', 'test', 'draft-pr', 'release-pr', 'release']) {
|
|
60
72
|
if (includesValue(config.features ?? 'all', workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
|
|
61
73
|
}
|
package/src/commands/release.mjs
CHANGED
|
@@ -10,8 +10,9 @@ import { hasDeliveredHook, releaseDeliveryKey, selectHookDelivery } from '../lib
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Reconcile staging after Release Please updates main. The local mode is
|
|
13
|
-
* deterministic and suitable for CI; --github
|
|
14
|
-
*
|
|
13
|
+
* deterministic and suitable for CI; --github mirrors staging onto main's
|
|
14
|
+
* tip through the GitHub API (fast-forward, then forced, then a linear sync
|
|
15
|
+
* commit) and otherwise fails closed.
|
|
15
16
|
* @param {string} root
|
|
16
17
|
* @param {ReleaseOptions} options
|
|
17
18
|
*/
|
|
@@ -38,55 +39,31 @@ export function reconcileRelease(root, options) {
|
|
|
38
39
|
})
|
|
39
40
|
console.log(JSON.stringify({ base, head, ...plan }, null, 2))
|
|
40
41
|
if (plan.action === 'fail') throw new Error(plan.reason + (plan.unexpected?.length ? ` Unexpected paths: ${plan.unexpected.join(', ')}` : ''))
|
|
41
|
-
if (
|
|
42
|
-
if (!process.env.GITHUB_REPOSITORY) throw new Error('GITHUB_REPOSITORY is required for synchronization PRs.')
|
|
43
|
-
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
|
44
|
-
if (!token) throw new Error('GH_TOKEN or GITHUB_TOKEN is required for synchronization PRs.')
|
|
45
|
-
const existing = ghJson(target, ['pr', 'list', '--repo', process.env.GITHUB_REPOSITORY, '--state', 'open', '--base', head, '--search', `chore(release): synchronize ${base} -> ${head} in:title`, '--json', 'number,url,headRefName'])
|
|
46
|
-
if (Array.isArray(existing) && existing.length) {
|
|
47
|
-
return { ...plan, synchronizationPullRequest: existing[0].url ?? existing[0].number }
|
|
48
|
-
}
|
|
49
|
-
const synchronizationPullRequest = createSynchronizationPullRequest(target, process.env.GITHUB_REPOSITORY, base, head, mainSha, stagingSha, mainChangedPaths, token)
|
|
50
|
-
return { ...plan, synchronizationPullRequest }
|
|
51
|
-
}
|
|
52
|
-
if (plan.action !== 'fast-forward' || !options.github || options.dryRun) return plan
|
|
42
|
+
if (!['fast-forward', 'pull-request', 'aligned'].includes(plan.action) || !options.github || options.dryRun) return plan
|
|
53
43
|
if (!process.env.GITHUB_REPOSITORY) throw new Error('GITHUB_REPOSITORY is required for --github reconciliation.')
|
|
54
44
|
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
|
55
45
|
if (!token) throw new Error('GH_TOKEN or GITHUB_TOKEN is required for --github reconciliation.')
|
|
56
|
-
const
|
|
46
|
+
const targetSha = /** @type {string} */ (plan.targetSha)
|
|
47
|
+
// 1. Verified fast-forward through the API (linear, no force).
|
|
48
|
+
const fastForward = spawnSync('gh', [
|
|
57
49
|
'api', '--method', 'PATCH', `repos/${process.env.GITHUB_REPOSITORY}/git/refs/heads/${head}`,
|
|
58
|
-
'-f', `sha=${
|
|
50
|
+
'-f', `sha=${targetSha}`, '-F', 'force=false',
|
|
59
51
|
], { cwd: target, stdio: 'inherit', env: { ...process.env, GH_TOKEN: token } })
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const add = spawnSync('git', ['add', '--', ...changedPaths], { cwd: root, encoding: 'utf8' })
|
|
76
|
-
if (add.status !== 0) throw new Error(`Failed to stage synchronization metadata: ${add.stderr.trim()}`)
|
|
77
|
-
const identity = spawnSync('git', ['-c', 'user.name=github-actions[bot]', '-c', 'user.email=41898282+github-actions[bot]@users.noreply.github.com', 'commit', '-m', `chore(release): synchronize ${base} -> ${head}`], { cwd: root, encoding: 'utf8' })
|
|
78
|
-
if (identity.status !== 0) throw new Error(`Failed to commit synchronization metadata: ${identity.stderr.trim()}`)
|
|
79
|
-
const push = spawnSync('git', ['push', '--set-upstream', 'origin', branch], { cwd: root, encoding: 'utf8', env: { ...process.env, GH_TOKEN: token } })
|
|
80
|
-
if (push.status !== 0) throw new Error(`Failed to publish synchronization branch: ${push.stderr.trim()}`)
|
|
81
|
-
}
|
|
82
|
-
const result = spawnSync('gh', [
|
|
83
|
-
'pr', 'create', '--repo', repository,
|
|
84
|
-
'--base', head, '--head', branch,
|
|
85
|
-
'--title', `chore(release): synchronize ${base} -> ${head}`,
|
|
86
|
-
'--body', `Synchronize ${head} with ${base} after Release Please publication.\n\nCode Foundry verified that only approved release metadata differs between the branch tips, then applied that metadata to an isolated synchronization branch.`,
|
|
87
|
-
], { cwd: resolve(root), encoding: 'utf8', env: { ...process.env, GH_TOKEN: token } })
|
|
88
|
-
if (result.status !== 0) throw new Error(`Failed to create synchronization PR: ${result.stderr.trim()}`)
|
|
89
|
-
return result.stdout.trim()
|
|
52
|
+
if (fastForward.status === 0) return { ...plan, synchronization: 'fast-forward' }
|
|
53
|
+
// 2. Forced update when staging may be rewritten without violating the
|
|
54
|
+
// linear-history rule (main's tip is linear and already contains staging's
|
|
55
|
+
// content, so nothing unpromoted is lost).
|
|
56
|
+
const forced = spawnSync('gh', [
|
|
57
|
+
'api', '--method', 'PATCH', `repos/${process.env.GITHUB_REPOSITORY}/git/refs/heads/${head}`,
|
|
58
|
+
'-f', `sha=${targetSha}`, '-F', 'force=true',
|
|
59
|
+
], { cwd: target, stdio: 'inherit', env: { ...process.env, GH_TOKEN: token } })
|
|
60
|
+
if (forced.status === 0) return { ...plan, synchronization: 'forced' }
|
|
61
|
+
// 3. Linear sync commit fallback: staging keeps its history and receives
|
|
62
|
+
// main's tree as a single-parent commit, which the ruleset always accepts.
|
|
63
|
+
const syncSha = createLinearSyncCommit(target, stagingSha, targetSha)
|
|
64
|
+
const push = spawnSync('git', ['push', 'origin', `${syncSha}:refs/heads/${head}`], { cwd: target, stdio: 'inherit', env: { ...process.env, GH_TOKEN: token } })
|
|
65
|
+
if (push.status !== 0) throw new Error(`GitHub refused the linear synchronization of ${head}.`)
|
|
66
|
+
return { ...plan, syncSha, synchronization: 'linear-commit' }
|
|
90
67
|
}
|
|
91
68
|
|
|
92
69
|
/**
|
|
@@ -170,6 +147,24 @@ function git(root, args) {
|
|
|
170
147
|
return result.status === 0 ? result.stdout.trim() : ''
|
|
171
148
|
}
|
|
172
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Build a linear commit whose tree equals main's tip and whose parent is the
|
|
152
|
+
* current staging tip. Staging requires linear history and main's tip almost
|
|
153
|
+
* always sits behind promotion merge commits, so a direct fast-forward to
|
|
154
|
+
* main's tip is rejected by the branch ruleset ("must not contain merge
|
|
155
|
+
* commits"). A single-parent commit is always pushable and keeps staging
|
|
156
|
+
* content-identical to main.
|
|
157
|
+
* @param {string} root @param {string} parentSha @param {string} mainSha @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
function createLinearSyncCommit(root, parentSha, mainSha) {
|
|
160
|
+
const tree = git(root, ['rev-parse', `${mainSha}^{tree}`])
|
|
161
|
+
if (!tree) throw new Error('Failed to resolve the target tree.')
|
|
162
|
+
const identity = ['-c', 'user.name=github-actions[bot]', '-c', 'user.email=41898282+github-actions[bot]@users.noreply.github.com']
|
|
163
|
+
const result = spawnSync('git', [...identity, 'commit-tree', tree, '-p', parentSha, '-m', 'chore(release): synchronize staging with main'], { cwd: resolve(root), encoding: 'utf8' })
|
|
164
|
+
if (result.status !== 0) throw new Error(`Failed to create the synchronization commit: ${result.stderr.trim()}`)
|
|
165
|
+
return result.stdout.trim()
|
|
166
|
+
}
|
|
167
|
+
|
|
173
168
|
/** @param {string} root @param {string} from @param {string} to @returns {string[]} */
|
|
174
169
|
function diffNames(root, from, to) {
|
|
175
170
|
if (!from || !to || from === to) return []
|
package/src/commands/sync.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { detectLanguages, detectPackageManager, detectProfile, recommendRunners } from '../lib/profile.mjs'
|
|
7
7
|
import { configured, includesValue, readConfig } from '../lib/config.mjs'
|
|
8
|
-
import { buildReleaseConfig } from '../lib/release-manifest.mjs'
|
|
8
|
+
import { buildReleaseConfig, buildReleaseManifest } from '../lib/release-manifest.mjs'
|
|
9
9
|
import { customWorkflowFiles, overlayPolicy } from '../lib/overlay.mjs'
|
|
10
10
|
|
|
11
11
|
const standardFiles = [
|
|
@@ -118,6 +118,27 @@ export function syncRepository(options) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
const releaseManifest = buildReleaseManifest(target, mergeReleaseConfig(target, sourcePath(source, 'release-please-config.json')))
|
|
122
|
+
if (releaseManifest) {
|
|
123
|
+
const manifestPath = join(target, '.release-please-manifest.json')
|
|
124
|
+
/** @type {Record<string, string>} */
|
|
125
|
+
let existingManifest = {}
|
|
126
|
+
if (existsSync(manifestPath)) {
|
|
127
|
+
try { existingManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) }
|
|
128
|
+
catch { existingManifest = {} }
|
|
129
|
+
}
|
|
130
|
+
/** @type {Record<string, string>} */
|
|
131
|
+
const mergedManifest = {}
|
|
132
|
+
for (const directory of [...new Set([...Object.keys(releaseManifest), ...Object.keys(existingManifest)])].sort()) {
|
|
133
|
+
mergedManifest[directory] = existingManifest[directory] ?? releaseManifest[directory]
|
|
134
|
+
}
|
|
135
|
+
const content = `${JSON.stringify(mergedManifest, null, 2)}\n`
|
|
136
|
+
if (!existsSync(manifestPath) || readFileSync(manifestPath, 'utf8') !== content) {
|
|
137
|
+
changed.push('.release-please-manifest.json')
|
|
138
|
+
writeOrReport(manifestPath, content, dryRun)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
121
142
|
if (license !== 'preserve' && license !== 'none') {
|
|
122
143
|
const licenseFile = license === 'mit' ? 'MIT.txt' : license === 'agpl-3.0-or-later' ? 'AGPL-3.0-or-later.txt' : 'GPL-3.0-or-later.txt'
|
|
123
144
|
const sourceLicense = join(source, '.github/licenses', licenseFile)
|
|
@@ -161,8 +182,8 @@ export function syncRepository(options) {
|
|
|
161
182
|
return { changed, config }
|
|
162
183
|
}
|
|
163
184
|
|
|
164
|
-
/** @param {string} target @param {string} sourceFile @returns {string} */
|
|
165
|
-
function
|
|
185
|
+
/** @param {string} target @param {string} sourceFile @returns {Record<string, any>} */
|
|
186
|
+
function mergeReleaseConfig(target, sourceFile) {
|
|
166
187
|
let baseline = {}
|
|
167
188
|
try { baseline = JSON.parse(readFileSync(sourceFile, 'utf8')) }
|
|
168
189
|
catch { baseline = {} }
|
|
@@ -172,8 +193,12 @@ function renderReleaseConfig(target, sourceFile) {
|
|
|
172
193
|
try { existing = JSON.parse(readFileSync(destination, 'utf8')) }
|
|
173
194
|
catch { existing = baseline }
|
|
174
195
|
}
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
return buildReleaseConfig(target, { ...baseline, ...existing })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @param {string} target @param {string} sourceFile @returns {string} */
|
|
200
|
+
function renderReleaseConfig(target, sourceFile) {
|
|
201
|
+
return `${JSON.stringify(mergeReleaseConfig(target, sourceFile), null, 2)}\n`
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
/** @param {string} file @param {string} languages @param {string} features @param {Record<string, string>} config */
|
|
@@ -339,7 +364,7 @@ function createDefaultConfig(root, source) {
|
|
|
339
364
|
post_release: 'false', post_release_workflow: '', post_release_mode: 'auto',
|
|
340
365
|
opencode_security: 'false',
|
|
341
366
|
sync_mode: 'overlay', custom_workflows: 'preserve',
|
|
342
|
-
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: '
|
|
367
|
+
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
|
|
343
368
|
}
|
|
344
369
|
}
|
|
345
370
|
|
|
@@ -66,6 +66,45 @@ export function buildReleaseConfig(root, existing = {}) {
|
|
|
66
66
|
return result
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Build the initial Release Please manifest from current package versions.
|
|
71
|
+
* Returns null when the config does not enable manifest mode (legacy configs
|
|
72
|
+
* without `packages`/`release-type` run in simple mode and need no manifest).
|
|
73
|
+
* @param {string} root
|
|
74
|
+
* @param {Record<string, any>} config
|
|
75
|
+
* @returns {Record<string, string>|null}
|
|
76
|
+
*/
|
|
77
|
+
export function buildReleaseManifest(root, config = {}) {
|
|
78
|
+
const packages = config.packages && typeof config.packages === 'object' && !Array.isArray(config.packages)
|
|
79
|
+
? config.packages
|
|
80
|
+
: config['release-type'] ? { '.': {} } : null
|
|
81
|
+
if (!packages || !Object.keys(packages).length) return null
|
|
82
|
+
/** @type {Record<string, string>} */
|
|
83
|
+
const manifest = {}
|
|
84
|
+
for (const directory of Object.keys(packages)) {
|
|
85
|
+
manifest[directory] = readDirectoryVersion(join(root, directory === '.' ? '' : directory))
|
|
86
|
+
}
|
|
87
|
+
return manifest
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {string} directory @returns {string} */
|
|
91
|
+
function readDirectoryVersion(directory) {
|
|
92
|
+
const packagePath = join(directory, 'package.json')
|
|
93
|
+
if (existsSync(packagePath)) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'))
|
|
96
|
+
if (typeof parsed?.version === 'string' && parsed.version) return parsed.version
|
|
97
|
+
} catch { /* fall through to other manifests */ }
|
|
98
|
+
}
|
|
99
|
+
for (const file of ['pyproject.toml', 'Cargo.toml']) {
|
|
100
|
+
const path = join(directory, file)
|
|
101
|
+
if (!existsSync(path)) continue
|
|
102
|
+
const match = readFileSync(path, 'utf8').match(/^version\s*=\s*["']([^"']+)["']/m)
|
|
103
|
+
if (match?.[1]) return match[1]
|
|
104
|
+
}
|
|
105
|
+
return '0.0.0'
|
|
106
|
+
}
|
|
107
|
+
|
|
69
108
|
/** @param {string} root @param {Record<string, any>} config @returns {string[]} */
|
|
70
109
|
export function validateReleaseConfig(root, config) {
|
|
71
110
|
const detected = detectReleasePackages(root)
|
|
@@ -186,7 +186,7 @@ export function classifyReconciliation(input) {
|
|
|
186
186
|
return { action: 'none', reason: 'staging contains commits that are not on main; no release-only reconciliation is needed.' }
|
|
187
187
|
}
|
|
188
188
|
if (!mainChangedPaths.length && !stagingChangedPaths.length) {
|
|
189
|
-
return { action: 'aligned', reason: 'Branches have different history but identical content.' }
|
|
189
|
+
return { action: 'aligned', targetSha: mainSha, reason: 'Branches have different history but identical content.' }
|
|
190
190
|
}
|
|
191
191
|
const unexpectedMain = unexpectedReleasePaths(mainChangedPaths, allowed)
|
|
192
192
|
const unexpectedStaging = unexpectedReleasePaths(stagingChangedPaths, allowed)
|