code-foundry 0.31.18 → 0.32.1
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/CONTRIBUTING.md +2 -2
- package/.github/code-foundry.yml +2 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/draft-pr.yml +25 -1
- package/.github/workflows/release-pr.yml +1 -1
- package/.github/workflows/release.yml +7 -3
- package/.github/workflows/security.yml +1 -1
- package/.github/workflows/test.yml +1 -1
- package/CHANGELOG.md +17 -0
- package/docs/CONFIGURATION.md +1 -1
- package/docs/RELEASES.md +21 -4
- package/docs/WORKFLOWS.md +6 -5
- package/package.json +1 -1
- package/src/commands/doctor.mjs +12 -0
- package/src/commands/sync.mjs +31 -6
- package/src/lib/github-doctor.mjs +61 -13
- package/src/lib/release-manifest.mjs +39 -0
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -48,7 +48,7 @@ docs/* test/* refactor/* │ │
|
|
|
48
48
|
| `staging` | Integration branch | Target normal pull requests here. Required checks must pass before merge. |
|
|
49
49
|
| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `staging`; keep changes small and reviewable. |
|
|
50
50
|
|
|
51
|
-
The default Git workflow is `staging-release`: topic branches merge into `staging
|
|
51
|
+
The default Git workflow is `staging-release`: topic branches merge into `staging` (typically after rebasing), then a promotion PR moves validated changes into `main`, followed by the versioned release PR. The default release merge strategy is `merge`, matching your preference for merge-commit promotions from staging to main. Configure `merge_strategy` as `rebase` or `squash` in `.github/code-foundry.yml` if a repository intentionally differs. Re-align `staging` with `main` after a release when needed.
|
|
52
52
|
|
|
53
53
|
## Before you start
|
|
54
54
|
|
|
@@ -163,7 +163,7 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
|
|
|
163
163
|
|
|
164
164
|
The workflows use separate concurrency groups keyed by the commit under test. A newer run for the same commit cancels a duplicate event-triggered run, while newer commits cancel older runs and independent CI, Test, Security, and CodeQL workflows continue in parallel.
|
|
165
165
|
|
|
166
|
-
Required checks are enforced by branch protection. Do not duplicate their checklists in the pull request description; document validation commands and results instead.
|
|
166
|
+
Required checks are enforced by branch protection rulesets/branch protection. Do not duplicate their checklists in the pull request description; document validation commands and results instead.
|
|
167
167
|
|
|
168
168
|
### Release conventions
|
|
169
169
|
|
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: merge
|
|
20
|
+
release_merge_strategy: squash
|
|
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:
|
|
@@ -36,12 +36,36 @@ jobs:
|
|
|
36
36
|
if: steps.check-pr.outputs.existing == '0'
|
|
37
37
|
run: |
|
|
38
38
|
BRANCH="${{ github.ref_name }}"
|
|
39
|
+
PREFIX="${BRANCH%%/*}"
|
|
40
|
+
SUFFIX="${BRANCH#*/}"
|
|
41
|
+
PR_TITLE=""
|
|
42
|
+
|
|
43
|
+
case "$PREFIX" in
|
|
44
|
+
feat|fix|chore|refactor|docs|test|style|ci|build)
|
|
45
|
+
if [ "$SUFFIX" != "$BRANCH" ] && [ -n "$SUFFIX" ]; then
|
|
46
|
+
PR_TITLE="${PREFIX}: ${SUFFIX}"
|
|
47
|
+
fi
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
PR_TITLE="${PR_TITLE//[\/_]/ }"
|
|
54
|
+
PR_TITLE="$(printf '%s' "$PR_TITLE" | tr -s ' ' | sed 's/^ *//; s/ *$//')"
|
|
55
|
+
|
|
56
|
+
if [ -z "$PR_TITLE" ]; then
|
|
57
|
+
PR_TITLE=$(git log -1 --pretty=%s 2>/dev/null || true)
|
|
58
|
+
fi
|
|
59
|
+
if [ -z "$PR_TITLE" ]; then
|
|
60
|
+
PR_TITLE="$BRANCH"
|
|
61
|
+
fi
|
|
62
|
+
|
|
39
63
|
gh pr create \
|
|
40
64
|
--repo "$GITHUB_REPOSITORY" \
|
|
41
65
|
--base staging \
|
|
42
66
|
--head "$BRANCH" \
|
|
43
67
|
--draft \
|
|
44
|
-
--title "
|
|
68
|
+
--title "$PR_TITLE" \
|
|
45
69
|
--body "Draft PR created automatically by CI.
|
|
46
70
|
|
|
47
71
|
This PR is a draft and should not be merged until CI passes and a reviewer (or the Daily Review agent) marks it ready.
|
|
@@ -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: **merge**).
|
|
161
161
|
- After merge, Release Please opens a separate versioned release PR with the changelog.
|
|
162
162
|
EOF
|
|
163
163
|
|
|
@@ -72,10 +72,12 @@ jobs:
|
|
|
72
72
|
: {}
|
|
73
73
|
let releaseType = config.release_type || 'auto'
|
|
74
74
|
let npmPublish = config.npm_publish || 'false'
|
|
75
|
+
let releaseMergeStrategy = String(config.release_merge_strategy || config.merge_strategy || 'merge').trim().toLowerCase()
|
|
75
76
|
if (releaseType === 'auto') {
|
|
76
77
|
releaseType = fs.existsSync('package.json') ? 'node' : fs.existsSync('pyproject.toml') ? 'python' : fs.existsSync('Cargo.toml') ? 'rust' : fs.existsSync('version.txt') ? 'simple' : 'none'
|
|
77
78
|
}
|
|
78
79
|
if (!['node', 'python', 'rust', 'simple', 'none'].includes(releaseType)) throw new Error(`Unsupported release_type: ${releaseType}`)
|
|
80
|
+
if (!['rebase', 'squash', 'merge'].includes(releaseMergeStrategy)) throw new Error(`Unsupported merge_strategy: ${releaseMergeStrategy}`)
|
|
79
81
|
if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
|
|
80
82
|
let legacyReleaseType = ''
|
|
81
83
|
if (releaseType !== 'none') {
|
|
@@ -84,11 +86,13 @@ jobs:
|
|
|
84
86
|
const releaseConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'))
|
|
85
87
|
if (!releaseConfig.packages && !releaseConfig['release-type']) {
|
|
86
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.')
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
fs.appendFileSync(
|
|
90
94
|
process.env.GITHUB_OUTPUT,
|
|
91
|
-
`release_type=${releaseType}\nlegacy_release_type=${legacyReleaseType}\nnpm_publish=${npmPublish}\n`,
|
|
95
|
+
`release_type=${releaseType}\nlegacy_release_type=${legacyReleaseType}\nnpm_publish=${npmPublish}\nrelease_merge_strategy=${releaseMergeStrategy}\n`,
|
|
92
96
|
)
|
|
93
97
|
NODE
|
|
94
98
|
- name: Detect release credentials
|
|
@@ -161,14 +165,14 @@ jobs:
|
|
|
161
165
|
gh pr merge "$pr" \
|
|
162
166
|
--repo "$GITHUB_REPOSITORY" \
|
|
163
167
|
--admin \
|
|
164
|
-
|
|
168
|
+
--${{ steps.profile.outputs.release_merge_strategy }} \
|
|
165
169
|
--delete-branch
|
|
166
170
|
done
|
|
167
171
|
|
|
168
172
|
reconcile:
|
|
169
173
|
name: Release / Reconcile
|
|
170
174
|
needs: release
|
|
171
|
-
if: needs.release.
|
|
175
|
+
if: needs.release.result == 'success'
|
|
172
176
|
runs-on: ${{ inputs.runner }}
|
|
173
177
|
timeout-minutes: 15
|
|
174
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,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.32.1](https://github.com/0xPlayerOne/code-foundry/compare/v0.32.0...v0.32.1) (2026-07-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **ci:** key runtime concurrency by event so promotion PRs do not cancel push checks ([5a8d8fd](https://github.com/0xPlayerOne/code-foundry/commit/5a8d8fd80e62feabc887869e07219a77f2c46c1a))
|
|
9
|
+
* **release:** bootstrap release-please manifest during sync ([98c7e5d](https://github.com/0xPlayerOne/code-foundry/commit/98c7e5d9341eedf22d49611f19825a5ef8d6d596))
|
|
10
|
+
* **release:** satisfy JSDoc type-check for manifest bootstrap ([02ddb45](https://github.com/0xPlayerOne/code-foundry/commit/02ddb459405ee9d54c91afdc3cf5602790a4c2b4))
|
|
11
|
+
* **release:** squash release PRs so staging can fast-forward after releases ([5d9a788](https://github.com/0xPlayerOne/code-foundry/commit/5d9a78843fde4ce764237eacfde9b1720d15696e))
|
|
12
|
+
|
|
13
|
+
## [0.32.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.18...v0.32.0) (2026-07-31)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* improve draft PR titles from branch conventions ([#290](https://github.com/0xPlayerOne/code-foundry/issues/290)) ([7b215db](https://github.com/0xPlayerOne/code-foundry/commit/7b215db961347dfdb74241fc2c3489d390b34533))
|
|
19
|
+
|
|
3
20
|
## [0.31.18](https://github.com/0xPlayerOne/code-foundry/compare/v0.31.17...v0.31.18) (2026-07-30)
|
|
4
21
|
|
|
5
22
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -48,7 +48,7 @@ repository manifests and source
|
|
|
48
48
|
| `npm_publish` | `true` or `false` | Opt into npm publication |
|
|
49
49
|
| `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
|
|
50
50
|
| `git_workflow` | `staging-release` | Branch/release model; the standard model promotes `staging` into `main` |
|
|
51
|
-
| `merge_strategy` | `rebase`, `squash`, `merge` | Preferred merge method for contribution and release PRs; defaults to `
|
|
51
|
+
| `merge_strategy` | `rebase`, `squash`, `merge` | Preferred merge method for contribution and release PRs; defaults to `merge` |
|
|
52
52
|
| `runner` fields | GitHub runner names | Per-workflow runner policy |
|
|
53
53
|
|
|
54
54
|
Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
|
package/docs/RELEASES.md
CHANGED
|
@@ -12,9 +12,8 @@ Keep commits Conventional Commit-shaped (`feat:`, `fix:`, `docs:`, `ci:`,
|
|
|
12
12
|
`chore:`, and so on). Release Please uses them to select patch/minor/major
|
|
13
13
|
versions and generate grouped changelog notes.
|
|
14
14
|
|
|
15
|
-
The default `merge_strategy` is `
|
|
16
|
-
|
|
17
|
-
that repository intentionally uses a different merge convention. The current
|
|
15
|
+
The default `merge_strategy` is `merge`, matching promotion PRs from `staging` into `main`.
|
|
16
|
+
Configure it to `squash` or `rebase` in `.github/code-foundry.yml` if a repository intentionally differs. The current
|
|
18
17
|
supported `git_workflow` is `staging-release`.
|
|
19
18
|
|
|
20
19
|
The release workflow opens or updates a versioned PR after changes reach
|
|
@@ -31,12 +30,30 @@ Set these values in `.github/code-foundry.yml`:
|
|
|
31
30
|
```yaml
|
|
32
31
|
release_type: auto # auto, node, python, rust, simple, or none
|
|
33
32
|
npm_publish: false # true only for an npm package
|
|
33
|
+
merge_strategy: merge # merge, squash, or rebase; used for promotion PRs
|
|
34
|
+
release_merge_strategy: squash # merge, squash, or rebase; used for Release Please PRs
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
`merge_strategy` applies to promotion PRs (`staging` into `main`); merging
|
|
38
|
+
those with a merge commit keeps `staging` an ancestor of `main`.
|
|
39
|
+
`release_merge_strategy` applies to Release Please version PRs; squash-merging
|
|
40
|
+
them keeps `main`'s tip linear so the post-release reconciliation can
|
|
41
|
+
fast-forward `staging` to `main` without violating `staging`'s required-linear-
|
|
42
|
+
history rule. Defaults to `merge` when unset.
|
|
43
|
+
|
|
36
44
|
`auto` selects a supported manifest. Use `simple` with `version.txt` for a
|
|
37
45
|
repository without a package manifest and `none` for a repository that should
|
|
38
46
|
not release automatically.
|
|
39
47
|
|
|
48
|
+
Release Please runs in manifest mode whenever the release config declares
|
|
49
|
+
`packages` or a top-level `release-type`; `code-foundry sync` bootstraps
|
|
50
|
+
`.release-please-manifest.json` from the current package versions the first
|
|
51
|
+
time it is needed. Release Please owns the manifest after the first release
|
|
52
|
+
and sync never overwrites existing manifest versions. Legacy configs without
|
|
53
|
+
`packages` or `release-type` continue to run in simple mode and need no
|
|
54
|
+
manifest; `code-foundry doctor` fails when a manifest-mode config is missing
|
|
55
|
+
its manifest.
|
|
56
|
+
|
|
40
57
|
## Pull request permissions
|
|
41
58
|
|
|
42
59
|
Release Please falls back to the repository's `GITHUB_TOKEN` when a
|
|
@@ -54,6 +71,6 @@ the generated version pull request before using the token to merge it.
|
|
|
54
71
|
|
|
55
72
|
1. Merge tested changes from `staging` into `main`.
|
|
56
73
|
2. Review the generated Release Please PR and changelog.
|
|
57
|
-
3. Merge the release PR with the repository
|
|
74
|
+
3. Merge the release PR with the repository.s configured `release_merge_strategy` (default `merge`, recommended `squash`).
|
|
58
75
|
4. Confirm the GitHub Release and any package publication.
|
|
59
76
|
5. Synchronize `staging` with the new `main` release commit.
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -61,13 +61,14 @@ checks disabled.
|
|
|
61
61
|
|
|
62
62
|
## Branch protection
|
|
63
63
|
|
|
64
|
-
Use
|
|
65
|
-
|
|
64
|
+
Use repository rulesets (or legacy branch protection settings) to mirror the
|
|
65
|
+
required checks for each protected branch. Review the repository's enabled
|
|
66
|
+
features and enforce only checks that actually run:
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
69
|
Apply only checks for enabled workflows.
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
Keep strict status checks, linear history, and conversation resolution enabled
|
|
72
|
-
For a repository with optional features disabled, do not require
|
|
73
|
-
will never run.
|
|
72
|
+
Keep strict status checks, linear history, and conversation resolution enabled
|
|
73
|
+
where required. For a repository with optional features disabled, do not require
|
|
74
|
+
checks that will never run.
|
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/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: 'merge', release_merge_strategy: 'squash',
|
|
343
368
|
}
|
|
344
369
|
}
|
|
345
370
|
|
|
@@ -15,23 +15,22 @@ export function doctorGithub(root) {
|
|
|
15
15
|
const warnings = []
|
|
16
16
|
/** @type {Record<string, any>} */
|
|
17
17
|
const details = { repository }
|
|
18
|
+
const rulesets = hydrateRulesets(repository, ghJson(['api', `repos/${repository}/rulesets`]))
|
|
19
|
+
const mainRulesets = rulesetsForBranch(rulesets, 'main')
|
|
18
20
|
const protection = ghJson(['api', `repos/${repository}/branches/main/protection`])
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!protection.required_status_checks) warnings.push('main protection has no required status checks.')
|
|
26
|
-
}
|
|
21
|
+
const required = mainRulesets.length ? requiredContextsFromRulesets(mainRulesets) : requiredContextsFromProtection(protection)
|
|
22
|
+
details.requiredChecks = required
|
|
23
|
+
if (!protection && !mainRulesets.length) warnings.push('main branch protection or repository rulesets are not readable or are not configured.')
|
|
24
|
+
else if (!required.length) warnings.push(mainRulesets.length ? 'main ruleset has no required status checks.' : 'main protection has no required status checks.')
|
|
25
|
+
const duplicateNames = required.filter((name) => /\b([^/]+) \/ \1\b/i.test(name))
|
|
26
|
+
if (duplicateNames.length) errors.push(`required checks contain duplicate workflow prefixes: ${duplicateNames.join(', ')}`)
|
|
27
27
|
|
|
28
28
|
const sha = String(ghJson(['api', `repos/${repository}/git/ref/heads/main`])?.object?.sha ?? '')
|
|
29
29
|
const checks = sha ? ghJson(['api', `repos/${repository}/commits/${sha}/check-runs?per_page=100`])?.check_runs ?? [] : []
|
|
30
30
|
const observed = checks.map(/** @param {any} check */ (check) => check.name).filter(Boolean)
|
|
31
31
|
details.observedChecks = observed
|
|
32
32
|
if (!observed.length) warnings.push('no check runs were observed on the current main commit; exact check validation is deferred until CI runs.')
|
|
33
|
-
if (
|
|
34
|
-
const required = requiredContexts(protection)
|
|
33
|
+
if (required.length) {
|
|
35
34
|
const missing = required.filter((name) => observed.length && !observed.includes(name))
|
|
36
35
|
if (missing.length) warnings.push(`required checks not observed on current main: ${missing.join(', ')}`)
|
|
37
36
|
}
|
|
@@ -70,13 +69,62 @@ export function doctorGithub(root) {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/** @param {any} protection @returns {string[]} */
|
|
73
|
-
function
|
|
72
|
+
function requiredContextsFromProtection(protection) {
|
|
74
73
|
return [...new Set([
|
|
75
|
-
...(protection
|
|
76
|
-
...(protection
|
|
74
|
+
...(protection?.required_status_checks?.contexts ?? []),
|
|
75
|
+
...(protection?.required_status_checks?.checks ?? []).map(/** @param {any} check */ (check) => check.context),
|
|
77
76
|
].filter(Boolean))]
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
/** @param {any[]} rulesets @returns {string[]} */
|
|
80
|
+
function requiredContextsFromRulesets(rulesets) {
|
|
81
|
+
return [...new Set(
|
|
82
|
+
(Array.isArray(rulesets) ? rulesets : []).flatMap((/** @type {any} */ ruleset) =>
|
|
83
|
+
(ruleset.rules ?? []).flatMap((/** @type {any} */ rule) =>
|
|
84
|
+
rule?.type === 'required_status_checks' ?
|
|
85
|
+
(rule.parameters?.required_status_checks ?? []).map((/** @type {any} */ check) => check?.context).filter(Boolean) :
|
|
86
|
+
[]
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
)]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {string} repository @param {any[] | null} rulesets @returns {any[]} */
|
|
93
|
+
function hydrateRulesets(repository, rulesets) {
|
|
94
|
+
if (!Array.isArray(rulesets)) return []
|
|
95
|
+
return rulesets.map((ruleset) => {
|
|
96
|
+
if (!ruleset?.id) return ruleset
|
|
97
|
+
const details = ghJson(['api', `repos/${repository}/rulesets/${ruleset.id}`])
|
|
98
|
+
return details && typeof details === 'object' ? details : ruleset
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {any[] | null} rulesets @param {string} branch @returns {any[]} */
|
|
103
|
+
function rulesetsForBranch(rulesets, branch) {
|
|
104
|
+
if (!Array.isArray(rulesets)) return []
|
|
105
|
+
return rulesets.filter((ruleset) => (
|
|
106
|
+
ruleset?.target === 'branch' &&
|
|
107
|
+
Array.isArray(ruleset?.conditions?.ref_name?.include) &&
|
|
108
|
+
Array.isArray(ruleset?.conditions?.ref_name?.exclude) &&
|
|
109
|
+
includesBranch(ruleset.conditions.ref_name.include, branch) &&
|
|
110
|
+
!includesBranch(ruleset.conditions.ref_name.exclude, branch)
|
|
111
|
+
))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {(string | null | undefined)[]} patterns @param {string} branch @returns {boolean} */
|
|
115
|
+
function includesBranch(patterns, branch) {
|
|
116
|
+
const target = `refs/heads/${branch}`
|
|
117
|
+
return patterns.some((pattern) => {
|
|
118
|
+
if (!pattern) return false
|
|
119
|
+
if (pattern === '~ALL') return true
|
|
120
|
+
if (pattern === target || pattern === branch) return true
|
|
121
|
+
if (!/[\*?]/.test(pattern)) return false
|
|
122
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*').replace(/\\\?/g, '.')
|
|
123
|
+
const regex = new RegExp(`^${escaped}$`)
|
|
124
|
+
return regex.test(target) || regex.test(branch)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
80
128
|
/** @param {string} root @returns {{ errors: string[], warnings: string[] }} */
|
|
81
129
|
function inspectWorkflows(root) {
|
|
82
130
|
/** @type {string[]} */
|
|
@@ -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)
|