diff --git a/.github/workflows/build_container.yml b/.github/workflows/build_container.yml index 1b97a6a..7c81d4a 100644 --- a/.github/workflows/build_container.yml +++ b/.github/workflows/build_container.yml @@ -1,13 +1,16 @@ -name: Build R container +name: Build R containers -# The image tag is read from conf/containers.config, so that single line is both what the -# pipeline pulls and what this workflow publishes -- the two cannot drift. -# Runs on pushes to a feature branch, whenever the container changes. +# The image tag of the container is read from conf/containers.config, so that the tag is +# what the pipeline pulls and what this workflow publishes. Each container is built independently, +# so updating one tag in conf/containers.config rebuilds only that image: +# feature branch - rebuilds its tag on every push, unless main or devel is pinned to that tag +# main or devel - builds only a tag that has never been published, so most merges are a no-op +# manual run - 'force' rebuilds and overwrites the tag on any branch on: push: paths: - - 'containers/r/**' + - 'containers/**' - 'conf/containers.config' - '.github/workflows/build_container.yml' workflow_dispatch: @@ -22,81 +25,130 @@ concurrency: cancel-in-progress: true jobs: - build: - name: Build and publish + discover: + name: Select containers to build runs-on: ubuntu-latest permissions: contents: read - packages: write + packages: read + outputs: + containers: ${{ steps.select.outputs.containers }} steps: - uses: actions/checkout@v4 - - name: Resolve image from conf/containers.config - id: image + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Select containers to build + id: select run: | - IMAGE=$(sed -n "s|.*withLabel: *'r'.*container *= *\"\([^\"]*\)\".*|\1|p" conf/containers.config) - if [ -z "$IMAGE" ]; then - echo "::error::could not parse the 'r' container from conf/containers.config" - exit 1 + RELEASED=false + case "${{ github.ref_name }}" in main|devel) RELEASED=true ;; esac + + # Each base branch is fetched into its own ref so both stay readable in the loop + BASES="" + for BASE in devel main; do + if git fetch --no-tags --depth=1 origin "$BASE:refs/base/$BASE" 2>/dev/null; then + BASES="$BASES refs/base/$BASE" + fi + done + + # devel is at or ahead of main, so it is the baseline for "did this branch edit a container" + DEVEL=refs/base/devel + + # a container is only rebuilt by a push that touches it, so the previous push is the + # baseline for that. devel is the fallback when that commit cannot be fetched, which is + # the case on the first push of a branch, on a force push and on a manual run + PUSH_BASE=$DEVEL + BEFORE="${{ github.event.before }}" + if [ -n "$BEFORE" ] && git fetch --no-tags --depth=1 origin "$BEFORE" 2>/dev/null; then + PUSH_BASE=$BEFORE fi - case "$IMAGE" in - ghcr.io/goekelab/*) ;; - *) echo "::error::refusing to push outside ghcr.io/goekelab: $IMAGE"; exit 1 ;; - esac - echo "image=$IMAGE" >> "$GITHUB_OUTPUT" - echo "Resolved container: $IMAGE" - - - name: Refuse to overwrite a tag main or devel is pinned to - if: github.ref_name != 'main' && github.ref_name != 'devel' - run: | - IMAGE="${{ steps.image.outputs.image }}" - for BASE in main devel; do - git fetch --no-tags --depth=1 origin "$BASE" 2>/dev/null || continue - PINNED=$(git show "FETCH_HEAD:conf/containers.config" 2>/dev/null \ - | sed -n "s|.*withLabel: *'r'.*container *= *\"\([^\"]*\)\".*|\1|p") - if [ "$IMAGE" = "$PINNED" ]; then - echo "::error::$BASE is pinned to $IMAGE -- bump the tag in conf/containers.config before rebuilding it" + + # Every containers/