Skip to content

feat: add signed APT repository publishing#21

Open
tyvsmith wants to merge 1 commit intomainfrom
feat/apt-repo-publishing
Open

feat: add signed APT repository publishing#21
tyvsmith wants to merge 1 commit intomainfrom
feat/apt-repo-publishing

Conversation

@tyvsmith
Copy link
Copy Markdown
Owner

Summary

  • Add two-channel signed APT repository hosted on GitHub Pages at tysmith.me/facelock/apt/
    • main channel: TPM-enabled build (Debian trixie container, --features tpm)
    • legacy channel: non-TPM build (Ubuntu 24.04, version suffix ~legacy1)
  • New build-deb-tpm job in Debian trixie container with dynamic libtss2 dependency detection
  • New publish-apt job using reprepro + GPG signing (via gpg-preset-passphrase for non-interactive CI)
  • Pages workflow downloads apt-repo.tar.gz from latest release, assembles into _site/apt/
  • Updated docs with APT install instructions for both channels
  • Fixed maintainer email to facelock@m.tysmith.me across all packaging files

Verification plan

  1. This PR: CI validates YAML syntax and existing tests pass
  2. Pre-release tag (v0.1.0-rc11): Tests build-deb-tpm and legacy versioning (apt publish skipped)
  3. Stable tag: Full end-to-end — build → apt publish → Pages deploy → user install

Files changed

File Change
dist/apt/conf/distributions New — reprepro config for main/legacy suites
.github/workflows/release.yml Add build-deb-tpm, publish-apt, trigger-pages; update build-deb for legacy
.github/workflows/pages.yml Download apt-repo from latest release into _site/apt/
justfile Add test-apt-repo recipe, APT secret checks in preflight
docs/releasing.md APT setup, GPG key rotation, two-channel docs
book/src/quickstart.md Package install instructions (AUR, APT, COPR)
README.md Full install section with all package managers
dist/debian/changelog Fix maintainer email

Test plan

  • CI passes (YAML valid, existing tests green)
  • Tag v0.1.0-rc11build-deb-tpm succeeds in trixie container
  • Tag v0.1.0-rc11build-deb produces ~legacy1 versioned deb
  • Tag stable → publish-apt runs reprepro + GPG signing
  • Tag stable → apt-repo.tar.gz appears in release assets
  • Tag stable → Pages redeploy includes /apt/ path
  • apt update with facelock source succeeds without GPG warnings

🤖 Generated with Claude Code

Add two-channel APT repo (main=TPM, legacy=non-TPM) built with reprepro
and GPG-signed during release. Hosted at tysmith.me/facelock/apt/ alongside
existing docs site.

Changes:
- Add build-deb-tpm job (Debian trixie container, --features tpm)
- Update build-deb for legacy channel (version suffix ~legacy1)
- Add publish-apt job (reprepro + GPG signing via gpg-preset-passphrase)
- Add trigger-pages job (gh workflow run after apt publish)
- Update pages.yml to download apt-repo.tar.gz from latest release
- Add dist/apt/conf/distributions (reprepro config)
- Add test-apt-repo justfile recipe and APT secret preflight checks
- Update docs (releasing.md, quickstart.md, README.md) with install instructions
- Fix maintainer email to facelock@m.tysmith.me across all packaging files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 29, 2026 02:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an automated, signed APT repository publishing flow for Facelock releases (two suites: TPM-enabled “main” and non-TPM “legacy”), hosted via GitHub Pages, and documents installation/releasing steps.

Changes:

  • Add reprepro distributions config for main and legacy suites.
  • Extend release workflow to build both Debian packages and publish a signed APT repo tarball to the GitHub Release, then trigger Pages.
  • Update Pages workflow + docs/README/book quickstart with APT installation and release/key management instructions.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
dist/apt/conf/distributions New reprepro suite definitions for main and legacy.
.github/workflows/release.yml Adds TPM deb build, APT repo publish/signing, and Pages trigger; updates legacy deb versioning.
.github/workflows/pages.yml Pulls apt-repo.tar.gz from latest release and publishes under _site/apt/.
justfile Adds local test-apt-repo and expands release preflight secret checks.
docs/releasing.md Documents two-channel Debian packaging and APT repo signing setup/rotation.
book/src/quickstart.md Adds APT installation instructions for main/legacy channels.
README.md Adds APT install section and updates release workflow description.
dist/debian/changelog Updates maintainer email.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +702 to +703
REPO_DIR="$(pwd)/apt-repo"
gpg --export > "${REPO_DIR}/tysmith-archive-keyring.gpg"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gpg --export without a key selector exports all public keys in the keyring. To avoid accidentally shipping unrelated keys (or changing output if additional keys are present in the environment), export only the intended signing key (e.g., by fingerprint captured earlier) when generating tysmith-archive-keyring.gpg.

Suggested change
REPO_DIR="$(pwd)/apt-repo"
gpg --export > "${REPO_DIR}/tysmith-archive-keyring.gpg"
set -euo pipefail
REPO_DIR="$(pwd)/apt-repo"
# Determine the fingerprint of the signing key (first secret key)
SIGNING_FPR="$(gpg --list-secret-keys --with-colons | awk -F: '$1 == "sec" { print $5; exit }')"
if [ -z "${SIGNING_FPR}" ]; then
echo "Error: No secret GPG key found; cannot export signing keyring." >&2
exit 1
fi
echo "Exporting public key for signing key fingerprint: ${SIGNING_FPR}"
gpg --export "${SIGNING_FPR}" > "${REPO_DIR}/tysmith-archive-keyring.gpg"

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +47
if gh release download --pattern 'apt-repo.tar.gz' --dir /tmp/apt-download --repo "${{ github.repository }}" 2>/dev/null; then
echo "Downloaded apt-repo.tar.gz from latest release"
mkdir -p _site/apt
tar -xzf /tmp/apt-download/apt-repo.tar.gz -C _site/apt/
echo "=== APT repo files included in site ==="
find _site/apt -type f | sort
else
echo "No apt-repo.tar.gz found in any release. Deploying docs only."
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If gh release download fails (API rate limit/transient error), this workflow currently proceeds and deploys a site without _site/apt/, which would remove the previously-published APT repo from Pages. Consider treating "release exists but asset download failed" as a hard error (fail the job) or otherwise ensuring an existing APT repo isn't unintentionally wiped on deploy.

Suggested change
if gh release download --pattern 'apt-repo.tar.gz' --dir /tmp/apt-download --repo "${{ github.repository }}" 2>/dev/null; then
echo "Downloaded apt-repo.tar.gz from latest release"
mkdir -p _site/apt
tar -xzf /tmp/apt-download/apt-repo.tar.gz -C _site/apt/
echo "=== APT repo files included in site ==="
find _site/apt -type f | sort
else
echo "No apt-repo.tar.gz found in any release. Deploying docs only."
# First, query the latest release assets and capture any error.
release_err_file="$(mktemp)"
asset_names="$(gh release view --json assets --jq '.assets[].name' --repo "${{ github.repository }}" 2>"${release_err_file}" || true)"
if [ -s "${release_err_file}" ]; then
if grep -qi 'release not found' "${release_err_file}"; then
echo "No releases found. Deploying docs only."
echo "This is expected before the first stable release with APT publishing."
rm -f "${release_err_file}"
exit 0
else
echo "ERROR: Failed to query latest release for APT assets. Aborting to avoid wiping existing APT repo." >&2
cat "${release_err_file}" >&2 || true
rm -f "${release_err_file}"
exit 1
fi
fi
rm -f "${release_err_file}"
if printf '%s\n' "${asset_names}" | grep -qx 'apt-repo.tar.gz'; then
echo "Found apt-repo.tar.gz asset in latest release. Attempting download..."
if gh release download --pattern 'apt-repo.tar.gz' --dir /tmp/apt-download --repo "${{ github.repository }}"; then
echo "Downloaded apt-repo.tar.gz from latest release"
mkdir -p _site/apt
tar -xzf /tmp/apt-download/apt-repo.tar.gz -C _site/apt/
echo "=== APT repo files included in site ==="
find _site/apt -type f | sort
else
echo "ERROR: apt-repo.tar.gz asset exists but download failed. Aborting to avoid wiping existing APT repo." >&2
exit 1
fi
else
echo "No apt-repo.tar.gz asset found in latest release. Deploying docs only."

Copilot uses AI. Check for mistakes.
5. Validates Nix flake evaluation
6. Publishes to AUR (if `AUR_SSH_KEY` secret is configured)
7. Triggers COPR rebuild (if `COPR_WEBHOOK_URL` secret is configured)
8. Publishes signed APT repository (if `APT_GPG_PRIVATE_KEY` is configured)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow summary says APT publishing happens if APT_GPG_PRIVATE_KEY is configured, but the implementation/documentation elsewhere require both APT_GPG_PRIVATE_KEY and APT_GPG_PASSPHRASE. Suggest updating this line to mention both secrets so operators don't end up with a partially-configured setup.

Suggested change
8. Publishes signed APT repository (if `APT_GPG_PRIVATE_KEY` is configured)
8. Publishes signed APT repository (if `APT_GPG_PRIVATE_KEY` and `APT_GPG_PASSPHRASE` secrets are configured)

Copilot uses AI. Check for mistakes.
sed -i '/^SignWith:/d' "${REPO_DIR}/conf/distributions"

# Find any .deb files (from just test-deb or CI artifacts)
DEB_FILES=$(find . -maxdepth 1 -name 'facelock_*.deb' -o -path './target/*.deb' 2>/dev/null | head -2)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test-apt-repo's find command uses -maxdepth 1, so the -path './target/*.deb' branch can never match (it prevents descending into ./target). This makes the recipe fail to pick up debs from target/ as intended; consider restructuring the find with parentheses and a depth that actually includes target/ (or run two finds).

Suggested change
DEB_FILES=$(find . -maxdepth 1 -name 'facelock_*.deb' -o -path './target/*.deb' 2>/dev/null | head -2)
DEB_FILES=$((find . -maxdepth 1 -name 'facelock_*.deb'; find ./target -maxdepth 1 -name '*.deb') 2>/dev/null | head -2)

Copilot uses AI. Check for mistakes.
run: |
if [ -z "$APT_GPG_PRIVATE_KEY" ]; then
echo "APT_GPG_PRIVATE_KEY secret not configured. Skipping APT publish."
echo "See docs/releasing.md for setup instructions."
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step says it's skipping when APT_GPG_PRIVATE_KEY is missing, but it exits with status 1, which will fail the publish-apt job (and block downstream jobs). If APT publishing is meant to be optional when secrets are not configured (like AUR/COPR), exit 0 (or gate the job on secret presence) and consider also validating APT_GPG_PASSPHRASE explicitly for a clearer failure mode.

Suggested change
echo "See docs/releasing.md for setup instructions."
echo "See docs/releasing.md for setup instructions."
exit 0
fi
if [ -z "$APT_GPG_PASSPHRASE" ]; then
echo "APT_GPG_PASSPHRASE secret not configured, but APT_GPG_PRIVATE_KEY is set."
echo "Refusing to publish APT repository without a passphrase. See docs/releasing.md for setup instructions."

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants