The `Screenshot diff` workflow's `paths:` filter included `pages/**`
which matches `pages/api/**` too, so API-only PRs triggered the
visual-diff workflow even though they can't possibly move a single
rendered pixel. PR #19 (cors-tighten) and PR #20 (add-rate-limiting)
both empirically hit this — each was an API-only sweep, and each
burned ~55s of CI runtime on a `Screenshot diff` job that
`continue-on-error: true` then swallowed. Documented as a queued
follow-up in `.convoys/ship-readiness.md` § Queued convoys, ratified
for fix in this convoy.
The fix is a single negated-glob entry inserted immediately after
`pages/**` in the `paths:` list. GitHub Actions evaluates `paths:`
with minimatch and supports `!`-prefixed exclusions per the
published path-filter cheatsheet, but the order matters: a `!pattern`
only takes effect if it appears AFTER an include that already
matched the path. Keeping `!pages/api/**` second in the list (right
after `pages/**`, before all the other includes) is the canonical
shape. All five existing entries are preserved verbatim; only the
one exclusion entry plus an inline comment explaining the ordering
rule and the empirical motivation is added.
`preview-smoke.yml` is intentionally untouched — verified its `on:`
block has no `paths:` filter at all (it triggers on every PR
targeting main, with skip-via-PR-body-directive in the gate job),
so there's no false-positive shape to fix there. Smoke SHOULD run on
every PR including API-only ones because changes to `pages/api/**`
can break the home redirect + sign-in + `/api/health` endpoints the
smoke spec exercises.
Co-authored-by: Cursor <cursoragent@cursor.com>
* architect: plan fix-vercel-deployment-protection-in-ci convoy (1 brief)
Flip convoy status queued -> in-progress and append the architect
output per the role-architect contract:
- Decisions (post-IA round) A/B/C/D with recommendations + rationale,
routed back to operator for gate-1 ratification (A and D especially).
- Architecture section: file plan, risk list (R1-R8), test plan,
decomposition (1 brief, justified), slice_dependencies block.
- brief-1: inject VERCEL_AUTOMATION_BYPASS_SECRET into both workflows
via query-param-on-path (Decision A), tighten max_timeout 600 -> 120
(Decision B), extend gate to skip fork PRs (Decision D), and forward
the secret as an env var to the Playwright/visual step for forward-
compat with adopt-playwright-smoke.
Boot-the-brief findings preempted in the brief:
- wait-for-vercel-preview@v1.3.2 source confirms only `targetUrl` (the
bare deployment URL) is ever logged or emitted as outputs.url; the
bypass query in `path:` is structurally invisible to logs and to
downstream consumers of ${{ steps.vercel.outputs.url }}.
- The action exposes no custom-header input -- Option B is mechanically
impossible for the wait step without forking the action.
- `path:` MUST begin with a leading `/` because the action parses it
via `new URL(path, url)`.
- PR #16 run logs confirm the 401 timeout failure mode the convoy targets.
- Playwright config doesn't exist yet (adopt-playwright-smoke owns it);
this brief only plumbs the env var.
No workflow YAML, no Playwright config, no AGENTS.md changes in this
commit -- those land in the implementer phase per the architect contract.
Awaiting human gate 1 (Decisions A + B + D ratification + brief approval)
before implementer dispatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): plumb VERCEL_AUTOMATION_BYPASS_SECRET into preview-smoke + visual-diff (Brief 1 of fix-vercel-deployment-protection-in-ci)
Closes the CI-infra half of P0 #7's follow-up. PR #16 (squash commit
7e97254) added scoped permissions to both workflows but exposed that
Vercel Deployment Protection 401s anonymous GitHub-runner requests,
causing both Playwright smoke and Screenshot diff to time out at 10
minutes on every PR. This brief plumbs the bypass secret end-to-end
so the wait-action's healthcheck reaches 200.
Per architect Decision A (.convoys/fix-vercel-deployment-protection-in-ci.md):
- preview-smoke.yml + visual-diff.yml: wait-for-vercel-preview's
`path:` input now carries the bypass as a query parameter
`?x-vercel-protection-bypass=${{ secrets.* }}&x-vercel-set-bypass-cookie=true`.
The action only logs the bare targetUrl (verified in action.js:357,360,363)
so the secret stays out of workflow logs.
Per Decision B:
- max_timeout: 600 -> 120. PR #16 evidence shows Vercel previews are up
within seconds of job start; 120s gives ample headroom and surfaces
misconfigurations in ~2 minutes instead of ~10.
Per Decision D (NEW -- surfaced by Boot-the-brief):
- gate: job's Decide step now checks github.event.pull_request.head.repo.fork
FIRST. Forks lack repo secrets, so they would otherwise burn ~4 minutes
per PR on a misleading 401. The fork-check emits `::notice::` and short-
circuits before the existing skip-via-PR-body directive runs.
Forward-compat for adopt-playwright-smoke:
- Both workflows' Playwright/screenshot-capture step now exports
VERCEL_AUTOMATION_BYPASS_SECRET as env. The actual Playwright config
consumes it via extraHTTPHeaders in adopt-playwright-smoke's brief.
BASE_URL stays as the bare \${{ steps.vercel.outputs.url }} (no query
string) so it remains safe to echo.
Verification:
- YAML parses (js-yaml load on both files: preview-smoke jobs [gate, smoke];
visual-diff jobs [gate, visual])
- actionlint not run (binary not installed locally); recommend installing
for future PRs. Future adopt-actionlint convoy could add it to CI.
- No `set -x`, `echo`, `cat`, or `printf` of the secret or bypass URL
in any modified step
- permissions: and concurrency: blocks unchanged (PR #16 contracts preserved)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): route github.event.pull_request.body through env: to avoid shell injection
Pre-existing latent bug surfaced by PR #17's CI run. The Decide step's
inline `${{ github.event.pull_request.body }}` interpolation pastes
arbitrary PR-body text directly into a bash script. When the body
contains shell metacharacters (parens, pipes, backticks, redirections,
etc.) the resulting script either errors out at YAML-load time OR —
worse — executes attacker-controlled shell.
This bit PR #17 with a real syntax error ("unexpected token `('")
because the PR body contains parenthesized phrases like
"(was: 10-minute timeout)". Every Decide-step run in this repo has
been one badly-formatted PR body away from breaking the gate.
Fix: forward `github.event.pull_request.body` and
`github.event.pull_request.head.repo.fork` through the step's `env:`
block as `PR_BODY` and `PR_IS_FORK`, then quote them in shell
(`"$PR_BODY"`, `"$PR_IS_FORK"`). The env-var path leaves the values
as plain strings rather than syntactically embedded code, which is the
standard GitHub Actions hardening pattern (see GitHub's "Security
hardening for GitHub Actions" → "Using a third-party action").
Same change in both workflows; ~9 LOC each.
This fix is technically beyond Brief 1's scope (which targeted only
Vercel-bypass plumbing) but is added in this convoy because the bug
actively blocks Brief 1's success criterion from being validated on
PR #17. Documented in the convoy file's "Anything flagged but not
acted on" follow-up pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): drop x-vercel-set-bypass-cookie=true from wait-action path
The wait-action's healthcheck was still 401ing despite the bypass query
being correct. Root cause: with `x-vercel-set-bypass-cookie=true`, Vercel
returns 307 + Set-Cookie (`_vercel_jwt`), but axios in Node has no cookie
jar — it follows the redirect to the bare URL without the cookie, which
then 401s.
Local verification (run by operator):
curl -sI "https://<preview>/?x-vercel-protection-bypass=<secret>" | head -1
→ HTTP/2 200 (works, no cookie needed)
curl -sI "https://<preview>/?x-vercel-protection-bypass=<secret>&x-vercel-set-bypass-cookie=true" | head -1
→ HTTP/2 307 (the redirect-without-cookie path that breaks axios)
For a one-shot healthcheck, the per-request bypass query is enough.
The cookie variant stays reserved for the future Playwright config
(adopt-playwright-smoke) where a real browser cookie jar exists.
Added an inline comment in preview-smoke.yml explaining this so the
next agent doesn't accidentally re-add the cookie param.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Both Playwright-on-Vercel workflows fail at the very first action
(`patrickedqvist/wait-for-vercel-preview@v1.3.2`) with 403 "Resource
not accessible by integration". The cause is the repo-default workflow
token being read-only-by-default with no scopes declared by the workflow.
Add minimal scoped permission blocks per the GitHub Actions least-privilege
guidance:
preview-smoke.yml:
contents: read
deployments: read # wait-for-vercel-preview queries GitHub Deployments
pull-requests: read # correlate deployment with this PR
statuses: read # some Vercel deployments use commit statuses
visual-diff.yml:
contents: read
deployments: read
pull-requests: write # final step posts "Visual Diff" comment via Issues API
statuses: read
Verified against the failure on PR #15:
- Playwright smoke: "Resource not accessible by integration" on
wait-for-vercel-preview → fixed by deployments+statuses+pull-requests:read
- Screenshot diff: 403 from POST /repos/.../issues/15/comments with
`x-accepted-github-permissions: issues=write; pull_requests=write`
in the response → fixed by pull-requests:write (covers Issues API
for PR comments; issues:write would also work but pull-requests:write
is the idiomatic scope)
Unblocks visual-regression signal on every future PR. No code changes,
no test changes — workflow YAML only.
Co-authored-by: Cursor <cursoragent@cursor.com>