Skip to content

Merge pull request #56 from link-foundation/issue-55-dc5914da7ab7 #156

Merge pull request #56 from link-foundation/issue-55-dc5914da7ab7

Merge pull request #56 from link-foundation/issue-55-dc5914da7ab7 #156

Workflow file for this run

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"