Build container images for GHCR and Dockerhub #294
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build container images for GHCR and Dockerhub | |
on: | |
push: | |
branches: | |
- devel | |
- RELEASE_* | |
paths-ignore: | |
- 'extensions/**' | |
workflow_dispatch: | |
schedule: | |
- cron: '0 18 * * 5' | |
jobs: | |
build-amd64: | |
runs-on: ubuntu-latest | |
strategy: | |
fail-fast: false | |
matrix: | |
base: | |
- {image: 'rocker/r-ver', amdtag: '4.5.1', outname: 'r-ver'} | |
- {image: 'rocker/rstudio', amdtag: '4.5.1', outname: 'bioconductor_docker'} | |
- {image: 'rocker/tidyverse', amdtag: '4.5.1', outname: 'tidyverse'} | |
- {image: 'rocker/shiny', amdtag: '4.5.1', outname: 'shiny'} | |
# - {image: 'ghcr.io/bioconductor/rocker-cuda', amdtag: 'devel-amd64', outname: 'cuda'} | |
# - {image: 'ghcr.io/bioconductor/rocker-ml', amdtag: 'devel-amd64', outname: 'ml'} | |
# - {image: 'ghcr.io/bioconductor/rocker-ml-verse', amdtag: 'devel-amd64', outname: 'ml-verse'} | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Free root space | |
uses: almahmoud/free-root-space@main | |
with: | |
verbose: true | |
- name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Login to GHCR | |
uses: docker/login-action@v3 | |
with: | |
registry: ghcr.io | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: Login to Dockerhub | |
uses: docker/login-action@v3 | |
with: | |
username: ${{ secrets.DOCKER_USERNAME }} | |
password: ${{ secrets.DOCKER_PASSWORD }} | |
- name: Extract metadata for container image | |
id: meta | |
uses: docker/metadata-action@v5 | |
with: | |
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.base.outname }} | |
tags: | | |
type=raw,value={{branch}} | |
- name: Extract container name without tag | |
id: vars | |
run: | | |
echo container=$(echo '${{ steps.meta.outputs.tags }}' | awk -F':' '{print $1}') >> $GITHUB_OUTPUT | |
- name: Build and push AMD64 by digest | |
id: build | |
uses: docker/build-push-action@v6 | |
with: | |
build-args: | | |
BASE_IMAGE=${{ matrix.base.image }} | |
amd64_tag=${{ matrix.base.amdtag }} | |
file: Dockerfile | |
platforms: linux/amd64 | |
labels: ${{ steps.meta.outputs.labels }} | |
outputs: type=image,name=${{ steps.vars.outputs.container }},push-by-digest=true,name-canonical=true,push=true | |
- name: Export digest | |
run: | | |
mkdir -p /tmp/digests | |
digest="${{ steps.build.outputs.digest }}" | |
touch "/tmp/digests/${digest#sha256:}" | |
- name: Upload digest | |
uses: actions/upload-artifact@v4 | |
with: | |
name: digests-${{ matrix.base.outname }}-amd64 | |
path: /tmp/digests/* | |
if-no-files-found: error | |
retention-days: 1 | |
build-arm64: | |
runs-on: ubuntu-latest-arm64 | |
strategy: | |
fail-fast: false | |
matrix: | |
base: | |
- {image: 'rocker/r-ver', armtag: '4.5.1', outname: 'r-ver'} | |
- {image: 'rocker/rstudio', armtag: '4.5.1', outname: 'bioconductor_docker'} | |
steps: | |
- uses: actions/checkout@v3 | |
- name: Free root space | |
uses: almahmoud/free-root-space@main | |
with: | |
verbose: true | |
- name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Login to GHCR | |
uses: docker/login-action@v3 | |
with: | |
registry: ghcr.io | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: Login to Dockerhub | |
uses: docker/login-action@v3 | |
with: | |
username: ${{ secrets.DOCKER_USERNAME }} | |
password: ${{ secrets.DOCKER_PASSWORD }} | |
- name: Extract metadata | |
id: meta | |
uses: docker/metadata-action@v5 | |
with: | |
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.base.outname }} | |
tags: | | |
type=raw,value={{branch}} | |
- name: Extract container name without tag | |
id: vars | |
run: | | |
echo container=$(echo '${{ steps.meta.outputs.tags }}' | awk -F':' '{print $1}') >> $GITHUB_OUTPUT | |
- name: Build and push ARM64 by digest | |
id: build | |
uses: docker/build-push-action@v6 | |
with: | |
build-args: | | |
BASE_IMAGE=${{ matrix.base.image }} | |
arm64_tag=${{ matrix.base.armtag }} | |
file: Dockerfile | |
platforms: linux/arm64 | |
labels: ${{ steps.meta.outputs.labels }} | |
outputs: type=image,name=${{ steps.vars.outputs.container }},push-by-digest=true,name-canonical=true,push=true | |
- name: Export digest | |
run: | | |
mkdir -p /tmp/digests | |
digest="${{ steps.build.outputs.digest }}" | |
touch "/tmp/digests/${digest#sha256:}" | |
- name: Upload digest | |
uses: actions/upload-artifact@v4 | |
with: | |
name: digests-${{ matrix.base.outname }}-arm64 | |
path: /tmp/digests/* | |
if-no-files-found: error | |
retention-days: 1 | |
merge: | |
needs: [build-amd64, build-arm64] | |
runs-on: ubuntu-latest | |
if: always() | |
strategy: | |
fail-fast: false | |
matrix: | |
base: | |
- {image: 'rocker/r-ver', amdtag: '4.5.1', outname: 'r-ver', platforms: 'amd64,arm64'} | |
- {image: 'rocker/rstudio', amdtag: '4.5.1', outname: 'bioconductor_docker', platforms: 'amd64,arm64'} | |
- {image: 'rocker/tidyverse', amdtag: '4.5.1', outname: 'tidyverse', platforms: 'amd64'} | |
- {image: 'rocker/shiny', amdtag: '4.5.1', outname: 'shiny', platforms: 'amd64'} | |
#- {image: 'ghcr.io/bioconductor/rocker-cuda', amdtag: 'devel-amd64', outname: 'cuda', platforms: 'amd64'} | |
#- {image: 'ghcr.io/bioconductor/rocker-ml', amdtag: 'devel-amd64', outname: 'ml', platforms: 'amd64'} | |
#- {image: 'ghcr.io/bioconductor/rocker-ml-verse', amdtag: 'devel-amd64', outname: 'ml-verse', platforms: 'amd64'} | |
steps: | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Login to GHCR | |
uses: docker/login-action@v3 | |
with: | |
registry: ghcr.io | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: Login to Dockerhub | |
uses: docker/login-action@v3 | |
with: | |
username: ${{ secrets.DOCKER_USERNAME }} | |
password: ${{ secrets.DOCKER_PASSWORD }} | |
- name: Echo platforms to build | |
id: buildlist | |
shell: bash | |
run: | | |
PLATFORMLIST="${{matrix.base.platforms}}" | |
IFS=','; for item in $PLATFORMLIST; do echo "$item=build" >> $GITHUB_OUTPUT; done | |
- name: Download AMD64 digests | |
if: steps.buildlist.outputs.amd64 == 'build' | |
uses: actions/download-artifact@v4 | |
with: | |
name: digests-${{ matrix.base.outname }}-amd64 | |
path: /tmp/digests/amd64 | |
- name: Download ARM64 digests | |
if: steps.buildlist.outputs.arm64 == 'build' | |
uses: actions/download-artifact@v4 | |
with: | |
name: digests-${{ matrix.base.outname }}-arm64 | |
path: /tmp/digests/arm64 | |
- name: Set image tags | |
id: meta1 | |
uses: docker/metadata-action@v5 | |
with: | |
images: | | |
ghcr.io/${{ github.repository_owner }}/${{ matrix.base.outname }} | |
tags: | | |
type=raw,value={{branch}} | |
- name: Set image tags | |
id: meta2 | |
uses: docker/metadata-action@v5 | |
with: | |
images: | | |
docker.io/${{ github.repository_owner }}/${{ matrix.base.outname }} | |
ghcr.io/${{ github.repository_owner }}/${{ matrix.base.outname }} | |
tags: | | |
type=raw,value={{branch}} | |
- name: Create manifest list and push with retries | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 30 | |
max_attempts: 10 | |
shell: bash | |
command: | | |
set -x | |
# Prepare tags | |
echo '${{ steps.meta2.outputs.tags }}' > /tmp/tags | |
rm /tmp/tagargs || true | |
cat /tmp/tags | xargs -i bash -c 'printf "%s" "-t {} " >> /tmp/tagargs' | |
TAG_ARGS="$(cat /tmp/tagargs)" | |
TAGS="$(cat /tmp/tags)" | |
# Add version tags for RELEASE branches (e.g., RELEASE_3_21 -> 3.21) | |
for tag in $TAGS; do | |
if [[ "$tag" == *"RELEASE_"* ]]; then | |
# Extract the version number (e.g., RELEASE_3_21 -> 3.21) | |
VERSION=$(echo "$tag" | sed -E 's/.*RELEASE_([0-9]+)_([0-9]+).*/\1.\2/') | |
if [ ! -z "$VERSION" ]; then | |
# Add the numeric version tag (e.g., 3.21) | |
REGISTRY=$(echo "$tag" | cut -d':' -f1) | |
TAG_ARGS="$TAG_ARGS -t ${REGISTRY}:${VERSION}" | |
fi | |
fi | |
done | |
R_VER=$(docker pull ${{ matrix.base.image }}:${{ matrix.base.amdtag }} 2>&1 > /dev/null && \ | |
docker inspect ${{ matrix.base.image }}:${{ matrix.base.amdtag }} | \ | |
jq -r '.[].Config.Env[]|select(match("^R_VERSION"))|.[index("=")+1:]') | |
if [ ! -z "$R_VER" ]; then | |
for tag in $TAGS; do | |
TAG_ARGS="$TAG_ARGS -t ${tag}-R-${R_VER} -t ${tag}-r-${R_VER}" | |
# For RELEASE branches, also add version-R tags | |
if [[ "$tag" == *"RELEASE_"* ]]; then | |
VERSION=$(echo "$tag" | sed -E 's/.*RELEASE_([0-9]+)_([0-9]+).*/\1.\2/') | |
if [ ! -z "$VERSION" ]; then | |
REGISTRY=$(echo "$tag" | cut -d':' -f1) | |
TAG_ARGS="$TAG_ARGS -t ${REGISTRY}:${VERSION}-R-${R_VER} -t ${REGISTRY}:${VERSION}-r-${R_VER}" | |
fi | |
fi | |
done | |
fi | |
# Add alternative tags without _docker in name | |
if [[ "${{ matrix.base.outname }}" == *"_docker"* ]]; then | |
for tag in $TAGS; do | |
# Extract registry and branch/version from tag | |
REGISTRY=$(echo "$tag" | cut -d':' -f1) | |
VERSION=$(echo "$tag" | cut -d':' -f2) | |
# Create new registry without _docker suffix | |
NEW_REGISTRY=$(echo "$REGISTRY" | sed 's/_docker//g') | |
# Add tag without _docker in name | |
TAG_ARGS="$TAG_ARGS -t ${NEW_REGISTRY}:${VERSION}" | |
# If it's a release branch, also add the numeric version tag | |
if [[ "$VERSION" == RELEASE_* ]]; then | |
NUM_VERSION=$(echo "$VERSION" | sed -E 's/RELEASE_([0-9]+)_([0-9]+).*/\1.\2/') | |
if [ ! -z "$NUM_VERSION" ]; then | |
TAG_ARGS="$TAG_ARGS -t ${NEW_REGISTRY}:${NUM_VERSION}" | |
fi | |
fi | |
# If R_VER exists, add R version tags for the non-_docker version | |
if [ ! -z "$R_VER" ]; then | |
TAG_ARGS="$TAG_ARGS -t ${NEW_REGISTRY}:${VERSION}-R-${R_VER} -t ${NEW_REGISTRY}:${VERSION}-r-${R_VER}" | |
# If it's a release branch, also add R version tags for numeric version | |
if [[ "$VERSION" == RELEASE_* ]]; then | |
NUM_VERSION=$(echo "$VERSION" | sed -E 's/RELEASE_([0-9]+)_([0-9]+).*/\1.\2/') | |
if [ ! -z "$NUM_VERSION" ]; then | |
TAG_ARGS="$TAG_ARGS -t ${NEW_REGISTRY}:${NUM_VERSION}-R-${R_VER} -t ${NEW_REGISTRY}:${NUM_VERSION}-r-${R_VER}" | |
fi | |
fi | |
fi | |
done | |
fi | |
# Create manifest list | |
DIGESTS="" | |
BASE_REGISTRY=$(echo '${{ steps.meta1.outputs.tags }}' | awk -F':' '{print $1}') | |
for eachdir in $(ls /tmp/digests); do | |
DIGESTS_ARCH=$(cd /tmp/digests/$eachdir && find . -type f -exec echo "$BASE_REGISTRY@sha256{}" \; | sed 's/\.\//:/') | |
DIGESTS="$DIGESTS $DIGESTS_ARCH" | |
done | |
echo "$TAG_ARGS" | |
echo "$DIGESTS" | |
docker buildx imagetools create $TAG_ARGS $DIGESTS | |
- name: Inspect images | |
run: | | |
# Get registry prefixes | |
REGISTRY_PREFIXES=$(cat /tmp/tags | awk -F':' '{print $1}' | sort -u) | |
BRANCH=$(cat /tmp/tags | head -1 | awk -F':' '{print $2}') | |
# Inspect all created tags | |
for prefix in $REGISTRY_PREFIXES; do | |
# Base tag | |
echo "Inspecting: $prefix:$BRANCH" | |
docker buildx imagetools inspect "$prefix:$BRANCH" | |
# Without _docker suffix if applicable | |
if [[ "${{ matrix.base.outname }}" == *"_docker"* ]]; then | |
NO_DOCKER_PREFIX=$(echo "$prefix" | sed 's/_docker//g') | |
echo "Inspecting: $NO_DOCKER_PREFIX:$BRANCH" | |
docker buildx imagetools inspect "$NO_DOCKER_PREFIX:$BRANCH" | |
fi | |
# Version tag for release branches | |
if [[ "$BRANCH" == RELEASE_* ]]; then | |
VERSION=$(echo "$BRANCH" | sed -E 's/RELEASE_([0-9]+)_([0-9]+).*/\1.\2/') | |
if [ ! -z "$VERSION" ]; then | |
echo "Inspecting: $prefix:$VERSION" | |
docker buildx imagetools inspect "$prefix:$VERSION" | |
# Without _docker suffix if applicable | |
if [[ "${{ matrix.base.outname }}" == *"_docker"* ]]; then | |
echo "Inspecting: $NO_DOCKER_PREFIX:$VERSION" | |
docker buildx imagetools inspect "$NO_DOCKER_PREFIX:$VERSION" | |
fi | |
fi | |
fi | |
done | |
trigger-extensions: | |
needs: [merge] | |
runs-on: ubuntu-latest | |
if: success() | |
steps: | |
- name: Trigger extensions workflow | |
uses: actions/github-script@v7 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const branch = context.ref.replace('refs/heads/', ''); | |
await github.rest.actions.createWorkflowDispatch({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
workflow_id: 'build_extensions.yaml', | |
ref: branch | |
}); | |
console.log(`Triggered build_extensions.yaml workflow on branch ${branch}`) |