From 93deb1ba517ffd2c32ca6afbdbf60cf882ff7d1d Mon Sep 17 00:00:00 2001 From: "guys-inc-ops[bot]" <321481384+guys-inc-ops[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 03:31:25 +0000 Subject: [PATCH] Prime gpg-agent so reprepro can sign in CI The publish job failed at the export step: gpgme gave error Pinentry:32870: Inappropriate ioctl for device ERROR: Could not finish exporting 'stable'! The rotated signing subkey is passphrase-protected; the previous one was not. reprepro signs through gpgme, which has no mechanism for being handed a passphrase - it can only ask an agent, and with no TTY in CI that request has nowhere to go. Passing --passphrase to gpg does not help, because reprepro never invokes gpg directly. The agent has to already hold the passphrase, so preset it against the signing subkey's keygrip before reprepro runs. Verified locally: with the agent primed, gpg signs with no passphrase argument at all, which is exactly the condition gpgme needs. The preset is skipped when APT_GPG_PASSPHRASE is unset, so an unprotected key still works. Also adds the certify-capability guard already used in archivist's release workflow. This repository's CI holds a signing subkey too, and nothing here checked that. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015AH1v3tR8Xw2DmKqJSipPd --- .github/workflows/publish-apt.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/publish-apt.yml b/.github/workflows/publish-apt.yml index a44fc656a7..2e83598b0a 100644 --- a/.github/workflows/publish-apt.yml +++ b/.github/workflows/publish-apt.yml @@ -46,9 +46,38 @@ jobs: - name: Import GPG signing subkey env: APT_GPG_PRIVATE_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }} + APT_GPG_PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }} run: | + set -euo pipefail mkdir -p "$GNUPGHOME" && chmod 700 "$GNUPGHOME" + printf 'allow-preset-passphrase\nallow-loopback-pinentry\n' > "$GNUPGHOME/gpg-agent.conf" printf '%s' "$APT_GPG_PRIVATE_KEY" | gpg --batch --import + gpgconf --kill gpg-agent || true + gpgconf --launch gpg-agent + + # reprepro signs through gpgme, which has no way to be handed a + # passphrase - it can only ask an agent. With no TTY in CI that ends as + # "Pinentry: Inappropriate ioctl for device" and a failed export. So + # prime the agent up front with the signing subkey's keygrip instead. + if [ -n "${APT_GPG_PASSPHRASE:-}" ]; then + KEYGRIP=$(gpg --batch --with-keygrip --list-secret-keys --with-colons \ + | awk -F: '$1=="ssb"{s=1;next} s&&$1=="grp"{print $10; s=0}') + if [ -z "$KEYGRIP" ]; then + echo "::error::No signing subkey found in the imported secret." + exit 1 + fi + printf '%s' "$APT_GPG_PASSPHRASE" \ + | /usr/lib/gnupg/gpg-preset-passphrase --preset "$KEYGRIP" + fi + + # Only a signing subkey belongs in CI. A stubbed primary and a real one + # both report caps=cSC in field 12; field 15 is what separates them, + # "#" meaning no private material is present. + if gpg --batch --list-secret-keys --with-colons \ + | awk -F: '$1=="sec" && $12 ~ /c/ && $15 != "#" {found=1} END{exit !found}'; then + echo "::error::The imported secret carries private material for a certify-capable key." + exit 1 + fi gpg --list-secret-keys --keyid-format=long - name: Resolve release tag