diff --git a/.github/scripts/build-matrix.sh b/.github/scripts/build-matrix.sh index 92cc501..092a94d 100755 --- a/.github/scripts/build-matrix.sh +++ b/.github/scripts/build-matrix.sh @@ -2,16 +2,22 @@ set -euo pipefail # Builds a dynamic GitHub Actions matrix for run-samples.yml. -# Usage: build-matrix.sh [base_sha] +# Usage: build-matrix.sh [base_sha] [arch_filter] # "all" runs every test; "changed" only runs tests whose watch_folders have modified files RUN_MODE="${1:-all}" BASE_SHA="${2:-}" +# "both" runs each test on every architecture it supports; "amd64"/"arm64" restrict it to one +ARCH_FILTER="${3:-both}" +case "$ARCH_FILTER" in + both | amd64 | arm64) ;; + *) echo "Invalid arch filter: '$ARCH_FILTER' (expected both, amd64 or arm64)" >&2; exit 1 ;; +esac # Get JSON metadata for all tests from run-samples.sh --list TEST_META=$(./run-samples.sh --list) TOTAL=$(echo "$TEST_META" | jq length) -echo "Run mode: $RUN_MODE | Total tests: $TOTAL" +echo "Run mode: $RUN_MODE | Arch filter: $ARCH_FILTER | Total tests: $TOTAL" # Changes to these files affect all tests, so any modification triggers a full run INFRA_FILES="run-samples.sh Makefile .github/workflows/run-samples.yml .github/scripts/build-matrix.sh pyproject.toml requirements-dev.txt requirements-runtime.txt" @@ -53,15 +59,37 @@ fi # Output the matrix JSON for GitHub Actions if [[ -z "${INDICES:-}" ]]; then + MATRIX='{"include":[]}' +else + # Convert space-separated indices to JSON array, then build the matrix object. + # Each selected test is expanded into one entry per architecture it supports, so a + # test that runs on both arches becomes two independent jobs. Tests that declare only + # amd64 depend on an amd64-only container image — see run-samples.sh for the details. + IDX_JSON=$(echo "$INDICES" | tr ' ' '\n' | jq -R 'tonumber' | jq -s '.') + MATRIX=$(echo "$TEST_META" | jq -c --argjson idx "$IDX_JSON" --arg filter "$ARCH_FILTER" \ + '{include: [ + $idx[] as $i | .[$i] as $test | + $test.arches[] | + select($filter == "both" or . == $filter) | + { + shard: $test.shard, + splits: $test.splits, + arch: ., + runner: (if . == "arm64" then "ubuntu-22.04-arm" else "ubuntu-22.04" end), + name: ($test.name + " [" + . + "]") + } + ]}') +fi + +# An arch filter can select tests that support no matching architecture, so decide +# has_tests from the expanded matrix rather than from the selected indices. +JOB_COUNT=$(echo "$MATRIX" | jq '.include | length') +if [[ "$JOB_COUNT" -eq 0 ]]; then echo "No tests to run." echo "has_tests=false" >> "$GITHUB_OUTPUT" echo 'matrix={"include":[]}' >> "$GITHUB_OUTPUT" else echo "has_tests=true" >> "$GITHUB_OUTPUT" - # Convert space-separated indices to JSON array, then build the matrix object - IDX_JSON=$(echo "$INDICES" | tr ' ' '\n' | jq -R 'tonumber' | jq -s '.') - MATRIX=$(echo "$TEST_META" | jq -c --argjson idx "$IDX_JSON" \ - '{include: [$idx[] as $i | .[$i] | {shard, splits, name}]}') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" - echo "Matrix:" && echo "$MATRIX" | jq . + echo "Matrix ($JOB_COUNT jobs):" && echo "$MATRIX" | jq . fi diff --git a/.github/workflows/run-samples.yml b/.github/workflows/run-samples.yml index 659eb24..efd07da 100644 --- a/.github/workflows/run-samples.yml +++ b/.github/workflows/run-samples.yml @@ -20,10 +20,20 @@ on: - all - changed default: changed + arch: + description: "Architecture: 'both' runs every test on each architecture it supports" + required: false + type: choice + options: + - both + - amd64 + - arm64 + default: both -# Default run mode for pull_request events (change to 'changed' to only run affected tests) +# Defaults for pull_request events (change DEFAULT_RUN_MODE to 'changed' to only run affected tests) env: DEFAULT_RUN_MODE: changed + DEFAULT_ARCH: both # Least-privilege default: no permissions unless a job explicitly opts in. permissions: {} @@ -48,14 +58,17 @@ jobs: - name: Build dynamic matrix id: build-matrix env: - # Untrusted dispatch input routed via env, not interpolated into the shell (template-injection) + # Untrusted dispatch inputs routed via env, not interpolated into the shell (template-injection) RUN_MODE_INPUT: ${{ github.event.inputs.run_mode }} + ARCH_INPUT: ${{ github.event.inputs.arch }} run: | - # Pick run mode: manual dispatch uses the dropdown, PRs use the env default + # Pick run mode and architecture: manual dispatch uses the dropdowns, PRs use the env defaults if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then RUN_MODE="${RUN_MODE_INPUT}" + ARCH="${ARCH_INPUT}" else RUN_MODE="${{ env.DEFAULT_RUN_MODE }}" + ARCH="${{ env.DEFAULT_ARCH }}" fi # In "changed" mode, determine the base commit to diff against @@ -68,9 +81,9 @@ jobs: fi fi - .github/scripts/build-matrix.sh "$RUN_MODE" "$BASE_SHA" + .github/scripts/build-matrix.sh "$RUN_MODE" "$BASE_SHA" "$ARCH" - # Each test runs in its own job — one matrix entry per test from the setup job + # Each test runs in its own job — one matrix entry per test/architecture from the setup job scripts: name: "Test: ${{ matrix.name }}" needs: setup @@ -79,7 +92,9 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} - runs-on: ubuntu-22.04 + # ubuntu-22.04 for amd64, ubuntu-22.04-arm for arm64 — the matrix carries the label. + # GitHub-hosted arm64 runners are free for public repositories. + runs-on: ${{ matrix.runner }} permissions: contents: read @@ -162,10 +177,14 @@ jobs: - name: Start LocalStack # Run the emulator in detached mode using the virtual environment. + # The readiness wait is generous because the architecture matrix roughly doubles + # the number of concurrent jobs, and the contention for Docker pulls and runner + # I/O pushed some starts past a 120s budget. A healthy emulator still returns as + # soon as it is ready, so a larger timeout costs nothing on the happy path. run: | source .venv/bin/activate python -m localstack_cli.cli.main start -d - python -m localstack_cli.cli.main wait -t 120 + python -m localstack_cli.cli.main wait -t 300 env: IMAGE_NAME: ${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} LOCALSTACK_AUTH_TOKEN: ${{ secrets.TEST_LOCALSTACK_AUTH_TOKEN }} @@ -198,6 +217,9 @@ jobs: - name: Install MSSQL ODBC and Tools # Required for the 'web-app-sql-database' sample which uses 'sqlcmd' to # initialize and verify the database schema in the local emulator. + # amd64 only: the sample is backed by mcr.microsoft.com/mssql/server, which has + # no arm64 image, so it never runs on the arm64 runners. + if: matrix.arch == 'amd64' run: | sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc diff --git a/README.md b/README.md index ba70232..8634f4e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,38 @@ export LOCALSTACK_AUTH_TOKEN= ./run-samples.sh ``` +### Architecture support (amd64 / arm64) + +Some samples run **natively** on `arm64` (Apple Silicon, arm64 CI runners); the rest depend on +container images Microsoft publishes for `amd64` alone, so there is no `arm64` image to pull. + +| Sample | Native amd64 | Native arm64 | Backing image | +| --- | :---: | :---: | --- | +| `function-app-*` | ✅ | ✅ | built from a multi-arch `python` / `node` / `dotnet` base | +| `web-app-custom-image` | ✅ | ✅ | the image the sample builds itself | +| `aci-blob-storage` | ✅ | ✅ | the image the sample builds itself | +| `web-app-*` (code deployment) | ✅ | emulated | `mcr.microsoft.com/oryx/` | +| `eventhubs` | ✅ | emulated | deploys a dashboard web app (Oryx, as above) | +| `servicebus/java` | ✅ | emulated | `mcr.microsoft.com/azure-app-service/java` | +| `web-app-sql-database` | ✅ | emulated | `mcr.microsoft.com/mssql/server` | + +**"emulated" does not mean broken.** The emulator never pins `--platform`, so on an `arm64` host +Docker pulls the `amd64` image and runs it under whatever emulation the runtime provides. Docker +Desktop on Apple Silicon does this via Rosetta, so these samples do work on a Mac — just more +slowly. They fail only on an `arm64` host with no emulation registered, such as GitHub's +`ubuntu-*-arm` runners, which is why CI schedules only the natively-supported samples on `arm64`. + +`run-samples.sh` warns when it runs one of these on an `arm64` host; set `SKIP_AMD64_ONLY=1` to skip +them instead. The authoritative list lives in `ARM64_SAMPLE_DIRS` in [run-samples.sh](./run-samples.sh); +the CI matrix is generated from it, so adding a sample there is all that is needed to have it +covered natively on both architectures. + +> **Note:** Function Apps on `arm64` were fixed by +> [localstack-pro#8102](https://github.com/localstack/localstack-pro/pull/8102) and work on the +> current `localstack/localstack-azure:latest`. If you pin an older emulator image, expect every +> Function App deployment to fail with a misleading `500 ... No route to host`: those images +> unpack `amd64` Azure Functions Core Tools into an `arm64` image, so the host process cannot run. + ### Troubleshooting: Line Endings If you encounter errors like `invalid option name` or `: command not found` when running on Linux/WSL, it's likely due to Windows-style line endings (CRLF). You can fix this by running: ```bash diff --git a/run-samples.sh b/run-samples.sh index ad060d3..a175385 100755 --- a/run-samples.sh +++ b/run-samples.sh @@ -78,9 +78,82 @@ BICEP_SAMPLES=( ALL_SAMPLES=("${SAMPLES[@]}" "${TERRAFORM_SAMPLES[@]}" "${BICEP_SAMPLES[@]}") TOTAL=${#ALL_SAMPLES[@]} +# 1c. Architecture support +# Samples listed here run *natively* on both amd64 and arm64 (Apple Silicon, arm64 CI +# runners). For everything else, the container image the emulator starts is published by +# Microsoft for amd64 alone — there is no arm64 manifest: +# - `az webapp create --runtime ...` (code deployment) runs the app on +# mcr.microsoft.com/oryx/, which is amd64-only. That rules out every +# web-app-* sample, plus eventhubs (it deploys a dashboard web app). +# - Java App Service uses mcr.microsoft.com/azure-app-service/java — amd64-only. +# - SQL Database is backed by mcr.microsoft.com/mssql/server — amd64-only. +# The samples below avoid all of those: Function Apps get an image built from a +# multi-arch base (arm64 support added in localstack-pro#8102), and the custom-image +# Web App and ACI samples run an image the sample itself builds. Their Cosmos DB, +# Service Bus, Storage and Front Door dependencies all publish arm64 manifests. +# +# "amd64-only" means *not native* — not "cannot run". The emulator never pins +# --platform, so on an arm64 host Docker pulls the amd64 manifest and runs it under +# whatever emulation the runtime provides. Docker Desktop on Apple Silicon emulates it +# via Rosetta, which is why these samples do work on a Mac, just more slowly. They fail +# on an arm64 host with no emulation registered, such as GitHub's ubuntu-*-arm runners, +# so CI schedules only the natively-supported samples below on arm64. +# +# LocalStack's own test suite draws the same line: after localstack-pro#8102, Web App +# tests that only assert SCM/deployment records run on arm64, while the ones that +# exercise a *running* Oryx app (test_deploy_zip_that_accesses_storage, +# test_deploy_zip_without_basic_auth) are still marked @only_on_amd64. +ARM64_SAMPLE_DIRS=( + "samples/aci-blob-storage/python" + "samples/function-app-front-door/python" + "samples/function-app-managed-identity/python" + "samples/function-app-service-bus/dotnet" + "samples/function-app-storage-http/dotnet" + "samples/web-app-custom-image/python" +) + +# Fail loudly if an entry no longer matches a registered sample: a rename would otherwise +# silently drop that sample's arm64 coverage while CI stays green. +for _arm64_dir in "${ARM64_SAMPLE_DIRS[@]}"; do + if ! printf '%s\n' "${ALL_SAMPLES[@]}" | cut -d'|' -f1 | sed -E 's#/(terraform|bicep)$##' | grep -qxF "$_arm64_dir"; then + echo >&2 "ARM64_SAMPLE_DIRS entry '$_arm64_dir' matches no registered sample - fix the path or remove the entry." + exit 1 + fi +done + +# Architecture support is a property of the sample itself, not of the deployment method, +# so strip the /terraform or /bicep suffix before matching against ARM64_SAMPLE_DIRS. +supports_arm64() { + local path="$1" family dir + case "$path" in + */terraform | */bicep) family=$(dirname "$path") ;; + *) family="$path" ;; + esac + for dir in "${ARM64_SAMPLE_DIRS[@]}"; do + [[ "$dir" == "$family" ]] && return 0 + done + return 1 +} + +# Normalise `uname -m` to the architecture names used above. Anything not recognisably +# arm64 is reported as amd64, so an exotic host (armv7l, ppc64le, ...) takes the amd64 +# path and gets no arm64 warning: it errs toward "run it and let the deployment fail" +# rather than skipping silently. +host_arch() { + case "$(uname -m)" in + aarch64 | arm64) echo "arm64" ;; + x86_64 | amd64) echo "amd64" ;; + *) + echo >&2 "Note: unrecognised architecture '$(uname -m)'; treating it as amd64." + echo "amd64" + ;; + esac +} + # 2. Handle --list flag: output JSON metadata for CI matrix generation (no tools required) -# Each entry has: shard (1-based index), splits (total count), name, and watch_folders. -# CI uses watch_folders to detect which tests are affected by changed files. +# Each entry has: shard (1-based index), splits (total count), name, watch_folders, +# and arches. CI uses watch_folders to detect which tests are affected by changed +# files, and arches to decide which architectures to run the test on. if [[ "${1:-}" == "--list" ]]; then echo "[" for (( i=0; i /dev/null echo "Deploying..."