Releasing OdyTTY

September 9, 2026 ยท View on GitHub

Use this guide to cut a tagged OdyTTY release, verify its 17 published assets, and confirm the Scoop, Homebrew, and AUR channels updated. Replace X.Y.Z with the version being released.

Contents

Continuous Integration

OdyTTY uses GitHub-hosted runners for these workflows:

WorkflowTriggerResult
.github/workflows/ci.ymlPushes and pull requests to masterFormats, builds, lints, and tests on Ubuntu, macOS, and Windows; enforces the production-file guard and checks shipped shell scripts and release-gate fixtures; publishes no artifacts
.github/workflows/release.ymlvX.Y.Z tags or manual validationBuilds all seven release artifact types; tag runs also publish the release and update package channels
.github/workflows/rustsec-audit.ymlPull requests touching the release or fuzz-workspace manifests and lockfiles, or the audit script or workflow; a weekly schedule; and manual dispatchRuns cargo audit against both locked dependency graphs; the release job runs the same audit before publishing; publishes no artifacts
.github/workflows/deep-fuzz.ymlWeekly schedule or manual dispatchRuns the ignored parser/protocol and graphics fuzz tiers at 40,000 iterations and retains logs for 14 days
.github/workflows/coverage-fuzz.ymlWeekly schedule or manual dispatchRuns bounded corpus-retaining parser, UTF-8/state-transition, and graphics-payload fuzz targets and retains corpora, crash artifacts, and logs for 14 days
.github/workflows/dynamic-analysis.ymlWeekly schedule or manual dispatchRuns the pinned Miri subset and bounded AddressSanitizer and ThreadSanitizer lanes and retains their logs for 14 days

Third-party workflow actions are pinned to reviewed commit SHAs rather than floating tags.

The CI gate runs these commands on all supported platforms:

cargo fmt --check
cargo build --release --locked
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked

The macOS leg runs the test command single-threaded as cargo test --locked -- --test-threads=1, with a per-attempt timeout and one automatic retry for the known runner PTY-teardown deadlock. A genuine test failure still fails the job.

Release publishing never runs for fork pull requests. A normal release lands the version commit on master, waits for a completed successful CI workflow whose head_sha is that exact commit, and only then pushes the release tag. The tag workflow re-checks the same exact-SHA result and fails closed if it is missing, queued, in progress, cancelled, failed, or from another commit.

Release Readiness

Every release requires docs/releases/X.Y.Z.md, linked from the release-notes index, with a matching # OdyTTY vX.Y.Z heading and a short opening summary. Run python3 scripts/release-notes.py --version X.Y.Z --check before tagging. Also run python3 scripts/documentation-guard.py --release-version X.Y.Z. This checks publication markers and the target TODO milestone; it does not verify the evidence behind completion claims. Follow the documentation maintenance policy for candidate and post-publication status. The notes check rejects a missing, mismatched, or unreleased summary. CI checks the current package version; the source producer and publication workflow enforce the same requirement for the release version.

Publication places that summary and a link to the version-pinned canonical notes above the download and verification information in .github/release-downloads.md. The full notes stay in the repository, and automatically generated GitHub changes remain below the existing preamble. Historical releases and assets are unchanged.

The release must agree with the current public references:

SurfaceAuthoritative reference
Terminal and native-app behaviorFeature Reference
Default shortcuts and rebindingKeybindings
Settings, defaults, and environment variablesRuntime Knobs
Supported install paths and artifact namesInstall Guide

Bounded ttf-parser advisory exception

RustSec classifies RUSTSEC-2026-0192 as an informational unmaintained-crate warning with no patched release. OdyTTY reaches ttf-parser 0.25.1 directly for font metadata and transitively through owned_ttf_parser / ab_glyph; the Wayland decoration path also reaches ab_glyph through sctk-adwaita. The C API is not built or called. Production font files are capped at 256 MiB before parsing -- a collection is read one validated face at a time, so the cap applies to what is retained rather than to the file that contains it -- parser failures skip the candidate or fall back, and glyph/font corpus tests exercise the Rust paths used by the application.

The upstream repository moved to the HarfBuzz organization and, as of 2026-08-06, describes itself as maintenance-mode software where correctness, panic, and security fixes remain in scope. No post-transfer release exists on crates.io yet, so the advisory cannot currently be cleared by a compatible upgrade. A direct move to the already-used maintained Fontations components would change metadata parsing and requires font-corpus, shaping/rendering, MSRV, performance-apparatus, and Linux/macOS/Windows parity evidence before it can replace this parser. The advisory is informational, not a disclosed vulnerability. The exception therefore expires on 2026-10-15 and is owned by the core maintainers. .github/scripts/rustsec-audit.sh fails on or after that date while the exact advisory remains present; there is no automatic extension.

Before expiry, take the first applicable removal path:

  1. upgrade to an upstream release that clears the RustSec scan;
  2. remove the exception if RustSec withdraws the advisory after reassessing the transferred maintenance ownership; or
  3. replace direct metadata use with the maintained skrifa/fontations stack and update or replace the remaining ab_glyph and window-decoration dependency paths until cargo tree -i ttf-parser is empty.

Primary references: the RustSec advisory and the upstream maintenance statement.

CI and the binary-producing release jobs build the exact checked-out commit on Linux, macOS, and Windows. Source-package consumers such as the AUR package and Homebrew formula build the published source archive instead. Releases also carry desktop metadata and hicolor icons for Linux packages.

The upstream release does not currently publish Nix, Flatpak, or Snap packages. A package must not silently change the user's default terminal; it should register OdyTTY as available and leave selection to the user.

For v0.13.0, blocking Linux, macOS, and Windows CI passed, but maintainer-controlled Windows and macOS hardware was unavailable for native install and runtime smoke passes before or after publication. Those checks are recorded as not performed, not inferred from hosted CI or earlier releases. A defect found later is fixed in a patch release; the missing v0.13.0 evidence is not rewritten as a success.

For v0.14.0, publication and blocking three-platform CI passed at tag commit 15a688844393225d50252af05b45a7ef7ae89351. Verification covered all 17 assets, the signed checksum manifest, seven byte-identical alias pairs, platform provenance, and Scoop/Homebrew/AUR propagation. Profile and navigator hands-on acceptance and testing of all release images are complete. On 2026-09-08, an isolated clean release build of the signed source archive passed on Linux under the prescribed resource limits. Its archive SHA-256 is 388ed7c448c1cc28f63d286013778496b6f34b59f2e1db6fe5301c7d5bf592bc. That build used Rust 1.97.1 because rustup was unavailable; it establishes source package buildability, not another verification of the pinned 1.96 MSRV. No application reinstall or repeat image acceptance was part of this carryover.

Release Artifacts

Each release publishes seven artifact types under both an always-latest alias and a byte-identical version-pinned name:

ArtifactAlways-latest aliasVersion-pinned name
Debian packageodytty-amd64.debodytty-X.Y.Z-amd64.deb
RPM packageodytty-x86_64.rpmodytty-X.Y.Z-x86_64.rpm
Linux binary tarballodytty-linux-x86_64.tar.gzodytty-X.Y.Z-linux-x86_64.tar.gz
Linux AppImageodytty-x86_64.AppImageodytty-X.Y.Z-x86_64.AppImage
macOS Apple Silicon app zipodytty-macos-arm64.zipodytty-X.Y.Z-macos-arm64.zip
Windows portable zipodytty-windows-x86_64.zipodytty-X.Y.Z-windows-x86_64.zip
Source archiveodytty.tar.gzodytty-X.Y.Z.tar.gz

The version-pinned installer odytty-X.Y.Z-install.sh is an additional asset from v0.14.0 onward. It has no always-latest alias. SHA256SUMS covers the fourteen package files and this installer. Each package alias and its pinned twin have the same hash because they contain the same bytes. SHA256SUMS.minisig authenticates that checksum manifest with the OdyTTY Minisign release key, bringing the total to seventeen assets. Verify the manifest signature and the installer's checksum before executing the installer; see the install guide.

Every binary-producing release job supplies the exact release commit as ODYTTY_BUILD_SHA, which the About panel displays as its Commit value. The source archive is produced with git archive; its .git_archival.txt token is therefore substituted with the abbreviated commit so AUR, Homebrew formula, and other archive builds retain truthful provenance without a .git directory.

Durable download links use the aliases under releases/latest/download/. Pinned names select one specific version. See the install artifact table for the user-facing download contract.

Releases from v0.11.0 onward sign SHA256SUMS with Minisign. This manifest signature authenticates every published filename and digest without changing the byte-identical alias copies. Releases before v0.11.0 are checksum-only and remain unsigned; they were not retroactively signed.

Build Provenance Attestation

Releases from v0.12.0 onward also publish a GitHub build provenance attestation for every artifact. It is additive: Minisign signing is unchanged, and neither replaces the other because they answer different questions.

  • The Minisign signature proves SHA256SUMS was signed by the holder of the OdyTTY release key. It says who vouched for the manifest.
  • The attestation binds each artifact's SHA-256 digest to the workflow, repository, commit, and run that produced it, signed with a short-lived Sigstore certificate issued against the workflow's OIDC identity. It says where the bytes came from.

A stolen release key forges the first but not the second. Modifying an artifact after the workflow finishes invalidates both. The attestation does not prove that the source or build workflow is safe; it makes the source and workflow identity inspectable. It is keyless, so there is no additional secret to guard or rotate: the signing key exists only in memory for the duration of the run.

Attestations are stored by GitHub against the repository and keyed by artifact digest, not attached to the release as a file. Verification is therefore an online lookup rather than a downloaded signature, and because the always-latest alias and its version-pinned twin are byte-identical, both names verify against the same attestation. That is the same invariant SHA256SUMS already shows as matching hashes.

The release workflow verifies its own attestation before publishing, the same way it already verifies its own Minisign signature. If an attestation cannot be produced or verified, the release does not publish.

Users verify with the GitHub CLI (2.97.0 or newer). Older releases contain known attestation-verification bypasses and must not be used for this check:

gh attestation verify odytty-x86_64.AppImage --repo ghreprimand/odytty

Constrain the signer to this project's release workflow to reject an attestation produced by any other workflow, including one in a fork:

gh attestation verify odytty-x86_64.AppImage \
  --repo ghreprimand/odytty \
  --signer-workflow ghreprimand/odytty/.github/workflows/release.yml

Platform Code Signing Boundary

Manifest signing and build provenance are both separate from platform code signing, and neither substitutes for it. This is a cost decision, recorded plainly rather than left to be discovered at download time:

  • macOS: the app is ad-hoc signed, without Apple Developer ID signing or notarization. Gatekeeper will refuse to open it on first launch; a user has to right-click and choose Open, or clear the quarantine attribute, to run it.
  • Windows: the executable is unsigned. SmartScreen shows an unknown-publisher warning, and a user has to choose "More info" and then "Run anyway".
  • Linux: unaffected. No platform signing authority is involved, so the checksum manifest and its signature are the whole trust chain.

Apple notarization requires a paid Apple Developer Program membership and Windows Authenticode requires a paid code-signing certificate. Neither is purchased, so neither is claimed. The attestation narrows the gap without closing it: it proves where a binary was built, which is what an operating system's publisher check does not tell you anyway, but it does not stop the operating system warning.

Release Signing Key

The canonical public key is docs/keys/odytty-release.pub. Its Minisign comment records the key identifier used as the published fingerprint. The same key must also be linked from the project website and each signed release's notes so users can compare it through more than one publication surface.

The canonical release key is already published. The following command is for initial key provisioning or an explicitly planned rotation, not a routine release step. Generate a new pair offline on a maintainer-controlled machine that is not a CI runner:

umask 077
release_key_file=/secure/offline/path/odytty-release.key
minisign -G -W \
  -p docs/keys/odytty-release.pub \
  -s "$release_key_file"

Keep an offline backup of the secret-key file. Store its complete contents in the GitHub Actions secret MINISIGN_SECRET_KEY; never commit it. The key uses Minisign's unencrypted -W format because the release is noninteractive and the GitHub secret is the encrypted custody boundary. The release job fails closed when the secret is absent, the public key is still a placeholder, or the key pair does not match.

For a rotation, retain the previous public key under a dated filename in docs/keys/, commit the replacement canonical key before using it, and announce both key identifiers in the transition release. When the previous key is still available, sign the transition announcement with it. Replace the Actions secret only after the new public key is published. Old releases remain verifiable with their archived public keys; never rewrite or remove their signatures.

Create A Release

1. Update Release Metadata

Set Cargo.toml to X.Y.Z and refresh Cargo.lock. Keep the declared MSRV in Cargo.toml aligned with rust-toolchain.toml if the Rust version changes.

Add the newest <release> entry to dist/linux/io.unfinished_works.odytty.metainfo.xml. Add the release headline to the current devlog/YYYY-MM.md archive linked from DEVLOG.md, using this shape:

## YYYY-MM-DD -- Release vX.Y.Z -- Summary

If the release begins a new month, create that archive and add its exact newest-first relative link to DEVLOG.md in the same commit.

Before the version commit, reconcile README, SPEC, TODO, release notes, feature guides, known gaps, installer/package documentation, and the website handoff. Keep candidate status distinct from verified publication; leave only explicitly separated post-publication checks open. Run both documentation and notes guards. After publication, record the actual artifact/channel outcomes and update all published-version markers together. Never move an existing tag to repair prose.

Commit these changes together and push master. Wait for the complete CI workflow on that exact commit to pass: the Linux, macOS, and Windows matrix jobs plus the shell-script/install-smoke job. Do not tag a newer local commit or a commit whose matching CI run is incomplete.

2. Run Local Release Checks

Run the full suite rather than cargo test --lib. CLI and attach integration tests require the compiled odytty binary.

cargo fmt --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cargo build --release --locked
target/release/odytty --version
desktop-file-validate dist/linux/io.unfinished_works.odytty.desktop
appstreamcli validate --pedantic dist/linux/io.unfinished_works.odytty.metainfo.xml

The tag workflow smoke-tests the Windows and macOS binaries before packaging, the assembled AppImage, and the binary inside the Linux tarball staging tree. The .deb and .rpm packages receive metadata and file-list validation but are not executed.

Then run the memory regression guard. It is a release step rather than a CI job because its subject is a running process on a real adapter, which the hosted runners cannot supply; the reasoning is recorded in memory.md and the guard's own self-test runs in CI.

# Linux: capture at the geometry the ceilings were recorded at, then:
memory_report_log="${XDG_STATE_HOME:-$HOME/.local/state}/odytty/odytty-memory-report.log"
python3 scripts/memory-regression-guard.py \
    --log "$memory_report_log" \
    --environment-class workstation-nvidia-wayland \
    --geometry 1600x1000 \
    --skip-first 8

Exit 0 is a clean run; 1 is a regression or a measurement gap; 2 is a fault in the guard's own inputs, such as an environment class with no recorded ceilings. A machine with no recorded class is not a pass and is not a failure of the build: record a baseline for it, or run the guard on a machine that has one. A ceiling is never widened to clear a red run.

Version 0.12.1 does not repeat the benchmark campaign or live GPU memory capture. The patch changes SSH argument construction, release-secret scope, help/reference text, and version metadata; it does not touch terminal storage, rendering, GPU allocation, or presentation timing. Those measurements are recorded as not run, never promoted to a pass, and the published v0.12.0 results remain the applicable performance evidence.

The published v0.12.1 artifacts subsequently passed bounded post-publish checks on macOS, Windows, and the Linux package channels. That validates the exercised release paths without turning the carried-forward performance evidence into a new measurement or claiming broader hardware coverage.

Version 0.12.2 also does not repeat the benchmark campaign or live GPU memory capture. It adds two platform-neutral CSI cursor controls and their compatibility fixtures without changing terminal storage, rendering, GPU allocation, or presentation timing. The v0.12.0 results remain the applicable performance evidence, and the omitted v0.12.2 measurements are recorded as not run. The tagged release passed same-commit Linux, macOS, and Windows CI, its locked dependency audit, artifact smoke tests, Minisign signing, GitHub provenance verification, and Homebrew, Scoop, and AUR publication. Bounded post-publish checks confirmed that all three package-manager channels offered v0.12.2. A live rerun of the reported pacman workload is also not run until independent confirmation is received.

Version 0.13.0 also does not repeat the benchmark campaign or live GPU memory capture. It adds paste policy, command-range metadata and actions, and bounded notification/progress state without changing terminal storage, GPU allocation, or presentation timing. The frozen v0.12.0 performance results remain the applicable evidence; a fresh performance campaign is not run and is not claimed for v0.13.0. Native Linux, macOS, and Windows delivery remains subject to the independent platform evidence recorded for this release rather than being inferred from parser or command-specification tests.

Tag v0.13.0 points to release commit 2b44465912c8bf98e15d74c7be4987722e293959. Exact-commit CI run 33386345374 passed on Ubuntu, macOS, and Windows. Release run 33387227629 then passed all seven producers and artifact smoke tests, the locked dependency audit, checksum signing, constrained provenance verification, GitHub Release publication, and Scoop, Homebrew, and AUR publication. An independent post-publish check downloaded all 16 assets, verified every checksum and the Minisign signature, compared all seven alias/pinned pairs byte-for-byte, verified the Linux, Windows, and macOS attestations against the release workflow, built the pinned source archive with the locked graph, and confirmed the source and packaged Linux binaries report odytty 0.13.0. Public Scoop, Homebrew cask/formula, and AUR metadata all carry v0.13.0 and the matching published hashes. Native macOS and Windows on-device runtime checks remain not performed for the hardware reason above.

3. Push The Release Tag

Confirm git rev-parse HEAD is the same SHA shown by the completed successful CI run, then tag that commit:

version=X.Y.Z
git tag "v${version}"
git push origin "v${version}"

OdyTTY release tags are lightweight tags that point directly at the version commit. Do not tag a different local commit after the matching CI run passes.

The tag starts all producer jobs, publishes the GitHub Release, and then runs the three package-channel jobs.

4. Verify The Published Release

Confirm the release has 17 assets: seven package aliases, seven pinned package copies, odytty-X.Y.Z-install.sh, SHA256SUMS, and SHA256SUMS.minisig. Verify the Minisign signature first, then verify every package and the installer against the authenticated manifest. Confirm that every alias/pinned pair has matching hashes and compare each pair byte-for-byte.

Then verify the build provenance attestation from a clean machine, as a user would. The workflow already verified its own attestation before publishing, but that proves the upload succeeded, not that a third party can check it:

gh attestation verify odytty-x86_64.AppImage \
  --repo ghreprimand/odytty \
  --signer-workflow ghreprimand/odytty/.github/workflows/release.yml

Repeat for the Windows and macOS zips. Confirm the reported commit matches the tag. The attestation is a repository-level record rather than a release asset, so it does not change the 17-asset count.

Download the pinned source archive and confirm it builds:

cargo build --release --locked

Open About (or use Copy diagnostics) in each downloadable binary that can be exercised on its target platform and confirm Commit is the full commit identified by git rev-parse "v${version}^{}". Open the source-archive build and confirm its abbreviated value matches the same commit; it must not read unknown or unavailable.

Also confirm the release title, tag, Cargo.toml version, and metainfo release entry all use X.Y.Z.

Release Workflow Jobs

release.yml has seven producer jobs, one tag-only CI-verification job, and four tag-only publication/channel jobs:

JobArtifact or channelGuard
sourceSource tarballRuns for tag and manual validation
appimageLinux AppImageRuns for tag and manual validation
linux-tarballStandalone Linux binary tarballRuns for tag and manual validation
debDebian packageRuns for tag and manual validation
rpmRPM packageRuns for tag and manual validation
windowsWindows portable zipRuns for tag and manual validation
macosAd-hoc-signed macOS app zipRuns for tag and manual validation
verify-ciExact tagged-commit CI resultTag only; requires a completed successful ci.yml run whose head_sha equals the tag commit
releaseGitHub Release, aliases, pinned copies, SHA256SUMS, its Minisign signature, and a build provenance attestation over every artifactTag only; requires verify-ci and all seven producers. Attests and self-verifies before publishing, so a failed attestation publishes nothing
scoopIn-repo Scoop manifestTag only; runs after release and pushes with GITHUB_TOKEN
homebrewExternal Homebrew tapTag only; runs after release; validates locally and publishes when HOMEBREW_TAP_DEPLOY_KEY is present
aurAUR packageTag only; runs after release through aur-publish.yml; publishes when AUR_SSH_PRIVATE_KEY is present

The Homebrew and AUR credentials are configured for the live channels. Their missing-key paths remain clean validation-only no-ops so forks and replacement repositories can use the workflow safely.

Verify Publishing Channels

After release finishes, verify every channel:

ChannelExpected automatic resultFallback
Scoopbucket/odytty.json is committed to master with the new version, Windows zip URL, and hashUpdate those three fields from the published SHA256SUMS
HomebrewThe cask and source formula are stamped and pushed to ghreprimand/homebrew-odyttyFollow the Homebrew publishing guide
AURThe odytty package is stamped and pushed to the AURFollow the AUR publishing guide

For Scoop, the client reads the pinned version, url, and hash. The autoupdate block is metadata for maintainer tooling and does not update installed clients by itself.

The Scoop hash comes from the odytty-X.Y.Z-windows-x86_64.zip row in SHA256SUMS. The Homebrew cask uses the macOS zip row, while the formula and AUR package use the source-tarball row. SHA256SUMS.minisig is an additional release asset, so none of those channel formats or stamping paths changes when manifest signing is enabled.

If only the AUR channel fails after the GitHub Release is already published, do not replay every release producer. After the AUR service is available, open Actions -> AUR publish -> Run workflow, select master, and enter vX.Y.Z. The equivalent command is:

gh workflow run aur-publish.yml --ref master -f version=vX.Y.Z

The workflow is idempotent and exits successfully when that package version is already present.

Build A Fallback Source Archive

The workflow normally builds the archive. To create the same versioned tarball locally when GitHub Actions is unavailable:

version=X.Y.Z
git archive --format=tar.gz --prefix="odytty-${version}/" \
  -o "odytty-${version}.tar.gz" "v${version}"
sha256sum "odytty-${version}.tar.gz" > SHA256SUMS

Using git archive is part of the provenance contract: it substitutes the .git_archival.txt token so a build from the extracted archive reports the tagged commit instead of unavailable.

Verify The Odyssey Package

Build the Odyssey package from the published archive rather than the working tree. Then verify ownership and the installed version:

cd ~/pkgbuilds/odytty
odyssey-build
pacman -Qi odytty
pacman -Qo /usr/bin/odytty \
  /usr/share/applications/io.unfinished_works.odytty.desktop \
  /usr/share/metainfo/io.unfinished_works.odytty.metainfo.xml
odytty --version

Package-Monitor Compatibility

The GitHub Release is the upstream signal for package monitors. These values must agree:

tag: vX.Y.Z
release title: vX.Y.Z
Cargo.toml version: X.Y.Z
archive: odytty-X.Y.Z.tar.gz

Do not publish a release whose source archive fails:

cargo build --release --locked

Odyssey-Mon Upstream Tracking

Odyssey-Mon reads the installed package version from pacman -Qi odytty and compares it with GitHub tags. Configure the upstream source as:

type: github
owner: ghreprimand
repo: odytty
tag_prefix: v

Odyssey-Mon normalizes the leading v and the package-release suffix, so an upstream vX.Y.Z tag compares with an installed X.Y.Z-1 package.

Versioning

Use semantic versions for source releases:

v0.1.0
...
v0.1.9
v0.2.0

For recipe-only packaging changes, keep the source version and increment the downstream package release field, such as pkgrel=2 in a PKGBUILD.