Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ docs/* test/* refactor/* │ │
| `staging` | Integration branch | Target normal pull requests here. Required checks must pass before merge. |
| `feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, `test/*` | Focused work | Branch from `staging`; keep changes small and reviewable. |

The Git workflow is `staging-release`: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **squashes** into `main` (`release_merge_strategy: squash`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other merge strategy. Re-align `staging` with `main` after a release when needed.
The Git workflow is `staging-release`: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **rebases** into `main` (`release_merge_strategy: rebase`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other merge strategy. Re-align `staging` with `main` after a release when needed.

## Before you start

Expand Down Expand Up @@ -179,7 +179,7 @@ Security checks can be skipped when repository visibility or the GitHub plan doe
| ----------------- | --------- | ------------------------------------------------------- | --------------------------------------------------------- |
| Working branch | `staging` | Squash | All applicable required checks pass |
| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |
| Release Please version PR | `main` | Squash (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |
| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |

Reviewers focus on correctness, security, maintainability, test coverage, operational impact, and compatibility. Authors remain responsible for responding to feedback and verifying the final commit.

Expand Down
2 changes: 1 addition & 1 deletion .github/code-foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm_publish: true
license: agpl-3.0-or-later
git_workflow: staging-release
merge_strategy: rebase
release_merge_strategy: squash
release_merge_strategy: rebase
runner: ubuntu-latest
unit_runner: ubuntu-slim
cache_packages: auto
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,18 @@ jobs:
: {}
let releaseType = config.release_type || 'auto'
let npmPublish = config.npm_publish || 'false'
// The merge audit topology requires squash for Release Please version
// pull requests into main. Release automation never defaults to a
// merge method: an unset or non-squash release_merge_strategy fails
// the release job before any pull request is created or merged.
// The merge audit topology requires rebase for Release Please version
// pull requests into main, matching the linear-history ruleset.
// Release automation never defaults to a merge method: an unset or
// non-rebase release_merge_strategy fails the release job before any
// pull request is created or merged.
const releaseMergeStrategy = String(config.release_merge_strategy || '').trim().toLowerCase()
if (releaseType === 'auto') {
releaseType = fs.existsSync('package.json') ? 'node' : fs.existsSync('pyproject.toml') ? 'python' : fs.existsSync('Cargo.toml') ? 'rust' : fs.existsSync('version.txt') ? 'simple' : 'none'
}
if (!['node', 'python', 'rust', 'simple', 'none'].includes(releaseType)) throw new Error(`Unsupported release_type: ${releaseType}`)
if (releaseMergeStrategy !== 'squash') {
throw new Error(`release_merge_strategy must be "squash" for automated release merges; got ${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}`)
if (releaseMergeStrategy !== 'rebase') {
throw new Error(`release_merge_strategy must be "rebase" for automated release merges; got ${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}`)
}
if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
let legacyReleaseType = ''
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ checks, runners, coverage, caching, and custom workflow extensions.

The contribution policy uses the `staging-release` workflow: feature PRs squash
into `staging`, the promotion PR rebases into `main` (`merge_strategy: rebase`),
and Release Please version PRs squash into `main` (`release_merge_strategy:
squash`). Release automation never defaults to a merge method and never merges
and Release Please version PRs rebase into `main` (`release_merge_strategy:
rebase`). Release automation never defaults to a merge method and never merges
with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on
any other strategy. GitHub Stacks is not part of this topology and does not
reduce the required workflow runs.
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ repository manifests and source
| `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
| `git_workflow` | `staging-release` | Branch/release model; the standard model promotes `staging` into `main` |
| `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; the staging-release topology requires rebase |
| `release_merge_strategy` | `squash` | Merge method for Release Please version PRs into `main`; release automation fails closed unless squash |
| `release_merge_strategy` | `rebase` | Merge method for Release Please version PRs into `main`; release automation fails closed unless rebase |
| `runner` fields | GitHub runner names | Per-workflow runner policy |

Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
Expand Down
10 changes: 5 additions & 5 deletions docs/RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ versions and generate grouped changelog notes.
The merge audit pins one merge method per transition in this topology. Feature
and fix branches land on `staging` with **squash** merges, the `staging` → `main`
promotion PR merges with **rebase** (`merge_strategy: rebase`), and Release
Please version PRs merge with **squash** (`release_merge_strategy: squash`).
Please version PRs merge with **rebase** (`release_merge_strategy: rebase`).
Release automation never defaults to a merge method and never merges with
`--admin`: the release workflow fails closed unless `release_merge_strategy` is
exactly `squash`. The current supported `git_workflow` is `staging-release`.
exactly `rebase`. The current supported `git_workflow` is `staging-release`.

The release workflow opens or updates a versioned PR after changes reach
`main`. Merging that PR updates the changelog, creates the Git tag and GitHub
Expand All @@ -35,13 +35,13 @@ Set these values in `.github/code-foundry.yml`:
release_type: auto # auto, node, python, rust, simple, or none
npm_publish: false # true only for an npm package
merge_strategy: rebase # required: staging -> main promotion PRs rebase
release_merge_strategy: squash # required: Release Please version PRs squash only
release_merge_strategy: rebase # required: Release Please version PRs rebase only
```

`merge_strategy` applies to promotion PRs (`staging` into `main`) and
`release_merge_strategy` to Release Please version PRs; feature PRs into
`staging` use squash merges. The topology requires `merge_strategy: rebase`
and `release_merge_strategy: squash`; `code-foundry doctor` and
and `release_merge_strategy: rebase`; `code-foundry doctor` and
`code-foundry sync` reject any other value, and the release workflow fails
closed instead of falling back to `merge`. Both keep `main` fully linear,
which is what makes the post-release reconciliation possible: release-only
Expand Down Expand Up @@ -103,6 +103,6 @@ already passed).

1. Merge tested changes from `staging` into `main`.
2. Review the generated Release Please PR and changelog.
3. Merge the release PR with the repository's configured `release_merge_strategy` (**squash**; the release workflow fails closed on any other value).
3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase**; the release workflow fails closed on any other value).
4. Confirm the GitHub Release and any package publication.
5. Synchronize `staging` with the new `main` release commit.
6 changes: 3 additions & 3 deletions docs/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ enabled for the repository profile.

The merge audit pins one merge method per transition. `code-foundry doctor`
and `code-foundry sync` fail closed on any other strategy, and the release
workflow refuses to run unless its strategy is exactly `squash`.
workflow refuses to run unless its strategy is exactly `rebase`.

| Transition | Merge method | Enforcement |
| --- | --- | --- |
| Feature/fix PR into `staging` | Squash | Contribution policy; see `CONTRIBUTING.md` |
| `staging` → `main` promotion PR | Rebase (`merge_strategy: rebase`) | `merge_strategy` must be `rebase`; merge commits are rejected |
| Release Please version PR into `main` | Squash (`release_merge_strategy: squash`) | Release automation fails closed unless `squash`; never defaults to `merge`, never uses `--admin` |
| Release Please version PR into `main` | Rebase (`release_merge_strategy: rebase`) | Release automation fails closed unless `rebase`; never defaults to `merge`, never uses `--admin` |

Release auto-merge waits for required checks and then polls `mergeStateStatus`
until it is `CLEAN`, or `UNSTABLE` with `mergeable` `MERGEABLE`, before
Expand All @@ -59,7 +59,7 @@ additional required checks after earlier ones have passed (for example
merge policy-blocked. The mergeability poll is bounded and fails closed on
conflicts or timeout; releases without an automation token remain manual.

Keeping `main` linear — rebase promotions and squash release PRs — is what
Keeping `main` linear — rebase promotions and rebase release PRs — is what
lets the post-release reconciliation fast-forward or replay `staging` safely.

Protect `staging` with the aggregate `Validation / Gate`, squash-only pull
Expand Down
4 changes: 2 additions & 2 deletions src/commands/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export function doctor(root, options = {}) {
error(`merge_strategy must be "rebase" for the staging-release promotion topology; got "${mergeStrategy}".`)
}
const releaseMergeStrategy = config.release_merge_strategy ?? ''
if (includesValue(features, 'release') && releaseMergeStrategy !== 'squash') {
error(`release_merge_strategy must be "squash" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`)
if (includesValue(features, 'release') && releaseMergeStrategy !== 'rebase') {
error(`release_merge_strategy must be "rebase" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`)
}
for (const workflow of ['validation', 'draft-pr', 'release-pr', 'release']) {
if (includesValue(features, workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
Expand Down
6 changes: 3 additions & 3 deletions src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export function syncRepository(options) {
throw new Error(`Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`)
}
const releaseMergeStrategy = configured(config.release_merge_strategy, '')
if (includesValue(features, 'release') && releaseMergeStrategy !== 'squash') {
throw new Error(`Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires squash for Release Please version pull requests and never defaults to merge.`)
if (includesValue(features, 'release') && releaseMergeStrategy !== 'rebase') {
throw new Error(`Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase for Release Please version pull requests and never defaults to merge.`)
}
const license = configured(config.license, existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later')
const changed = []
Expand Down Expand Up @@ -460,7 +460,7 @@ function createDefaultConfig(root, source) {
post_release: 'false', post_release_workflow: '', post_release_mode: 'auto',
opencode_security: 'false',
sync_mode: 'overlay', custom_workflows: 'preserve',
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: 'rebase', release_merge_strategy: 'squash',
license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
}
}

Expand Down
14 changes: 7 additions & 7 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ describe('code-foundry CLI', () => {
assert.doesNotMatch(createStep, /echo "\$AUTOMATION_TOKEN"|printenv|GITHUB_OUTPUT/)
assert.doesNotMatch(workflow, /gh pr create/)
})
it('fails closed on a non-squash release merge strategy and never uses --admin', () => {
it('fails closed on a non-rebase release merge strategy and never uses --admin', () => {
const workflow = readFileSync('.github/workflows/release.yml', 'utf8')

assert.match(workflow, /if \(!releaseConfig\.packages && !releaseConfig\['release-type'\]\)/)
Expand All @@ -791,7 +791,7 @@ describe('code-foundry CLI', () => {
assert.match(workflow, /release-type: \$\{\{ steps\.profile\.outputs\.legacy_release_type \}\}/)
assert.match(workflow, /release validate-prs/)
assert.doesNotMatch(workflow, /--admin/)
assert.match(workflow, /release_merge_strategy must be "squash"/)
assert.match(workflow, /release_merge_strategy must be "rebase"/)
assert.match(workflow, /release automation never defaults to merge/)
assert.doesNotMatch(workflow, /release_merge_strategy \|\| config\.merge_strategy/)
assert.doesNotMatch(workflow, /\|\| 'merge'/)
Expand Down Expand Up @@ -873,20 +873,20 @@ describe('code-foundry CLI', () => {
return errors
}

writeFileSync(join(root, '.github/code-foundry.yml'), 'languages: typescript\npackage_manager: bun\nmerge_strategy: rebase\nrelease_merge_strategy: squash\n')
writeFileSync(join(root, '.github/code-foundry.yml'), 'languages: typescript\npackage_manager: bun\nmerge_strategy: rebase\nrelease_merge_strategy: rebase\n')
syncRepository({ target: root, source: process.cwd() })
assert.doesNotThrow(() => doctor(root))

writeFileSync(join(root, '.github/code-foundry.yml'), 'languages: typescript\npackage_manager: bun\nmerge_strategy: merge\nrelease_merge_strategy: merge\n')
const promotion = captureErrors(() => doctor(root))
assert.ok(promotion.some((message) => /merge_strategy must be "rebase"/.test(message)), promotion.join('\n'))
assert.ok(promotion.some((message) => /release_merge_strategy must be "squash"/.test(message)), promotion.join('\n'))
assert.ok(promotion.some((message) => /release_merge_strategy must be "rebase"/.test(message)), promotion.join('\n'))
assert.throws(() => syncRepository({ target: root, source: process.cwd() }), /Unsupported merge_strategy: merge/)

writeFileSync(join(root, '.github/code-foundry.yml'), 'languages: typescript\npackage_manager: bun\nmerge_strategy: rebase\nrelease_merge_strategy: rebase\n')
writeFileSync(join(root, '.github/code-foundry.yml'), 'languages: typescript\npackage_manager: bun\nmerge_strategy: rebase\nrelease_merge_strategy: squash\n')
const release = captureErrors(() => doctor(root))
assert.ok(release.some((message) => /release_merge_strategy must be "squash"/.test(message)), release.join('\n'))
assert.throws(() => syncRepository({ target: root, source: process.cwd() }), /Unsupported release_merge_strategy: rebase/)
assert.ok(release.some((message) => /release_merge_strategy must be "rebase"/.test(message)), release.join('\n'))
assert.throws(() => syncRepository({ target: root, source: process.cwd() }), /Unsupported release_merge_strategy: squash/)

// Release automation is the only consumer of release_merge_strategy;
// a profile without the release feature never needs the key.
Expand Down