Skip to content

ci(DEVA11Y-735): PR smoke test for a11y-scan SPM plugin (end-to-end scan) - #35

Merged
Crash0v3rrid3 merged 7 commits into
mainfrom
chore/spm-pr-smoke-test
Aug 6, 2026
Merged

ci(DEVA11Y-735): PR smoke test for a11y-scan SPM plugin (end-to-end scan)#35
Crash0v3rrid3 merged 7 commits into
mainfrom
chore/spm-pr-smoke-test

Conversation

@Crash0v3rrid3

@Crash0v3rrid3 Crash0v3rrid3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What

Adds .github/workflows/spm-smoke-test.yml — a GitHub Actions workflow that smoke-tests the a11y-scan SwiftPM command plugin on every PR, with two jobs:

  1. a11y-scan end-to-end (SwiftPM) (macOS) — the credentialed scan. cd tests/spm && swift build compiles the a11y-scan command plugin (via the path dependency) and the sample sources — the repo root is a plugin-only package with no buildable target, so it isn't built directly. Then cd tests/spm && swift test with RUN_A11Y_SCAN=1 + credentials un-gates the repo's existing testA11yScanPluginRuns, which drives scripts/run-a11y-scan.sh — a real scan that downloads the BrowserStack CLI, authenticates, and scans the sample SwiftUI sources with intentional a11y issues. It reuses the harness the repo already ships rather than duplicating the swift package plugin … scan invocation, so there's a single source of truth for the invocation.
  2. Launcher scripts (bash syntax) (Ubuntu, no secrets) — bash -n over every launcher under scripts/. The bash/zsh/fish variants are all bash scripts (they differ only in which login shell they source creds from), so one syntax gate covers them. Static-syntax only — the scripts self-update, register git hooks and need creds, so they aren't executed here; checksum integrity stays covered by verify-selfupdate-checksums.yml.

This also corrects a latent bug in the harness the scan depends on: run-a11y-scan.sh (both the spm and xcode-app copies) passed --allow-network-connections 'all(ports: [])' — PackageDescription API syntax that isn't a valid CLI value and doesn't satisfy the plugin's declared need for ports 80/443, so SwiftPM refused the scan. Corrected to all:80,443.

Design notes

  • Fork-safe. The scan needs BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY, and secrets are never exposed to fork PRs, so the scan job is gated with github.event.pull_request.head.repo.fork == false (plus workflow_dispatch for manual runs). The scripts-lint job needs no secrets and runs on all PRs including forks.
  • Degrades gracefully. The scan step is guarded on the secrets being non-empty (if: env.BROWSERSTACK_USERNAME != '' && …). GitHub exposes an unset secret as an empty string (present, not nil), so without the guard the scan would run with empty creds and fail; with it, an unconfigured repo simply skips the scan and the job stays green on the build step.
  • Proves the scan worked, not just ran. testA11yScanPluginRuns runs the scan twice and checks the tool's own exit-code contract: --non-strict must exit 0 (downloaded, authenticated, ran cleanly) and strict must exit non-zero (the intentional issues were actually detected). A clean-but-empty scan (silent no-op) fails the strict check.
  • Resilient to transient upstream failures. The scan hits BrowserStack (network + auth + CLI download), so the step retries up to 3x with backoff; a consistent failure still fails the gate. swift test reuses the first attempt's build, so retries only re-run the scan.
  • actions/checkout is pinned by SHA (v4.2.2) to match Semgrep.yml / verify-selfupdate-checksums.yml (supply-chain hardening, DEVA11Y-476).

Verified (CI + local)

  • CI is green on both jobs. The end-to-end run exercises the two-run fidelity check with real credentials — strict mode exits non-zero, i.e. the sample's planted a11y issues are genuinely detected, not just "the plugin ran."
  • Locally (macOS, Swift 6.2): tests/spm builds (Compiling plugin a11y-scan + sample sources); swift test with no RUN_A11Y_SCAN skips the e2e test and passes, confirming the no-secrets path stays green.

Notes / open questions

  • BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY are already configured on the repo (the credentialed scan runs green); no secret setup needed.
  • Covers the SwiftPM harness (tests/spm/). The tests/xcode-app/ build-phase harness is not wired up here — happy to add an Xcode job in a follow-up.
  • actions/[email protected] emits a non-blocking Node 20 deprecation warning; left as-is to match the repo's existing pinned workflows.
  • No DEVA11Y ticket linked yet — branch is chore/spm-pr-smoke-test; rename/link as needed.

🤖 Generated with Claude Code

Crash0v3rrid3 and others added 3 commits August 3, 2026 18:22
Runs an end-to-end accessibility scan on every PR: builds the plugin and
executes a real scan against the tests/spm harness (sample SwiftUI sources
with intentional a11y issues), reusing the repo's own gated integration
test (testA11yScanPluginRuns) so the invocation stays in one place.

The scan downloads the BrowserStack CLI and makes authenticated calls, so
it is gated to same-repo PRs (secrets are never exposed to fork PRs) and
manual dispatch. Without the BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY
secrets configured the e2e test XCTSkips and the job still passes.

actions/checkout pinned by SHA to match existing workflows (DEVA11Y-476).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The repo root is a plugin-only package with no buildable target, so
`swift build` there fails ("does not contain a buildable target").
Building the tests/spm harness compiles the a11y-scan command plugin via
the path dependency plus the sample sources, so use that as the build step.

Verified locally on Swift 6.2: tests/spm `swift build` compiles the plugin,
and `swift test` passes with the e2e scan test skipping when RUN_A11Y_SCAN
is unset.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
run-a11y-scan.sh passed `--allow-network-connections 'all(ports: [])'` —
that is PackageDescription API syntax, not a valid CLI value, and the empty
port list did not satisfy the a11y-scan plugin's declared need for ports
80/443. SwiftPM therefore refused the scan:

  error: Plugin 'a11y-scan' wants permission to allow all network
  connections on ports: 80, 443. Use `--allow-network-connections
  all:80,443` to allow this.

Surfaced by the new PR smoke-test job, which is the first thing to run the
scan in CI. Fix per SwiftPM's own guidance: `all:80,443`. Applied to the
SwiftPM and Xcode harness scripts and the tests/spm README (same bug in all
three). Verified locally: the CLI now clears the permission gate and runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator Author

Smoke gate is green ✅ — and it caught a real pre-existing bug

The first CI run failed at the scan step:

error: Plugin 'a11y-scan' wants permission to allow all network connections on ports: 80, 443. Use `--allow-network-connections all:80,443` to allow this.

Root cause (not in the workflow): the harness scripts passed --allow-network-connections 'all(ports: [])' — that's PackageDescription API syntax pasted into a CLI flag, and the empty port list didn't satisfy the plugin's declared need for ports 80/443. This had never been exercised in CI before, so it went unnoticed. Fixed to all:80,443 in:

  • tests/spm/scripts/run-a11y-scan.sh
  • tests/xcode-app/scripts/run-a11y-scan.sh
  • tests/spm/README.md

After the fix, the end-to-end scan runs and passes (42s) with the repo secrets. BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY are clearly already configured on the repo — the scan authenticated and completed.

(Non-blocking) The Node 20 deprecation warning is from actions/checkout v4.2.2, which the repo's other workflows also pin — left as-is for consistency.

@Crash0v3rrid3
Crash0v3rrid3 marked this pull request as ready for review August 4, 2026 10:23
@Crash0v3rrid3
Crash0v3rrid3 requested a review from a team as a code owner August 4, 2026 10:23
Crash0v3rrid3 and others added 2 commits August 4, 2026 15:53
Adds a second, secret-free job that syntax-checks all six launcher scripts
under scripts/ (bash/zsh/fish x cli/spm) with `bash -n`. They are all bash
scripts (the zsh/fish variants only differ in which login shell they source
credentials from), so a single bash syntax gate covers them. Runs on all
PRs including forks; scripts are not executed (they self-update, register
git hooks and need credentials). Complements verify-selfupdate-checksums.yml,
which covers checksum integrity but not syntax.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator Author

Added: scripts-lint job — syntax-checks all scripts/ launchers

Per request, the workflow now has a second job that runs bash -n on every launcher script under scripts/ (all six: bash/zsh/fish × cli/spm). They're all bash scripts — the zsh/fish variants only differ in which login shell they source creds from — so one bash syntax gate covers them.

  • Runs on ubuntu-latest, needs no secrets, so it runs on all PRs including forks (unlike the credentialed e2e job).
  • Scripts are not executed (they self-update, register git hooks and need creds) — this is a static syntax gate. Checksum integrity stays covered by verify-selfupdate-checksums.yml.

Latest run: both jobs green — a11y-scan end-to-end (SwiftPM) ✅ and Launcher scripts (bash syntax) ✅ (all 6 scripts bash -n OK).

Out of scope, FYI: shellcheck flags SC2096 on the #!/usr/bin/env bash -il shebang in all six scripts (env accepts only one arg on most OSes → breaks direct ./script.sh execution on Linux). Pre-existing on main; not touched here. Worth a follow-up.

Crash0v3rrid3 and others added 2 commits August 4, 2026 16:36
Code-review follow-ups on the smoke workflow:

- Graceful degradation was only claimed, not real. GitHub exposes an unset
  secret as an empty string (present, not nil), and the reused test skips
  only on `env[...] != nil`, so a repo without the secrets would run the
  scan with empty creds and fail (script's `:?` under set -euo pipefail),
  not skip. Guard the scan step on the secrets being non-empty so it is
  skipped when absent and the job stays green on the build step. Fix the
  header comment to match.
- Correct the scripts-lint comment: the launchers' shebang is
  `#!/usr/bin/env bash -il`, not `#!/usr/bin/env bash`.
- Drop the fork-controllable filename from the `::notice/::error file=`
  workflow commands (workflow-command injection vector on fork PRs); log
  plain lines instead. bash -n still prints the real error location.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses the two open code-review findings on the smoke workflow:

- Fidelity (#2): the e2e test asserted only that --non-strict exits 0, so a
  scan that authenticated but found nothing (silent no-op) would pass green.
  Now run the scan twice and use the tool's own exit-code contract: strict
  mode must exit non-zero, proving the intentional issues in SampleViews.swift
  were actually detected — not just that the plugin ran. Uses exit codes, not
  brittle output matching. Also drains output for diagnostics and treats an
  empty credential value as absent (skip) to match the workflow guard.

- Flakiness (#3): the scan hits BrowserStack (network + auth + CLI download)
  on every same-repo PR. Wrapped it in a bounded retry (3 attempts, 20s
  backoff) so a transient upstream hiccup doesn't red-block a PR; a consistent
  failure still fails the gate. swift test reuses the first build, so retries
  only re-run the scan.

Verified locally: tests compile; no-creds path still skips cleanly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator Author

Both open review findings addressed ✅ — CI green (c24c577)

#2 — smoke fidelity. The e2e test (testA11yScanPluginRuns) previously asserted only --non-strict exit 0, so a scan that authenticated but found nothing (silent no-op) would pass green. It now runs the scan twice and uses the tool's own exit-code contract:

  • --non-strict -> exit 0 (downloaded CLI, authenticated, ran cleanly), and
  • strict -> exit non-zero (the intentional issues in SampleViews.swift were actually detected).

Exit codes, not brittle output-matching. Added a runScan() helper that drains stdout+stderr to EOF before waitUntilExit (no pipe-buffer deadlock) and includes output in failure messages. Also tightened the cred guard to skip on empty-string (not just nil), matching the workflow guard. Validated on CI with real creds: the green run means strict exited non-zero, i.e. the scan genuinely flagged the sample's a11y issues.

#3 — flaky prod-dependent gate. The scan hits BrowserStack (network + auth + CLI download) on every same-repo PR. Wrapped the scan step in a bounded retry (3 attempts, 20s backoff) so a transient upstream hiccup doesn't red-block a PR; a consistent failure still fails the gate. swift test reuses the first attempt's build, so retries only re-run the scan (CLI is cached). Retry didn't need to fire this run.

Verified: tests compile, no-creds path still skips cleanly, both CI jobs green with the new logic exercised. Remaining non-blocking item: the Node 20 deprecation warning on actions/[email protected] (repo-wide pin).

@Crash0v3rrid3 Crash0v3rrid3 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review (automated) — 6 inline finding(s). Full report in the PR comment below. Verdict: Passed.

attempts=3
for i in $(seq 1 "$attempts"); do
echo "::group::a11y-scan smoke attempt $i/$attempts"
if swift test; then

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Gate passes if the E2E test XCTSkips — nothing asserts it ran

swift test exits 0 when testA11yScanPluginRuns throws XCTSkip, and nothing here asserts the test actually executed. Today the guards line up and the scan provably runs, but any drift — the env var renamed on one side, the test renamed or moved, an extra guard added — silently turns this gate into a swift build check that still shows green. That is the exact silent-pass class this workflow exists to prevent.

Suggestion: scope and assert execution, e.g. swift test --filter 'A11yDemoLibTests/testA11yScanPluginRuns' 2>&1 | tee out.log, then fail the step if the log contains skipped or lacks a passed line for that test. A --filter that matches nothing also exits non-zero, which catches a rename.

Reviewer: stack:devtools-review-changes

"a11y-scan did not run cleanly in --non-strict mode (exit \(clean.status)).\n\(clean.output)")

let strict = try runScan(script: script, packageDir: packageDir, strict: true)
XCTAssertNotEqual(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Strict assertion accepts any non-zero exit, not just "issues found"

The plugin forwards several distinct non-zero codes — exit 2 (unwritable cache), exit 4 (RBAC denial), the curl status on a failed CLI download, exit 1 on abnormal termination — all of which satisfy != 0. Since the strict run is a separate process from the non-strict one, a transient failure in that second run is indistinguishable from "the planted issues were detected", so this fidelity check can pass for the wrong reason.

Suggestion: assert the specific lint-failure code (strict.status == 1) rather than any non-zero, and additionally assert strict.output references SampleViews.swift or a known rule id, so the test proves the planted issues caused the failure.

Reviewer: stack:devtools-review-changes

run: |
set -uo pipefail
shopt -s globstar nullglob
scripts=(scripts/**/*.sh)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] scripts-lint glob misses the scripts this PR actually changes

scripts/**/*.sh matches only the 6 launchers under root scripts/. The two scripts this PR modifies — tests/spm/scripts/run-a11y-scan.sh and tests/xcode-app/scripts/run-a11y-scan.sh — fall outside the glob, so this job would not have caught a syntax error in the very files being changed. tests/xcode-app/scripts/run-a11y-scan.sh (wired into the Xcode build phase via project.yml) has no coverage from either job.

Suggestion: broaden to scripts=(scripts/**/*.sh tests/**/scripts/*.sh) or git ls-files '*.sh', and soften the comment above to match real coverage. Scoping to scripts/ was the original request, so widening is a judgement call.

Reviewer: stack:devtools-review-changes

run: |
set -uo pipefail
attempts=3
for i in $(seq 1 "$attempts"); do

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Retry re-runs deterministic assertion failures three times

The loop cannot distinguish a transient upstream hiccup from a deterministic assertion failure, so a genuine regression is retried three times. Each attempt runs two full scans (non-strict + strict), each downloading/exec'ing the CLI and making authenticated round trips, plus 2x20s backoff — all inside timeout-minutes: 25. If a scan pair exceeds ~8 minutes, a real regression surfaces as an opaque job timeout with no assertion message instead of the intended diagnostic.

Suggestion: retry only on transient signatures (grep the captured output for network/download/5xx markers) and fail fast on an XCTest assertion failure; or drop to 2 attempts and raise timeout-minutes so the retry budget provably fits.

Reviewer: stack:devtools-review-changes

process.standardError = pipe

try process.run()
let collected = pipe.fileHandleForReading.readDataToEndOfFile()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Child process has no timeout; output only surfaces on failure

standardInput is left at the default (unlike the plugin's own runCLI, which wires it explicitly), so if the CLI stalls or waits on stdin, readDataToEndOfFile() blocks and the job emits zero log output until the 25-minute timeout — an empty ::group:: and no diagnostic to debug from.

Suggestion: set process.standardInput = FileHandle.nullDevice, add a watchdog that terminates the process after N minutes, and/or print the captured output unconditionally so the scan log reaches CI on success and on hang.

Reviewer: stack:devtools-review-changes

name: SPM plugin smoke test

on:
pull_request:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] No paths: filter and no push: main trigger (drift from the sibling workflow)

Two divergences from verify-selfupdate-checksums.yml: there is no paths: filter, so a 25-minute macOS job making real authenticated scans runs on every PR including docs-only ones; and there is no push: branches: [main], so nothing re-verifies the plugin after merge and main can break unnoticed until the next PR.

Suggestion: add a paths: filter (Plugins/**, tests/**, Package.swift, and this workflow) plus a push: branches: [main] trigger to match the sibling workflow.

Reviewer: stack:devtools-review-changes

@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator Author

Claude Code PR Review

PR: #35Head: c24c577Reviewers: stack:devtools-review-changes

Summary

Adds a PR-gating GitHub Actions workflow that smoke-tests the a11y-scan SwiftPM command plugin — a credentialed, fork-gated macOS job that builds tests/spm and runs a real authenticated BrowserStack scan through the existing testA11yScanPluginRuns, plus a secret-free bash -n lint of the scripts/ launchers — and corrects the SwiftPM --allow-network-connections value in two scan scripts and a README.

Review Table

Priority Category Check Status Notes
High Security No hardcoded secrets or credentials Pass Credentials flow from secrets.* into job env only; nothing committed.
High Security Authentication/authorization checks present Pass Fork-gating plus a non-empty-secret step guard; test-side guard hardened from != nil to ?.isEmpty == false.
High Security Input validation and sanitization Pass Deliberately avoids interpolating fork-controllable filenames into ::error file= workflow commands, with a comment explaining why.
High Security No IDOR — resource ownership validated N/A No resource-access surface.
High Security No SQL injection (parameterized queries) N/A No database layer.
High Correctness Logic is correct, handles edge cases Pass Flag fix verified correct by reproduction (see Verification). Retry/assertion refinements noted as Medium below, not correctness breaks.
High Correctness Error handling is explicit, no swallowed exceptions Pass Scan failures propagate and fail the gate today; the drift-hardening gap is tracked as Medium finding 1.
High Correctness No race conditions or concurrency issues Pass Pipe drained to EOF before waitUntilExit (deadlock correctly avoided, and commented); concurrency + cancel-in-progress set.
Medium Testing New code has corresponding tests Pass The workflow is the test; it reuses the existing harness instead of duplicating the invocation.
Medium Testing Error paths and edge cases tested Fail Strict-run assertion accepts any non-zero exit (finding 2); tests/xcode-app/scripts/run-a11y-scan.sh is modified with no CI coverage (finding 3).
Medium Testing Existing tests still pass (no regressions) Pass Verified: tests/spm builds; swift test passes (1 pass, 1 skip, exit 0); bash -n clean on all 6 launchers; CI green on both jobs at c24c577.
Medium Performance No N+1 queries or unbounded data fetching N/A No data-fetch layer.
Medium Performance Long-running tasks use background jobs N/A CI job; timeout-minutes bounded (25 / 5).
Medium Quality Follows existing codebase patterns Fail SHA-pinned checkout and permissions: contents: read match the sibling workflow, but this one omits the paths: filter and push: branches:[main] trigger verify-selfupdate-checksums.yml uses (finding 6).
Medium Quality Changes are focused (single concern) Pass Workflow plus the harness fix it depends on; coherent.
Low Quality Meaningful names, no dead code Pass Clear job/step names; runScan is a clean extraction.
Low Quality Comments explain why, not what Fail Two comments overstate: "Every script under scripts/" implies coverage the glob does not give the PR's own changed scripts, and the strict-mode comment claims a stronger guarantee than != 0 delivers.
Low Quality No unnecessary dependencies added Pass Only actions/checkout, SHA-pinned.

Verification performed

The reviewer's headline claim — that all(ports: []) is valid, that the flag change was misjustified, and that it silently narrows the sandbox from all-ports to 80/443 — was tested and refuted. Reproducing the shipped launcher's own invocation against a Dummy package that depends on this plugin:

  • --allow-network-connections 'all(ports: [])' -> error: Plugin 'a11y-scan' wants permission to allow all network connections on ports: 80, 443. ... Use --allow-network-connections all:80,443 to allow this.
  • bare --allow-network-connections all -> also refused, same error.
  • --allow-network-connections all:80,443 -> scan runs.

So the old value never granted anything for this plugin (the scan was refused outright), nothing was narrowed, and the fix in this PR is correct and necessary. The reviewer's port-22 egress probe used a throwaway plugin with different declared permissions, so it does not transfer. That finding is recorded as not confirmed.

That test did, however, surface a real pre-existing defect — see Pre-existing below.

Findings

1.

  • File: .github/workflows/spm-smoke-test.yml:75
  • Severity: Medium
  • Reviewer: stack:devtools-review-changes
  • Issue: swift test exits 0 when testA11yScanPluginRuns throws XCTSkip, and nothing asserts the E2E test actually executed. Today the guards line up and the scan provably runs, but any drift — the env var renamed on one side, the test renamed or moved, an extra guard added — silently converts this gate from "real scan" into "swift build only" while still showing green. That is precisely the silent-pass class this workflow exists to prevent.
  • Suggestion: swift test --filter 'A11yDemoLibTests/testA11yScanPluginRuns' 2>&1 | tee out.log, then fail the step if the log contains skipped or lacks a passed line for that test. A --filter matching nothing also exits non-zero, which catches a rename.

2.

  • File: tests/spm/Tests/A11yDemoLibTests/A11yDemoLibTests.swift:52
  • Severity: Medium
  • Reviewer: stack:devtools-review-changes
  • Issue: XCTAssertNotEqual(strict.status, 0) is weaker than its comment claims. The plugin forwards several distinct non-zero codes — exit 2 (unwritable cache), exit 4 (RBAC denial), the curl status on a failed CLI download, exit 1 on abnormal termination — all of which satisfy != 0. Because the strict run is a separate process from the non-strict one, a transient failure in that second run is indistinguishable from "the planted issues were detected", so the fidelity check can pass for the wrong reason.
  • Suggestion: Assert the specific lint-failure code (strict.status == 1) rather than any non-zero, and additionally assert strict.output references SampleViews.swift or a known rule id, so the test proves the planted issues caused the failure.

3.

  • File: .github/workflows/spm-smoke-test.yml:107
  • Severity: Medium
  • Reviewer: stack:devtools-review-changes
  • Issue: scripts=(scripts/**/*.sh) matches only the 6 launchers under root scripts/. The two scripts this PR actually modifies — tests/spm/scripts/run-a11y-scan.sh and tests/xcode-app/scripts/run-a11y-scan.sh — fall outside the glob, so the new lint job would not have caught a syntax error in the very files being changed. tests/xcode-app/scripts/run-a11y-scan.sh (wired into the Xcode build phase via project.yml) has no coverage from either job.
  • Suggestion: Broaden to scripts=(scripts/**/*.sh tests/**/scripts/*.sh) or git ls-files '*.sh', and soften the job comment to match real coverage. Note this was scoped to scripts/ by request, so widening is a judgement call.

4.

  • File: .github/workflows/spm-smoke-test.yml:73
  • Severity: Medium
  • Reviewer: stack:devtools-review-changes
  • Issue: The retry loop cannot distinguish a transient upstream hiccup from a deterministic assertion failure, so a genuine regression is re-run three times. Each attempt runs two full scans (non-strict + strict), each downloading/exec'ing the CLI and making authenticated round trips, plus 2x20s backoff — all inside timeout-minutes: 25. If a scan pair exceeds ~8 minutes, a real regression surfaces as an opaque job timeout with no assertion message instead of the intended diagnostic.
  • Suggestion: Retry only on transient signatures (grep captured output for network/download/5xx markers) and fail fast on an XCTest assertion failure; or drop to 2 attempts and raise timeout-minutes so the retry budget provably fits.

5.

  • File: tests/spm/Tests/A11yDemoLibTests/A11yDemoLibTests.swift:71
  • Severity: Low
  • Reviewer: stack:devtools-review-changes
  • Issue: The child process has no timeout and its output is buffered until an assertion fails. standardInput is left at the default (unlike the plugin's own runCLI, which wires it explicitly), so if the CLI stalls or waits on stdin, readDataToEndOfFile() blocks and the job emits zero log output until the 25-minute timeout — an empty ::group:: and no diagnostic.
  • Suggestion: Set process.standardInput = FileHandle.nullDevice, add a watchdog that terminates after N minutes, and/or print the captured output unconditionally so the scan log reaches CI on success and on hang.

6.

  • File: .github/workflows/spm-smoke-test.yml:19
  • Severity: Low
  • Reviewer: stack:devtools-review-changes
  • Issue: Trigger config drifts from the sibling verify-selfupdate-checksums.yml in two ways: no paths: filter, so a 25-minute macOS job making real authenticated scans runs on every PR including docs-only ones; and no push: branches: [main], so nothing re-verifies the plugin after merge and main can break unnoticed until the next PR.
  • Suggestion: Add a paths: filter (Plugins/**, tests/**, Package.swift, the workflow itself) and a push: branches: [main] trigger.

7.

  • File: .github/workflows/spm-smoke-test.yml:37
  • Severity: Low
  • Reviewer: stack:devtools-review-changes
  • Issue: Fork PRs skip spm-smoke entirely, and GitHub treats a skipped job as satisfying a required status check — so an external contribution that breaks the plugin can still show a green gate. This is inherent to secrets-and-forks and the PR documents it, but it determines which check is safe to mark required.
  • Suggestion: Mark only scripts-lint as required, and either add a secret-free fork job that at least runs swift build on tests/spm, or document that fork PRs need a maintainer workflow_dispatch before merge.

8.

  • File: .github/workflows/spm-smoke-test.yml:16
  • Severity: Low
  • Reviewer: stack:devtools-review-changes
  • Issue: Branch chore/spm-pr-smoke-test and the PR title carry no JIRA ID, against the chore/<JIRA-ID>-short-desc convention the repo follows (e.g. a757dda ... (DEVA11Y-475) (#34)). Base ref main is correct.
  • Suggestion: DEVA11Y-735 now exists for this work — link it and rename the branch/title to include it before merge.

Pre-existing (not introduced by this PR; non-gating)

  • File: scripts/bash/spm.sh:145 (and scripts/zsh/spm.sh:157, scripts/fish/spm.sh:158)
  • Severity: High
  • Issue: The shipped, user-facing SPM launchers still pass --allow-network-connections 'all(ports: [])' — the exact value proven above to be refused by SwiftPM for this plugin. Reproducing the launcher's own invocation (a Dummy package depending on AccessibilityDevTools, which is what the launcher synthesizes) fails with the permission error and never runs the scan. This is the same bug this PR fixes in the test harnesses, still live on the production path.
  • Suggestion: Apply all:80,443 to all three launchers and regenerate their .sha256 sidecars (required by verify-selfupdate-checksums.yml). Worth its own ticket — it is out of scope for this PR's diff and so does not gate this review, but it likely affects real users today.

Raised by other reviewers (not independently confirmed)

  • stack:devtools-review-changes (High): claimed all(ports: []) is valid, the change was misjustified, and it narrows egress from all-ports to 80/443. Refuted by direct reproduction (see Verification) — both all(ports: []) and bare all are refused for this plugin; only all:80,443 works, so nothing was narrowed and the fix is sound. Not carried as a finding.

Human reviewer signal

@maunilm approved this PR on 2026-08-06T07:26:09Z with no inline comments and no unresolved review threads. Nothing to reconcile: no concerns to confirm, and no carried-forward findings to prune (this is the first Claude review on this PR).


Verdict: PASS — no High-severity issues in this PR's own changes; the workflow demonstrably runs a real, authenticated scan and both jobs are green at c24c577. The four Medium items are hardening of a verification mechanism and are worth doing before this becomes a required check; the High item is pre-existing breakage on the shipped launcher path and deserves its own ticket.

@Crash0v3rrid3
Crash0v3rrid3 merged commit bea914e into main Aug 6, 2026
7 checks passed
@Crash0v3rrid3 Crash0v3rrid3 changed the title ci: PR smoke test for a11y-scan SPM plugin (end-to-end scan) ci(DEVA11Y-735): PR smoke test for a11y-scan SPM plugin (end-to-end scan) Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants