|
13 | 13 | permissions: |
14 | 14 | contents: read |
15 | 15 |
|
| 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 | + |
16 | 23 | jobs: |
17 | 24 | publish: |
18 | 25 | runs-on: ubuntu-latest |
19 | 26 | if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.AUR_PACKAGE_NAME != '' |
20 | 27 | steps: |
21 | | - - name: Resolve tag and version |
| 28 | + - name: Resolve and validate tag |
22 | 29 | id: meta |
23 | 30 | env: |
24 | 31 | GH_EVENT_TAG: ${{ github.event.release.tag_name }} |
|
30 | 37 | echo "::error::No tag resolved from release event or workflow input" |
31 | 38 | exit 1 |
32 | 39 | 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 |
33 | 52 | VERSION="${TAG#v}" |
34 | 53 | echo "tag=$TAG" >> "$GITHUB_OUTPUT" |
35 | 54 | echo "version=$VERSION" >> "$GITHUB_OUTPUT" |
@@ -66,91 +85,244 @@ jobs: |
66 | 85 | echo "name=$PACMAN_NAME" >> "$GITHUB_OUTPUT" |
67 | 86 | echo "Found pacman asset: $PACMAN_NAME" |
68 | 87 |
|
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) |
70 | 94 | if: steps.aur_secret.outputs.configured == 'true' |
71 | | - id: sha |
72 | 95 | env: |
73 | | - REPO: ${{ github.repository }} |
74 | | - TAG: ${{ steps.meta.outputs.tag }} |
75 | | - ASSET: ${{ steps.asset.outputs.name }} |
| 96 | + PACKAGE: ${{ vars.AUR_PACKAGE_NAME }} |
76 | 97 | run: | |
77 | 98 | 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 |
82 | 100 |
|
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 |
84 | 106 | if: steps.aur_secret.outputs.configured == 'true' |
| 107 | + working-directory: aur-repo |
85 | 108 | 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 }} |
88 | 110 | run: | |
89 | 111 | 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" |
92 | 115 | exit 1 |
93 | 116 | 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 |
106 | 117 |
|
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 |
108 | 171 | if: steps.aur_secret.outputs.configured == 'true' |
| 172 | + working-directory: aur-repo |
109 | 173 | env: |
110 | | - PACKAGE: ${{ vars.AUR_PACKAGE_NAME }} |
| 174 | + VERSION: ${{ steps.meta.outputs.version }} |
| 175 | + ASSET: ${{ steps.asset.outputs.name }} |
111 | 176 | run: | |
112 | 177 | 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)." |
114 | 229 |
|
115 | 230 | - name: Install makepkg |
116 | 231 | if: steps.aur_secret.outputs.configured == 'true' |
117 | 232 | run: | |
118 | 233 | 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. |
119 | 244 | 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 |
125 | 246 | command -v makepkg >/dev/null || { |
126 | 247 | echo "::error::makepkg still missing after install." |
127 | 248 | exit 1 |
128 | 249 | } |
| 250 | + # Sans tube : `makepkg --version | head -1` prend un SIGPIPE que |
| 251 | + # `pipefail` remonte en échec d'étape. |
| 252 | + makepkg --version |
129 | 253 |
|
130 | | - - name: Update PKGBUILD and .SRCINFO |
| 254 | + - name: Regenerate .SRCINFO |
131 | 255 | if: steps.aur_secret.outputs.configured == 'true' |
132 | 256 | 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 }} |
139 | 257 | run: | |
140 | 258 | 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 |
144 | 259 | makepkg --printsrcinfo > .SRCINFO |
145 | 260 | echo "Updated .SRCINFO" |
146 | 261 |
|
| 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 | +
|
147 | 317 | - name: Commit and push |
148 | 318 | if: steps.aur_secret.outputs.configured == 'true' |
149 | 319 | working-directory: aur-repo |
150 | 320 | env: |
151 | 321 | VERSION: ${{ steps.meta.outputs.version }} |
| 322 | + PACKAGE: ${{ vars.AUR_PACKAGE_NAME }} |
152 | 323 | run: | |
153 | 324 | set -euo pipefail |
| 325 | + git remote set-url --push origin "ssh://aur@aur.archlinux.org/${PACKAGE}.git" |
154 | 326 | git config user.name "github-actions[bot]" |
155 | 327 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
156 | 328 | git add PKGBUILD .SRCINFO |
|
0 commit comments