chore(deps): Dependabot digest rotation for pinned Semgrep image [DEVA11Y-476] #24
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
| # Smoke-tests the `a11y-scan` SwiftPM command plugin end-to-end on every PR: | |
| # builds the plugin and runs a real accessibility scan against the tests/spm | |
| # harness (sample SwiftUI sources with intentional a11y issues). It reuses the | |
| # repository's own gated integration test (testA11yScanPluginRuns) so the scan | |
| # invocation stays defined in exactly one place. | |
| # | |
| # The scan downloads the BrowserStack CLI and makes authenticated network calls, | |
| # so it needs BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY repo secrets. Those | |
| # secrets are never exposed to fork PRs, so that job is gated to same-repo PRs | |
| # (and manual dispatch); fork PRs skip it. The scan step is itself guarded on the | |
| # secrets being present, so if they are not configured the scan is skipped and the | |
| # job still passes on the build step alone. | |
| # | |
| # A second job (scripts-lint) syntax-checks every launcher script under scripts/. | |
| # It needs no secrets, so it runs on all PRs including forks. | |
| name: SPM plugin smoke test | |
| on: | |
| pull_request: | |
| branches: [main, master] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: spm-smoke-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| spm-smoke: | |
| name: a11y-scan end-to-end (SwiftPM) | |
| runs-on: macos-14 | |
| timeout-minutes: 25 | |
| # Secrets are unavailable to fork PRs, so the authenticated scan can only run | |
| # on same-repo PRs or a manual dispatch. Fork PRs skip this job. | |
| if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false | |
| env: | |
| BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} | |
| BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} | |
| # Un-gates tests/spm/Tests/A11yDemoLibTests/testA11yScanPluginRuns, which is | |
| # skipped unless RUN_A11Y_SCAN=1 and BrowserStack credentials are present. | |
| RUN_A11Y_SCAN: "1" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Swift toolchain | |
| run: swift --version | |
| # The repo root is a plugin-only package (no buildable target), so it is | |
| # not built directly. Building the tests/spm harness compiles both the | |
| # a11y-scan command plugin (via the path dependency) and the sample sources. | |
| - name: Build harness (compiles the a11y-scan plugin) | |
| working-directory: tests/spm | |
| run: swift build | |
| # Guarded on the secrets actually being set: GitHub exposes an unset secret | |
| # as an empty string (present, not nil), so without this guard the scan would | |
| # run with empty credentials and fail. When the secrets are absent this step | |
| # is skipped and the job stays green on the build step alone. | |
| # | |
| # The scan hits BrowserStack (network + auth + CLI download), so a transient | |
| # upstream hiccup should not red-block a PR. Retry the scan up to 3 times with | |
| # backoff; a consistent failure still fails the gate. `swift test` reuses the | |
| # first attempt's build, so retries only re-run the scan. | |
| - name: End-to-end scan smoke (tests/spm) | |
| if: env.BROWSERSTACK_USERNAME != '' && env.BROWSERSTACK_ACCESS_KEY != '' | |
| working-directory: tests/spm | |
| run: | | |
| set -uo pipefail | |
| attempts=3 | |
| for i in $(seq 1 "$attempts"); do | |
| echo "::group::a11y-scan smoke attempt $i/$attempts" | |
| if swift test; then | |
| echo "::endgroup::" | |
| exit 0 | |
| fi | |
| echo "::endgroup::" | |
| if [ "$i" -lt "$attempts" ]; then | |
| echo "Attempt $i failed; retrying in 20s (occasional transient upstream failures are expected)." | |
| sleep 20 | |
| fi | |
| done | |
| echo "::error::a11y-scan smoke failed after $attempts attempts." | |
| exit 1 | |
| scripts-lint: | |
| name: Launcher scripts (bash syntax) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| # Every script under scripts/ — the bash, zsh and fish variants alike — is | |
| # a bash script (`#!/usr/bin/env bash -il`); the variants differ only in which | |
| # login shell they source BrowserStack creds from. So all of them are | |
| # syntax-checked with `bash -n`. The scripts self-update, register git | |
| # hooks and need credentials, so they are not executed here — this is a | |
| # static syntax gate. (Checksum integrity is covered separately by | |
| # verify-selfupdate-checksums.yml.) | |
| - name: Syntax-check all launcher scripts (bash -n) | |
| run: | | |
| set -uo pipefail | |
| shopt -s globstar nullglob | |
| scripts=(scripts/**/*.sh) | |
| if [ ${#scripts[@]} -eq 0 ]; then | |
| echo "::error::No .sh scripts found under scripts/ — checkout or glob is wrong." | |
| exit 1 | |
| fi | |
| status=0 | |
| for script in "${scripts[@]}"; do | |
| # Plain log lines, not ::notice file=/::error file= workflow commands: | |
| # scripts/ filenames are attacker-controllable on fork PRs, and | |
| # interpolating them into a workflow command is an injection vector. | |
| if bash -n "$script"; then | |
| echo "OK $script" | |
| else | |
| echo "FAILED $script (bash -n syntax error above)" | |
| status=1 | |
| fi | |
| done | |
| exit "$status" | |
| # Functionally exercises verify_binary_integrity (DEVA11Y-473/474) across its full | |
| # fail-open / fail-closed matrix against a local python fixture server. Because the | |
| # control fails OPEN today (no sidecars published yet), a `bash -n` gate alone would | |
| # give ZERO red signal if this function were broken — the regression would only surface | |
| # the day the sidecars go live. This job is the guard against that. No secrets needed. | |
| verify-integrity-fn: | |
| name: CLI binary integrity check (functional matrix) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Exercise verify_binary_integrity (fail-open / fail-closed matrix) | |
| run: | | |
| set -uo pipefail | |
| SRC=scripts/bash/cli.sh | |
| # Lift the two functions verbatim off the branch (name() { ... } to first bare }). | |
| extract() { awk -v fn="$1" 'index($0,fn"() {")==1{p=1} p{print} p&&/^}$/{exit}' "$SRC"; } | |
| eval "$(extract _self_update_sha256)" | |
| eval "$(extract verify_binary_integrity)" | |
| WORK=$(mktemp -d); cd "$WORK" | |
| printf 'REAL-BINARY-PAYLOAD' > good.zip | |
| GOOD=$(_self_update_sha256 good.zip) | |
| mkdir srv | |
| ( cd srv && python3 -m http.server 8799 >/dev/null 2>&1 & echo $! > "$WORK/pid" ) | |
| sleep 1 | |
| BASE="http://127.0.0.1:8799/asset.zip" | |
| pass=0; fail=0 | |
| # NOTE: GitHub's default `shell: bash -e {0}` injects -e, which `set -uo | |
| # pipefail` does not undo. verify_binary_integrity intentionally returns | |
| # non-zero on the fail-closed cases, so capture rc with `&& rc=0 || rc=$?` | |
| # rather than `; rc=$?` — the latter aborts the whole step under -e before | |
| # the `[ "$fail" -eq 0 ]` gate at the end ever runs. | |
| check() { local d="$1" erc="$2" ez="$3" url="$4" out rc z | |
| cp good.zip z.zip | |
| out=$(verify_binary_integrity z.zip "$url" 2>&1) && rc=0 || rc=$? | |
| z=gone; [ -f z.zip ] && z=kept | |
| if [ "$rc" = "$erc" ] && [ "$z" = "$ez" ]; then echo "PASS | $d | rc=$rc zip=$z"; pass=$((pass+1)) | |
| else echo "FAIL | $d | got rc=$rc zip=$z want rc=$erc zip=$ez | $out"; fail=$((fail+1)); fi | |
| } | |
| check "empty resolved URL (skip)" 0 kept "" | |
| printf '%s asset.zip\n' "$GOOD" > srv/asset.zip.sha256 | |
| check "good sidecar" 0 kept "$BASE" | |
| check "good + ?token= query" 0 kept "${BASE}?token=abc" | |
| rm -f srv/asset.zip.sha256 | |
| check "missing sidecar (404)" 0 kept "$BASE" | |
| printf '%s asset.zip\n' "$(echo "$GOOD" | tr 'a-z' 'A-Z')" > srv/asset.zip.sha256 | |
| check "uppercase published hash" 0 kept "$BASE" | |
| printf '%s asset.zip\n' "0000000000000000000000000000000000000000000000000000000000000000" > srv/asset.zip.sha256 | |
| check "wrong checksum" 2 gone "$BASE" | |
| : > srv/asset.zip.sha256 | |
| check "empty sidecar (200)" 2 gone "$BASE" | |
| printf '<Error><Code>AccessDenied</Code></Error>' > srv/asset.zip.sha256 | |
| check "malformed 200 (error page)" 0 kept "$BASE" | |
| kill "$(cat "$WORK/pid")" 2>/dev/null || true | |
| echo "----"; echo "PASS=$pass FAIL=$fail" | |
| [ "$fail" -eq 0 ] | |
| # Positive assertion that the SERVER half (DEVA11Y-473/474) has shipped: resolves the | |
| # real (unauthenticated) download redirect to the versioned asset and probes the | |
| # <asset>.sha256 sidecar the client verifies against. Advisory (::warning) today because | |
| # the sidecars are not published yet and verification is inert by design; flip the marked | |
| # line to a hard failure once SDK-assets publishes them so this proves verification runs. | |
| sidecar-availability: | |
| name: CLI checksum sidecar published (advisory until server half ships) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Assert <asset>.sha256 resolves to a 64-hex digest | |
| run: | | |
| set -uo pipefail | |
| resolved=$(curl -fsSL -o /dev/null -w '%{url_effective}' \ | |
| "https://api.browserstack.com/sdk/v1/download_cli?os=macos&os_arch=arm64" || true) | |
| if [ -z "$resolved" ]; then | |
| echo "::warning::Could not resolve the CLI asset URL; skipping sidecar availability check." | |
| exit 0 | |
| fi | |
| sum_url="${resolved%%\?*}.sha256" | |
| code=$(curl -fsSL -o body.txt -w '%{http_code}' "$sum_url" || echo 000) | |
| # `|| true`: on the expected 403 (server half not shipped) curl writes no | |
| # body.txt, so awk exits non-zero and pipefail propagates it — which under | |
| # the GHA-injected `bash -e` would abort the step and turn this advisory | |
| # ::warning into a red check. Keep it advisory until the sidecars ship. | |
| first=$(awk '{print $1; exit}' body.txt 2>/dev/null | tr 'A-Z' 'a-z' || true) | |
| if [ "$code" = "200" ] && printf '%s' "$first" | grep -Eq '^[0-9a-f]{64}$'; then | |
| echo "Sidecar published and well-formed at ${sum_url}" | |
| else | |
| # TODO(DEVA11Y-473/474 server half): change the next block to `exit 1` once | |
| # SDK-assets publishes <asset>.sha256 (public-read). Until then verification | |
| # is inert, so this stays advisory rather than red-blocking every PR. | |
| echo "::warning::No well-formed checksum sidecar at ${sum_url} yet (HTTP ${code}); CLI binary integrity verification is INERT until the server half ships. Flip this check to a hard failure once sidecars are published." | |
| fi |