Fix ARM64 build timeout by using native ARM64 runner #19
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/**' | |
| - '.github/workflows/release.yml' | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| workflow_dispatch: | |
| inputs: | |
| force_build: | |
| description: 'Force Docker image build' | |
| required: false | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| # === DETECT CHANGES === | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| docker-changed: ${{ steps.changes.outputs.docker }} | |
| scripts-changed: ${{ steps.changes.outputs.scripts }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow }} | |
| should-build: ${{ steps.should-build.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| - 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) | |
| # 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 workflow changes | |
| if echo "$CHANGED_FILES" | grep -qE '^\.github/workflows/'; then | |
| echo "workflow=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "workflow=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Determine if build is needed | |
| id: should-build | |
| run: | | |
| if [ "${{ github.event.inputs.force_build }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| elif [ "${{ steps.changes.outputs.docker }}" = "true" ] || [ "${{ steps.changes.outputs.scripts }}" = "true" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "result=false" >> $GITHUB_OUTPUT | |
| fi | |
| # === BUILD AND TEST DOCKER IMAGE (PR) === | |
| docker-build-test: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.should-build == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build Docker image (test) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: false | |
| tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Test Docker image | |
| run: | | |
| echo "Building image for testing..." | |
| docker build -t sandbox-test . | |
| echo "" | |
| echo "=== Testing installed tools ===" | |
| echo "Note: Using entrypoint script which initializes all environments" | |
| echo "" | |
| echo "Testing Node.js..." | |
| docker run --rm sandbox-test node --version || echo "Node.js test failed" | |
| echo "" | |
| echo "Testing Python..." | |
| docker run --rm sandbox-test python --version || echo "Python test failed" | |
| echo "" | |
| echo "Testing Go..." | |
| docker run --rm sandbox-test go version || echo "Go test failed" | |
| echo "" | |
| echo "Testing Rust..." | |
| docker run --rm sandbox-test rustc --version || echo "Rust test failed" | |
| echo "" | |
| echo "Testing Java..." | |
| docker run --rm sandbox-test java -version || echo "Java test failed" | |
| echo "" | |
| echo "Testing Bun..." | |
| docker run --rm sandbox-test bun --version || echo "Bun test failed" | |
| echo "" | |
| echo "Testing Deno..." | |
| docker run --rm sandbox-test deno --version || echo "Deno test failed" | |
| echo "" | |
| echo "Testing GitHub CLI..." | |
| docker run --rm sandbox-test gh --version || echo "GitHub CLI test failed" | |
| echo "" | |
| echo "Testing Lean..." | |
| docker run --rm sandbox-test lean --version || echo "Lean test failed" | |
| echo "" | |
| echo "Testing Perl..." | |
| docker run --rm sandbox-test perl --version || echo "Perl test failed" | |
| echo "" | |
| echo "Testing PHP..." | |
| docker run --rm sandbox-test php --version || echo "PHP test failed" | |
| echo "" | |
| echo "=== All tests completed ===" | |
| # === BUILD AND PUSH DOCKER IMAGE (MAIN) === | |
| docker-build-push: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest | |
| type=sha,prefix= | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| - name: Build and push Docker image (amd64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # === BUILD AND PUSH ARM64 IMAGE === | |
| # Using native ARM64 runner to avoid QEMU emulation performance issues | |
| # See: docs/case-studies/issue-7/README.md for analysis of the previous timeout | |
| # Reference: https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/ | |
| 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, docker-build-push] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| # Note: QEMU setup is not needed on native ARM64 runner | |
| # Removing this step eliminates the 10-30x performance penalty from emulation | |
| # - name: Set up QEMU | |
| # uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| flavor: | | |
| suffix=-arm64 | |
| tags: | | |
| type=raw,value=latest | |
| type=sha,prefix= | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| - name: Build and push Docker image (arm64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| platforms: linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # === CREATE MULTI-ARCH MANIFEST === | |
| docker-manifest: | |
| runs-on: ubuntu-latest | |
| needs: [docker-build-push, docker-build-push-arm64] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create and push multi-arch manifest | |
| run: | | |
| # Create manifest for latest tag | |
| docker manifest create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ | |
| --amend ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ | |
| --amend ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-arm64 | |
| docker manifest push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest | |
| echo "Multi-arch manifest created and pushed successfully" |