Skip to content

Refactor: backporting features, fixes from v2.1.0 #42

Refactor: backporting features, fixes from v2.1.0

Refactor: backporting features, fixes from v2.1.0 #42

Workflow file for this run

# Required repository secrets:
# DOCKERHUB_USERNAME - Docker Hub account username, used for `docker login` and pushes.
# DOCKERHUB_TOKEN - Docker Hub access token (NOT an account password; a 2FA-protected
# password cannot be used) with "Read, Write, Delete" scope. Pushing
# images only needs Read & Write, but peter-evans/dockerhub-description
# requires the Delete scope as well -- a Read & Write token returns
# 403 Forbidden when updating the description. Create one at
# https://hub.docker.com/settings/security.
name: build-test-push
on:
push:
branches:
- master
pull_request:
concurrency:
# Cancel superseded runs on a branch/PR when a new commit is pushed, but let master
# runs finish so an in-flight publish isn't interrupted (half-built manifest list or
# an unpublished directory).
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
env:
REPO_NAME: emmercm/libtorrent
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
directory:
- "0.16"
- "1.0"
- "1.1"
- "1.2-py2"
- "1.2-py3"
- "2.0"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Run hadolint
# Pinned to a full version: hadolint-action does not publish a moving major tag
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: ${{ matrix.directory }}/Dockerfile
# Decide which directories to build: those whose files changed, plus any whose
# image tag is not yet published (e.g. a version bump, which changes this file
# rather than the directory). Unchanged, already-published directories are skipped.
setup:
runs-on: ubuntu-latest
outputs:
# build job's matrix: cross product of selected directories and platforms
build_matrix: ${{ steps.select.outputs.build_matrix }}
# merge job's matrix: selected directories only (one manifest list per directory)
merge_matrix: ${{ steps.select.outputs.merge_matrix }}
# whether any directory was selected to build at all
has_builds: ${{ steps.select.outputs.has_builds }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Detect changed directories
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
id: filter
with:
filters: |
"0.16": "0.16/**"
"1.0": "1.0/**"
"1.1": "1.1/**"
"1.2-py2": "1.2-py2/**"
"1.2-py3": "1.2-py3/**"
"2.0": "2.0/**"
- name: Select directories to build
id: select
env:
CHANGED: ${{ steps.filter.outputs.changes }}
# Buildable directories with their libtorrent version and extra tags.
# The implicit ${VERSION} and ${VERSION}-alpine tags are always added too.
# https://github.com/arvidn/libtorrent/releases
CANDIDATES: |
[
{"directory": "0.16", "version": "0.16.17", "tags": "0.16 0.16-alpine 0 0-alpine"},
{"directory": "1.0", "version": "1.0.11", "tags": "1.0 1.0-alpine"},
{"directory": "1.1", "version": "1.1.14", "tags": "1.1 1.1-alpine"},
{"directory": "1.2-py2", "version": "1.2.11", "tags": ""},
{"directory": "1.2-py3", "version": "1.2.20", "tags": "1.2 1.2-alpine 1 1-alpine"},
{"directory": "2.0", "version": "2.0.13", "tags": "2.0 2.0-alpine 2 2-alpine latest"}
]
run: |
SELECTED='[]'
while read -r ENTRY; do
DIRECTORY=$(jq -r '.directory' <<< "${ENTRY}")
VERSION=$(jq -r '.version' <<< "${ENTRY}")
if jq -e --arg d "${DIRECTORY}" 'index($d) != null' <<< "${CHANGED}" > /dev/null; then
echo "${DIRECTORY}: files changed, building"
elif ! curl -fsS "https://hub.docker.com/v2/repositories/${REPO_NAME}/tags/${VERSION}" &> /dev/null; then
echo "${DIRECTORY}: ${REPO_NAME}:${VERSION} not yet published, building"
else
echo "${DIRECTORY}: unchanged and already published, skipping"
continue
fi
SELECTED=$(jq -c ". + [${ENTRY}]" <<< "${SELECTED}")
done < <(jq -c '.[]' <<< "${CANDIDATES}")
# Each platform builds on the closest runner architecture to minimize QEMU
# emulation: amd64 natively on amd64, and the arm variants on ARM hosts
# (arm64v8 natively; armv7/armv6 via aarch32 emulation).
PLATFORMS='[
{"name": "linux/amd64", "slug": "amd64", "runner": "ubuntu-latest"},
{"name": "linux/arm64/v8", "slug": "arm64v8", "runner": "ubuntu-24.04-arm"},
{"name": "linux/arm/v7", "slug": "armv7", "runner": "ubuntu-24.04-arm"},
{"name": "linux/arm/v6", "slug": "armv6", "runner": "ubuntu-24.04-arm"}
]'
echo "Selected directories: ${SELECTED}"
echo "merge_matrix={\"include\":${SELECTED}}" >> "${GITHUB_OUTPUT}"
echo "build_matrix={\"entry\":${SELECTED},\"platform\":$(jq -c . <<< "${PLATFORMS}")}" >> "${GITHUB_OUTPUT}"
if [[ "${SELECTED}" == "[]" ]]; then
echo "has_builds=false" >> "${GITHUB_OUTPUT}"
else
echo "has_builds=true" >> "${GITHUB_OUTPUT}"
fi
# One job per (directory, platform). Each builds and tests a single-platform image,
# then (on master) pushes it to the registry by digest only -- no tags. The tags are
# applied later by the merge job, so nothing usable is published until everything passes.
build:
needs:
- lint
- setup
if: needs.setup.outputs.has_builds == 'true'
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup.outputs.build_matrix) }}
env:
DIRECTORY: ${{ matrix.entry.directory }}
VERSION: ${{ matrix.entry.version }}
PLATFORM: ${{ matrix.platform.name }}
# Cache scope keeps each directory+platform's layers separate
CACHE_SCOPE: ${{ matrix.entry.directory }}-${{ matrix.platform.slug }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# Needed to run the armv7/armv6 (aarch32) images on the arm64 host during testing
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build image for testing
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: ${{ env.DIRECTORY }}
platforms: ${{ env.PLATFORM }}
build-args: |
VERSION=${{ env.VERSION }}
tags: ${{ env.REPO_NAME }}:${{ env.VERSION }}
load: true
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
- name: Test image
run: |
# container-structure-test runs on the host, so fetch the runner's architecture
case "${RUNNER_ARCH}" in
X64) CST_ARCH=amd64 ;;
ARM64) CST_ARCH=arm64 ;;
*) echo "Unsupported runner architecture: ${RUNNER_ARCH}" >&2; exit 1 ;;
esac
curl -LO "https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-${CST_ARCH}"
chmod +x "container-structure-test-linux-${CST_ARCH}"
sudo mv "container-structure-test-linux-${CST_ARCH}" /usr/local/bin/container-structure-test
container-structure-test test --config "${DIRECTORY}/container-structure-test.yml" --image "${REPO_NAME}:${VERSION}"
- name: Log in to Docker Hub
if: github.ref == 'refs/heads/master'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push image by digest
id: push
if: github.ref == 'refs/heads/master'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: ${{ env.DIRECTORY }}
platforms: ${{ env.PLATFORM }}
build-args: |
VERSION=${{ env.VERSION }}
outputs: type=image,name=${{ env.REPO_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
- name: Export digest
if: github.ref == 'refs/heads/master'
run: |
mkdir -p digests
echo "${{ steps.push.outputs.digest }}" > "digests/${{ matrix.platform.slug }}"
- name: Upload digest
if: github.ref == 'refs/heads/master'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: digests-${{ matrix.entry.directory }}-${{ matrix.platform.slug }}
path: digests/*
if-no-files-found: error
# One job per directory: combine its per-platform digests into a single multi-arch
# manifest list and apply all of the directory's tags. Runs only after every build
# job succeeded, so a failure anywhere prevents any tags from being published.
merge:
needs:
- setup
- build
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup.outputs.merge_matrix) }}
env:
VERSION: ${{ matrix.version }}
TAGS: ${{ matrix.tags }}
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: digests-${{ matrix.directory }}-*
path: digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifest list
run: |
TAG_ARGS=()
for TAG in "${VERSION}" "${VERSION}-alpine" ${TAGS}; do
TAG_ARGS+=(--tag "${REPO_NAME}:${TAG}")
done
SOURCES=()
for FILE in digests/*; do
SOURCES+=("${REPO_NAME}@$(cat "${FILE}")")
done
docker buildx imagetools create "${TAG_ARGS[@]}" "${SOURCES[@]}"
- name: Inspect manifest list
run: docker buildx imagetools inspect "${REPO_NAME}:${VERSION}"
# Independent of the image build: the Docker Hub description tracks README.md, so
# it is updated on every master push even when no directory needed rebuilding.
update-description:
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Read short description from README
id: readme
run: echo "short=$(grep '^\w' README.md | head -1)" >> "${GITHUB_OUTPUT}"
- name: Update Docker Hub description
# Can't use 2FA: https://github.com/docker/roadmap/issues/115
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.REPO_NAME }}
short-description: ${{ steps.readme.outputs.short }}
# !!! This check should be required by GitHub !!!
build-status-check:
if: always()
needs:
- lint
- setup
- build
runs-on: ubuntu-latest
steps:
- name: All checks passed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
with:
allowed-skips: build
jobs: ${{ toJSON(needs) }}