--- name: Update RPM 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 the Release even with no new upstream version" required: false type: boolean default: false force_refresh: description: "Re-download the source and rebuild even when nothing changed" required: false type: boolean default: false no_git_push: description: "Dry run: build, but neither upload nor 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 # rpmbuild runs as the unprivileged `builder` user inside a Fedora image; # the spec is built with `dist` undefined, so the noarch package is the # same for every RPM distribution the registry serves. # # Prebuilt by build-image.yaml (see dockerfile) so the toolchain is not # reinstalled in every job every night. container: image: git.bjphoster.com/rpm/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, jq, curl and the packaging 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 # The same credential serves git (clone/push of the package repos) and # the package registry upload, which autoupdate.sh reads back through # `git credential fill`. GIT_TOKEN therefore needs write:repository AND # write:package. - name: Configure git env: GIT_NAME: ${{ vars.GIT_NAME }} GIT_EMAIL: ${{ vars.GIT_EMAIL }} GIT_HOST: ${{ vars.GIT_HOST || 'git.bjphoster.com' }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} run: | set -euo pipefail # Validate HERE rather than letting a missing secret surface as an # opaque 401 several steps later. for v in GIT_NAME GIT_EMAIL GIT_TOKEN; do if [ -z "${!v//[[:space:]]/}" ]; then echo "::error::${v} is empty or unset (see README: required secrets and variables)" >&2 exit 1 fi done 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 # 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 /root/.gitconfig /root/.git-credentials /home/builder/ chown -R builder:builder /home/builder - name: Update ${{ matrix.package }} env: GIT_HOST: ${{ vars.GIT_HOST || 'git.bjphoster.com' }} # 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 }}"