Files
triangles_v5/doc/release-process.md
Sami Ahmed 148cfd63c7 release: v6.1.5
36 commits since v6.1.4 (2026-07-04). User-facing:
- UI: olive-green for unconfirmed/immature stakes
- Wallet: close-hang on Windows from detached Tor/I2P threads fixed
- Consensus: live PoS checks during stale-tip IBD

Maintainer-visible:
- CHANGELOG.md added at the repo root
- doc/release-process.md corrected to match the actual signing keys
  (RSA-4096 Krystie release key + Sami personal tag-signing key)
2026-07-07 21:10:41 -07:00

10 KiB

Triangles Release Process

Canonical release pipeline for SamiAhmed7777/triangles_v5. This document is the source of truth for how a release is cut. The implementation lives in scripts/verify-reproducible-build.sh and scripts/sign-release.sh.

Goals

  1. Reproducible — any two builders with the same source tree, same toolchain, and same flags produce byte-identical binaries.
  2. Signed — every release artifact has a detached PGP signature that verifiers can check against a known public key.
  3. Verifiable end-to-end — a third party can confirm a release is legitimate using only gpg and sha256sum, both installed by default on every Linux distribution.

Pipeline overview

   source tag (e.g. v6.1.4)
            │
            ▼
   ┌─────────────────────┐
   │  CI builds all 4     │  .github/workflows/build-all.yml
   │  targets on each     │  (ubuntu / windows / macos)
   │  platform            │
   └──────────┬───────────┘
              │  produces: daemon.tar.gz, qt.tar.gz, .deb, .dmg, .exe, ...
              ▼
   ┌─────────────────────┐
   │  Local maintainer     │  scripts/sign-release.sh <release-dir>
   │  signs artifacts      │  (uses release signing key in local keyring)
   └──────────┬───────────┘
              │  produces: SHA256SUMS, *.asc detached signatures
              ▼
   ┌─────────────────────┐
   │  Push to GitHub       │  .github/workflows/distribute.yml
   │  release + Docker    │  (uploads artifacts, builds Docker image,
   │  + Homebrew tap +     │   updates Homebrew formula, submits
   │  WinGet + Snap        │   WinGet + Snap PRs)
   └──────────┬───────────┘
              │
              ▼
   ┌─────────────────────┐
   │  Verifier             │  scripts/sign-release.sh --verify <dir>
   │  independently        │  + gpg --import <release-pubkey>
   │  confirms             │
   └─────────────────────┘

Reproducibility — how it works today

The Triangles build is already reproducible for Release builds with the following properties:

Property Implementation
BUILD_DESC Git describe output, written to build.h at build time
BUILD_DATE Commit timestamp (NOT wall-clock), from git log -n 1 --format=%ci
__DATE__/__TIME__ fallback Dead code in practice — build.h always defines BUILD_DATE
Build paths in binaries Mapped with -ffile-prefix-map=${CMAKE_SOURCE_DIR}=. so absolute source paths do not leak into debug info

Verifying reproducibility

Run on a clean checkout:

scripts/verify-reproducible-build.sh

This builds trianglesd twice into two separate build directories and compares SHA256 hashes. Exits 0 on success.

Options:

  • BUILD_TYPE=Debug scripts/verify-reproducible-build.sh
  • TARGET=triangles-qt scripts/verify-reproducible-build.sh
  • BUILD_DIR_A=/tmp/A BUILD_DIR_B=/tmp/B scripts/verify-reproducible-build.sh

Signing — how it works

Generate (or import) a release signing key

One-time setup (the maintainer's machine):

# The release-signing key currently in use is:
#
#   uid:  Krystie Triangles Release <krystie-triangles-release@dns2.sami.tailnet>
#   fp:   523A 8183 3EB7 2015 73E1 EFE1 DCF2 5799 6810 7984
#   sub:  6913 E136 10F6 9818 3429 CE20 C2DC 6061 8C85 A159
#   algo: RSA-4096, created 2026-04-29, expires 2028-04-28
#
# This is an unattended signing key used by the release CI to sign
# release artifacts (daemon.tar.gz, qt.tar.gz, .deb, .dmg, .exe, .AppImage)
# without a human in the loop. It is stored as a GitHub Actions secret.
#
# Git tags are signed by the maintainer's personal key
# (uid `Sami <hello@sami-ahmed.net>`, fp `53AA 858E F0DD D528 EC2C 2ABD
# 0BF7 F887 2FE0 E859`) so the tag and the artifacts can be verified
# independently.

# To print the public key for the current release-signing key:
gpg --armor --export 0xDCF2579968107984 > release-pubkey.asc

# To export the maintainer's tag-signing secret key (for backup):
gpg --export-secret-keys 0x0BF7F8872FE0E859 > release-seckey-BACKUP.asc
chmod 600 release-seckey-BACKUP.asc

Import an existing key (e.g. on a new maintainer machine):

gpg --import release-seckey-BACKUP.asc

Sign a release directory

After CI has produced the artifacts in a known directory:

scripts/sign-release.sh /path/to/release-dir

This will:

  1. Generate SHA256SUMS for every release artifact (.tar.gz, .deb, .dmg, .exe, .zip, .AppImage)
  2. Write a detached PGP signature (<artifact>.asc) for each artifact
  3. Write a detached PGP signature over SHA256SUMS itself
  4. Refuse to run if the signing key isn't in the local keyring (safety)

Verify a release

A third party (user, exchange, package maintainer) verifies with:

# 1. Import the public key (one-time).
gpg --import release-pubkey.asc

# 2. Verify everything in the release directory.
scripts/sign-release.sh --verify /path/to/release-dir

This checks:

  • SHA256SUMS.asc against SHA256SUMS (the master signature)
  • Each <artifact>.asc against its <artifact> (belt-and-suspenders)
  • Each artifact's SHA256 against SHA256SUMS (integrity)

Why both per-artifact signatures AND a SHA256SUMS signature?

  • SHA256SUMS + signature: small, fast to verify, single point of trust. If the SHA256SUMS.asc checks out and a file's SHA256 matches an entry, you're done — you trust that entry.
  • Per-artifact signatures: defense against a hypothetical attack where someone modifies SHA256SUMS but not the artifacts (or vice versa). Two independent signature chains.

For most verifiers, checking SHA256SUMS.asc + sha256sum -c SHA256SUMS is sufficient. The per-artifact .asc files are insurance.

CI integration

.github/workflows/build-all.yml already produces the artifacts. The remaining work (separate PR) is to add a "sign" job that runs scripts/sign-release.sh against the assembled release directory using a key stored as a GitHub Actions secret.

Required secrets (one-time setup in repo Settings → Secrets):

  • GPG_PRIVATE_KEY — base64-encoded release-seckey-BACKUP.asc (see GitHub docs on encrypted secrets)
  • GPG_PASSPHRASE — passphrase for the signing key (if any)
  • GITHUB_TOKEN — already provided by Actions

Suggested job sketch (in .github/workflows/build-all.yml after all build jobs complete):

sign:
    name: Sign release artifacts
    needs: [build-linux-daemon, build-linux-qt, build-windows-daemon, build-windows-qt, build-macos]
    runs-on: ubuntu-22.04
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/checkout@v4

      - name: Import signing key
        run: |
          echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 -d | gpg --import

      - name: Sign artifacts
        run: scripts/sign-release.sh release-artifacts/
        env:
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

Public key distribution

The release public key MUST be published in at least three independent places so a keyserver takedown or DNS hijack cannot prevent verification:

  1. This repositoryrelease-pubkey.asc at the repo root, committed on every release tag.
  2. The websitehttps://cryptographic-triangles.org/release-pubkey.asc
  3. Public keyservers — submit to keys.openpgp.org, keyserver.ubuntu.com, pgp.mit.edu. Each is independently operated.

Distribution list refreshed with every key rotation (rare; treat as multi-year commitment).

Failure modes & recovery

Scenario Recovery
Signing key compromised Revoke via pre-published revocation certificate. Re-cut release. Document incident.
Signing key lost (no backup) Cannot sign new releases. Existing artifacts still verify against the published public key. Treat as catastrophic; re-mint a new key and treat the chain as fork-vulnerable until community updates.
Public key not yet distributed User gets gpg: Can't check signature: No public key. Provide clear "first verify the key fingerprint out-of-band" instructions on the website.
CI secret leaked Rotate the signing key immediately; treat all artifacts signed with the old key as suspect.
SHA256SUMS signed but artifacts don't match sha256sum -c fails. Either an artifact was corrupted in transit, or someone tampered. Re-download from GitHub and re-verify.

Checklist for cutting a release

  • Source tree is clean (no uncommitted changes)
  • scripts/verify-reproducible-build.sh passes (builds are reproducible)
  • All CI jobs on the release tag are green
  • Release artifacts are in a single directory (release-artifacts/)
  • scripts/sign-release.sh release-artifacts/ runs without error
  • scripts/sign-release.sh --verify release-artifacts/ passes
  • release-pubkey.asc is current and committed to the repo
  • GitHub release created with all artifacts + SHA256SUMS + SHA256SUMS.asc
  • distribute.yml workflow ran (Docker Hub, Homebrew, WinGet, Snap)
  • Announcement posted (Twitter/Mastodon, Discord/Telegram, mailing list if any)

Future work

  • Reproducibility hardening: add -ffile-prefix-map to compile flags so absolute source paths don't leak into the binary (would also fix the simd.c:265 UBSan build-id drift).
  • Gitian-style deterministic builds: containerized build environment pinned to a specific GCC/binutils version, so multiple independent verifiers can rebuild from source and get identical hashes.
  • Transparency log: publish each release artifact hash to a Sigstore / sigsum / Certificate Transparency-style log so any tampering is publicly auditable.
  • Key rotation policy: document how/when the signing key gets rotated (probably never, but state the policy).