dind passthrough: warn when DIND_HOST_PASSTHROUGH_IMAGES is set but no host socket is mounted (issue #102) #272
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 and Release Docker Image | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'Dockerfile' | |
| - 'scripts/**' | |
| - 'ubuntu/**' | |
| - '.github/workflows/release.yml' | |
| - '.changeset/**' | |
| - 'VERSION' | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # Manual release support - allows instant version bump and release, or building current version | |
| workflow_dispatch: | |
| inputs: | |
| release_mode: | |
| description: 'Release mode: build-only (no push/release), bump-and-release (bump version then build+release), release-only (build+release current VERSION without bumping)' | |
| required: true | |
| type: choice | |
| default: 'build-only' | |
| options: | |
| - build-only | |
| - bump-and-release | |
| - release-only | |
| bump_type: | |
| description: 'Version bump type (only for bump-and-release mode)' | |
| required: false | |
| type: choice | |
| default: 'patch' | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| description: | |
| description: 'Release description (optional, for bump-and-release mode)' | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # GitHub Container Registry | |
| GHCR_REGISTRY: ghcr.io | |
| GHCR_IMAGE_NAME: ${{ github.repository }} | |
| # Docker Hub | |
| DOCKERHUB_REGISTRY: docker.io | |
| DOCKERHUB_IMAGE_NAME: konard/box | |
| jobs: | |
| # === VERSION CHECK (PRs only) === | |
| # Prohibit manual version changes in VERSION file - versions should only be changed by CI/CD | |
| version-check: | |
| name: Check for Manual Version Changes | |
| runs-on: ubuntu-24.04 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for version changes in VERSION file | |
| env: | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| chmod +x scripts/release/check-version.sh | |
| ./scripts/release/check-version.sh | |
| # === CHANGESET CHECK (PRs only) === | |
| # Ensure PRs with code changes include a changeset | |
| changeset-check: | |
| name: Check for Changesets | |
| runs-on: ubuntu-24.04 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for changesets | |
| env: | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| # Skip for branches that don't need changesets | |
| if [[ "$GITHUB_HEAD_REF" == changeset-release/* ]] || [[ "$GITHUB_HEAD_REF" == changeset-manual-release-* ]]; then | |
| echo "Skipping changeset check for release PR" | |
| exit 0 | |
| fi | |
| # Check if there are code changes (Dockerfile, scripts, etc.) | |
| git fetch origin "$GITHUB_BASE_REF" 2>/dev/null || true | |
| CODE_CHANGES=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" | grep -E '^(Dockerfile|scripts/|ubuntu/|\.github/workflows/)' || true) | |
| if [ -z "$CODE_CHANGES" ]; then | |
| echo "No code changes detected, changeset not required" | |
| exit 0 | |
| fi | |
| echo "Code changes detected:" | |
| echo "$CODE_CHANGES" | |
| echo "" | |
| chmod +x scripts/release/validate-changeset.sh | |
| ./scripts/release/validate-changeset.sh | |
| # === AUTOMATIC VERSION BUMP (push to main with changesets) === | |
| apply-changesets: | |
| name: Apply Changesets | |
| runs-on: ubuntu-24.04 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| outputs: | |
| version_bumped: ${{ steps.apply.outputs.version_bumped }} | |
| new_version: ${{ steps.apply.outputs.new_version }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for changesets | |
| id: check | |
| run: | | |
| chmod +x scripts/release/check-changesets.sh | |
| ./scripts/release/check-changesets.sh | |
| - name: Apply changesets | |
| id: apply | |
| if: steps.check.outputs.has_changesets == 'true' | |
| run: | | |
| chmod +x scripts/release/apply-changesets.sh | |
| ./scripts/release/apply-changesets.sh | |
| # === MANUAL VERSION BUMP (workflow_dispatch with bump-and-release) === | |
| version-bump: | |
| runs-on: ubuntu-24.04 | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'bump-and-release' | |
| permissions: | |
| contents: write | |
| outputs: | |
| version: ${{ steps.bump.outputs.new_version }} | |
| version_bumped: ${{ steps.bump.outputs.bumped }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Bump version | |
| id: bump | |
| run: | | |
| # Read current version | |
| CURRENT_VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "Current version: $CURRENT_VERSION" | |
| # Parse version components | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| # Bump based on type | |
| case "${{ github.event.inputs.bump_type }}" in | |
| major) | |
| MAJOR=$((MAJOR + 1)) | |
| MINOR=0 | |
| PATCH=0 | |
| ;; | |
| minor) | |
| MINOR=$((MINOR + 1)) | |
| PATCH=0 | |
| ;; | |
| patch) | |
| PATCH=$((PATCH + 1)) | |
| ;; | |
| esac | |
| NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" | |
| echo "New version: $NEW_VERSION" | |
| # Update VERSION file | |
| echo "$NEW_VERSION" > VERSION | |
| # Commit and push | |
| git add VERSION | |
| DESCRIPTION="${{ github.event.inputs.description }}" | |
| if [ -n "$DESCRIPTION" ]; then | |
| git commit -m "$NEW_VERSION: $DESCRIPTION" | |
| else | |
| git commit -m "$NEW_VERSION" | |
| fi | |
| git push origin main | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "bumped=true" >> $GITHUB_OUTPUT | |
| # === DETECT CHANGES (per-image granularity) === | |
| detect-changes: | |
| runs-on: ubuntu-24.04 | |
| needs: [apply-changesets, version-bump] | |
| # Always run, but wait for version jobs if they're running | |
| if: always() && (needs.apply-changesets.result == 'success' || needs.apply-changesets.result == 'skipped') && (needs.version-bump.result == 'success' || needs.version-bump.result == 'skipped') | |
| outputs: | |
| # Legacy change detection | |
| docker-changed: ${{ steps.changes.outputs.docker }} | |
| scripts-changed: ${{ steps.changes.outputs.scripts }} | |
| ubuntu-changed: ${{ steps.changes.outputs.ubuntu }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow }} | |
| version-changed: ${{ steps.changes.outputs.version }} | |
| should-build: ${{ steps.should-build.outputs.result }} | |
| version: ${{ steps.version.outputs.version }} | |
| # Per-image change detection | |
| js-changed: ${{ steps.image-changes.outputs.js }} | |
| essentials-changed: ${{ steps.image-changes.outputs.essentials }} | |
| full-changed: ${{ steps.image-changes.outputs.full }} | |
| common-changed: ${{ steps.image-changes.outputs.common }} | |
| # Per-language change detection | |
| python-changed: ${{ steps.language-changes.outputs.python }} | |
| go-changed: ${{ steps.language-changes.outputs.go }} | |
| rust-changed: ${{ steps.language-changes.outputs.rust }} | |
| java-changed: ${{ steps.language-changes.outputs.java }} | |
| kotlin-changed: ${{ steps.language-changes.outputs.kotlin }} | |
| ruby-changed: ${{ steps.language-changes.outputs.ruby }} | |
| php-changed: ${{ steps.language-changes.outputs.php }} | |
| perl-changed: ${{ steps.language-changes.outputs.perl }} | |
| swift-changed: ${{ steps.language-changes.outputs.swift }} | |
| lean-changed: ${{ steps.language-changes.outputs.lean }} | |
| rocq-changed: ${{ steps.language-changes.outputs.rocq }} | |
| cpp-changed: ${{ steps.language-changes.outputs.cpp }} | |
| assembly-changed: ${{ steps.language-changes.outputs.assembly }} | |
| # dind-box change detection (issue #80) | |
| dind-changed: ${{ steps.image-changes.outputs.dind }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 2 | |
| ref: ${{ github.ref }} | |
| # Re-fetch if version was bumped to get latest commit | |
| - name: Fetch latest changes | |
| if: needs.version-bump.outputs.version_bumped == 'true' || needs.apply-changesets.outputs.version_bumped == 'true' | |
| run: | | |
| git fetch origin main | |
| git checkout main | |
| git pull origin main | |
| - name: Get version from VERSION file | |
| id: version | |
| run: | | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Detected version: $VERSION" | |
| - name: Detect changes | |
| id: changes | |
| run: | | |
| # For push events, compare with previous commit | |
| # For PR events, compare with base branch | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| else | |
| BASE_SHA="${{ github.event.before }}" | |
| fi | |
| # Get changed files | |
| CHANGED_FILES=$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD) | |
| echo "Changed files:" | |
| echo "$CHANGED_FILES" | |
| # Check for Docker-related changes | |
| if echo "$CHANGED_FILES" | grep -qE '^Dockerfile$'; then | |
| echo "docker=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "docker=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Check for scripts changes | |
| if echo "$CHANGED_FILES" | grep -qE '^scripts/'; then | |
| echo "scripts=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "scripts=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Check for ubuntu/ modular scripts changes | |
| if echo "$CHANGED_FILES" | grep -qE '^ubuntu/'; then | |
| echo "ubuntu=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "ubuntu=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Check for workflow changes | |
| if echo "$CHANGED_FILES" | grep -qE '^\.github/workflows/'; then | |
| echo "workflow=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "workflow=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Check for VERSION file changes | |
| if echo "$CHANGED_FILES" | grep -qE '^VERSION$'; then | |
| echo "version=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Save changed files for per-image detection | |
| echo "$CHANGED_FILES" > /tmp/changed-files.txt | |
| - name: Detect per-image changes | |
| id: image-changes | |
| run: | | |
| CHANGED_FILES=$(cat /tmp/changed-files.txt) | |
| # JS box changes | |
| if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/js/'; then | |
| echo "js=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "js=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Essentials box changes | |
| if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/essentials-box/'; then | |
| echo "essentials=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "essentials=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Full box changes (full-box dir, root Dockerfile, or scripts) | |
| if echo "$CHANGED_FILES" | grep -qE '^(ubuntu/24\.04/full-box/|Dockerfile$|scripts/)'; then | |
| echo "full=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "full=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Common.sh changes (affects all images) | |
| if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/common\.sh$'; then | |
| echo "common=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "common=false" >> $GITHUB_OUTPUT | |
| fi | |
| # dind-box changes (issue #80) | |
| if echo "$CHANGED_FILES" | grep -qE '^(ubuntu/24\.04/dind/|docs/dind/|tests/dind/)'; then | |
| echo "dind=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "dind=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Detect per-language changes | |
| id: language-changes | |
| run: | | |
| CHANGED_FILES=$(cat /tmp/changed-files.txt) | |
| for lang in python go rust java kotlin ruby php perl swift lean rocq cpp assembly; do | |
| if echo "$CHANGED_FILES" | grep -qE "^ubuntu/24\.04/${lang}/"; then | |
| echo "${lang}=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "${lang}=false" >> $GITHUB_OUTPUT | |
| fi | |
| done | |
| - name: Determine if build is needed | |
| id: should-build | |
| run: | | |
| # For workflow_dispatch, always build (regardless of mode) | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: manual dispatch (mode: ${{ github.event.inputs.release_mode }})" | |
| exit 0 | |
| fi | |
| # Trigger build on any relevant changes | |
| if [ "${{ steps.changes.outputs.docker }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: Dockerfile changes" | |
| elif [ "${{ steps.changes.outputs.scripts }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: scripts changes" | |
| elif [ "${{ steps.changes.outputs.ubuntu }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: ubuntu modular scripts changes" | |
| elif [ "${{ steps.changes.outputs.workflow }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: workflow changes" | |
| elif [ "${{ steps.changes.outputs.version }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: VERSION file changes" | |
| elif [ "${{ steps.image-changes.outputs.dind }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: dind docs/tests changes" | |
| else | |
| echo "result=false" >> $GITHUB_OUTPUT | |
| echo "No build needed: no relevant changes detected" | |
| fi | |
| # === BUILD AND TEST DOCKER IMAGES (PR) — issue #82 === | |
| # | |
| # On every pull request we exercise *all* image configurations in parallel, | |
| # each on its own clean GitHub-hosted VM with maximum free disk space. The | |
| # parallel layout mirrors the production release matrix: | |
| # | |
| # pr-test-js (1 job) - JS base box | |
| # pr-test-essentials (1 job) - essentials box on JS | |
| # pr-test-language (matrix: 11) - one job per language box | |
| # pr-test-full (1 job) - full-box (all languages) | |
| # pr-test-dind (matrix: 14) - one job per dind variant | |
| # (js, essentials, 11 languages, full) | |
| # docker-build-test (1 job) - aggregator for branch protection | |
| # | |
| # Each job rebuilds its required base chain locally with plain `docker build` | |
| # against the host Docker daemon. We deliberately do NOT use buildx with the | |
| # docker-container driver here because subsequent `FROM box-js` / | |
| # `FROM box-essentials` resolution in that driver would attempt a registry | |
| # pull (which fails on PR forks and on Docker Hub outages). The trade-off is | |
| # that each VM rebuilds JS and essentials from source — fine because every VM | |
| # has 30 GB freed up front and the parallel matrix means wall-clock is bound | |
| # by the slowest single image, not by the depth of the chain. | |
| # | |
| # Every build job runs `jlumbroso/free-disk-space@main` first to reclaim | |
| # ~30 GB before building, fixing the "no space left on device" regression | |
| # captured in run 25075335426 (see docs/case-studies/issue-82/). | |
| # | |
| # See: docs/case-studies/issue-82/CASE-STUDY.md | |
| # --- Tier 1: JS box --- | |
| pr-test-js: | |
| name: pr-test / js | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| needs: [detect-changes, version-check, changeset-check] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.version-check.result == 'success' || needs.version-check.result == 'skipped') && | |
| (needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Build JS box (amd64) | |
| run: | | |
| set -e | |
| echo "=== Building JS box ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t box-js . | |
| - name: Test JS box | |
| run: | | |
| set -e | |
| echo "=== Testing JS box ===" | |
| docker run --rm box-js bash -c '. $HOME/.nvm/nvm.sh && node --version' | |
| docker run --rm box-js bash -c 'export PATH=$HOME/.bun/bin:$PATH && bun --version' | |
| docker run --rm box-js bash -c 'export PATH=$HOME/.deno/bin:$PATH && deno --version' | |
| echo "=== JS box tests passed ===" | |
| # --- Tier 2: essentials box --- | |
| pr-test-essentials: | |
| name: pr-test / essentials | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| needs: [detect-changes, version-check, changeset-check, pr-test-js] | |
| if: | | |
| always() && | |
| needs.pr-test-js.result == 'success' && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Build JS + essentials chain | |
| run: | | |
| set -e | |
| echo "=== Building JS box ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t box-js . | |
| echo "" | |
| echo "=== Building essentials box (on JS) ===" | |
| docker build -f ubuntu/24.04/essentials-box/Dockerfile \ | |
| --build-arg JS_IMAGE=box-js -t box-essentials . | |
| - name: Test essentials box | |
| run: | | |
| set -e | |
| echo "=== Testing essentials box ===" | |
| docker run --rm box-essentials gh --version | |
| docker run --rm box-essentials glab --version | |
| docker run --rm box-essentials gh-setup-git-identity --version | |
| docker run --rm box-essentials glab-setup-git-identity --version | |
| echo "=== Essentials box tests passed ===" | |
| # --- Tier 3: per-language boxes (parallel matrix, one VM each) --- | |
| pr-test-language: | |
| name: pr-test / ${{ matrix.language }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 | |
| needs: [detect-changes, version-check, changeset-check, pr-test-essentials] | |
| if: | | |
| always() && | |
| needs.pr-test-essentials.result == 'success' && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Build JS + essentials + ${{ matrix.language }} chain | |
| run: | | |
| set -e | |
| echo "=== Building JS box ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t box-js . | |
| echo "" | |
| echo "=== Building essentials box (on JS) ===" | |
| docker build -f ubuntu/24.04/essentials-box/Dockerfile \ | |
| --build-arg JS_IMAGE=box-js -t box-essentials . | |
| echo "" | |
| echo "=== Building ${{ matrix.language }} box (on essentials) ===" | |
| docker build -f "ubuntu/24.04/${{ matrix.language }}/Dockerfile" \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| -t "box-${{ matrix.language }}" . | |
| - name: Test ${{ matrix.language }} box | |
| run: | | |
| set -e | |
| LANG="${{ matrix.language }}" | |
| IMG="box-${LANG}" | |
| echo "=== Testing ${LANG} box (${IMG}) ===" | |
| case "$LANG" in | |
| python) | |
| docker run --rm "$IMG" python3 --version | |
| docker run --rm "$IMG" pip3 --version | |
| ;; | |
| go) | |
| docker run --rm "$IMG" go version | |
| ;; | |
| rust) | |
| docker run --rm "$IMG" rustc --version | |
| docker run --rm "$IMG" cargo --version | |
| docker run --rm "$IMG" rustup --version | |
| ;; | |
| java) | |
| docker run --rm "$IMG" java -version | |
| ;; | |
| kotlin) | |
| docker run --rm "$IMG" kotlin -version | |
| ;; | |
| ruby) | |
| docker run --rm "$IMG" ruby --version | |
| docker run --rm "$IMG" gem --version | |
| ;; | |
| php) | |
| docker run --rm "$IMG" php --version | |
| echo "=== PHP install method ===" | |
| docker run --rm "$IMG" cat /home/box/.php-install-method | |
| ;; | |
| perl) | |
| docker run --rm "$IMG" perl --version | |
| ;; | |
| swift) | |
| docker run --rm "$IMG" swift --version | |
| ;; | |
| lean) | |
| docker run --rm "$IMG" lean --version | |
| ;; | |
| rocq) | |
| # rocq is invoked via "rocq --version" but historically coqc; check both | |
| docker run --rm "$IMG" bash -c 'rocq --version 2>/dev/null || coqc --version' | |
| ;; | |
| esac | |
| echo "=== ${LANG} box tests passed ===" | |
| # --- Tier 4: full box (integration smoke test, all languages on one VM) --- | |
| pr-test-full: | |
| name: pr-test / full | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 90 | |
| needs: [detect-changes, version-check, changeset-check, pr-test-language] | |
| if: | | |
| always() && | |
| needs.pr-test-language.result == 'success' && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Build full chain (JS -> essentials -> languages -> full) | |
| run: | | |
| set -e | |
| echo "=== Building JS box ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t box-js . | |
| echo "" | |
| echo "=== Building essentials box (on JS) ===" | |
| docker build -f ubuntu/24.04/essentials-box/Dockerfile \ | |
| --build-arg JS_IMAGE=box-js -t box-essentials . | |
| echo "" | |
| echo "=== Building language images (on essentials) ===" | |
| for lang in python go rust java kotlin ruby php perl swift lean rocq; do | |
| echo "" | |
| echo "--- Building ${lang} box ---" | |
| docker build -f "ubuntu/24.04/${lang}/Dockerfile" \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| -t "box-${lang}" . | |
| done | |
| echo "" | |
| echo "=== Building full box (multi-stage from all language images) ===" | |
| docker build -f ubuntu/24.04/full-box/Dockerfile \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| --build-arg PYTHON_IMAGE=box-python \ | |
| --build-arg GO_IMAGE=box-go \ | |
| --build-arg RUST_IMAGE=box-rust \ | |
| --build-arg JAVA_IMAGE=box-java \ | |
| --build-arg KOTLIN_IMAGE=box-kotlin \ | |
| --build-arg RUBY_IMAGE=box-ruby \ | |
| --build-arg PHP_IMAGE=box-php \ | |
| --build-arg PERL_IMAGE=box-perl \ | |
| --build-arg SWIFT_IMAGE=box-swift \ | |
| --build-arg LEAN_IMAGE=box-lean \ | |
| --build-arg ROCQ_IMAGE=box-rocq \ | |
| -t box-test . | |
| - name: Test full box | |
| run: | | |
| set -e | |
| echo "=== Testing full box ===" | |
| echo "Note: Using entrypoint script which initializes all environments" | |
| # JavaScript/TypeScript runtimes | |
| docker run --rm box-test node --version | |
| docker run --rm box-test bun --version | |
| docker run --rm box-test deno --version | |
| # Python (pyenv) | |
| docker run --rm box-test python3 --version | |
| docker run --rm box-test pip3 --version | |
| # Go | |
| docker run --rm box-test go version | |
| # Rust (rustup + cargo + rustc) | |
| docker run --rm box-test rustc --version | |
| docker run --rm box-test cargo --version | |
| docker run --rm box-test rustup --version | |
| # Java/JVM (SDKMAN) | |
| docker run --rm box-test java -version | |
| docker run --rm box-test kotlin -version | |
| # Ruby (rbenv) | |
| docker run --rm box-test ruby --version | |
| docker run --rm box-test gem --version | |
| # PHP | |
| docker run --rm box-test php --version | |
| # Perl (perlbrew) | |
| docker run --rm box-test perl --version | |
| # Swift | |
| docker run --rm box-test swift --version | |
| # Lean/Mathlib (elan) | |
| docker run --rm box-test lean --version | |
| # Dotnet | |
| docker run --rm box-test dotnet --version | |
| # R language | |
| docker run --rm box-test Rscript --version | |
| # CLI tools | |
| docker run --rm box-test gh --version | |
| docker run --rm box-test glab --version | |
| docker run --rm box-test gh-setup-git-identity --version | |
| docker run --rm box-test glab-setup-git-identity --version | |
| # expect (interactive automation tool, issue #64) | |
| docker run --rm box-test expect -v | |
| echo "" | |
| echo "=== PHP install method check ===" | |
| docker run --rm box-php cat /home/box/.php-install-method | |
| docker run --rm box-test cat /home/box/.php-install-method | |
| echo "" | |
| echo "=== All full box tests passed ===" | |
| # --- Tier 5: dind-box variants (parallel matrix: 14 variants, one VM each) — issue #80 --- | |
| # Each dind variant rebuilds its base chain (JS + essentials + optional lang/full) | |
| # locally and then layers dind on top, so this matrix only depends on | |
| # pr-test-essentials having validated the base layer; it can run in parallel | |
| # with the per-language and full-box jobs to minimise wall-clock time. | |
| pr-test-dind: | |
| name: pr-test / dind-${{ matrix.variant }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 60 | |
| needs: [detect-changes, version-check, changeset-check, pr-test-essentials] | |
| if: | | |
| always() && | |
| needs.pr-test-essentials.result == 'success' && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # variant -> base box flavour. Special value "full" rebuilds the entire chain; | |
| # everything else needs only JS+essentials+<lang>. | |
| variant: [js, essentials, python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq, full] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Build base box for dind variant | |
| run: | | |
| set -e | |
| VARIANT="${{ matrix.variant }}" | |
| echo "=== Building JS box (base layer) ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t box-js . | |
| if [ "$VARIANT" = "js" ]; then | |
| BASE_IMAGE="box-js" | |
| else | |
| echo "" | |
| echo "=== Building essentials box ===" | |
| docker build -f ubuntu/24.04/essentials-box/Dockerfile \ | |
| --build-arg JS_IMAGE=box-js -t box-essentials . | |
| if [ "$VARIANT" = "essentials" ]; then | |
| BASE_IMAGE="box-essentials" | |
| elif [ "$VARIANT" = "full" ]; then | |
| echo "" | |
| echo "=== Building all language images (full-box prerequisites) ===" | |
| for lang in python go rust java kotlin ruby php perl swift lean rocq; do | |
| echo "--- Building ${lang} box ---" | |
| docker build -f "ubuntu/24.04/${lang}/Dockerfile" \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| -t "box-${lang}" . | |
| done | |
| echo "" | |
| echo "=== Building full box ===" | |
| docker build -f ubuntu/24.04/full-box/Dockerfile \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| --build-arg PYTHON_IMAGE=box-python \ | |
| --build-arg GO_IMAGE=box-go \ | |
| --build-arg RUST_IMAGE=box-rust \ | |
| --build-arg JAVA_IMAGE=box-java \ | |
| --build-arg KOTLIN_IMAGE=box-kotlin \ | |
| --build-arg RUBY_IMAGE=box-ruby \ | |
| --build-arg PHP_IMAGE=box-php \ | |
| --build-arg PERL_IMAGE=box-perl \ | |
| --build-arg SWIFT_IMAGE=box-swift \ | |
| --build-arg LEAN_IMAGE=box-lean \ | |
| --build-arg ROCQ_IMAGE=box-rocq \ | |
| -t box-full . | |
| BASE_IMAGE="box-full" | |
| else | |
| echo "" | |
| echo "=== Building ${VARIANT} box ===" | |
| docker build -f "ubuntu/24.04/${VARIANT}/Dockerfile" \ | |
| --build-arg ESSENTIALS_IMAGE=box-essentials \ | |
| -t "box-${VARIANT}" . | |
| BASE_IMAGE="box-${VARIANT}" | |
| fi | |
| fi | |
| echo "BASE_IMAGE=${BASE_IMAGE}" >> "$GITHUB_ENV" | |
| - name: Build dind-box variant | |
| run: | | |
| set -e | |
| echo "=== Building dind-${{ matrix.variant }} on ${BASE_IMAGE} ===" | |
| docker build -f ubuntu/24.04/dind/Dockerfile \ | |
| --build-arg BASE_IMAGE="${BASE_IMAGE}" \ | |
| -t "box-dind-${{ matrix.variant }}" . | |
| - name: Test dind-box variant | |
| run: | | |
| set -e | |
| IMG="box-dind-${{ matrix.variant }}" | |
| echo "=== Testing ${IMG} (no privileged mode in PR CI) ===" | |
| # Regression check for issue #88: docker exec and entrypoint-bypassed | |
| # shells use the image's configured USER, so dind variants must keep | |
| # Config.User aligned with the regular box images. | |
| CONFIG_USER=$(docker inspect --format '{{.Config.User}}' "$IMG") | |
| if [ "$CONFIG_USER" != "box" ]; then | |
| echo "Expected ${IMG} Config.User to be box, got '${CONFIG_USER:-<empty>}'" | |
| exit 1 | |
| fi | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'test "$(whoami)" = box' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'test "$HOME" = /home/box' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'sudo -n /usr/bin/dockerd --version' | |
| # Verify Docker CLI, dockerd binary, buildx and compose plugins are present. | |
| # We do NOT start dockerd here because GitHub-hosted runners disallow --privileged. | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'docker --version' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'dockerd --version' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'docker buildx version' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'docker compose version' | |
| docker run --rm --entrypoint=/bin/bash "$IMG" -c 'cat /etc/box/variant' | |
| echo "=== ${IMG} smoke tests passed ===" | |
| - name: Test documented dind examples | |
| if: matrix.variant == 'js' | |
| env: | |
| DIND_IMAGE: box-dind-js | |
| DIND_WAIT_SECONDS: "60" | |
| run: | | |
| set -e | |
| echo "=== Testing documented dind examples against ${DIND_IMAGE} ===" | |
| tests/dind/example-basic-docker-ps.sh | |
| tests/dind/example-commit-cycle.sh | |
| tests/dind/example-sudoers-extension.sh | |
| tests/dind/example-storage-driver-vfs.sh | |
| tests/dind/example-preload-images.sh | |
| echo "=== Documented dind examples passed ===" | |
| # --- Aggregator: single status check for branch protection --- | |
| # GitHub branch protection rules can require this one job to pass instead of | |
| # listing every parallel pr-test-* job individually. | |
| docker-build-test: | |
| name: docker-build-test | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, version-check, changeset-check, pr-test-js, pr-test-essentials, pr-test-language, pr-test-full, pr-test-dind] | |
| if: | | |
| always() && | |
| github.event_name == 'pull_request' && | |
| needs.detect-changes.outputs.should-build == 'true' | |
| steps: | |
| - name: Aggregate pr-test results | |
| run: | | |
| set -e | |
| echo "pr-test-js: ${{ needs.pr-test-js.result }}" | |
| echo "pr-test-essentials: ${{ needs.pr-test-essentials.result }}" | |
| echo "pr-test-language: ${{ needs.pr-test-language.result }}" | |
| echo "pr-test-full: ${{ needs.pr-test-full.result }}" | |
| echo "pr-test-dind: ${{ needs.pr-test-dind.result }}" | |
| fail=0 | |
| for r in \ | |
| "${{ needs.pr-test-js.result }}" \ | |
| "${{ needs.pr-test-essentials.result }}" \ | |
| "${{ needs.pr-test-language.result }}" \ | |
| "${{ needs.pr-test-full.result }}" \ | |
| "${{ needs.pr-test-dind.result }}"; do | |
| case "$r" in | |
| success|skipped) ;; | |
| *) fail=1 ;; | |
| esac | |
| done | |
| if [ "$fail" -ne 0 ]; then | |
| echo "::error::One or more pr-test jobs did not succeed." | |
| exit 1 | |
| fi | |
| echo "All pr-test jobs succeeded." | |
| # === BUILD JS BOX (amd64) === | |
| # JS box is the base layer - built first, other images depend on it | |
| build-js-amd64: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) && | |
| ( | |
| needs.detect-changes.outputs.js-changed == 'true' || | |
| needs.detect-changes.outputs.common-changed == 'true' || | |
| needs.detect-changes.outputs.version-changed == 'true' || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| built: ${{ steps.result.outputs.built }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Build and push JS box (amd64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/js/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-amd64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 | |
| provenance: false | |
| cache-from: type=gha,scope=js-amd64 | |
| cache-to: type=gha,scope=js-amd64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push JS box (amd64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/js/Dockerfile \ | |
| --platform linux/amd64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-amd64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=js-amd64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === BUILD JS BOX (arm64) === | |
| build-js-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 120 | |
| needs: [detect-changes] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) && | |
| ( | |
| needs.detect-changes.outputs.js-changed == 'true' || | |
| needs.detect-changes.outputs.common-changed == 'true' || | |
| needs.detect-changes.outputs.version-changed == 'true' || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| built: ${{ steps.result.outputs.built }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Build and push JS box (arm64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/js/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-arm64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 | |
| provenance: false | |
| cache-from: type=gha,scope=js-arm64 | |
| cache-to: type=gha,scope=js-arm64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push JS box (arm64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/js/Dockerfile \ | |
| --platform linux/arm64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-arm64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=js-arm64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === CREATE JS MULTI-ARCH MANIFEST === | |
| js-manifest: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-js-amd64, build-js-arm64] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.build-js-amd64.result == 'success' && | |
| needs.build-js-arm64.result == 'success' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Create and push JS multi-arch manifests (GHCR) | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION} \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION}-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION}-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION} | |
| echo "JS box GHCR multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Create and push JS multi-arch manifests (Docker Hub) | |
| if: steps.dockerhub-login.outcome == 'success' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION} \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION}-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION}-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION} | |
| echo "JS box Docker Hub multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Skip JS Docker Hub multi-arch manifests | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub manifest skipped::Skipping JS Docker Hub multi-arch manifests because Docker Hub login outcome was ${{ steps.dockerhub-login.outcome }}. GHCR manifests were still published." | |
| # === BUILD ESSENTIALS BOX (amd64) === | |
| # Built on top of JS box - waits for JS to complete (if JS was rebuilt) | |
| build-essentials-amd64: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-js-amd64] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-js-amd64.result == 'success' || needs.build-js-amd64.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) && | |
| ( | |
| needs.detect-changes.outputs.js-changed == 'true' || | |
| needs.detect-changes.outputs.essentials-changed == 'true' || | |
| needs.detect-changes.outputs.common-changed == 'true' || | |
| needs.detect-changes.outputs.scripts-changed == 'true' || | |
| needs.detect-changes.outputs.version-changed == 'true' || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| built: ${{ steps.result.outputs.built }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine JS base image | |
| id: js-base | |
| run: | | |
| # Use freshly built JS image if JS was rebuilt, otherwise use latest | |
| if [ "${{ needs.build-js-amd64.outputs.built }}" = "true" ]; then | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64" >> $GITHUB_OUTPUT | |
| else | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push essentials box (amd64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/essentials-box/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| build-args: | | |
| JS_IMAGE=${{ steps.js-base.outputs.image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-amd64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 | |
| provenance: false | |
| cache-from: type=gha,scope=essentials-amd64 | |
| cache-to: type=gha,scope=essentials-amd64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push essentials box (amd64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/essentials-box/Dockerfile \ | |
| --platform linux/amd64 \ | |
| --build-arg JS_IMAGE=${{ steps.js-base.outputs.image }} \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-amd64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=essentials-amd64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === BUILD ESSENTIALS BOX (arm64) === | |
| build-essentials-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 120 | |
| needs: [detect-changes, build-js-arm64] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-js-arm64.result == 'success' || needs.build-js-arm64.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) && | |
| ( | |
| needs.detect-changes.outputs.js-changed == 'true' || | |
| needs.detect-changes.outputs.essentials-changed == 'true' || | |
| needs.detect-changes.outputs.common-changed == 'true' || | |
| needs.detect-changes.outputs.scripts-changed == 'true' || | |
| needs.detect-changes.outputs.version-changed == 'true' || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| built: ${{ steps.result.outputs.built }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine JS base image | |
| id: js-base | |
| run: | | |
| if [ "${{ needs.build-js-arm64.outputs.built }}" = "true" ]; then | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64" >> $GITHUB_OUTPUT | |
| else | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push essentials box (arm64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/essentials-box/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| build-args: | | |
| JS_IMAGE=${{ steps.js-base.outputs.image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-arm64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 | |
| provenance: false | |
| cache-from: type=gha,scope=essentials-arm64 | |
| cache-to: type=gha,scope=essentials-arm64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push essentials box (arm64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/essentials-box/Dockerfile \ | |
| --platform linux/arm64 \ | |
| --build-arg JS_IMAGE=${{ steps.js-base.outputs.image }} \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-arm64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=essentials-arm64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === CREATE ESSENTIALS MULTI-ARCH MANIFEST === | |
| essentials-manifest: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-essentials-amd64, build-essentials-arm64] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.build-essentials-amd64.result == 'success' && | |
| needs.build-essentials-arm64.result == 'success' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Create and push essentials multi-arch manifests (GHCR) | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION} \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION}-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION}-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION} | |
| echo "Essentials box GHCR multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Create and push essentials multi-arch manifests (Docker Hub) | |
| if: steps.dockerhub-login.outcome == 'success' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION} \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION} | |
| echo "Essentials box Docker Hub multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Skip essentials Docker Hub multi-arch manifests | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub manifest skipped::Skipping essentials Docker Hub multi-arch manifests because Docker Hub login outcome was ${{ steps.dockerhub-login.outcome }}. GHCR manifests were still published." | |
| # === BUILD LANGUAGE IMAGES (amd64) === | |
| # All language images are built in parallel on top of essentials box | |
| build-languages-amd64: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 # Issue #53: Add timeout to fail fast on hangs (normal builds take ~10-15 min) | |
| needs: [detect-changes, build-essentials-amd64] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-essentials-amd64.result == 'success' || needs.build-essentials-amd64.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Check if this language needs building | |
| id: check-lang | |
| run: | | |
| LANG="${{ matrix.language }}" | |
| LANG_CHANGED="${{ needs.detect-changes.outputs[format('{0}-changed', matrix.language)] }}" | |
| ESSENTIALS_CHANGED="${{ needs.detect-changes.outputs.essentials-changed }}" | |
| COMMON_CHANGED="${{ needs.detect-changes.outputs.common-changed }}" | |
| VERSION_CHANGED="${{ needs.detect-changes.outputs.version-changed }}" | |
| JS_CHANGED="${{ needs.detect-changes.outputs.js-changed }}" | |
| if [ "$LANG_CHANGED" = "true" ] || \ | |
| [ "$ESSENTIALS_CHANGED" = "true" ] || \ | |
| [ "$COMMON_CHANGED" = "true" ] || \ | |
| [ "$JS_CHANGED" = "true" ] || \ | |
| [ "$VERSION_CHANGED" = "true" ] || \ | |
| [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "Building ${LANG}: change detected or workflow_dispatch" | |
| else | |
| echo "should_build=false" >> $GITHUB_OUTPUT | |
| echo "Skipping ${LANG}: no relevant changes" | |
| fi | |
| - name: Checkout repository | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.check-lang.outputs.should_build == 'true' && steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine essentials base image | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| id: essentials-base | |
| run: | | |
| if [ "${{ needs.build-essentials-amd64.outputs.built }}" = "true" ]; then | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64" >> $GITHUB_OUTPUT | |
| else | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push ${{ matrix.language }} box (amd64) | |
| id: build-push | |
| continue-on-error: true | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/${{ matrix.language }}/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| build-args: | | |
| ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64 | |
| provenance: false | |
| cache-from: type=gha,scope=${{ matrix.language }}-amd64 | |
| cache-to: type=gha,scope=${{ matrix.language }}-amd64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push ${{ matrix.language }} box (amd64) on failure | |
| if: steps.check-lang.outputs.should_build == 'true' && steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| TAGS=( | |
| "${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64" | |
| "${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64" | |
| "${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64" | |
| "${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64" | |
| ) | |
| TAG_ARGS="" | |
| for tag in "${TAGS[@]}"; do | |
| TAG_ARGS="$TAG_ARGS --tag $tag" | |
| done | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/${{ matrix.language }}/Dockerfile \ | |
| --platform linux/amd64 \ | |
| --build-arg ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} \ | |
| $TAG_ARGS \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=${{ matrix.language }}-amd64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # PHP-specific: Add local/global suffix tags based on install method (Issue #44) | |
| - name: Tag PHP image with install method suffix (amd64) | |
| if: steps.check-lang.outputs.should_build == 'true' && matrix.language == 'php' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Pull the image and inspect the marker file | |
| docker pull ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 | |
| PHP_METHOD=$(docker run --rm ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 cat /home/box/.php-install-method 2>/dev/null || echo "unknown") | |
| echo "PHP install method (amd64): $PHP_METHOD" | |
| if [ "$PHP_METHOD" = "local" ] || [ "$PHP_METHOD" = "global" ]; then | |
| # Tag with method suffix on both registries | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 \ | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-php:latest-amd64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 \ | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 \ | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:latest-amd64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64 \ | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:${VERSION}-amd64-${PHP_METHOD} | |
| docker push ${{ env.DOCKERHUB_IMAGE_NAME }}-php:latest-amd64-${PHP_METHOD} | |
| docker push ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-amd64-${PHP_METHOD} | |
| docker push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:latest-amd64-${PHP_METHOD} | |
| docker push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:${VERSION}-amd64-${PHP_METHOD} | |
| echo "Tagged PHP image with -${PHP_METHOD} suffix" | |
| fi | |
| # === BUILD LANGUAGE IMAGES (arm64) === | |
| build-languages-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 45 # Issue #53: Reduced from 120 to 45 min - fail fast on network hangs | |
| needs: [detect-changes, build-essentials-arm64] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-essentials-arm64.result == 'success' || needs.build-essentials-arm64.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Check if this language needs building | |
| id: check-lang | |
| run: | | |
| LANG="${{ matrix.language }}" | |
| LANG_CHANGED="${{ needs.detect-changes.outputs[format('{0}-changed', matrix.language)] }}" | |
| ESSENTIALS_CHANGED="${{ needs.detect-changes.outputs.essentials-changed }}" | |
| COMMON_CHANGED="${{ needs.detect-changes.outputs.common-changed }}" | |
| VERSION_CHANGED="${{ needs.detect-changes.outputs.version-changed }}" | |
| JS_CHANGED="${{ needs.detect-changes.outputs.js-changed }}" | |
| if [ "$LANG_CHANGED" = "true" ] || \ | |
| [ "$ESSENTIALS_CHANGED" = "true" ] || \ | |
| [ "$COMMON_CHANGED" = "true" ] || \ | |
| [ "$JS_CHANGED" = "true" ] || \ | |
| [ "$VERSION_CHANGED" = "true" ] || \ | |
| [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "Building ${LANG}: change detected or workflow_dispatch" | |
| else | |
| echo "should_build=false" >> $GITHUB_OUTPUT | |
| echo "Skipping ${LANG}: no relevant changes" | |
| fi | |
| - name: Checkout repository | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.check-lang.outputs.should_build == 'true' && steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine essentials base image | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| id: essentials-base | |
| run: | | |
| if [ "${{ needs.build-essentials-arm64.outputs.built }}" = "true" ]; then | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64" >> $GITHUB_OUTPUT | |
| else | |
| echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push ${{ matrix.language }} box (arm64) | |
| id: build-push | |
| continue-on-error: true | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/${{ matrix.language }}/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| build-args: | | |
| ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64 | |
| provenance: false | |
| cache-from: type=gha,scope=${{ matrix.language }}-arm64 | |
| cache-to: type=gha,scope=${{ matrix.language }}-arm64,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push ${{ matrix.language }} box (arm64) on failure | |
| if: steps.check-lang.outputs.should_build == 'true' && steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| TAGS=( | |
| "${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64" | |
| "${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64" | |
| "${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64" | |
| "${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64" | |
| ) | |
| TAG_ARGS="" | |
| for tag in "${TAGS[@]}"; do | |
| TAG_ARGS="$TAG_ARGS --tag $tag" | |
| done | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/${{ matrix.language }}/Dockerfile \ | |
| --platform linux/arm64 \ | |
| --build-arg ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} \ | |
| $TAG_ARGS \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=${{ matrix.language }}-arm64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # PHP-specific: Add local/global suffix tags based on install method (Issue #44) | |
| - name: Tag PHP image with install method suffix (arm64) | |
| if: steps.check-lang.outputs.should_build == 'true' && matrix.language == 'php' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Pull the image and inspect the marker file | |
| docker pull ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 | |
| PHP_METHOD=$(docker run --rm ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 cat /home/box/.php-install-method 2>/dev/null || echo "unknown") | |
| echo "PHP install method (arm64): $PHP_METHOD" | |
| if [ "$PHP_METHOD" = "local" ] || [ "$PHP_METHOD" = "global" ]; then | |
| # Tag with method suffix on both registries | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 \ | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-php:latest-arm64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 \ | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 \ | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:latest-arm64-${PHP_METHOD} | |
| docker tag ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64 \ | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:${VERSION}-arm64-${PHP_METHOD} | |
| docker push ${{ env.DOCKERHUB_IMAGE_NAME }}-php:latest-arm64-${PHP_METHOD} | |
| docker push ${{ env.DOCKERHUB_IMAGE_NAME }}-php:${VERSION}-arm64-${PHP_METHOD} | |
| docker push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:latest-arm64-${PHP_METHOD} | |
| docker push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-php:${VERSION}-arm64-${PHP_METHOD} | |
| echo "Tagged PHP image with -${PHP_METHOD} suffix" | |
| fi | |
| # === CREATE LANGUAGE MULTI-ARCH MANIFESTS === | |
| languages-manifest: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-languages-amd64, build-languages-arm64] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.build-languages-amd64.result == 'success' && | |
| needs.build-languages-arm64.result == 'success' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Create and push ${{ matrix.language }} multi-arch manifests (GHCR) | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| LANG="${{ matrix.language }}" | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION} \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION}-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION}-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION} | |
| echo "${LANG} box GHCR multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Create and push ${{ matrix.language }} multi-arch manifests (Docker Hub) | |
| if: steps.dockerhub-login.outcome == 'success' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| LANG="${{ matrix.language }}" | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION} \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION}-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION}-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION} | |
| echo "${LANG} box Docker Hub multi-arch manifests pushed for latest and ${VERSION}" | |
| - name: Skip ${{ matrix.language }} Docker Hub multi-arch manifests | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub manifest skipped::Skipping ${{ matrix.language }} Docker Hub multi-arch manifests because Docker Hub login outcome was ${{ steps.dockerhub-login.outcome }}. GHCR manifests were still published." | |
| # === BUILD AND PUSH FULL BOX (MAIN - amd64) === | |
| docker-build-push: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-languages-amd64, build-essentials-amd64] | |
| # Run on push to main with changes, OR on workflow_dispatch | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-essentials-amd64.result == 'success' || needs.build-essentials-amd64.result == 'skipped') && | |
| (needs.build-languages-amd64.result == 'success' || needs.build-languages-amd64.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| version: ${{ needs.detect-changes.outputs.version }} | |
| digest: ${{ steps.build.outputs.digest }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main # Always use latest main for releases | |
| # Fix for issue #41: Free disk space to prevent "No space left on device" errors | |
| # This step removes unnecessary pre-installed software from the runner to free ~30 GB | |
| # See: docs/case-studies/issue-41/CASE-STUDY.md | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false # Keep tool cache for setup-* action compatibility | |
| android: true # Free ~14 GB | |
| dotnet: true # Free ~2.7 GB | |
| haskell: true # Free ~0 GB (not pre-installed on ubuntu-24.04) | |
| large-packages: true # Free ~5.3 GB | |
| docker-images: true # Clean existing Docker images | |
| swap-storage: true # Free ~4 GB | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Building version: $VERSION" | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine base images | |
| id: base-images | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Essentials base image | |
| if [ "${{ needs.build-essentials-amd64.outputs.built }}" = "true" ]; then | |
| echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-amd64" >> $GITHUB_OUTPUT | |
| else | |
| echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64" >> $GITHUB_OUTPUT | |
| fi | |
| # Language images - use version tag if languages were built, otherwise latest | |
| for lang in python go rust java kotlin ruby php perl swift lean rocq; do | |
| LANG_UPPER=$(echo "$lang" | tr '[:lower:]' '[:upper:]') | |
| # If the language matrix ran successfully, use versioned tag | |
| if [ "${{ needs.build-languages-amd64.result }}" = "success" ]; then | |
| echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:${VERSION}-amd64" >> $GITHUB_OUTPUT | |
| else | |
| echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:latest-amd64" >> $GITHUB_OUTPUT | |
| fi | |
| done | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }} | |
| ${{ env.DOCKERHUB_IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest | |
| type=raw,value=${{ steps.version.outputs.version }} | |
| type=sha,prefix= | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| - name: Extract metadata for amd64-specific tags | |
| id: meta-amd64 | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }} | |
| ${{ env.DOCKERHUB_IMAGE_NAME }} | |
| flavor: | | |
| suffix=-amd64 | |
| tags: | | |
| type=raw,value=latest | |
| type=raw,value=${{ steps.version.outputs.version }} | |
| type=sha,prefix= | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| - name: Build and push full box (amd64) | |
| id: build | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/full-box/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| build-args: | | |
| ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} | |
| PYTHON_IMAGE=${{ steps.base-images.outputs.python }} | |
| GO_IMAGE=${{ steps.base-images.outputs.go }} | |
| RUST_IMAGE=${{ steps.base-images.outputs.rust }} | |
| JAVA_IMAGE=${{ steps.base-images.outputs.java }} | |
| KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} | |
| RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} | |
| PHP_IMAGE=${{ steps.base-images.outputs.php }} | |
| PERL_IMAGE=${{ steps.base-images.outputs.perl }} | |
| SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} | |
| LEAN_IMAGE=${{ steps.base-images.outputs.lean }} | |
| ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} | |
| tags: | | |
| ${{ steps.meta.outputs.tags }} | |
| ${{ steps.meta-amd64.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| provenance: false # Prevents unknown/unknown platform in registry | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push full box (amd64) on failure | |
| if: steps.build.outcome == 'failure' | |
| env: | |
| ALL_TAGS: | | |
| ${{ steps.meta.outputs.tags }} | |
| ${{ steps.meta-amd64.outputs.tags }} | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| TAG_ARGS="" | |
| while IFS= read -r tag; do | |
| tag=$(echo "$tag" | xargs) | |
| [ -z "$tag" ] && continue | |
| TAG_ARGS="$TAG_ARGS --tag $tag" | |
| done <<< "$ALL_TAGS" | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/full-box/Dockerfile \ | |
| --platform linux/amd64 \ | |
| --build-arg ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} \ | |
| --build-arg PYTHON_IMAGE=${{ steps.base-images.outputs.python }} \ | |
| --build-arg GO_IMAGE=${{ steps.base-images.outputs.go }} \ | |
| --build-arg RUST_IMAGE=${{ steps.base-images.outputs.rust }} \ | |
| --build-arg JAVA_IMAGE=${{ steps.base-images.outputs.java }} \ | |
| --build-arg KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} \ | |
| --build-arg RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} \ | |
| --build-arg PHP_IMAGE=${{ steps.base-images.outputs.php }} \ | |
| --build-arg PERL_IMAGE=${{ steps.base-images.outputs.perl }} \ | |
| --build-arg SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} \ | |
| --build-arg LEAN_IMAGE=${{ steps.base-images.outputs.lean }} \ | |
| --build-arg ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} \ | |
| $TAG_ARGS \ | |
| --provenance=false \ | |
| --cache-from type=gha \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # Smoke test the published image to verify all toolchains work (issue #62) | |
| # Tests each toolchain command to catch cases where tools are missing/broken | |
| - name: Smoke test released full box (amd64) | |
| run: | | |
| set -e | |
| VERSION="${{ steps.version.outputs.version }}" | |
| IMAGE="${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}" | |
| echo "=== Smoke testing released image: ${IMAGE} ===" | |
| # JavaScript/TypeScript runtimes | |
| docker run --rm "${IMAGE}" node --version | |
| docker run --rm "${IMAGE}" bun --version | |
| docker run --rm "${IMAGE}" deno --version | |
| # Python (pyenv) | |
| docker run --rm "${IMAGE}" python3 --version | |
| docker run --rm "${IMAGE}" pip3 --version | |
| # Go | |
| docker run --rm "${IMAGE}" go version | |
| # Rust (rustup + cargo + rustc) | |
| docker run --rm "${IMAGE}" rustc --version | |
| docker run --rm "${IMAGE}" cargo --version | |
| docker run --rm "${IMAGE}" rustup --version | |
| # Java/JVM (SDKMAN) | |
| docker run --rm "${IMAGE}" java -version | |
| docker run --rm "${IMAGE}" kotlin -version | |
| # Ruby (rbenv) | |
| docker run --rm "${IMAGE}" ruby --version | |
| docker run --rm "${IMAGE}" gem --version | |
| # PHP | |
| docker run --rm "${IMAGE}" php --version | |
| # Perl (perlbrew) | |
| docker run --rm "${IMAGE}" perl --version | |
| # Swift | |
| docker run --rm "${IMAGE}" swift --version | |
| # Lean/Mathlib (elan) | |
| docker run --rm "${IMAGE}" lean --version | |
| # Dotnet | |
| docker run --rm "${IMAGE}" dotnet --version | |
| # R language | |
| docker run --rm "${IMAGE}" Rscript --version | |
| # CLI tools | |
| docker run --rm "${IMAGE}" gh --version | |
| docker run --rm "${IMAGE}" glab --version | |
| # expect (interactive automation tool, issue #64) | |
| docker run --rm "${IMAGE}" expect -v | |
| echo "=== All smoke tests passed for ${IMAGE} ===" | |
| # === BUILD AND PUSH ARM64 IMAGE === | |
| # Using native ARM64 runner for optimal build performance | |
| docker-build-push-arm64: | |
| runs-on: ubuntu-24.04-arm # Native ARM64 runner (free for public repos since Jan 2025) | |
| timeout-minutes: 120 # Safety timeout to prevent runaway builds | |
| needs: [detect-changes, build-languages-arm64, build-essentials-arm64, docker-build-push] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.build-essentials-arm64.result == 'success' || needs.build-essentials-arm64.result == 'skipped') && | |
| (needs.build-languages-arm64.result == 'success' || needs.build-languages-arm64.result == 'skipped') && | |
| needs.docker-build-push.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| digest: ${{ steps.build.outputs.digest }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main # Always use latest main for releases | |
| # Fix for issue #41: Free disk space to prevent "No space left on device" errors | |
| # ARM64 runners have more disk space (~45 GB) but we still clean up for safety | |
| # See: docs/case-studies/issue-41/CASE-STUDY.md | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false # Keep tool cache for setup-* action compatibility | |
| android: true # Free ~14 GB | |
| dotnet: true # Free ~2.7 GB | |
| haskell: true # Free ~0 GB (not pre-installed) | |
| large-packages: true # Free ~5.3 GB | |
| docker-images: true # Clean existing Docker images | |
| swap-storage: true # Free ~4 GB | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Building version: $VERSION" | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Determine base images | |
| id: base-images | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Essentials base image | |
| if [ "${{ needs.build-essentials-arm64.outputs.built }}" = "true" ]; then | |
| echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-arm64" >> $GITHUB_OUTPUT | |
| else | |
| echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64" >> $GITHUB_OUTPUT | |
| fi | |
| # Language images | |
| for lang in python go rust java kotlin ruby php perl swift lean rocq; do | |
| if [ "${{ needs.build-languages-arm64.result }}" = "success" ]; then | |
| echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:${VERSION}-arm64" >> $GITHUB_OUTPUT | |
| else | |
| echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:latest-arm64" >> $GITHUB_OUTPUT | |
| fi | |
| done | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }} | |
| ${{ env.DOCKERHUB_IMAGE_NAME }} | |
| flavor: | | |
| suffix=-arm64 | |
| tags: | | |
| type=raw,value=latest | |
| type=raw,value=${{ steps.version.outputs.version }} | |
| type=sha,prefix= | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| - name: Build and push full box (arm64) | |
| id: build | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/full-box/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| build-args: | | |
| ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} | |
| PYTHON_IMAGE=${{ steps.base-images.outputs.python }} | |
| GO_IMAGE=${{ steps.base-images.outputs.go }} | |
| RUST_IMAGE=${{ steps.base-images.outputs.rust }} | |
| JAVA_IMAGE=${{ steps.base-images.outputs.java }} | |
| KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} | |
| RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} | |
| PHP_IMAGE=${{ steps.base-images.outputs.php }} | |
| PERL_IMAGE=${{ steps.base-images.outputs.perl }} | |
| SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} | |
| LEAN_IMAGE=${{ steps.base-images.outputs.lean }} | |
| ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| provenance: false # Prevents unknown/unknown platform in registry | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # Retry push on transient GHCR 403 errors (Issue #78) | |
| - name: Retry push full box (arm64) on failure | |
| if: steps.build.outcome == 'failure' | |
| env: | |
| ALL_TAGS: ${{ steps.meta.outputs.tags }} | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| TAG_ARGS="" | |
| while IFS= read -r tag; do | |
| tag=$(echo "$tag" | xargs) | |
| [ -z "$tag" ] && continue | |
| TAG_ARGS="$TAG_ARGS --tag $tag" | |
| done <<< "$ALL_TAGS" | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/full-box/Dockerfile \ | |
| --platform linux/arm64 \ | |
| --build-arg ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} \ | |
| --build-arg PYTHON_IMAGE=${{ steps.base-images.outputs.python }} \ | |
| --build-arg GO_IMAGE=${{ steps.base-images.outputs.go }} \ | |
| --build-arg RUST_IMAGE=${{ steps.base-images.outputs.rust }} \ | |
| --build-arg JAVA_IMAGE=${{ steps.base-images.outputs.java }} \ | |
| --build-arg KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} \ | |
| --build-arg RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} \ | |
| --build-arg PHP_IMAGE=${{ steps.base-images.outputs.php }} \ | |
| --build-arg PERL_IMAGE=${{ steps.base-images.outputs.perl }} \ | |
| --build-arg SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} \ | |
| --build-arg LEAN_IMAGE=${{ steps.base-images.outputs.lean }} \ | |
| --build-arg ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} \ | |
| $TAG_ARGS \ | |
| --provenance=false \ | |
| --cache-from type=gha \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # === CREATE MULTI-ARCH MANIFEST === | |
| docker-manifest: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, docker-build-push, docker-build-push-arm64] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.docker-build-push.result == 'success' && | |
| needs.docker-build-push-arm64.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Creating manifest for version: $VERSION" | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Create and push multi-arch manifest (GHCR) | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Create manifest for latest tag on GHCR | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest | |
| # Create manifest for version tag on GHCR | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION} \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION}-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION}-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION} | |
| echo "GHCR multi-arch manifest created and pushed successfully for latest and ${VERSION}" | |
| - name: Create and push multi-arch manifest (Docker Hub) | |
| if: steps.dockerhub-login.outcome == 'success' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Create manifest for latest tag on Docker Hub | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}:latest \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}:latest-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}:latest-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}:latest | |
| # Create manifest for version tag on Docker Hub | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION} \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION} | |
| echo "Docker Hub multi-arch manifest created and pushed successfully for latest and ${VERSION}" | |
| - name: Skip Docker Hub multi-arch manifest | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub manifest skipped::Skipping Docker Hub multi-arch manifest because Docker Hub login outcome was ${{ steps.dockerhub-login.outcome }}. GHCR manifests were still published." | |
| # === BUILD DIND-BOX VARIANTS (amd64) - issue #80 === | |
| # Layers Docker Engine on top of every base box image so each variant has a | |
| # "<base>-dind" sibling. Runs after the source variant's manifest is published. | |
| build-dind-amd64: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| needs: [detect-changes, js-manifest, essentials-manifest, languages-manifest, docker-manifest] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # variant -> base box flavour. Special value "full" maps to konard/box. | |
| # Everything else maps to konard/box-<variant>. | |
| variant: [js, essentials, python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq, full] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.js-manifest.result == 'success' || needs.js-manifest.result == 'skipped') && | |
| (needs.essentials-manifest.result == 'success' || needs.essentials-manifest.result == 'skipped') && | |
| (needs.languages-manifest.result == 'success' || needs.languages-manifest.result == 'skipped') && | |
| (needs.docker-manifest.result == 'success' || needs.docker-manifest.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Compute image names | |
| id: names | |
| run: | | |
| NAME="${{ matrix.variant }}" | |
| # Source base image: full -> "konard/box"; others -> "konard/box-<name>" | |
| # Target dind image: full -> "konard/box-dind"; others -> "konard/box-<name>-dind" | |
| if [ "$NAME" = "full" ]; then | |
| BASE_SUFFIX="" | |
| DIND_SUFFIX="-dind" | |
| else | |
| BASE_SUFFIX="-$NAME" | |
| DIND_SUFFIX="-${NAME}-dind" | |
| fi | |
| echo "base_image=${{ env.DOCKERHUB_IMAGE_NAME }}${BASE_SUFFIX}:${{ steps.version.outputs.version }}-amd64" >> $GITHUB_OUTPUT | |
| echo "dind_suffix=${DIND_SUFFIX}" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Build and push ${{ matrix.variant }} dind-box (amd64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/dind/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| build-args: | | |
| BASE_IMAGE=${{ steps.names.outputs.base_image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-amd64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-amd64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-amd64 | |
| provenance: false | |
| cache-from: type=gha,scope=dind-${{ matrix.variant }}-amd64 | |
| cache-to: type=gha,scope=dind-${{ matrix.variant }}-amd64,mode=max | |
| - name: Retry push ${{ matrix.variant }} dind-box (amd64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/dind/Dockerfile \ | |
| --platform linux/amd64 \ | |
| --build-arg BASE_IMAGE=${{ steps.names.outputs.base_image }} \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-amd64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-amd64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-amd64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=dind-${{ matrix.variant }}-amd64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # === BUILD DIND-BOX VARIANTS (arm64) - issue #80 === | |
| build-dind-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 45 | |
| needs: [detect-changes, js-manifest, essentials-manifest, languages-manifest, docker-manifest] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: [js, essentials, python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq, full] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| (needs.js-manifest.result == 'success' || needs.js-manifest.result == 'skipped') && | |
| (needs.essentials-manifest.result == 'success' || needs.essentials-manifest.result == 'skipped') && | |
| (needs.languages-manifest.result == 'success' || needs.languages-manifest.result == 'skipped') && | |
| (needs.docker-manifest.result == 'success' || needs.docker-manifest.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| # Issue #82: free disk space on every build job's runner before pulling | |
| # base images / running buildx so that COPY --from=*-stage and large | |
| # apt installs never hit "no space left on device" mid-build. | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Compute image names | |
| id: names | |
| run: | | |
| NAME="${{ matrix.variant }}" | |
| if [ "$NAME" = "full" ]; then | |
| BASE_SUFFIX="" | |
| DIND_SUFFIX="-dind" | |
| else | |
| BASE_SUFFIX="-$NAME" | |
| DIND_SUFFIX="-${NAME}-dind" | |
| fi | |
| echo "base_image=${{ env.DOCKERHUB_IMAGE_NAME }}${BASE_SUFFIX}:${{ steps.version.outputs.version }}-arm64" >> $GITHUB_OUTPUT | |
| echo "dind_suffix=${DIND_SUFFIX}" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: ./.github/actions/setup-buildx-resilient | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Build and push ${{ matrix.variant }} dind-box (arm64) | |
| id: build-push | |
| continue-on-error: true | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/dind/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| build-args: | | |
| BASE_IMAGE=${{ steps.names.outputs.base_image }} | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-arm64 | |
| ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-arm64 | |
| ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-arm64 | |
| provenance: false | |
| cache-from: type=gha,scope=dind-${{ matrix.variant }}-arm64 | |
| cache-to: type=gha,scope=dind-${{ matrix.variant }}-arm64,mode=max | |
| - name: Retry push ${{ matrix.variant }} dind-box (arm64) on failure | |
| if: steps.build-push.outcome == 'failure' | |
| run: | | |
| echo "First push attempt failed, retrying with backoff..." | |
| for attempt in 1 2 3; do | |
| echo "==> Retry attempt $attempt/3..." | |
| if docker buildx build \ | |
| --file ubuntu/24.04/dind/Dockerfile \ | |
| --platform linux/arm64 \ | |
| --build-arg BASE_IMAGE=${{ steps.names.outputs.base_image }} \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-arm64 \ | |
| --tag ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:latest-arm64 \ | |
| --tag ${{ env.DOCKERHUB_IMAGE_NAME }}${{ steps.names.outputs.dind_suffix }}:${{ steps.version.outputs.version }}-arm64 \ | |
| --provenance=false \ | |
| --cache-from type=gha,scope=dind-${{ matrix.variant }}-arm64 \ | |
| --push \ | |
| .; then | |
| echo "==> Push succeeded on retry attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| delay=$((10 * attempt)) | |
| echo "==> Retry failed, waiting ${delay}s before next attempt..." | |
| sleep "$delay" | |
| fi | |
| done | |
| echo "==> All retry attempts failed" | |
| exit 1 | |
| # === CREATE DIND-BOX MULTI-ARCH MANIFESTS - issue #80 === | |
| dind-manifest: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, build-dind-amd64, build-dind-arm64] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: [js, essentials, python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq, full] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.build-dind-amd64.result == 'success' && | |
| needs.build-dind-arm64.result == 'success' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Issue #82: tolerate Docker Hub login failure (e.g. expired DOCKERHUB_TOKEN) | |
| # so that pushes to GHCR still proceed instead of taking down every build job. | |
| - name: Log in to Docker Hub | |
| id: dockerhub-login | |
| continue-on-error: true | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: "Check Docker Hub login (issue #82)" | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub login failed::Docker Hub login failed (outcome=${{ steps.dockerhub-login.outcome }}). The DOCKERHUB_TOKEN repository secret is most likely expired or revoked. See docs/case-studies/issue-82/CASE-STUDY.md for the rotation runbook. The job will continue and push to GHCR; guarded Docker Hub publish steps will be skipped, while unguarded Docker Hub push attempts may fail without failing the job." | |
| - name: Compute dind suffix | |
| id: names | |
| run: | | |
| NAME="${{ matrix.variant }}" | |
| if [ "$NAME" = "full" ]; then | |
| echo "dind_suffix=-dind" >> $GITHUB_OUTPUT | |
| else | |
| echo "dind_suffix=-${NAME}-dind" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create and push ${{ matrix.variant }} dind multi-arch manifests (GHCR) | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DSUF="${{ steps.names.outputs.dind_suffix }}" | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:latest \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:latest-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:latest-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:latest | |
| docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:${VERSION} \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:${VERSION}-amd64 \ | |
| --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:${VERSION}-arm64 | |
| docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}${DSUF}:${VERSION} | |
| echo "Pushed GHCR multi-arch manifest for ${{ env.GHCR_IMAGE_NAME }}${DSUF} (latest, ${VERSION})" | |
| - name: Create and push ${{ matrix.variant }} dind multi-arch manifests (Docker Hub) | |
| if: steps.dockerhub-login.outcome == 'success' | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DSUF="${{ steps.names.outputs.dind_suffix }}" | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:latest \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:latest-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:latest-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:latest | |
| docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:${VERSION} \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:${VERSION}-amd64 \ | |
| --amend ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:${VERSION}-arm64 | |
| docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF}:${VERSION} | |
| echo "Pushed multi-arch manifest for ${{ env.DOCKERHUB_IMAGE_NAME }}${DSUF} (latest, ${VERSION})" | |
| - name: Skip ${{ matrix.variant }} dind Docker Hub multi-arch manifests | |
| if: steps.dockerhub-login.outcome != 'success' | |
| run: | | |
| echo "::warning title=Docker Hub manifest skipped::Skipping ${{ matrix.variant }} dind Docker Hub multi-arch manifests because Docker Hub login outcome was ${{ steps.dockerhub-login.outcome }}. GHCR manifests were still published." | |
| # === CREATE GITHUB RELEASE === | |
| create-release: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, docker-manifest, js-manifest, essentials-manifest, languages-manifest, dind-manifest] | |
| if: | | |
| always() && | |
| needs.detect-changes.result == 'success' && | |
| needs.docker-manifest.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main') || | |
| (github.event_name == 'workflow_dispatch') | |
| ) | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Get latest version | |
| id: version | |
| run: | | |
| git pull origin main || true | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Creating release for version: $VERSION" | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GHCR_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }} | |
| DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE_NAME }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DATE=$(date +%Y-%m-%d) | |
| REPO="${{ github.repository }}" | |
| # Create release notes file with comprehensive clickable links | |
| # Issue #39: All tags should have clickable links for both Docker Hub and GHCR | |
| cat > /tmp/release-notes.md << ENDOFNOTES | |
| ## Docker Images | |
| ### Docker Hub - Combo Boxes | |
| | Image | Multi-arch | AMD64 | ARM64 | | |
| |-------|------------|-------|-------| | |
| | Full Box | [\`${DOCKERHUB_IMAGE}:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}/tags?name=${VERSION}-arm64) | | |
| | Essentials | [\`${DOCKERHUB_IMAGE}-essentials:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-essentials/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-essentials/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-essentials/tags?name=${VERSION}-arm64) | | |
| | JS | [\`${DOCKERHUB_IMAGE}-js:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-js/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-js/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-js/tags?name=${VERSION}-arm64) | | |
| ### Docker Hub - Language Boxes | |
| | Language | Multi-arch | AMD64 | ARM64 | | |
| |----------|------------|-------|-------| | |
| | Python | [\`${DOCKERHUB_IMAGE}-python:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-python/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-python/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-python/tags?name=${VERSION}-arm64) | | |
| | Go | [\`${DOCKERHUB_IMAGE}-go:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-go/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-go/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-go/tags?name=${VERSION}-arm64) | | |
| | Rust | [\`${DOCKERHUB_IMAGE}-rust:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rust/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rust/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rust/tags?name=${VERSION}-arm64) | | |
| | Java | [\`${DOCKERHUB_IMAGE}-java:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-java/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-java/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-java/tags?name=${VERSION}-arm64) | | |
| | Kotlin | [\`${DOCKERHUB_IMAGE}-kotlin:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-kotlin/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-kotlin/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-kotlin/tags?name=${VERSION}-arm64) | | |
| | Ruby | [\`${DOCKERHUB_IMAGE}-ruby:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-ruby/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-ruby/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-ruby/tags?name=${VERSION}-arm64) | | |
| | PHP | [\`${DOCKERHUB_IMAGE}-php:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-php/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-php/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-php/tags?name=${VERSION}-arm64) | | |
| | Perl | [\`${DOCKERHUB_IMAGE}-perl:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-perl/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-perl/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-perl/tags?name=${VERSION}-arm64) | | |
| | Swift | [\`${DOCKERHUB_IMAGE}-swift:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-swift/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-swift/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-swift/tags?name=${VERSION}-arm64) | | |
| | Lean | [\`${DOCKERHUB_IMAGE}-lean:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-lean/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-lean/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-lean/tags?name=${VERSION}-arm64) | | |
| | Rocq | [\`${DOCKERHUB_IMAGE}-rocq:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rocq/tags?name=${VERSION}) | [\`${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rocq/tags?name=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rocq/tags?name=${VERSION}-arm64) | | |
| ### GitHub Container Registry - Combo Boxes | |
| | Image | Multi-arch | AMD64 | ARM64 | | |
| |-------|------------|-------|-------| | |
| | Full Box | [\`${GHCR_IMAGE}:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box?tag=${VERSION}-arm64) | | |
| | Essentials | [\`${GHCR_IMAGE}-essentials:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-essentials?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-essentials?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-essentials?tag=${VERSION}-arm64) | | |
| | JS | [\`${GHCR_IMAGE}-js:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-js?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-js?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-js?tag=${VERSION}-arm64) | | |
| ### GitHub Container Registry - Language Boxes | |
| | Language | Multi-arch | AMD64 | ARM64 | | |
| |----------|------------|-------|-------| | |
| | Python | [\`${GHCR_IMAGE}-python:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-python?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-python?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-python?tag=${VERSION}-arm64) | | |
| | Go | [\`${GHCR_IMAGE}-go:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-go?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-go?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-go?tag=${VERSION}-arm64) | | |
| | Rust | [\`${GHCR_IMAGE}-rust:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-rust?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-rust?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-rust?tag=${VERSION}-arm64) | | |
| | Java | [\`${GHCR_IMAGE}-java:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-java?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-java?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-java?tag=${VERSION}-arm64) | | |
| | Kotlin | [\`${GHCR_IMAGE}-kotlin:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-kotlin?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-kotlin?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-kotlin?tag=${VERSION}-arm64) | | |
| | Ruby | [\`${GHCR_IMAGE}-ruby:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-ruby?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-ruby?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-ruby?tag=${VERSION}-arm64) | | |
| | PHP | [\`${GHCR_IMAGE}-php:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-php?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-php?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-php?tag=${VERSION}-arm64) | | |
| | Perl | [\`${GHCR_IMAGE}-perl:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-perl?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-perl?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-perl?tag=${VERSION}-arm64) | | |
| | Swift | [\`${GHCR_IMAGE}-swift:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-swift?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-swift?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-swift?tag=${VERSION}-arm64) | | |
| | Lean | [\`${GHCR_IMAGE}-lean:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-lean?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-lean?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-lean?tag=${VERSION}-arm64) | | |
| | Rocq | [\`${GHCR_IMAGE}-rocq:${VERSION}\`](https://github.com/${REPO}/pkgs/container/box-rocq?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/box-rocq?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/box-rocq?tag=${VERSION}-arm64) | | |
| ## Architecture | |
| \`\`\` | |
| JS box (konard/box-js) | |
| → Essentials box (konard/box-essentials) | |
| ├─ box-python ├─ box-go ├─ box-rust | |
| ├─ box-java ├─ box-kotlin ├─ box-ruby | |
| ├─ box-php ├─ box-perl ├─ box-swift | |
| ├─ box-lean └─ box-rocq | |
| → Full box (konard/box) [merges all language images] | |
| \`\`\` | |
| ## Quick Start | |
| Pull multi-arch (auto-selects your platform): | |
| \`\`\`sh | |
| docker pull ${DOCKERHUB_IMAGE}:${VERSION} | |
| \`\`\` | |
| Pull specific architecture: | |
| \`\`\`sh | |
| # AMD64 | |
| docker pull ${DOCKERHUB_IMAGE}:${VERSION}-amd64 | |
| # ARM64 (Apple Silicon, Raspberry Pi, etc.) | |
| docker pull ${DOCKERHUB_IMAGE}:${VERSION}-arm64 | |
| \`\`\` | |
| Pull from GHCR: | |
| \`\`\`sh | |
| docker pull ${GHCR_IMAGE}:${VERSION} | |
| \`\`\` | |
| ## Links | |
| - [Docker Hub](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) | |
| - [GHCR Package](https://github.com/${REPO}/pkgs/container/box) | |
| Released on ${DATE} | |
| ENDOFNOTES | |
| # Append dind-box tables in a separate step to stay under GitHub's | |
| # 21000-char per-step expression limit (issue #80). | |
| - name: Append dind-box tables to release notes | |
| env: | |
| GHCR_REG: ${{ env.GHCR_REGISTRY }} | |
| GHCR_NAME: ${{ env.GHCR_IMAGE_NAME }} | |
| DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE_NAME }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| { | |
| printf '\n### Docker Hub - dind-box (Docker-in-Docker variants, issue #80)\n\n' | |
| printf 'Each variant runs an inner Docker daemon. Run with `docker run --privileged` (default) or `docker run --runtime=sysbox-runc` (recommended for shared hosts). `docker ps -a` inside the container only lists containers created by that container - see [docs/case-studies/issue-80](https://github.com/%s/blob/v%s/docs/case-studies/issue-80/CASE-STUDY.md).\n\n' "$REPO" "$VERSION" | |
| printf '| Image | Multi-arch | AMD64 | ARM64 |\n' | |
| printf '|-------|------------|-------|-------|\n' | |
| for variant in "Full|" "Essentials|-essentials" "JS|-js" "Python|-python" "Go|-go" "Rust|-rust" "Java|-java" "Kotlin|-kotlin" "Ruby|-ruby" "PHP|-php" "Perl|-perl" "Swift|-swift" "Lean|-lean" "Rocq|-rocq"; do | |
| label="${variant%%|*}" | |
| suffix="${variant#*|}" | |
| dh="${DOCKERHUB_IMAGE}${suffix}-dind" | |
| printf '| %s + dind | [`%s:%s`](https://hub.docker.com/r/%s/tags?name=%s) | [`%s-amd64`](https://hub.docker.com/r/%s/tags?name=%s-amd64) | [`%s-arm64`](https://hub.docker.com/r/%s/tags?name=%s-arm64) |\n' \ | |
| "$label" "$dh" "$VERSION" "$dh" "$VERSION" "$VERSION" "$dh" "$VERSION" "$VERSION" "$dh" "$VERSION" | |
| done | |
| printf '\n### GitHub Container Registry - dind-box (Docker-in-Docker variants, issue #80)\n\n' | |
| printf '| Image | Multi-arch | AMD64 | ARM64 |\n' | |
| printf '|-------|------------|-------|-------|\n' | |
| for variant in "Full|box" "Essentials|box-essentials" "JS|box-js" "Python|box-python" "Go|box-go" "Rust|box-rust" "Java|box-java" "Kotlin|box-kotlin" "Ruby|box-ruby" "PHP|box-php" "Perl|box-perl" "Swift|box-swift" "Lean|box-lean" "Rocq|box-rocq"; do | |
| label="${variant%%|*}" | |
| pkg="${variant#*|}-dind" | |
| gh_image="${GHCR_REG}/${GHCR_NAME%/*}/${pkg}" | |
| printf '| %s + dind | [`%s:%s`](https://github.com/%s/pkgs/container/%s?tag=%s) | [`%s-amd64`](https://github.com/%s/pkgs/container/%s?tag=%s-amd64) | [`%s-arm64`](https://github.com/%s/pkgs/container/%s?tag=%s-arm64) |\n' \ | |
| "$label" "$gh_image" "$VERSION" "$REPO" "$pkg" "$VERSION" "$VERSION" "$REPO" "$pkg" "$VERSION" "$VERSION" "$REPO" "$pkg" "$VERSION" | |
| done | |
| } >> /tmp/release-notes.md | |
| - name: Publish GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| if gh release view "v${VERSION}" &>/dev/null; then | |
| echo "Release v${VERSION} already exists, updating..." | |
| gh release edit "v${VERSION}" --title "v${VERSION}" --notes-file /tmp/release-notes.md | |
| else | |
| echo "Creating new release v${VERSION}..." | |
| gh release create "v${VERSION}" --title "v${VERSION}" --notes-file /tmp/release-notes.md | |
| fi | |
| echo "GitHub Release v${VERSION} created/updated successfully" |