Merge pull request #56 from link-foundation/issue-55-dc5914da7ab7 #156
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 | |
| workflow_dispatch: | |
| inputs: | |
| release_mode: | |
| description: 'Release mode' | |
| required: true | |
| type: choice | |
| default: 'build-only' | |
| options: | |
| - build-only | |
| - bump-and-release | |
| 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/sandbox | |
| 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@v4 | |
| 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@v4 | |
| 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@v4 | |
| 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@v4 | |
| 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 }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| 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 sandbox 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 sandbox changes | |
| if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/essentials-sandbox/'; then | |
| echo "essentials=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "essentials=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Full sandbox changes (full-sandbox dir, root Dockerfile, or scripts) | |
| if echo "$CHANGED_FILES" | grep -qE '^(ubuntu/24\.04/full-sandbox/|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 | |
| - 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 with bump-and-release, always build | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| if [ "${{ github.event.inputs.release_mode }}" = "bump-and-release" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: manual bump-and-release" | |
| exit 0 | |
| elif [ "${{ github.event.inputs.release_mode }}" = "build-only" ]; then | |
| echo "result=true" >> $GITHUB_OUTPUT | |
| echo "Build triggered by: manual build-only" | |
| exit 0 | |
| fi | |
| 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" | |
| else | |
| echo "result=false" >> $GITHUB_OUTPUT | |
| echo "No build needed: no relevant changes detected" | |
| fi | |
| # === BUILD AND TEST DOCKER IMAGE (PR) === | |
| docker-build-test: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, version-check, changeset-check] | |
| # Use always() to prevent implicit success() check from skipping this job | |
| # when upstream jobs are skipped (see docs/case-studies/issue-23) | |
| 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@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build layered images (JS -> essentials -> languages -> full) | |
| run: | | |
| echo "=== Building JS sandbox ===" | |
| docker build -f ubuntu/24.04/js/Dockerfile -t sandbox-js . | |
| echo "" | |
| echo "=== Building essentials sandbox (on JS) ===" | |
| docker build -f ubuntu/24.04/essentials-sandbox/Dockerfile \ | |
| --build-arg JS_IMAGE=sandbox-js -t sandbox-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} sandbox ---" | |
| docker build -f "ubuntu/24.04/${lang}/Dockerfile" \ | |
| --build-arg ESSENTIALS_IMAGE=sandbox-essentials \ | |
| -t "sandbox-${lang}" . | |
| done | |
| echo "" | |
| echo "=== Building full sandbox (multi-stage from all language images) ===" | |
| docker build -f ubuntu/24.04/full-sandbox/Dockerfile \ | |
| --build-arg ESSENTIALS_IMAGE=sandbox-essentials \ | |
| --build-arg PYTHON_IMAGE=sandbox-python \ | |
| --build-arg GO_IMAGE=sandbox-go \ | |
| --build-arg RUST_IMAGE=sandbox-rust \ | |
| --build-arg JAVA_IMAGE=sandbox-java \ | |
| --build-arg KOTLIN_IMAGE=sandbox-kotlin \ | |
| --build-arg RUBY_IMAGE=sandbox-ruby \ | |
| --build-arg PHP_IMAGE=sandbox-php \ | |
| --build-arg PERL_IMAGE=sandbox-perl \ | |
| --build-arg SWIFT_IMAGE=sandbox-swift \ | |
| --build-arg LEAN_IMAGE=sandbox-lean \ | |
| --build-arg ROCQ_IMAGE=sandbox-rocq \ | |
| -t sandbox-test . | |
| - name: Test JS sandbox | |
| run: | | |
| echo "=== Testing JS sandbox ===" | |
| docker run --rm sandbox-js bash -c '. $HOME/.nvm/nvm.sh && node --version' || echo "Node.js test failed" | |
| docker run --rm sandbox-js bash -c 'export PATH=$HOME/.bun/bin:$PATH && bun --version' || echo "Bun test failed" | |
| docker run --rm sandbox-js bash -c 'export PATH=$HOME/.deno/bin:$PATH && deno --version' || echo "Deno test failed" | |
| echo "=== JS sandbox tests completed ===" | |
| - name: Test essentials sandbox | |
| run: | | |
| echo "=== Testing essentials sandbox ===" | |
| docker run --rm sandbox-essentials gh --version || echo "GitHub CLI test failed" | |
| docker run --rm sandbox-essentials glab --version || echo "GitLab CLI test failed" | |
| docker run --rm sandbox-essentials gh-setup-git-identity --version || echo "gh-setup-git-identity test failed" | |
| docker run --rm sandbox-essentials glab-setup-git-identity --version || echo "glab-setup-git-identity test failed" | |
| echo "=== Essentials sandbox tests completed ===" | |
| - name: Test full sandbox | |
| run: | | |
| echo "=== Testing full sandbox ===" | |
| echo "Note: Using entrypoint script which initializes all environments" | |
| docker run --rm sandbox-test node --version || echo "Node.js test failed" | |
| docker run --rm sandbox-test python --version || echo "Python test failed" | |
| docker run --rm sandbox-test go version || echo "Go test failed" | |
| docker run --rm sandbox-test rustc --version || echo "Rust test failed" | |
| docker run --rm sandbox-test java -version || echo "Java test failed" | |
| docker run --rm sandbox-test bun --version || echo "Bun test failed" | |
| docker run --rm sandbox-test deno --version || echo "Deno test failed" | |
| docker run --rm sandbox-test gh --version || echo "GitHub CLI test failed" | |
| docker run --rm sandbox-test glab --version || echo "GitLab CLI test failed" | |
| docker run --rm sandbox-test gh-setup-git-identity --version || echo "gh-setup-git-identity test failed" | |
| docker run --rm sandbox-test glab-setup-git-identity --version || echo "glab-setup-git-identity test failed" | |
| docker run --rm sandbox-test lean --version || echo "Lean test failed" | |
| docker run --rm sandbox-test perl --version || echo "Perl test failed" | |
| docker run --rm sandbox-test php --version || echo "PHP test failed" | |
| echo "" | |
| echo "=== PHP install method check ===" | |
| docker run --rm sandbox-php cat /home/sandbox/.php-install-method || echo "PHP method marker not found" | |
| docker run --rm sandbox-test cat /home/sandbox/.php-install-method || echo "PHP method marker not found in full sandbox" | |
| echo "" | |
| echo "=== All tests completed ===" | |
| # === BUILD JS SANDBOX (amd64) === | |
| # JS sandbox 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@v4 | |
| 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: 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.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push JS sandbox (amd64) | |
| uses: docker/build-push-action@v5 | |
| 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 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === BUILD JS SANDBOX (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@v4 | |
| 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: 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.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push JS sandbox (arm64) | |
| uses: docker/build-push-action@v5 | |
| 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 | |
| - 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@v4 | |
| 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@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create and push JS multi-arch manifests | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # GHCR | |
| 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} | |
| # Docker Hub | |
| 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 sandbox multi-arch manifests pushed for latest and ${VERSION}" | |
| # === BUILD ESSENTIALS SANDBOX (amd64) === | |
| # Built on top of JS sandbox - 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@v4 | |
| 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: 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.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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 sandbox (amd64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/essentials-sandbox/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 | |
| - name: Mark as built | |
| id: result | |
| run: echo "built=true" >> $GITHUB_OUTPUT | |
| # === BUILD ESSENTIALS SANDBOX (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@v4 | |
| 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: 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.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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 sandbox (arm64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/essentials-sandbox/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 | |
| - 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@v4 | |
| 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@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create and push essentials multi-arch manifests | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # GHCR | |
| 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} | |
| # Docker Hub | |
| 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 sandbox multi-arch manifests pushed for latest and ${VERSION}" | |
| # === BUILD LANGUAGE IMAGES (amd64) === | |
| # All language images are built in parallel on top of essentials sandbox | |
| 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@v4 | |
| with: | |
| ref: main | |
| - 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: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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 }} sandbox (amd64) | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v5 | |
| 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 | |
| # 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/sandbox/.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@v4 | |
| with: | |
| ref: main | |
| - 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: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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 }} sandbox (arm64) | |
| if: steps.check-lang.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v5 | |
| 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 | |
| # 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/sandbox/.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@v4 | |
| 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@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create and push ${{ matrix.language }} multi-arch manifests | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| LANG="${{ matrix.language }}" | |
| # GHCR | |
| 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} | |
| # Docker Hub | |
| 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} sandbox multi-arch manifests pushed for latest and ${VERSION}" | |
| # === BUILD AND PUSH FULL SANDBOX (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@v4 | |
| 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: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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@v5 | |
| 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@v5 | |
| 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 sandbox (amd64) | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/full-sandbox/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 | |
| # === 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@v4 | |
| 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: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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@v5 | |
| 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 sandbox (arm64) | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ubuntu/24.04/full-sandbox/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 | |
| # === 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@v4 | |
| 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@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - 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) | |
| 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}" | |
| # === CREATE GITHUB RELEASE === | |
| create-release: | |
| runs-on: ubuntu-24.04 | |
| needs: [detect-changes, docker-manifest, js-manifest, essentials-manifest, languages-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@v4 | |
| 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 Sandboxes | |
| | Image | Multi-arch | AMD64 | ARM64 | | |
| |-------|------------|-------|-------| | |
| | Full Sandbox | [\`${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 Sandboxes | |
| | 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 Sandboxes | |
| | Image | Multi-arch | AMD64 | ARM64 | | |
| |-------|------------|-------|-------| | |
| | Full Sandbox | [\`${GHCR_IMAGE}:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox?tag=${VERSION}-arm64) | | |
| | Essentials | [\`${GHCR_IMAGE}-essentials:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-essentials?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-essentials?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-essentials?tag=${VERSION}-arm64) | | |
| | JS | [\`${GHCR_IMAGE}-js:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-js?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-js?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-js?tag=${VERSION}-arm64) | | |
| ### GitHub Container Registry - Language Sandboxes | |
| | Language | Multi-arch | AMD64 | ARM64 | | |
| |----------|------------|-------|-------| | |
| | Python | [\`${GHCR_IMAGE}-python:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-python?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-python?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-python?tag=${VERSION}-arm64) | | |
| | Go | [\`${GHCR_IMAGE}-go:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-go?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-go?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-go?tag=${VERSION}-arm64) | | |
| | Rust | [\`${GHCR_IMAGE}-rust:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-rust?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-rust?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-rust?tag=${VERSION}-arm64) | | |
| | Java | [\`${GHCR_IMAGE}-java:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-java?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-java?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-java?tag=${VERSION}-arm64) | | |
| | Kotlin | [\`${GHCR_IMAGE}-kotlin:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-kotlin?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-kotlin?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-kotlin?tag=${VERSION}-arm64) | | |
| | Ruby | [\`${GHCR_IMAGE}-ruby:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-ruby?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-ruby?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-ruby?tag=${VERSION}-arm64) | | |
| | PHP | [\`${GHCR_IMAGE}-php:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-php?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-php?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-php?tag=${VERSION}-arm64) | | |
| | Perl | [\`${GHCR_IMAGE}-perl:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-perl?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-perl?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-perl?tag=${VERSION}-arm64) | | |
| | Swift | [\`${GHCR_IMAGE}-swift:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-swift?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-swift?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-swift?tag=${VERSION}-arm64) | | |
| | Lean | [\`${GHCR_IMAGE}-lean:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-lean?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-lean?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-lean?tag=${VERSION}-arm64) | | |
| | Rocq | [\`${GHCR_IMAGE}-rocq:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-rocq?tag=${VERSION}) | [\`${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox-rocq?tag=${VERSION}-amd64) | [\`${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox-rocq?tag=${VERSION}-arm64) | | |
| ## Architecture | |
| \`\`\` | |
| JS sandbox (konard/sandbox-js) | |
| → Essentials sandbox (konard/sandbox-essentials) | |
| ├─ sandbox-python ├─ sandbox-go ├─ sandbox-rust | |
| ├─ sandbox-java ├─ sandbox-kotlin ├─ sandbox-ruby | |
| ├─ sandbox-php ├─ sandbox-perl ├─ sandbox-swift | |
| ├─ sandbox-lean └─ sandbox-rocq | |
| → Full sandbox (konard/sandbox) [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/sandbox) | |
| Released on ${DATE} | |
| ENDOFNOTES | |
| # Check if release already exists | |
| 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" |