Update the release pipeline to early-exit on non-Release branches.

This commit is contained in:
= 2026-08-09 19:01:13 -04:00
parent 2971b6e709
commit 03d0b6a50f
2 changed files with 113 additions and 4 deletions

View file

@ -99,6 +99,10 @@ jobs:
# for before it will run.
# libcap2-bin provides capsh, which the Test step uses to drop the two
# DAC capabilities so root obeys permission bits.
# jq parses the tags API for the releasable check below. Named rather
# than assumed: the release job gets it from its image, and a base
# image change that dropped it would turn that check into a silent
# pass rather than a failure.
# zsync and appstream are for build-appimage.sh: appimagetool shells
# out to zsyncmake rather than bundling it, and reports success while
# writing nothing when it is absent, so the script checks for it up
@ -107,7 +111,44 @@ jobs:
apt-get install -y --no-install-recommends \
build-essential perl pkg-config \
binutils dpkg-dev desktop-file-utils gzip libcap2-bin \
zsync appstream
zsync appstream jq
- name: Check the release tag is free
# The other half of the guard above, for the path that actually cuts
# most releases: pushing a Release* branch. There the tag comes from
# [workspace.package] rather than the ref, so the mistake is not a
# mismatched tag but a *forgotten bump* — the version still points at a
# release that already shipped.
#
# The release job checks this too and remains the authority; it just
# cannot check it until both build jobs are green, so forgetting the
# bump used to cost two full release builds, packaging and an artifact
# upload before anything said so. This says so in seconds.
#
# Deliberately as lenient as the release job: a tag at *this* commit is
# a re-run and fine, and an unreachable API reads as "not found" and
# lets the build proceed rather than failing on a network hiccup. An
# early check that blocks a good release is worse than one that misses
# a bad one, because the late check still catches it.
if: >-
startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
env:
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
tag="v$version"
api="${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}"
at=$(curl -sS -H "Authorization: token $RELEASE_TOKEN" \
"$api/repos/$GITHUB_REPOSITORY/tags/$tag" | jq -r '.commit.sha // empty')
if [ -n "$at" ] && [ "$at" != "$GITHUB_SHA" ]; then
echo "ERROR: $tag already exists at $at, not $GITHUB_SHA." >&2
echo "Bump [workspace.package] version in Cargo.toml, refresh Cargo.lock" >&2
echo "with 'cargo update -w', commit and push again." >&2
exit 1
fi
echo "OK: $tag is free (or already at this commit)"
- name: Trust the workspace
# checkout writes as root into a directory git then considers dubiously
@ -168,7 +209,23 @@ jobs:
echo "capabilities in test shell: $(grep CapEff /proc/self/status | tr -d "\t")"
cargo test --release --locked --workspace'
# Everything from here down produces release assets, so it runs only
# where a release can actually come out: a v* tag or a Release* branch.
# On master and pull requests the job stops after Build and Test, which
# is what those runs are for — the packaging that used to follow built a
# .deb, an AppImage, a tarball and a 14-day artifact upload that nothing
# would ever download, because the release job is skipped there anyway.
#
# The condition is repeated rather than hoisted into an env var: Actions
# has no workflow-level expression alias, YAML anchors are not supported,
# and `env.X` inside `if:` would fail *closed* on a runner that did not
# populate it — silently skipping packaging on a real release. Spelled
# out, it is the same form the release job's own gate uses.
- name: Build the .deb
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
# --no-build reuses the binaries from the Build step rather than
# recompiling. SOURCE_DATE_EPOCH pins the generated changelog date so
# repeat builds of the same commit are byte-identical.
@ -177,8 +234,14 @@ jobs:
./packaging/build-deb.sh --no-build
- name: Check the glibc floor
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
# The whole point of pinning the container. If someone bumps the image,
# this fails loudly instead of quietly shipping an uninstallable package.
# Gated with the .deb it inspects — there is no package to read without
# the step above.
run: |
deb=$(ls dist/*.deb)
depends=$(dpkg-deb -f "$deb" Depends)
@ -194,6 +257,10 @@ jobs:
echo "OK: glibc floor $floor <= $MAX_GLIBC"
- name: Build the AppImage
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
# After the glibc gate, so the cheaper check still fails first. --no-build
# reuses the binaries from the Build step, as the .deb step does, and
# SOURCE_DATE_EPOCH pins the date substituted into the AppStream release
@ -207,6 +274,10 @@ jobs:
./packaging/build-appimage.sh --no-build
- name: Package the binaries
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
# A tarball for anyone not installing the .deb, stripped to match what
# build-deb.sh ships.
run: |
@ -231,6 +302,12 @@ jobs:
# node16, which current Forgejo runner images no longer ship; the
# -node20 tags are Forgejo's builds for precisely this combination.
- uses: actions/upload-artifact@v3-node20
# `if-no-files-found: error` below would fail every master push once the
# packaging steps above are gated, so this carries the same gate.
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
with:
name: linux-x86_64
# The .zsync is not optional: the update URL baked into every AppImage
@ -322,7 +399,16 @@ jobs:
fi
done
# As in the linux job, the asset-producing steps run only where a release
# can come out. The DLL check above deliberately stays ungated: it
# validates the .exe itself rather than packaging it, costs an objdump,
# and is exactly the kind of regression worth catching on master rather
# than at release time.
- name: Build the installer
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
# --no-build reuses the binaries from the Build step rather than
# cross-compiling them a second time. The installer and the .zip below
# are alternatives, not a two-step download: the installer puts the app
@ -332,6 +418,10 @@ jobs:
run: ./packaging/build-installer.sh --no-build
- name: Package the binaries
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
run: |
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
@ -354,6 +444,12 @@ jobs:
# node16, which current Forgejo runner images no longer ship; the
# -node20 tags are Forgejo's builds for precisely this combination.
- uses: actions/upload-artifact@v3-node20
# Same gate as the packaging steps, for the same `if-no-files-found`
# reason as the linux job.
if: >-
startsWith(github.ref, 'refs/tags/v')
|| startsWith(github.ref, 'refs/heads/Release')
|| startsWith(github.ref, 'refs/heads/release')
with:
name: windows-x86_64
path: |

View file

@ -695,13 +695,26 @@ microseconds regardless of row count.
allocation counts, measure that separately before concluding a path is cheap,
and measure the GUI rather than a one-shot `quicksearch-cli` run — a
short-lived process cannot show what a typing session retains.
- `.forgejo/workflows/ci.yml`: builds both platforms on every push to `master`
and every pull request. To cut a release, bump `[workspace.package] version`
- `.forgejo/workflows/ci.yml`: builds and tests both platforms on every push to
`master` and every pull request. Those runs stop there — packaging (the
`.deb`, the AppImage, the tarball, the Windows installer and `.zip`, and the
artifact upload) runs only where a release can actually come out, which is a
`v*` tag or a `Release...` branch. The Windows non-system-DLL check is
deliberately not gated that way: it validates the `.exe` rather than
packaging it, so it runs everywhere and catches a regression on `master`
rather than at release time.
To cut a release, bump `[workspace.package] version`
in `Cargo.toml` and push the commit on a branch named `Release...`; CI runs
`cargo update -w` first, so a lockfile still pinning the old member versions
is not something you have to remember. That only re-resolves the workspace
crates, so the `--locked` build after it still fails on a dependency added or
bumped without committing `Cargo.lock`. Once both
bumped without committing `Cargo.lock`. Forgetting the version bump is caught
in seconds rather than after two full release builds: each release path gets
a guard before anything is compiled — a `v*` tag is checked against the
workspace version, and a `Release...` branch is checked against the tags that
already exist. Both are accelerators, not the authority; the release job
re-checks and remains the thing that actually refuses. Once both
build jobs are green, CI tags that commit `v<version>` and publishes a release
with the `.deb`, an AppImage and its `.zsync` sidecar, a Linux tarball, the
Windows installer and a Windows zip attached; pushing a `v*` tag by hand does