Releasing¶
Use a release branch and package validation before tagging. Published releases
and any optional published candidates are immutable. Fix forward; never move a
published tag or replace published assets.
The Release workflow is dispatch-only and never publishes automatically.
Both its validate and draft modes use the same package-build, complete-test,
no-skips and install-check jobs.
0.8.0 retrospective and next-release changes¶
The 0.8.0 release avoided force pushes and tag replacement. Master advanced normally; three branch fixes were consolidated into one follow-up commit while preserving the already-pushed workflow and feature commits. The final tag was created once, and the published release is immutable.
What worked:
- Untagged validation exposed real portability and dependency problems before
publication: fortified
writediagnostics, shell Unicode escapes under dash, a missing fontTools dependency on cache hits, and an incorrect Fedora package name. Fixing these was necessary; rerunning an unchanged failing job was not. - Five native package jobs exercised builds, full tests, installation and version checks. The manifest and attestations bound the shipped assets to one source and workflow commit.
- Draft-first publication kept incomplete assets out of the public release.
What did not work:
- We started package validation before ordinary CI was green, then canceled it.
- Using an RC package version for build-only verification created a second version to rebuild without providing a UAT benefit.
- Release notes and history consolidation happened after the successful trial, changing the identity that still needed final validation.
- Final validation of
fb1745ctook 14m44s (run 35826352413). Draft creation rebuilt the same source and version for another 12m19s (run 35863346353). Rebuilding did not preserve the already-verified artifacts; promotion should.
The next workflow change should implement build once, promote by run ID.
This is a proposed replacement, not a capability of the current draft mode:
- Finish feature selection, history consolidation, release notes and version bookkeeping before the final candidate build. Use the intended final package version even in untagged validation; use RC versions only for actual UAT.
- Require successful ordinary CI for the chosen source. Hosted compiler, sanitizer and stub results count; do not repeat the full matrix locally by default. During iteration, select checks according to the changed inputs. A packaging-only dependency correction does not require another unrelated renderer matrix before retrying packaging.
- Build and test all five packages once. Generate the manifest and attest all six assets in this successful validation run, before any release tag exists.
- Promote that explicit run ID without rebuilding. A promotion job must verify the trusted repository and workflow, successful run, source/workflow SHA, intended version, exact asset set, hashes and attestations. Missing, expired, partial or mismatched artifacts must fail closed. Never select "latest run".
- Create the tag only after promotion checks pass, pointing at the verified source. Attach those exact bytes to a draft, verify the draft downloads, then publish on maintainer instruction. Verify the published state and bytes.
A new source, version or packaging input requires a new validated artifact set. A retry of draft creation or publication does not require rebuilding valid, retained artifacts. Retain the manifest's original build run ID during promotion. Test rejection paths before adopting this workflow, especially wrong SHA/version, foreign workflow, failed run, missing artifact and modified bytes. Do not weaken hash/provenance verification to save time; remove duplicate compilation instead.
0.8 release procedure (current workflow)¶
For 0.8, freeze features at the accepted UTF-8 title and login-shell work. Ship
against the exact tested libghostty development commit in
tools/fetch-libghostty; do not wait for Ghostty 1.4.0. Its eventual release is
an independent maintainer checkpoint. Keep xterm-411 as the comparison oracle.
- Land the CI/packaging preparation as ordinary commits. The workflow must be present on the default branch before GitHub will offer manual dispatch.
- Create
release/0.8, open a PR tomaster, and restrict it to release blockers, packaging/CI fixes and release documentation. Ordinary test and documentation workflows also run on pushes torelease/**. Onlymasterdeploys the documentation site. - Validate exact candidate commits without creating tags or releases. Keep CI fixes in separate commits from changelog/version bookkeeping as they are made.
- For 0.8, use validation artifacts for build/test verification; no published
RC or RC tag is required. The validation version
0.8.0-rc.1may be reused across attempts: the source SHA and run ID identify each artifact set. - Bring the tested changes back to
masterwithout rewriting published history. Untagged branch fixes may be squashed into a coherent commit; keep feature commits intact. Keep the validation branch until publication checks finish, then delete it as described below. Revalidate the resulting SHA. Do not squash/rebase a tagged candidate. - Prepare final notes/version bookkeeping, validate that exact final commit with the final version, then tag, build its draft, verify and publish. Binary versions are embedded, so RC artifacts cannot be renamed or promoted byte-for-byte into final-version artifacts. Final packages must be rebuilt and checked.
This is a bounded release preparation, not a requirement to drain todo.md.
Do not introduce additional features while fixing packaging failures.
Validation before tags¶
From the frozen release branch, push the exact commit and require the complete compiler/sanitizer/stub matrix before release. Successful hosted Test results count; do not duplicate that matrix locally by default. Real-backend GCC, Clang and ASan use the no-skips gate. Then dispatch the same package pipeline used for release drafts:
source_sha=$(git rev-parse HEAD)
git push origin release/0.8
gh workflow run release.yml --ref release/0.8 \
-f mode=validate -f source="$source_sha" -f version=0.8.0-rc.1
The workflow's dispatch commit must equal source; a branch advancing between
selection and dispatch causes a fail-fast identity mismatch, not a mixed build.
Use the Actions run URL/ID to follow this particular run. Do not infer its
identity from whichever run happens to be newest.
Download the package-* artifacts and candidate-manifest from that run into
one empty directory (each package artifact contains one file), then check:
packaging/release-manifest --check "$candidate_dir" 0.8.0-rc.1 "$source_sha"
Validation mode does not require a completed changelog and creates neither a tag nor a release. Package results, the manifest and failure diagnostics remain available as Actions artifacts for 30 days. Inspect successful package install checks and do representative interactive shell/editor/multiplexer testing. A code fix creates a new candidate commit; repeat only the affected local checks while iterating, then the complete candidate gates before publication.
Candidate and final drafts¶
Prepare the 0.8.0 changelog entry with user-facing bullets linked to their
implementing commits. RC notes use that base entry (which may stay Unreleased)
and link to the exact candidate source; do not add a new changelog heading or
advance the development version for every RC. Draft preparation rejects missing
entries and unlinked bullets before building.
For the final release only, date the entry, reconcile its bullets against
git log <previous-tag>..HEAD, open the next development entry, and advance
Meson's project version in a separate bookkeeping commit containing only
CHANGELOG.md and meson.build. Validate this final commit with version=0.8.0.
Once the selected commit is validated, tag that commit and dispatch at the tag:
version=0.8.0-rc.1 # use 0.8.0 for the final draft
tag="v$version"
git tag -a "$tag" "$source_sha" -m "Revenant $version"
git push origin "$tag"
gh workflow run release.yml --ref "$tag" -f mode=draft -f tag="$tag"
Draft mode rebuilds and checks all packages; it does not promote unverified
Actions downloads. It writes an attestation for each of the five packages and
for release-manifest.json, and creates a draft with all six files attached.
RC drafts are explicitly prereleases and not latest. Final drafts also remain
not latest until publication. The workflow refuses an existing release rather
than replacing its assets. The source and workflow SHAs are the same, and every
build job checks out that SHA instead of repeatedly resolving a movable branch.
Verify, then publish explicitly¶
Before publishing, enable release immutability in the repository's release settings and restrict updates/deletion of release tags with a tag ruleset. These are repository settings, not declarations that YAML can apply. Enable them after the draft-first workflow is installed. Existing releases are not retroactively made immutable. GitHub's immutable-release procedure requires assets to be attached before publishing the draft.
Inspect the workflow result and its exact commit, release notes, and downloaded assets. A green build alone is not the publication decision:
verify_dir=$(mktemp -d)
gh release download "$tag" --dir "$verify_dir"
packaging/release-manifest --check "$verify_dir" "$version" "$source_sha"
for asset in "$verify_dir"/*; do
gh attestation verify "$asset" --repo toppk/revenant \
--signer-workflow toppk/revenant/.github/workflows/release.yml \
--source-digest "$source_sha" --signer-digest "$source_sha"
done
# Confirm that the remote tag has not changed since validation.
git fetch --no-tags origin "refs/tags/$tag"
test "$(git rev-parse 'FETCH_HEAD^{commit}')" = "$source_sha"
Require six assets, matching identities/hashes and verified provenance. Inspect installed package/binary versions and desktop integration, and run representative interactive use before the explicit publication step. The workflow already tests tarballs after extraction, and installs the deb, rpm and Arch packages.
# Candidate: explicitly not latest.
gh release edit "$tag" --verify-tag --draft=false --prerelease --latest=false
# Final instead: explicitly stable and latest.
# gh release edit "$tag" --verify-tag --draft=false --prerelease=false --latest
After publishing, check the release's state and download/verify the published assets again. If a published final release has a defect, make a patch release; if an RC has a defect, publish the next RC. Never delete and recreate a public release/tag as a repair strategy.
Clean up the release branch¶
After publication and verification of the public downloads, delete the temporary
release branch on the remote and locally. First confirm that its intended changes
are present on master, including any squashed fixes, and that no unreleased work
remains on it. The published tag, manifest and attestations identify what shipped;
a permanent release branch is not required for that purpose.
For example, after releasing 0.8.0:
git switch master
git push origin --delete release/0.8
git branch -d release/0.8
If the branch was squash-merged, Git may refuse -d because its original commits
are not ancestors of master. After verifying the consolidated changes, use
git branch -D release/0.8 to remove only that local branch reference. Do not
remove or move the release tag. Keep a release branch only when it has an explicit
ongoing maintenance purpose.
What the workflow does¶
- Resolve candidate identity validates either a full SHA plus application version
(
validate) or an existing version tag (draft). The workflow's dispatch ref must resolve to the same commit. Every downstream checkout uses the resolved SHA, and libghostty must be pinned to a full commit. Draft mode also checks the remote tag and linked changelog notes before spending time on builds. - tar.gz builds on x86_64 and aarch64 runners. The archive is
revenant-<version>-linux-<arch>.tar.gzcontaining a strippedrevenant, anxterm+symlink,README.md, andLICENSES/. - deb builds on
ubuntu-latestwithdebian/rules binaryfrompackaging/debian/(notdpkg-buildpackage: its.buildinfogeneration scans the whole runner package database and is discarded anyway), then installs the result and checksrevenant --version,xterm+ --version, and the desktop files. - rpm builds in a
fedora:latestcontainer withrpmbuild --build-in-placefrompackaging/revenant.spec, then installs the result and runs the same binary and desktop-integration checks. - pkg.tar.zst builds in an
archlinux:latestcontainer withmakepkgfrompackaging/PKGBUILDviapackaging/build-arch(which drops to an unprivilegedbuilderuser, sincemakepkgrefuses root), then installs the result and runs the same checks. The container needsxorg-mkfontscaleand an explicitmkfontdirso Xvfb can serve the misc bitmap fonts. - Every build job requires libnotify with
-Dlibnotify=enabled, installs its development package and D-Bus (the desktop-notification test runs underdbus-run-session), installs Xvfb and the X bitmap fonts, pins fontTools, and stages the font fixtures withtools/stage-font-fixtures. Every packaging configuration passes-Dxvfb-tests=enabled, so Meson fails immediately if Xvfb or the libghostty backend is unavailable. Aftermeson test, each path runstools/check-release-tests, which fails unless every test Meson lists ran and passed with no skips. The only checks outside this gate are the interactive probes andtools/check-xterm-font-compat, which need a live xterm and display. - Font fixtures runs before the builders. It restores the staged
fixture tree from
actions/cache(keyed ontools/stage-font-fixtures,tools/font-fixtures/, andFONTTOOLS_VERSION), stages it withtools/stage-font-fixturesonly on a miss, and saves it. Each builder then restores the same key withactions/cache/restoreandfail-on-cache-miss, which is several times faster than passing a run artifact around. If the cache were evicted mid-run the builder fails loudly and a re-dispatch repopulates it. The upstream font sources are therefore fetched once per pin change rather than five times per release, and the builder images need none of the staging toolchain (fontTools, cpio, rpm2cpio, unzip). -
Every build job restores a Zig cache with
actions/cache. The key is self-describing:libghostty-
- - -zig -ghostty - -
tools/build-libghostty --print-target and tools/fetch-libghostty
--print-reference supply the target tuple and pinned commit, so the key
changes exactly when the libghostty inputs do. The distro is deliberately
absent: zig uses its own compiler and libc headers for an explicit target,
so one cache serves the tarball, deb, rpm, and Arch jobs of an
architecture. The runner CPU model is present because Ghostty compiles its
build-time generators for the native host CPU and zig has no override
(ziglang/zig#22663); a
restore-keys prefix restores the newest same-architecture cache on a
model mismatch and the union saved afterward converges to a cache that
hits on every model. A warm cache turns the libghostty build into a few
seconds.
- build-libghostty passes an explicit -Dtarget and -Dcpu
(x86_64-linux-gnu/x86_64_v3, aarch64-linux-gnu/baseline) so the
released library never depends on the build host. The x86_64-v3 floor is
stated in the install guide and in every release body; drop to
x86_64_v2 or baseline only in response to user reports.
- Artifact assembly requires exactly five expected package files and records
their hashes, version, source/workflow SHA, libghostty SHA and run identity in
release-manifest.json. Package artifacts, this manifest and diagnostic logs
are retained for 30 days. Logs are uploaded even on package-job failure and
are never included in release assets.
- Draft mode only attests the packages and manifest, renders linked release
notes, checks the remote tag again, and creates a new unpublished GitHub
release with every asset attached. It never publishes. An existing release
is refused rather than updated. Validation mode creates no GitHub release.
The synthetic sbix fixture is byte-exact only for the fontTools release
pinned in the workflow's FONTTOOLS_VERSION; regenerate
tools/font-fixtures/manifest.json and move the pin together.
Zig is installed by packaging/install-zig, which downloads the version
pinned in the workflow's ZIG_VERSION from ziglang.org and verifies the
checksum against https://ziglang.org/download/index.json. Bump
ZIG_VERSION when tools/fetch-libghostty moves to a Ghostty revision that
needs a newer Zig.
Building packages locally¶
Every job calls scripts under packaging/ that work outside CI. They require
the build/test dependencies, Zig on PATH, and the pinned Ghostty/fixture inputs.
The application version is passed through Meson's release-version override;
package metadata uses native ordering so an RC upgrades to the final release:
| Surface | Example candidate | Final |
|---|---|---|
| Tag | v0.8.0-rc.1 |
v0.8.0 |
| Binary and tarball | 0.8.0-rc.1 |
0.8.0 |
| Debian/RPM version | 0.8.0~rc.1 |
0.8.0 |
| Arch pkgver | 0.8.0rc1 |
0.8.0 |
packaging/release-version owns this mapping. Each native-package CI job uses
its package manager to check rc.1 < rc.2 < rc.10 < final before building.
Package file names reflect the native version; revenant --version and
xterm+ --version always show the canonical application version.
packaging/build-tarball 0.8.0-rc.1
packaging/build-deb 0.8.0-rc.1
packaging/build-rpm 0.8.0-rc.1
packaging/build-arch 0.8.0-rc.1
python3 tests/release-packaging.py
Recovering from failures¶
- Validation failure: inspect retained logs, commit the fix normally, and validate the new SHA. No release or tag needs repairing.
- Transient infrastructure failure with unchanged source: rerun the failed jobs at the same commit. Do not move a tag to retry a download or runner outage.
- Failed or partial unpublished draft: inspect it before doing anything. An unpublished incomplete draft may be deleted and rebuilt at the same tag and commit, without deleting or moving the tag. The workflow will not overwrite an existing draft. If source changes, allocate the next candidate tag instead.
- Published RC/final defect: fix forward with a new RC/patch version. Keep the published tag, assets and history intact.
Known gaps¶
- The packages have no dependency on xterm. Once Revenant reads
/usr/share/X11/app-defaults/XTermandXTerm-colorfrom the distribution's xterm package instead of carrying its own copy, the deb and rpm need aDepends/Requiresonxterm(or on whichever subpackage owns those files). - No macOS or Windows builds; the X11 story there is unresolved.