feat: add l2 fork testing framework #1580
Workflow file for this run
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: branch build | |
| on: | |
| push: | |
| branches: | |
| - 'develop' | |
| pull_request: | |
| branches: | |
| - 'develop' | |
| paths: | |
| - 'op-*/**' | |
| - 'cannon/**' | |
| - 'packages/contracts-bedrock/**' | |
| - 'rust/**' | |
| - 'ops/docker/**' | |
| - 'ops/scripts/compute-git-versions.sh' | |
| - 'docker-bake.hcl' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - '.github/workflows/branches.yaml' | |
| - '.github/docker-images.json' | |
| - '.github/actions/docker-build-prep/**' | |
| schedule: | |
| # Daily builds at 2 AM UTC (matches CircleCI schedule) | |
| - cron: '0 2 * * *' | |
| jobs: | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| images_json: ${{ steps.detect.outputs.images_json }} | |
| go_check_images_json: ${{ steps.detect.outputs.go_check_images_json }} | |
| rust_check_images_json: ${{ steps.detect.outputs.rust_check_images_json }} | |
| has_images: ${{ steps.detect.outputs.has_images }} | |
| has_go_check_images: ${{ steps.detect.outputs.has_go_check_images }} | |
| has_rust_check_images: ${{ steps.detect.outputs.has_rust_check_images }} | |
| is_fork: ${{ steps.detect.outputs.is_fork }} | |
| steps: | |
| - name: Harden the runner | |
| uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'schedule' && 'develop' || '' }} | |
| - name: Detect affected images | |
| id: detect | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| THIS_REPO: ${{ github.repository }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| CONFIG=".github/docker-images.json" | |
| # Determine if this is a fork PR | |
| IS_FORK="false" | |
| if [[ "$EVENT_NAME" == "pull_request" ]]; then | |
| if [[ "$HEAD_REPO" != "$THIS_REPO" ]]; then | |
| IS_FORK="true" | |
| fi | |
| fi | |
| echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT" | |
| # For push to develop or scheduled builds, build all images | |
| if [[ "$EVENT_NAME" != "pull_request" ]]; then | |
| echo "Non-PR event ($EVENT_NAME): building all images" | |
| ALL_IMAGES=$(jq -c '[.images | keys[]]' "$CONFIG") | |
| GO_CHECK=$(jq -c '[.images | to_entries[] | select(.value.check == "go") | .key]' "$CONFIG") | |
| RUST_CHECK=$(jq -c '[.images | to_entries[] | select(.value.check == "rust") | .key]' "$CONFIG") | |
| echo "images_json=$ALL_IMAGES" >> "$GITHUB_OUTPUT" | |
| echo "go_check_images_json=$GO_CHECK" >> "$GITHUB_OUTPUT" | |
| echo "rust_check_images_json=$RUST_CHECK" >> "$GITHUB_OUTPUT" | |
| echo "has_images=true" >> "$GITHUB_OUTPUT" | |
| echo "has_go_check_images=true" >> "$GITHUB_OUTPUT" | |
| echo "has_rust_check_images=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # For PRs, detect which files changed (three-dot diff = only the PR's changes) | |
| CHANGED_FILES=$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}" 2>/dev/null || true) | |
| if [[ -z "$CHANGED_FILES" ]]; then | |
| echo "WARNING: Could not detect changed files — building all images as fallback" | |
| ALL_IMAGES=$(jq -c '[.images | keys[]]' "$CONFIG") | |
| GO_CHECK=$(jq -c '[.images | to_entries[] | select(.value.check == "go") | .key]' "$CONFIG") | |
| RUST_CHECK=$(jq -c '[.images | to_entries[] | select(.value.check == "rust") | .key]' "$CONFIG") | |
| echo "images_json=$ALL_IMAGES" >> "$GITHUB_OUTPUT" | |
| echo "go_check_images_json=$GO_CHECK" >> "$GITHUB_OUTPUT" | |
| echo "rust_check_images_json=$RUST_CHECK" >> "$GITHUB_OUTPUT" | |
| echo "has_images=true" >> "$GITHUB_OUTPUT" | |
| echo "has_go_check_images=true" >> "$GITHUB_OUTPUT" | |
| echo "has_rust_check_images=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Changed files:" | |
| echo "$CHANGED_FILES" | head -50 | |
| TOTAL=$(echo "$CHANGED_FILES" | wc -l) | |
| echo "($TOTAL files total)" | |
| # matches_pattern checks if a file matches a path pattern | |
| # "foo/**" matches any file under foo/ | |
| # "foo" matches the exact file foo | |
| matches_pattern() { | |
| local file="$1" | |
| local pattern="$2" | |
| if [[ "$pattern" == *"/**" ]]; then | |
| local prefix="${pattern%/**}" | |
| [[ "$file" == "$prefix/"* ]] && return 0 | |
| else | |
| [[ "$file" == "$pattern" ]] && return 0 | |
| fi | |
| return 1 | |
| } | |
| BUILD_ALL=false | |
| BUILD_ALL_GO=false | |
| # Check shared paths (trigger all images) | |
| SHARED_PATHS=$(jq -r '.shared_paths[]' "$CONFIG") | |
| while IFS= read -r pattern; do | |
| while IFS= read -r file; do | |
| if matches_pattern "$file" "$pattern"; then | |
| echo "Shared path match: $file matches $pattern → building all images" | |
| BUILD_ALL=true | |
| break 2 | |
| fi | |
| done <<< "$CHANGED_FILES" | |
| done <<< "$SHARED_PATHS" | |
| # Check shared Go paths (trigger all Go images) | |
| if [[ "$BUILD_ALL" != "true" ]]; then | |
| SHARED_GO_PATHS=$(jq -r '.shared_go_paths[]' "$CONFIG") | |
| while IFS= read -r pattern; do | |
| while IFS= read -r file; do | |
| if matches_pattern "$file" "$pattern"; then | |
| echo "Shared Go path match: $file matches $pattern → building all Go images" | |
| BUILD_ALL_GO=true | |
| break 2 | |
| fi | |
| done <<< "$CHANGED_FILES" | |
| done <<< "$SHARED_GO_PATHS" | |
| fi | |
| # Collect affected images | |
| declare -A AFFECTED | |
| if [[ "$BUILD_ALL" == "true" ]]; then | |
| # All images affected | |
| while IFS= read -r name; do | |
| AFFECTED["$name"]=1 | |
| done < <(jq -r '.images | keys[]' "$CONFIG") | |
| else | |
| if [[ "$BUILD_ALL_GO" == "true" ]]; then | |
| # All Go images affected | |
| while IFS= read -r name; do | |
| AFFECTED["$name"]=1 | |
| done < <(jq -r '.images | to_entries[] | select(.value.type == "go") | .key' "$CONFIG") | |
| fi | |
| # Check per-image paths for remaining images | |
| while IFS= read -r entry; do | |
| name=$(echo "$entry" | jq -r '.key') | |
| # Skip if already affected | |
| [[ -n "${AFFECTED[$name]+x}" ]] && continue | |
| paths=$(echo "$entry" | jq -r '.value.paths[]') | |
| while IFS= read -r pattern; do | |
| while IFS= read -r file; do | |
| if matches_pattern "$file" "$pattern"; then | |
| echo "Image path match: $file matches $pattern → building $name" | |
| AFFECTED["$name"]=1 | |
| break 2 | |
| fi | |
| done <<< "$CHANGED_FILES" | |
| done <<< "$paths" | |
| done < <(jq -c '.images | to_entries[]' "$CONFIG") | |
| fi | |
| # Build output arrays | |
| AFFECTED_LIST=$(printf '%s\n' "${!AFFECTED[@]}" | sort) | |
| IMAGES_JSON="[]" | |
| GO_CHECK_JSON="[]" | |
| RUST_CHECK_JSON="[]" | |
| if [[ -n "$AFFECTED_LIST" ]]; then | |
| IMAGES_JSON=$(echo "$AFFECTED_LIST" | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| GO_CHECK_JSON=$(echo "$AFFECTED_LIST" | while IFS= read -r name; do | |
| [[ -z "$name" ]] && continue | |
| check=$(jq -r --arg n "$name" '.images[$n].check' "$CONFIG") | |
| if [[ "$check" == "go" ]]; then echo "$name"; fi | |
| done | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| RUST_CHECK_JSON=$(echo "$AFFECTED_LIST" | while IFS= read -r name; do | |
| [[ -z "$name" ]] && continue | |
| check=$(jq -r --arg n "$name" '.images[$n].check' "$CONFIG") | |
| if [[ "$check" == "rust" ]]; then echo "$name"; fi | |
| done | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| fi | |
| echo "Affected images: $IMAGES_JSON" | |
| echo "Go check images: $GO_CHECK_JSON" | |
| echo "Rust check images: $RUST_CHECK_JSON" | |
| echo "images_json=$IMAGES_JSON" >> "$GITHUB_OUTPUT" | |
| echo "go_check_images_json=$GO_CHECK_JSON" >> "$GITHUB_OUTPUT" | |
| echo "rust_check_images_json=$RUST_CHECK_JSON" >> "$GITHUB_OUTPUT" | |
| HAS_IMAGES=$( [[ $(echo "$IMAGES_JSON" | jq 'length') -gt 0 ]] && echo "true" || echo "false" ) | |
| HAS_GO=$( [[ $(echo "$GO_CHECK_JSON" | jq 'length') -gt 0 ]] && echo "true" || echo "false" ) | |
| HAS_RUST=$( [[ $(echo "$RUST_CHECK_JSON" | jq 'length') -gt 0 ]] && echo "true" || echo "false" ) | |
| echo "has_images=$HAS_IMAGES" >> "$GITHUB_OUTPUT" | |
| echo "has_go_check_images=$HAS_GO" >> "$GITHUB_OUTPUT" | |
| echo "has_rust_check_images=$HAS_RUST" >> "$GITHUB_OUTPUT" | |
| prep: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| versions: ${{ steps.prep.outputs.versions }} | |
| date: ${{ steps.prep.outputs.date }} | |
| steps: | |
| - name: Harden the runner | |
| uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'schedule' && 'develop' || '' }} | |
| - uses: ./.github/actions/docker-build-prep | |
| id: prep | |
| build: | |
| needs: [prep, detect-changes] | |
| if: | | |
| needs.detect-changes.outputs.is_fork == 'false' | |
| && needs.detect-changes.outputs.has_images == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image_name: ${{ fromJson(needs.detect-changes.outputs.images_json) }} | |
| uses: ethereum-optimism/factory/.github/workflows/docker.yaml@f8f3cb4800e538003134fb5f50cc734c2c98d762 | |
| with: | |
| mode: bake | |
| image_name: ${{ matrix.image_name }} | |
| bake_file: docker-bake.hcl | |
| target: ${{ matrix.image_name }} | |
| gcp_project_id: ${{ vars.GCP_PROJECT_ID_OPLABS_TOOLS_ARTIFACTS }} | |
| registry: us-docker.pkg.dev/oplabs-tools-artifacts/images | |
| env: | | |
| GIT_VERSION=${{ fromJson(needs.prep.outputs.versions)[matrix.image_name] }} | |
| set: | | |
| *.args.GIT_COMMIT=${{ github.sha }} | |
| *.args.GIT_DATE=${{ needs.prep.outputs.date }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| build-fork: | |
| needs: [prep, detect-changes] | |
| if: | | |
| needs.detect-changes.outputs.is_fork == 'true' | |
| && needs.detect-changes.outputs.has_images == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image_name: ${{ fromJson(needs.detect-changes.outputs.images_json) }} | |
| uses: ethereum-optimism/factory/.github/workflows/docker.yaml@f8f3cb4800e538003134fb5f50cc734c2c98d762 | |
| with: | |
| mode: bake | |
| image_name: ${{ matrix.image_name }} | |
| bake_file: docker-bake.hcl | |
| target: ${{ matrix.image_name }} | |
| tag: 24h | |
| registry: ttl.sh/${{ github.sha }} | |
| env: | | |
| GIT_VERSION=${{ fromJson(needs.prep.outputs.versions)[matrix.image_name] }} | |
| set: | | |
| *.args.GIT_COMMIT=${{ github.sha }} | |
| *.args.GIT_DATE=${{ needs.prep.outputs.date }} | |
| permissions: | |
| contents: read | |
| check-cross-platform: | |
| needs: [build, build-fork, detect-changes] | |
| if: | | |
| always() | |
| && needs.detect-changes.outputs.has_go_check_images == 'true' | |
| && (needs.build.result == 'success' || needs.build-fork.result == 'success') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image_name: ${{ fromJson(needs.detect-changes.outputs.go_check_images_json) }} | |
| runner: | |
| - ubuntu-24.04 | |
| - ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| IMAGE: ${{ needs.build-fork.result == 'success' && format('ttl.sh/{0}/{1}:24h', github.sha, matrix.image_name) || format('us-docker.pkg.dev/oplabs-tools-artifacts/images/{0}:{1}', matrix.image_name, github.sha) }} | |
| steps: | |
| - name: Run image | |
| env: | |
| IMAGE_NAME: ${{ matrix.image_name }} | |
| run: docker run "$IMAGE" "$IMAGE_NAME" --version | |
| # Separate cross-platform check for Rust images (they use ENTRYPOINT instead of CMD) | |
| check-cross-platform-rust: | |
| needs: [build, build-fork, detect-changes] | |
| if: | | |
| always() | |
| && needs.detect-changes.outputs.has_rust_check_images == 'true' | |
| && (needs.build.result == 'success' || needs.build-fork.result == 'success') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image_name: ${{ fromJson(needs.detect-changes.outputs.rust_check_images_json) }} | |
| runner: | |
| - ubuntu-24.04 | |
| - ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| IMAGE: ${{ needs.build-fork.result == 'success' && format('ttl.sh/{0}/{1}:24h', github.sha, matrix.image_name) || format('us-docker.pkg.dev/oplabs-tools-artifacts/images/{0}:{1}', matrix.image_name, github.sha) }} | |
| steps: | |
| - name: Run image | |
| run: docker run "$IMAGE" --version | |
| # Gate job for branch protection — provides a stable check name regardless of dynamic matrix contents | |
| docker-build-result: | |
| needs: [detect-changes, build, build-fork, check-cross-platform, check-cross-platform-rust] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check results | |
| env: | |
| DETECT_RESULT: ${{ needs.detect-changes.result }} | |
| HAS_IMAGES: ${{ needs.detect-changes.outputs.has_images }} | |
| IS_FORK: ${{ needs.detect-changes.outputs.is_fork }} | |
| BUILD_RESULT: ${{ needs.build.result }} | |
| BUILD_FORK_RESULT: ${{ needs.build-fork.result }} | |
| HAS_GO: ${{ needs.detect-changes.outputs.has_go_check_images }} | |
| HAS_RUST: ${{ needs.detect-changes.outputs.has_rust_check_images }} | |
| GO_CHECK_RESULT: ${{ needs.check-cross-platform.result }} | |
| RUST_CHECK_RESULT: ${{ needs.check-cross-platform-rust.result }} | |
| run: | | |
| # Fail if detect-changes itself failed (otherwise empty outputs silently pass) | |
| if [[ "$DETECT_RESULT" != "success" ]]; then | |
| echo "detect-changes failed: $DETECT_RESULT" | |
| exit 1 | |
| fi | |
| # If no images needed building, that's a success | |
| if [[ "$HAS_IMAGES" != "true" ]]; then | |
| echo "No images needed building — success" | |
| exit 0 | |
| fi | |
| # Check that the applicable build job succeeded | |
| if [[ "$IS_FORK" == "true" ]]; then | |
| RESULT="$BUILD_FORK_RESULT" | |
| else | |
| RESULT="$BUILD_RESULT" | |
| fi | |
| if [[ "$RESULT" != "success" ]]; then | |
| echo "Build failed with result: $RESULT" | |
| exit 1 | |
| fi | |
| # Check cross-platform results (they use always() so check explicitly) | |
| if [[ "$HAS_GO" == "true" ]]; then | |
| if [[ "$GO_CHECK_RESULT" != "success" ]]; then | |
| echo "Go cross-platform check failed: $GO_CHECK_RESULT" | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "$HAS_RUST" == "true" ]]; then | |
| if [[ "$RUST_CHECK_RESULT" != "success" ]]; then | |
| echo "Rust cross-platform check failed: $RUST_CHECK_RESULT" | |
| exit 1 | |
| fi | |
| fi | |
| echo "All builds and checks passed" |