--- name: Update AUR Packages on: schedule: - cron: "0 0 * * *" workflow_dispatch: inputs: package: description: "Single package to update (blank = all)" required: false type: string force_rebuild: description: "Bump pkgrel even with no detected change" required: false type: boolean default: false force_refresh: description: "Re-download remote sources even without a version change" required: false type: boolean default: false no_git_push: description: "Dry run: do everything except push" required: false type: boolean default: false defaults: run: shell: bash jobs: # Resolves the package list for the update matrix below, so adding or # dropping a package is a variable edit on the org, not a commit here. # See README "Package list". plan: runs-on: ubuntu-latest outputs: packages: ${{ steps.resolve.outputs.packages }} steps: - name: Resolve the package list id: resolve # Both go through the environment, never pasted into the script: the # variable is free-form multi-line text. env: UPDATE_PACKAGES: ${{ vars.UPDATE_PACKAGES }} PACKAGE_INPUT: ${{ inputs.package }} run: | set -euo pipefail export LC_ALL=C : "${UPDATE_PACKAGES:=}" "${PACKAGE_INPUT:=}" re='^[A-Za-z0-9][A-Za-z0-9._+-]*$' pkgs=() declare -A seen=() # add NAME: validate, then append unless already listed. add() { if [[ ! $1 =~ $re ]]; then echo "::error::${from}: invalid package name '$1' (letters, digits and . _ + - only, starting with a letter or digit)" >&2 exit 1 fi if [ -z "${seen[$1]:-}" ]; then seen[$1]=1 pkgs+=("$1") fi } if [ -n "${PACKAGE_INPUT//[[:space:]]/}" ]; then # workflow_dispatch with a package name runs only that one, # whatever UPDATE_PACKAGES says. Surrounding whitespace is # trimmed; the rest must be a single valid name. from="workflow_dispatch input 'package'" want=${PACKAGE_INPUT#"${PACKAGE_INPUT%%[![:space:]]*}"} want=${want%"${want##*[![:space:]]}"} add "${want}" else # "- a\n- b", "a\nb", "- a - b" and "a b" are the same list: # comments go first, then every whitespace run (CR included, in # case the value was saved with CRLF line ends) becomes a line # break. Each line is then a name, a lone "-" list marker, or a # "-" glued to a name. from="UPDATE_PACKAGES" while IFS= read -r tok; do case ${tok} in "" | -) continue ;; -*) tok=${tok#-} ;; esac add "${tok}" done <<< "$(printf '%s\n' "${UPDATE_PACKAGES}" | sed -e 's/#.*$//' | tr -s '[:space:]' '\n')" fi # Fail rather than run nothing: an empty matrix would pass and look # exactly like a night with nothing to update. if [ "${#pkgs[@]}" -eq 0 ]; then echo '::error::UPDATE_PACKAGES is not set: add an org or repo variable with one package per line (leading "- " optional)' >&2 exit 1 fi # Names are validated above, so none needs JSON escaping. json= for p in "${pkgs[@]}"; do json+="${json:+,}\"${p}\"" done json="[${json}]" echo "${#pkgs[@]} package(s) from ${from}:" printf ' - %s\n' "${pkgs[@]}" echo "matrix: ${json}" echo "packages=${json}" >> "${GITHUB_OUTPUT}" update: needs: plan runs-on: ubuntu-latest # A real Arch makepkg is required. Ubuntu's build environment silently # emits an empty .SRCINFO, which the AUR then rejects -- that is what broke # reflex-appimage, nethlink-appimage and pman-helper. regen_srcinfo() in # common.sh fails loudly if this ever regresses. # # Prebuilt by build-image.yaml (see dockerfile) so the toolchain is not # reinstalled in all six jobs every night. container: image: git.bjphoster.com/aur/autoupdate:latest credentials: username: ${{ vars.REGISTRY_USER }} password: ${{ secrets.GIT_TOKEN }} strategy: # One broken package must not cancel the rest of the matrix. fail-fast: false matrix: # One entry per package, resolved by the plan job above from # vars.UPDATE_PACKAGES or the dispatch input. package: ${{ fromJSON(needs.plan.outputs.packages) }} steps: # No bootstrap step: node, git, openssh, jq, curl and the makepkg # toolchain all come from the prebuilt image above. node in particular # has to be present before this action runs, since actions/checkout is # JavaScript and the runner executes it with `node` from the image. - uses: actions/checkout@v4 - name: Configure git and SSH env: GIT_NAME: ${{ vars.GIT_NAME }} GIT_EMAIL: ${{ vars.GIT_EMAIL }} GIT_HOST: ${{ vars.GIT_HOST }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }} run: | set -euo pipefail git config --global user.name "${GIT_NAME}" git config --global user.email "${GIT_EMAIL}" git config --global credential.helper store echo "https://${GIT_EMAIL//@/%40}:${GIT_TOKEN}@${GIT_HOST}" > ~/.git-credentials chmod 600 ~/.git-credentials mkdir -p ~/.ssh && chmod 700 ~/.ssh # Validate the key HERE rather than letting it surface as an opaque # `Permission denied (publickey)` at push time, several steps later. if [ -z "${AUR_SSH_KEY//[[:space:]]/}" ]; then echo "::error::AUR_SSH_KEY secret is empty or unset" >&2 exit 1 fi printf '%s\n' "${AUR_SSH_KEY}" > ~/.ssh/aur chmod 600 ~/.ssh/aur if ! ssh-keygen -y -P "" -f ~/.ssh/aur >/dev/null 2>&1; then echo "::error::AUR_SSH_KEY is not a usable unencrypted private key." >&2 echo "Paste the whole file including the BEGIN/END lines; a passphrase-protected key cannot be used unattended." >&2 exit 1 fi echo "AUR key fingerprint: $(ssh-keygen -lf ~/.ssh/aur | awk '{print $2}')" ssh-keyscan -t rsa,ecdsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null cat >> ~/.ssh/config <<'EOF' Host aur.archlinux.org User aur IdentityFile ~/.ssh/aur IdentitiesOnly yes EOF chmod 600 ~/.ssh/config # The `builder` user is baked into the image; this only hands it the # workspace and the credentials written by the previous step. - name: Hand over to the build user run: | set -euo pipefail chown -R builder:builder "${GITHUB_WORKSPACE}" cp -r /root/.ssh /root/.gitconfig /root/.git-credentials /home/builder/ 2>/dev/null || true chown -R builder:builder /home/builder - name: Update ${{ matrix.package }} env: GIT_HOST: ${{ vars.GIT_HOST }} # Compared against both `true` and 'true' on purpose. A bare # `${{ inputs.x && '1' || '' }}` is wrong if the runner hands the # input over as the STRING "false", which is truthy -- that would # silently enable the flag on every run. FORCE_REBUILD: ${{ (inputs.force_rebuild == true || inputs.force_rebuild == 'true') && '1' || '' }} FORCE_REFRESH: ${{ (inputs.force_refresh == true || inputs.force_refresh == 'true') && '1' || '' }} NO_GIT_PUSH: ${{ (inputs.no_git_push == true || inputs.no_git_push == 'true') && '1' || '' }} run: | set -euo pipefail runuser -u builder -- env \ GIT_HOST="${GIT_HOST}" \ FORCE_REBUILD="${FORCE_REBUILD}" \ FORCE_REFRESH="${FORCE_REFRESH}" \ NO_GIT_PUSH="${NO_GIT_PUSH}" \ ./autoupdate.sh "${{ matrix.package }}"