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
27 changes: 27 additions & 0 deletions .github/workflows/release-please-bump-lockfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,33 @@ jobs:
return;
}

// Bind the artifact to the run that built it: the PR it
// names must be one GitHub attached to that run, at the same
// branch and commit. Otherwise a run built for one PR could
// hand over a lockfile claiming to be another's. Release PRs
// come from this repository, so the list is never empty for
// them; an empty list fails like a mismatch.
const run = context.payload.workflow_run;
const linked = (run.pull_requests || []).find(
(p) => p.number === prNumber,
);
if (!linked) {
core.setFailed(
`artifact names PR #${prNumber}, which is not a PR of run ${run.id}`,
);
return;
}
if (
linked.head.ref !== branch ||
linked.head.sha !== artifactHeadSha ||
run.head_sha !== artifactHeadSha
) {
core.setFailed(
`run ${run.id} built ${linked.head.ref}@${linked.head.sha}, artifact claims ${branch}@${artifactHeadSha}`,
);
return;
}

// Verify the PR is still open, still authored by the
// release-please bot, and its head ref still matches the
// artifact's claim.
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release-please-lockfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ name: Build Cargo.lock for release-please PR

"on":
pull_request:
branches: [main]
# Maintenance branches get release PRs too (see release-please.yml).
branches: [main, "release/**"]
types: [opened, synchronize, reopened]

permissions:
Expand Down
40 changes: 30 additions & 10 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ name: Release Please
# workflow keeps gating correctly on `startsWith(head_ref,
# 'release-please--')`.**

# Also on maintenance branches (`release/1.9.x`, …): a patch release of a
# shipped line is cut from there, so the features already on main stay
# out of it. Each run targets the branch that was pushed
# (`--target-branch`), and reads that branch's config and manifest.
# A push to a branch runs that branch's copy of this file, so a
# maintenance branch needs this version of it too.
"on":
push:
branches: [main]
branches: [main, "release/**"]

permissions:
contents: write
Expand All @@ -47,27 +53,41 @@ jobs:
with:
node-version: 20

# Phase A — open / update the release PR. Idempotent: re-runs
# on every push, only mutates GitHub when something changed.
- name: Update release PR
# release-please is pinned to an exact version: `@17` resolved to
# whatever 17.x npm served at each run, handed the write token.
# Bump it by hand, on both steps.
#
# Phase A — when the head commit IS the release PR's merge
# commit, cut the GitHub release + tag. No-op on every other
# push, so we can run both phases unconditionally.
#
# **First, not second.** Right after a release PR merges, the
# update below looks for the release of the version the manifest
# now names; when that release does not exist yet it walks back up
# to `commit-search-depth` (1500) commits, one API call each, and
# the job's timeout cut it before this step ever ran — no tag, and
# a manual rerun on every release (1.8.1, 1.9.0, 1.9.1). Tagged
# first, the update finds its starting point at once.
- name: Publish GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx --yes release-please@17 release-pr \
npx --yes release-please@17.11.2 github-release \
--token="${GITHUB_TOKEN}" \
--repo-url="${GITHUB_REPOSITORY}" \
--target-branch="${GITHUB_REF_NAME}" \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.json

# Phase B — when the head commit IS the release PR's merge
# commit, cut the GitHub release + tag. No-op on every other
# push, so we can run both phases unconditionally.
- name: Publish GitHub release
# Phase B — open / update the release PR. Idempotent: re-runs
# on every push, only mutates GitHub when something changed.
- name: Update release PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx --yes release-please@17 github-release \
npx --yes release-please@17.11.2 release-pr \
--token="${GITHUB_TOKEN}" \
--repo-url="${GITHUB_REPOSITORY}" \
--target-branch="${GITHUB_REF_NAME}" \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.json
68 changes: 60 additions & 8 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,18 @@ gh secret set SIGNTOOL_PFX_PASSWORD --body "<passphrase>"

### 1. Cut the release via release-please

You do **not** hand-bump versions anymore. Every push to `main`
runs [`.github/workflows/release-please.yml`](../.github/workflows/release-please.yml),
which:
You do **not** hand-bump versions anymore. Every push to `main` — or to
a maintenance branch, see
[Patch releases from a maintenance branch](#patch-releases-from-a-maintenance-branch) —
runs [`.github/workflows/release-please.yml`](../.github/workflows/release-please.yml)
against that branch, which:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

1. Parses the Conventional Commits since the last tag.
2. Computes the next semver (`feat:` → minor, `fix:` → patch,
`feat!:` / `BREAKING CHANGE:` → major, no relevant commits → no PR).
3. Opens or refreshes a **chore(main): release X.Y.Z** PR that
3. Opens or refreshes a release PR — **chore(main): release X.Y.Z** on
`main`, **chore(release/X.Y.x): release X.Y.Z+1** on a maintenance
branch — that
bumps every version manifest in lockstep:
- [`package.json`](../package.json) (canonical, owned by release-please)
- [`src-tauri/crates/app/tauri.conf.json`](../src-tauri/crates/app/tauri.conf.json) (`$.version`)
Expand All @@ -152,17 +156,30 @@ which:
To ship, **review and merge the release PR** (squash or merge,
release-please tolerates both). The merge causes release-please to:

- create the `vX.Y.Z` tag on `main`,
- create the `vX.Y.Z` tag on the branch it targets (`main`, or the
maintenance branch),
- create the matching GitHub Release with the auto-generated notes,
- update [`.release-please-manifest.json`](../.release-please-manifest.json)
to record the new version.

The new tag triggers [`release.yml`](../.github/workflows/release.yml)
exactly as before — no change needed downstream.
The tag is created with the workflow's `GITHUB_TOKEN`, and a tag pushed
that way **does not trigger** [`release.yml`](../.github/workflows/release.yml)
(GitHub does not start workflows from events the token itself caused).
Start it by hand once the tag exists:

```bash
gh workflow run release.yml -f tag_name=vX.Y.Z
```

The workflow tags first and updates the release PR second: in the other
order, the update right after a release PR merged went looking for a
release that did not exist yet, walked up to 1,500 commits one API call at
a time, and the job's timeout cut it before the tag was ever made.

### 2. Watch the workflow

Pushing the tag triggers `.github/workflows/release.yml`:
`.github/workflows/release.yml`, started for the tag as above (a tag
pushed by hand would start it on its own; release-please's does not):

- Builds Linux on `ubuntu-latest` — produces an `.AppImage`
(universal, also the updater payload), a `.deb` (Debian/Ubuntu),
Expand Down Expand Up @@ -215,6 +232,41 @@ updater errors. Common causes: pubkey mismatch (private/public
keys regenerated), endpoint 404, malformed `latest.json`,
signature corrupted on upload.

## Patch releases from a maintenance branch

Once `main` carries features for the next minor or major, a fix for the
version users have cannot be cut from it: release-please would bump to
the next minor and ship those features with it. A patch is cut from a
maintenance branch instead, `release/X.Y.x`, branched from the last tag
of that line:

1. **Create the branch** from the tag: `git push origin vX.Y.Z:refs/heads/release/X.Y.x`.
2. **Bring the workflows** to it. A push runs the pushed branch's copy of
[`release-please.yml`](../.github/workflows/release-please.yml), and
a pull request runs its base's copy of
[`release-please-lockfile-build.yml`](../.github/workflows/release-please-lockfile-build.yml);
a tag older than their `release/**` support has neither. Cherry-pick
the commit that added it.
3. **Cherry-pick each fix** from `main` (signed: `git cherry-pick -S`),
through a pull request into `release/X.Y.x` so CI runs on it. The
**Maintenance branches** ruleset protects `release/**` the way `Main`
protects `main` — no deletion or force push, signed commits, a pull
request with one approval — without the CodeQL gate, since CodeQL only
analyses pull requests into `main`. A push to these branches runs their
own copy of the workflow with a write token, which is why they must not
take direct pushes.
4. release-please, run on the branch with `--target-branch`, opens
`chore(release/X.Y.x): release X.Y.Z+1` against it; the Cargo.lock
pipeline fills it in as on `main`. Merge it, then start `release.yml`
for the new tag as above. Downstream (AUR, winget, COPR, apt) and the
updater manifest work the same, since everything starts from the tag.

`main` keeps its own manifest, so its next release still counts from its
last stable version; its changelog will list the cherry-picked fixes a
second time, under the version that ships them on `main`. For a major,
set `release-as` in [`release-please-config.json`](../release-please-config.json)
before merging the release PR.

## Beta channel

WaveFlow ships an **opt-in beta channel** so testers can run
Expand Down
Loading