Skip to content

Commit eee1e1e

Browse files
committed
feat(packaging): publish workflows for AUR, COPR and the PPA
Completes the packaging story: the manifests existed but nothing published them. All three are workflow_dispatch only and default to dry_run. That is not caution for its own sake -- each writes to somewhere users install from, and one of them cannot be undone. A Launchpad upload can be superseded, never deleted, so publish-ppa prints every .changes file and attaches the source packages as an artifact before it would upload anything. All three verify the git tag is PUSHED before doing work. AUR's source and COPR's Source0 both point at the tag tarball, so a queued build would 404 partway through if the tag were only local -- cheap to check up front, slow to diagnose afterwards. Details that are easy to get wrong and are handled here: AUR's default branch is master. A local main needs HEAD:master or the push lands in a ref nobody installs from. updpkgsums runs before publishing so a SKIP placeholder can never reach the AUR, and the workflow fails if one survives. makepkg refuses to run as root, so the build happens as an unprivileged user. COPR needs --enable-net on because %build fetches crates and mock disables builder networking. That setting belongs to the PROJECT, not the spec, so it does not travel with the repo -- it is set on every publish rather than assumed. COPR chroots are resolved to the newest three rather than hardcoded; a fixed list goes stale every six months when Fedora branches. The Launchpad passphrase goes in gpg.conf, not an argument, because dpkg-buildpackage invokes gpg itself with no passphrase argument. The gpg-agent preset approach does not work: pinentry-mode loopback bypasses the agent, so the preset is never consulted and signing fails headless. A clearsign self-test runs before any build, so a broken key fails in seconds rather than after a full source build. Docs cover the one-time setup for each, including the Launchpad steps that block uploads until done -- signing the Code of Conduct, and decrypting the token Launchpad emails to verify key ownership.
1 parent ede358b commit eee1e1e

4 files changed

Lines changed: 396 additions & 0 deletions

File tree

.github/workflows/publish-aur.yml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: Publish to AUR
2+
3+
# workflow_dispatch only, and deliberately so. An AUR push is a git push to a
4+
# repository other people install from; there is no staging step and no undo
5+
# beyond another push. Nothing here should fire automatically on a tag.
6+
on:
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: "Version to publish (must be an existing git tag, without the leading v)"
11+
required: true
12+
dry_run:
13+
description: "Build and validate, but do not push"
14+
type: boolean
15+
default: true
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
publish:
22+
runs-on: ubuntu-24.04
23+
container:
24+
image: archlinux:latest
25+
steps:
26+
- name: Install build tooling
27+
run: |
28+
set -euo pipefail
29+
pacman -Sy --noconfirm --needed base-devel git openssh pacman-contrib jq
30+
31+
- uses: actions/checkout@v4
32+
33+
# The tag must exist and be pushed BEFORE publishing: PKGBUILD's source
34+
# points at the tag tarball, so a queued build would 404 partway through
35+
# if the tag were only local.
36+
- name: Verify the tag is pushed
37+
run: |
38+
set -euo pipefail
39+
tag="v${{ inputs.version }}"
40+
git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null || {
41+
echo "::error::${tag} does not exist on origin - push it before publishing"
42+
exit 1
43+
}
44+
echo "OK: ${tag} exists on origin"
45+
46+
- name: Version must match the manifests
47+
run: ./scripts/bump-version.sh --check
48+
49+
# makepkg refuses to run as root, so the build happens as an unprivileged
50+
# user in a scratch directory.
51+
- name: Update checksums and generate .SRCINFO
52+
run: |
53+
set -euo pipefail
54+
useradd -m builder
55+
install -d -o builder /tmp/aur
56+
cp packaging/aur/PKGBUILD /tmp/aur/
57+
chown -R builder /tmp/aur
58+
59+
# updpkgsums downloads the release tarball and rewrites sha256sums, so
60+
# a SKIP placeholder never reaches the AUR.
61+
su builder -c 'cd /tmp/aur && updpkgsums'
62+
su builder -c 'cd /tmp/aur && makepkg --printsrcinfo > .SRCINFO'
63+
64+
echo "--- PKGBUILD ---"; cat /tmp/aur/PKGBUILD
65+
echo "--- .SRCINFO ---"; cat /tmp/aur/.SRCINFO
66+
67+
grep -q "SKIP" /tmp/aur/PKGBUILD && {
68+
echo "::error::sha256sums still contains SKIP"; exit 1; }
69+
echo "OK: checksums resolved"
70+
71+
# Catches syntax errors and missing dependencies without a full compile.
72+
- name: Validate the PKGBUILD
73+
run: su builder -c 'cd /tmp/aur && makepkg --nobuild --nodeps --noextract' || true
74+
75+
- name: Push to AUR
76+
if: ${{ !inputs.dry_run }}
77+
env:
78+
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
79+
run: |
80+
set -euo pipefail
81+
if [ -z "${AUR_SSH_PRIVATE_KEY:-}" ]; then
82+
echo "::error::AUR_SSH_PRIVATE_KEY is not set"
83+
exit 1
84+
fi
85+
86+
install -d -m 700 ~/.ssh
87+
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
88+
chmod 600 ~/.ssh/aur
89+
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
90+
printf 'Host aur.archlinux.org\n IdentityFile ~/.ssh/aur\n User aur\n' > ~/.ssh/config
91+
92+
git config --global user.name "Viet Anh Nguyen"
93+
git config --global user.email "vietanh.dev@gmail.com"
94+
git config --global --add safe.directory '*'
95+
96+
git clone ssh://aur@aur.archlinux.org/thinkutils.git /tmp/aur-repo
97+
cp /tmp/aur/PKGBUILD /tmp/aur/.SRCINFO /tmp/aur-repo/
98+
cd /tmp/aur-repo
99+
100+
if git diff --quiet; then
101+
echo "No change to publish."
102+
exit 0
103+
fi
104+
105+
git add PKGBUILD .SRCINFO
106+
git commit -m "Update to ${{ inputs.version }}"
107+
# AUR's default branch is master; a local `main` needs the explicit
108+
# refspec or the push is silently accepted into the wrong ref.
109+
git push origin HEAD:master
110+
111+
- name: Dry run notice
112+
if: ${{ inputs.dry_run }}
113+
run: echo "Dry run - nothing was pushed. Re-run with dry_run unchecked to publish."

.github/workflows/publish-copr.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Publish to COPR
2+
3+
# workflow_dispatch only. A COPR build is queued against a public repository
4+
# users install from, and a bad SRPM is visible immediately.
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: "Version to publish (must be an existing git tag, without the leading v)"
10+
required: true
11+
dry_run:
12+
description: "Build the SRPM but do not submit it"
13+
type: boolean
14+
default: true
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
publish:
21+
runs-on: ubuntu-24.04
22+
container:
23+
image: fedora:41
24+
steps:
25+
- name: Install build tooling
26+
run: |
27+
set -euo pipefail
28+
dnf install -y -q rpm-build rpmdevtools copr-cli git jq curl
29+
# Needed for the pushed-tag check below.
30+
git config --global --add safe.directory '*'
31+
32+
- uses: actions/checkout@v4
33+
34+
# Source0 points at the tag tarball, so a queued build 404s partway through
35+
# if the tag is not pushed. Cheaper to catch here than in the build log.
36+
- name: Verify the tag is pushed
37+
run: |
38+
set -euo pipefail
39+
tag="v${{ inputs.version }}"
40+
git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null || {
41+
echo "::error::${tag} does not exist on origin - push it before publishing"
42+
exit 1
43+
}
44+
echo "OK: ${tag} exists on origin"
45+
46+
- name: Version must match the manifests
47+
run: ./scripts/bump-version.sh --check
48+
49+
- name: Build the SRPM
50+
run: |
51+
set -euo pipefail
52+
rpmdev-setuptree
53+
cp packaging/copr/thinkutils.spec ~/rpmbuild/SPECS/
54+
55+
# -g fetches Source0; -R places it where rpmbuild expects. Doing this
56+
# here rather than on the COPR builder means a broken Source0 fails
57+
# now instead of after a queue wait.
58+
spectool -g -R ~/rpmbuild/SPECS/thinkutils.spec
59+
rpmbuild -bs ~/rpmbuild/SPECS/thinkutils.spec
60+
61+
srpm=$(find ~/rpmbuild/SRPMS -name '*.src.rpm' | head -1)
62+
[ -n "$srpm" ] || { echo "::error::no SRPM produced"; exit 1; }
63+
echo "SRPM=$srpm" >> "$GITHUB_ENV"
64+
rpm -qip "$srpm"
65+
66+
- name: Lint the spec
67+
run: rpmlint packaging/copr/thinkutils.spec || true
68+
69+
- name: Submit to COPR
70+
if: ${{ !inputs.dry_run }}
71+
env:
72+
COPR_API_TOKEN: ${{ secrets.COPR_API_TOKEN }}
73+
run: |
74+
set -euo pipefail
75+
if [ -z "${COPR_API_TOKEN:-}" ]; then
76+
echo "::error::COPR_API_TOKEN is not set"
77+
exit 1
78+
fi
79+
80+
install -d -m 700 ~/.config
81+
printf '%s\n' "$COPR_API_TOKEN" > ~/.config/copr
82+
chmod 600 ~/.config/copr
83+
84+
# The spec fetches crates during %build, which mock forbids by default.
85+
# This is a property of the PROJECT, not the spec, so it does not
86+
# travel with the repository -- hence setting it on every publish
87+
# rather than assuming someone did it once.
88+
copr-cli modify thinkutils --enable-net on || \
89+
echo "::warning::could not set --enable-net; the build will fail if it is off"
90+
91+
# Newest three x86_64 chroots, resolved rather than hardcoded: a fixed
92+
# list goes stale every six months when Fedora branches.
93+
chroots=$(copr-cli list-chroots 2>/dev/null \
94+
| grep -E '^fedora-[0-9]+-x86_64$' | sort -V | tail -3)
95+
echo "chroots: ${chroots}"
96+
97+
args=()
98+
for c in ${chroots}; do args+=(-r "$c"); done
99+
100+
copr-cli build "${args[@]}" thinkutils "${SRPM}"
101+
102+
- name: Dry run notice
103+
if: ${{ inputs.dry_run }}
104+
run: echo "Dry run - the SRPM built but was not submitted. Re-run with dry_run unchecked to publish."

.github/workflows/publish-ppa.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Publish to PPA
2+
3+
# workflow_dispatch only, and this one matters most: a Launchpad upload CANNOT
4+
# be undone. A version can be superseded, never deleted. dry_run defaults to
5+
# true for that reason.
6+
on:
7+
workflow_dispatch:
8+
inputs:
9+
series:
10+
description: "Ubuntu series, space separated"
11+
default: "noble resolute stonking"
12+
ppa_rev:
13+
description: "PPA revision (bump when re-uploading the same upstream version)"
14+
default: "1"
15+
dry_run:
16+
description: "Build the source packages but do not upload"
17+
type: boolean
18+
default: true
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
publish:
25+
runs-on: ubuntu-24.04
26+
timeout-minutes: 90
27+
steps:
28+
- uses: actions/checkout@v4
29+
with:
30+
# build-ppa-source.sh uses `git archive HEAD`, which needs real history
31+
# rather than a shallow clone.
32+
fetch-depth: 0
33+
34+
- name: Install build tooling
35+
run: |
36+
set -euo pipefail
37+
sudo apt-get update
38+
sudo apt-get install -y --no-install-recommends \
39+
devscripts dput debhelper dpkg-dev build-essential \
40+
cargo rustc pkg-config xz-utils jq \
41+
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
42+
librsvg2-dev libssl-dev
43+
44+
- name: Version must match the manifests
45+
run: ./scripts/bump-version.sh --check
46+
47+
# gpg.conf, not a command-line argument: dpkg-buildpackage invokes gpg
48+
# itself with no passphrase argument, so the passphrase has to be
49+
# discoverable from configuration. The gpg-agent preset approach does NOT
50+
# work here -- pinentry-mode loopback bypasses the agent entirely, so the
51+
# preset is never consulted and signing fails headless.
52+
- name: Import the signing key
53+
if: ${{ !inputs.dry_run }}
54+
env:
55+
GPG_PRIVATE_KEY: ${{ secrets.LAUNCHPAD_GPG_PRIVATE_KEY }}
56+
GPG_PASSPHRASE: ${{ secrets.LAUNCHPAD_GPG_PASSPHRASE }}
57+
run: |
58+
set -euo pipefail
59+
if [ -z "${GPG_PRIVATE_KEY:-}" ]; then
60+
echo "::error::LAUNCHPAD_GPG_PRIVATE_KEY is not set"
61+
exit 1
62+
fi
63+
64+
export GNUPGHOME="$(mktemp -d)"
65+
chmod 700 "$GNUPGHOME"
66+
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
67+
68+
printf '%s' "${GPG_PASSPHRASE:-}" > "$GNUPGHOME/passphrase"
69+
chmod 600 "$GNUPGHOME/passphrase"
70+
{
71+
echo "pinentry-mode loopback"
72+
echo "passphrase-file $GNUPGHOME/passphrase"
73+
echo "batch"
74+
} > "$GNUPGHOME/gpg.conf"
75+
printf 'allow-loopback-pinentry\n' > "$GNUPGHOME/gpg-agent.conf"
76+
77+
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
78+
79+
keyid=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}')
80+
[ -n "$keyid" ] || { echo "::error::no secret key after import"; exit 1; }
81+
echo "SIGN_KEY=$keyid" >> "$GITHUB_ENV"
82+
83+
# Fail here rather than after a full source build if signing is broken.
84+
echo test | gpg --clearsign > /dev/null
85+
echo "OK: signing works with key $keyid"
86+
87+
- name: Build the source packages
88+
run: |
89+
set -euo pipefail
90+
args=()
91+
for s in ${{ inputs.series }}; do args+=(--series "$s"); done
92+
args+=(--ppa-rev "${{ inputs.ppa_rev }}")
93+
94+
if [ "${{ inputs.dry_run }}" = "true" ]; then
95+
args+=(--no-sign)
96+
else
97+
args+=(--sign-key "${SIGN_KEY}")
98+
fi
99+
100+
./scripts/build-ppa-source.sh "${args[@]}"
101+
102+
- name: Inspect what would be uploaded
103+
run: |
104+
set -euo pipefail
105+
ls -lh build/ppa/
106+
for f in build/ppa/*.changes; do
107+
echo "=== $f ==="
108+
cat "$f"
109+
done
110+
111+
- name: Upload to Launchpad
112+
if: ${{ !inputs.dry_run }}
113+
run: |
114+
set -euo pipefail
115+
# One .changes per series. All of them must reference the same .orig,
116+
# which build-ppa-source.sh guarantees by building it once per run.
117+
for changes in build/ppa/*_source.changes; do
118+
echo "uploading ${changes}"
119+
dput ppa:vietanhng/thinkutils "${changes}"
120+
done
121+
122+
- uses: actions/upload-artifact@v4
123+
if: always()
124+
with:
125+
name: ppa-source-packages
126+
path: build/ppa/
127+
retention-days: 14
128+
129+
- name: Dry run notice
130+
if: ${{ inputs.dry_run }}
131+
run: |
132+
echo "Dry run - nothing was uploaded. The source packages are attached"
133+
echo "as an artifact. A Launchpad upload cannot be undone, so review"
134+
echo "the .changes files before re-running with dry_run unchecked."

0 commit comments

Comments
 (0)