From 1ff0dcb3ea2a9ce520d8af5e7f96025a65e8ab98 Mon Sep 17 00:00:00 2001 From: "Dris.S" <53239451+DrisDary@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:19:49 +0100 Subject: [PATCH 1/3] Run samples on arm64 as well as amd64 in CI The Samples CI only ever ran on amd64, so nothing caught the Azure Functions arm64 breakage that a reviewer hit on Apple Silicon (fixed in localstack-pro#8102). Add an architecture axis to the test matrix. Not every sample can run natively on arm64. The emulator starts these samples on container images Microsoft publishes for amd64 only, with no arm64 manifest to pull: - mcr.microsoft.com/oryx/ backs every `az webapp create --runtime ...` code deployment, covering all web-app-* samples and eventhubs (it deploys a dashboard web app) - mcr.microsoft.com/azure-app-service/java backs servicebus/java - mcr.microsoft.com/mssql/server backs web-app-sql-database The remaining six sample families are arch-agnostic: Function Apps build their image from a multi-arch base, and the custom-image Web App and ACI samples run an image the sample builds itself. run-samples.sh now declares this per sample in ARM64_SAMPLE_DIRS and emits an `arches` field from --list; build-matrix.sh expands each selected test into one job per supported architecture, mapping amd64 to ubuntu-22.04 and arm64 to ubuntu-22.04-arm. This takes a full run from 34 to 48 jobs. The MSSQL ODBC install is skipped on arm64, where no sample needs it. Note that "amd64-only" means not *native*: the emulator never pins --platform, so Docker Desktop on Apple Silicon runs these under Rosetta, just more slowly. run-samples.sh therefore warns rather than skips on an arm64 host, with SKIP_AMD64_ONLY=1 to opt out. CI schedules only the natively-supported samples because GitHub's arm64 runners register no binfmt/QEMU. --- .github/scripts/build-matrix.sh | 38 ++++++++++--- .github/workflows/run-samples.yml | 30 ++++++++--- README.md | 31 +++++++++++ run-samples.sh | 89 +++++++++++++++++++++++++++++-- 4 files changed, 171 insertions(+), 17 deletions(-) diff --git a/.github/scripts/build-matrix.sh b/.github/scripts/build-matrix.sh index 92cc501..f3402ca 100755 --- a/.github/scripts/build-matrix.sh +++ b/.github/scripts/build-matrix.sh @@ -2,16 +2,18 @@ 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}" # 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 +55,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..61e60b3 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 @@ -198,6 +213,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..ac6163d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,37 @@ 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` require the fix from +> [localstack-pro#8102](https://github.com/localstack/localstack-pro/pull/8102). Earlier emulator +> images install `amd64` Azure Functions Core Tools into an `arm64` image, and every Function App +> deployment fails with a misleading `500 ... No route to host`. + ### 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..c99b0bd 100755 --- a/run-samples.sh +++ b/run-samples.sh @@ -78,9 +78,66 @@ 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" +) + +# 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 +host_arch() { + case "$(uname -m)" in + aarch64 | arm64) echo "arm64" ;; + *) 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..." From 4306ea7a41a275c29d0707e9d4ad33a7141eac51 Mon Sep 17 00:00:00 2001 From: "Dris.S" <53239451+DrisDary@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:21:10 +0100 Subject: [PATCH 2/3] Give the emulator longer to start now that the matrix doubles job count --- .github/workflows/run-samples.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-samples.yml b/.github/workflows/run-samples.yml index 61e60b3..efd07da 100644 --- a/.github/workflows/run-samples.yml +++ b/.github/workflows/run-samples.yml @@ -177,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 }} From d105a495fe4a2fa6f311485ef86731665a72ffa9 Mon Sep 17 00:00:00 2001 From: "Dris.S" <53239451+DrisDary@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:35:30 +0100 Subject: [PATCH 3/3] Address review: validate arch filter, guard ARM64_SAMPLE_DIRS drift, document arch fallback --- .github/scripts/build-matrix.sh | 4 ++++ README.md | 9 +++++---- run-samples.sh | 20 ++++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/scripts/build-matrix.sh b/.github/scripts/build-matrix.sh index f3402ca..092a94d 100755 --- a/.github/scripts/build-matrix.sh +++ b/.github/scripts/build-matrix.sh @@ -9,6 +9,10 @@ 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) diff --git a/README.md b/README.md index ac6163d..8634f4e 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,11 @@ them instead. The authoritative list lives in `ARM64_SAMPLE_DIRS` in [run-sample 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` require the fix from -> [localstack-pro#8102](https://github.com/localstack/localstack-pro/pull/8102). Earlier emulator -> images install `amd64` Azure Functions Core Tools into an `arm64` image, and every Function App -> deployment fails with a misleading `500 ... No route to host`. +> **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: diff --git a/run-samples.sh b/run-samples.sh index c99b0bd..a175385 100755 --- a/run-samples.sh +++ b/run-samples.sh @@ -112,6 +112,15 @@ ARM64_SAMPLE_DIRS=( "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() { @@ -126,11 +135,18 @@ supports_arm64() { return 1 } -# Normalise `uname -m` to the architecture names used above +# 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" ;; - *) echo "amd64" ;; + x86_64 | amd64) echo "amd64" ;; + *) + echo >&2 "Note: unrecognised architecture '$(uname -m)'; treating it as amd64." + echo "amd64" + ;; esac }