Skip to content

Commit ba273bf

Browse files
committed
fix(ci): harden the AUR publish pipeline and repair its makepkg install
Le PKGBUILD publié sur l'AUR n'est pas versionné ici : il appartient au mainteneur du paquet et peut changer sans passer par une PR. Le workflow le clonait, le patchait et le repoussait sans jamais regarder ce qu'il contenait. - makepkg *source* le PKGBUILD ; il s'exécutait donc dans un runner qui détenait déjà la clé de déploiement. Clone en HTTPS, clé écrite seulement après validation. - Audit du préambule, allowlist des sources sur notre dépôt, et refus de pousser un diff qui déborde de pkgver/pkgrel/sha256sums. - Le tag de workflow_dispatch est du texte libre et finissait dans une expression sed : v1.0|e id| donnait une exécution de shell. Regex stricte, qui bloque aussi la publication d'une RC (pkgver interdit le tiret). - Un seul checksum était réécrit ; celui du LICENSE, épinglé sur le tag, périmait en silence et cassait makepkg chez les utilisateurs. - L'étape d'installation ne pouvait pas aboutir : pacman-package-manager ne fournit pas makepkg mais s'installe sans erreur, rendant le repli mort. makepkg seul ne suffit pas non plus, il résout pacman par type -P. Jamais détecté car le job n'avait jamais dépassé son if: — vars.AUR_PACKAGE_NAME n'existait pas, les 10 dernières releases sont skipped.
1 parent b5297b4 commit ba273bf

1 file changed

Lines changed: 217 additions & 45 deletions

File tree

.github/workflows/aur-publish.yml

Lines changed: 217 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ on:
1313
permissions:
1414
contents: read
1515

16+
env:
17+
# Toute source déclarée par le PKGBUILD doit vivre sous ce préfixe.
18+
# Le PKGBUILD est hébergé sur l'AUR, pas ici : son mainteneur peut le modifier
19+
# à tout moment sans passer par une PR. Cette allowlist est le seul point où
20+
# l'on constate qu'une source a été substituée avant de republier.
21+
ALLOWED_SOURCE_PREFIX: "https://github.com/getopenscreen/openscreen/"
22+
1623
jobs:
1724
publish:
1825
runs-on: ubuntu-latest
1926
if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.AUR_PACKAGE_NAME != ''
2027
steps:
21-
- name: Resolve tag and version
28+
- name: Resolve and validate tag
2229
id: meta
2330
env:
2431
GH_EVENT_TAG: ${{ github.event.release.tag_name }}
@@ -30,6 +37,18 @@ jobs:
3037
echo "::error::No tag resolved from release event or workflow input"
3138
exit 1
3239
fi
40+
# Borne stricte, pour deux raisons distinctes :
41+
# 1. Le tag finit dans des URLs et des réécritures de PKGBUILD.
42+
# `workflow_dispatch` accepte du texte libre — aucune règle de ref
43+
# git ne s'y applique — donc une valeur comme `v1.0|e id|` sortait
44+
# de l'expression `sed` et exécutait du shell dans le runner.
45+
# 2. `pkgver` interdit le tiret côté Arch. Le filtre `prerelease` ne
46+
# couvre que l'événement `release` : un dispatch manuel sur un tag
47+
# `-rc.N` produisait un paquet invalide publié aux utilisateurs.
48+
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
49+
echo "::error::Refusing tag '$TAG' — expected a stable vMAJOR.MINOR.PATCH tag"
50+
exit 1
51+
fi
3352
VERSION="${TAG#v}"
3453
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
3554
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@@ -66,91 +85,244 @@ jobs:
6685
echo "name=$PACMAN_NAME" >> "$GITHUB_OUTPUT"
6786
echo "Found pacman asset: $PACMAN_NAME"
6887
69-
- name: Download and compute sha256
88+
# Clone en HTTPS et non en SSH, délibérément. Tout ce qui suit manipule un
89+
# PKGBUILD tiers, et `makepkg` le *source* (c'est un script bash) pour en
90+
# extraire les variables. La clé de déploiement n'est donc écrite sur le
91+
# disque qu'à la toute dernière étape, une fois le contenu validé : du code
92+
# non revu ne s'exécute jamais dans un runner qui la détient.
93+
- name: Clone AUR repository (read-only, no deploy key on disk)
7094
if: steps.aur_secret.outputs.configured == 'true'
71-
id: sha
7295
env:
73-
REPO: ${{ github.repository }}
74-
TAG: ${{ steps.meta.outputs.tag }}
75-
ASSET: ${{ steps.asset.outputs.name }}
96+
PACKAGE: ${{ vars.AUR_PACKAGE_NAME }}
7697
run: |
7798
set -euo pipefail
78-
BASE="https://github.com/${REPO}/releases/download/${TAG}"
79-
curl -fsSL --retry 3 -o /tmp/pkg.pacman "${BASE}/${ASSET}"
80-
PKG_SHA=$(sha256sum /tmp/pkg.pacman | awk '{print $1}')
81-
echo "sha256=$PKG_SHA" >> "$GITHUB_OUTPUT"
99+
git clone "https://aur.archlinux.org/${PACKAGE}.git" aur-repo
82100
83-
- name: Setup SSH for AUR
101+
# Garde-fou principal. `makepkg --printsrcinfo` exécute le code de premier
102+
# niveau du PKGBUILD ; les corps de fonctions sont seulement définis, pas
103+
# appelés. On refuse donc de lancer makepkg sur un fichier dont le préambule
104+
# contient de quoi exécuter quoi que ce soit.
105+
- name: Audit PKGBUILD before executing it
84106
if: steps.aur_secret.outputs.configured == 'true'
107+
working-directory: aur-repo
85108
env:
86-
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
87-
AUR_KNOWN_HOSTS: ${{ vars.AUR_KNOWN_HOSTS }}
109+
PACKAGE: ${{ vars.AUR_PACKAGE_NAME }}
88110
run: |
89111
set -euo pipefail
90-
if [[ -z "$AUR_KNOWN_HOSTS" ]]; then
91-
echo "::error::AUR_KNOWN_HOSTS variable is required for secure AUR SSH"
112+
113+
if [[ ! -f PKGBUILD ]]; then
114+
echo "::error::No PKGBUILD in the AUR repository"
92115
exit 1
93116
fi
94-
mkdir -p ~/.ssh
95-
echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur_key
96-
chmod 600 ~/.ssh/aur_key
97-
printf '%s\n' "$AUR_KNOWN_HOSTS" > ~/.ssh/aur_known_hosts
98-
cat >> ~/.ssh/config <<'SSHCONF'
99-
Host aur.archlinux.org
100-
HostName aur.archlinux.org
101-
User aur
102-
IdentityFile ~/.ssh/aur_key
103-
StrictHostKeyChecking yes
104-
UserKnownHostsFile ~/.ssh/aur_known_hosts
105-
SSHCONF
106117
107-
- name: Clone AUR repository
118+
# Le préambule = tout ce qui précède la première définition de fonction.
119+
PREAMBLE=$(awk '/^[a-zA-Z_]+\(\)[[:space:]]*\{/{exit} {print}' PKGBUILD)
120+
121+
FAILED=0
122+
while IFS= read -r pattern; do
123+
if grep -qE "$pattern" <<<"$PREAMBLE"; then
124+
echo "::error::PKGBUILD preamble contains a code-execution construct: /$pattern/"
125+
FAILED=1
126+
fi
127+
done <<'PATTERNS'
128+
\$\(
129+
`
130+
(^|[;&|[:space:]])(eval|source|curl|wget|bash|sh|python3?)[[:space:]]
131+
PATTERNS
132+
133+
if [[ "$(grep -cE '^pkgname=' PKGBUILD)" != "1" ]]; then
134+
echo "::error::Expected exactly one pkgname= declaration"
135+
FAILED=1
136+
fi
137+
if ! grep -qE "^pkgname=${PACKAGE}\$" PKGBUILD; then
138+
echo "::error::pkgname does not match AUR_PACKAGE_NAME (${PACKAGE})"
139+
FAILED=1
140+
fi
141+
142+
# Les deux réécritures qui suivent travaillent ligne à ligne. Un tableau
143+
# étalé sur plusieurs lignes — formatage parfaitement légal en amont —
144+
# laisserait des lignes orphelines et produirait un PKGBUILD invalide.
145+
# makepkg finirait par le refuser en le sourçant, mais le diff, lui, ne
146+
# verrait rien : les lignes orphelines sont inchangées. Autant refuser
147+
# ici, avec un message qui dit quoi regarder.
148+
if [[ "$(grep -cE '^sha256sums=' PKGBUILD)" != "1" ]]; then
149+
echo "::error::Expected exactly one sha256sums= line"
150+
FAILED=1
151+
elif ! grep -E '^sha256sums=' PKGBUILD | grep -q ')'; then
152+
echo "::error::sha256sums= spans several lines; this workflow only rewrites a single-line array"
153+
FAILED=1
154+
fi
155+
156+
if [[ "$(grep -cE '^source=\(' PKGBUILD)" != "1" ]]; then
157+
echo "::error::Expected exactly one source=( declaration"
158+
FAILED=1
159+
elif ! awk '/^source=\(/{f=1} f&&/\)[[:space:]]*$/{found=1; exit} END{exit !found}' PKGBUILD; then
160+
echo "::error::Could not find the closing paren of source=()"
161+
FAILED=1
162+
fi
163+
164+
if [[ "$FAILED" != "0" ]]; then
165+
echo "::error::Refusing to run makepkg on this PKGBUILD. Review it by hand."
166+
exit 1
167+
fi
168+
echo "PKGBUILD preamble clean."
169+
170+
- name: Bump version and recompute every checksum
108171
if: steps.aur_secret.outputs.configured == 'true'
172+
working-directory: aur-repo
109173
env:
110-
PACKAGE: ${{ vars.AUR_PACKAGE_NAME }}
174+
VERSION: ${{ steps.meta.outputs.version }}
175+
ASSET: ${{ steps.asset.outputs.name }}
111176
run: |
112177
set -euo pipefail
113-
git clone "ssh://aur@aur.archlinux.org/${PACKAGE}.git" aur-repo
178+
179+
# awk plutôt que `sed -i "s|...|${VERSION}|"` : la valeur est passée par
180+
# -v et n'est jamais reparsée comme partie de l'expression.
181+
awk -v v="$VERSION" '
182+
/^pkgver=/ { print "pkgver=" v; next }
183+
/^pkgrel=/ { print "pkgrel=1"; next }
184+
{ print }
185+
' PKGBUILD > PKGBUILD.new && mv PKGBUILD.new PKGBUILD
186+
187+
# Les URLs du bloc source=(), une fois ${pkgver} résolu. On les
188+
# retélécharge toutes pour recalculer *tous* les checksums : l'ancienne
189+
# version n'en réécrivait qu'un seul, celui du .pacman. Le LICENSE est
190+
# pourtant épinglé sur `raw/v${pkgver}/LICENSE`, donc sa somme devient
191+
# fausse dès que ce fichier change — et makepkg échoue alors chez tous
192+
# les utilisateurs, pas chez nous.
193+
SRC_BLOCK=$(awk '/^source=\(/{f=1} f{print} f&&/\)[[:space:]]*$/{exit}' PKGBUILD)
194+
mapfile -t URLS < <(grep -oE 'https?://[^"'"'"'[:space:]]+' <<<"$SRC_BLOCK" \
195+
| sed -e "s/\\\${pkgver}/${VERSION}/g" -e "s/\\\$pkgver/${VERSION}/g")
196+
197+
if [[ "${#URLS[@]}" -eq 0 ]]; then
198+
echo "::error::Could not parse any source URL from the PKGBUILD"
199+
exit 1
200+
fi
201+
202+
SUMS=()
203+
for url in "${URLS[@]}"; do
204+
# Allowlist : une source pointant ailleurs que sur notre dépôt signifie
205+
# que le PKGBUILD distribue autre chose que ce que nous publions.
206+
if [[ "$url" != "${ALLOWED_SOURCE_PREFIX}"* ]]; then
207+
echo "::error::Source URL outside the allowlist: $url"
208+
echo "::error::Expected everything under ${ALLOWED_SOURCE_PREFIX}"
209+
exit 1
210+
fi
211+
echo "Fetching $url"
212+
curl -fsSL --retry 3 -o /tmp/src.bin "$url"
213+
SUMS+=("$(sha256sum /tmp/src.bin | awk '{print $1}')")
214+
done
215+
216+
# Le .pacman référencé par le PKGBUILD doit être exactement l'asset
217+
# trouvé sur la release. Sinon le paquet sert un binaire que cette
218+
# release n'a pas produit.
219+
if ! printf '%s\n' "${URLS[@]}" | grep -qE "/${ASSET}\$"; then
220+
echo "::error::PKGBUILD does not reference the release asset ${ASSET}"
221+
printf ' source: %s\n' "${URLS[@]}"
222+
exit 1
223+
fi
224+
225+
NEW_SUMS="sha256sums=($(printf "'%s' " "${SUMS[@]}" | sed 's/ $//'))"
226+
awk -v line="$NEW_SUMS" '/^sha256sums=/ { print line; next } { print }' \
227+
PKGBUILD > PKGBUILD.new && mv PKGBUILD.new PKGBUILD
228+
echo "Recomputed ${#SUMS[@]} checksum(s)."
114229
115230
- name: Install makepkg
116231
if: steps.aur_secret.outputs.configured == 'true'
117232
run: |
118233
set -euo pipefail
234+
# Les DEUX paquets, et c'est subtil :
235+
# - `pacman-package-manager` ne livre que pacman, pacman-conf,
236+
# pacman-db-upgrade, pacman-key et repo-add — jamais makepkg. Il
237+
# s'installait pourtant sans erreur, donc le `|| apt-get install
238+
# makepkg` qui suivait était du code mort et l'étape mourait
239+
# systématiquement sur « makepkg still missing after install ».
240+
# - `makepkg` seul ne suffit pas non plus : il dépend de libalpm mais
241+
# pas du binaire `pacman`, qu'il résout par `type -P pacman` au
242+
# démarrage. Sans lui il sort sur « An unknown error has occurred ».
243+
# Vérifié en local sur noble, l'image d'ubuntu-latest.
119244
sudo apt-get update -qq
120-
sudo apt-get install -y -qq pacman-package-manager 2>/dev/null || \
121-
sudo apt-get install -y -qq makepkg 2>/dev/null || {
122-
echo "::error::Unable to install makepkg. Install pacman-package-manager or makepkg."
123-
exit 1
124-
}
245+
sudo apt-get install -y -qq makepkg pacman-package-manager
125246
command -v makepkg >/dev/null || {
126247
echo "::error::makepkg still missing after install."
127248
exit 1
128249
}
250+
# Sans tube : `makepkg --version | head -1` prend un SIGPIPE que
251+
# `pipefail` remonte en échec d'étape.
252+
makepkg --version
129253
130-
- name: Update PKGBUILD and .SRCINFO
254+
- name: Regenerate .SRCINFO
131255
if: steps.aur_secret.outputs.configured == 'true'
132256
working-directory: aur-repo
133-
env:
134-
VERSION: ${{ steps.meta.outputs.version }}
135-
SHA256: ${{ steps.sha.outputs.sha256 }}
136-
ASSET: ${{ steps.asset.outputs.name }}
137-
REPO: ${{ github.repository }}
138-
TAG: ${{ steps.meta.outputs.tag }}
139257
run: |
140258
set -euo pipefail
141-
sed -i -E "s|^pkgver=.*|pkgver=${VERSION}|" PKGBUILD
142-
sed -i -E "s|^pkgrel=.*|pkgrel=1|" PKGBUILD
143-
sed -i -E "s|^sha256sums=\('[^']*'|sha256sums=('${SHA256}'|" PKGBUILD
144259
makepkg --printsrcinfo > .SRCINFO
145260
echo "Updated .SRCINFO"
146261
262+
# Dernière barrière avant le push : on republie sous notre automatisation,
263+
# donc on vérifie que le commit ne porte que le bump attendu. Toute autre
264+
# ligne touchée (package(), depends, source, install…) veut dire que le
265+
# PKGBUILD a changé en amont et mérite un œil humain avant publication.
266+
- name: Verify the diff contains only the expected bump
267+
if: steps.aur_secret.outputs.configured == 'true'
268+
working-directory: aur-repo
269+
run: |
270+
set -euo pipefail
271+
272+
UNEXPECTED=$(git diff --unified=0 -- PKGBUILD \
273+
| grep -E '^[+-]' \
274+
| grep -vE '^(\+\+\+|---)' \
275+
| grep -vE '^[+-](pkgver=|pkgrel=|sha256sums=)' || true)
276+
277+
{
278+
echo "### AUR PKGBUILD diff"
279+
echo '```diff'
280+
git diff -- PKGBUILD .SRCINFO || true
281+
echo '```'
282+
} >> "$GITHUB_STEP_SUMMARY"
283+
284+
if [[ -n "$UNEXPECTED" ]]; then
285+
echo "::error::PKGBUILD changed beyond pkgver/pkgrel/sha256sums:"
286+
echo "$UNEXPECTED"
287+
echo "::error::Refusing to publish. Review the upstream PKGBUILD by hand."
288+
exit 1
289+
fi
290+
echo "Diff limited to the expected bump."
291+
292+
# La clé n'apparaît sur le disque qu'ici, une fois tout le contenu validé.
293+
- name: Setup SSH for AUR
294+
if: steps.aur_secret.outputs.configured == 'true'
295+
env:
296+
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
297+
AUR_KNOWN_HOSTS: ${{ vars.AUR_KNOWN_HOSTS }}
298+
run: |
299+
set -euo pipefail
300+
if [[ -z "$AUR_KNOWN_HOSTS" ]]; then
301+
echo "::error::AUR_KNOWN_HOSTS variable is required for secure AUR SSH"
302+
exit 1
303+
fi
304+
mkdir -p ~/.ssh
305+
echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur_key
306+
chmod 600 ~/.ssh/aur_key
307+
printf '%s\n' "$AUR_KNOWN_HOSTS" > ~/.ssh/aur_known_hosts
308+
cat >> ~/.ssh/config <<'SSHCONF'
309+
Host aur.archlinux.org
310+
HostName aur.archlinux.org
311+
User aur
312+
IdentityFile ~/.ssh/aur_key
313+
StrictHostKeyChecking yes
314+
UserKnownHostsFile ~/.ssh/aur_known_hosts
315+
SSHCONF
316+
147317
- name: Commit and push
148318
if: steps.aur_secret.outputs.configured == 'true'
149319
working-directory: aur-repo
150320
env:
151321
VERSION: ${{ steps.meta.outputs.version }}
322+
PACKAGE: ${{ vars.AUR_PACKAGE_NAME }}
152323
run: |
153324
set -euo pipefail
325+
git remote set-url --push origin "ssh://aur@aur.archlinux.org/${PACKAGE}.git"
154326
git config user.name "github-actions[bot]"
155327
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
156328
git add PKGBUILD .SRCINFO

0 commit comments

Comments
 (0)