* 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>
23 KiB
| convoy | brief_number | depends_on | files | ||
|---|---|---|---|---|---|
| fix-vercel-deployment-protection-in-ci | 1 |
|
Brief 1: Inject VERCEL_AUTOMATION_BYPASS_SECRET into the preview-smoke + visual-diff workflows so the wait-for-vercel-preview healthcheck passes against protected Vercel previews
Goal (1 sentence)
Plumb ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} into wait-for-vercel-preview's path: input as a query parameter (Decision A) in both workflows, tighten max_timeout from 600 → 120 (Decision B), extend the gate: job to skip fork PRs (Decision D), and forward the same secret as an env var to the Playwright smoke / screenshot-capture step so adopt-playwright-smoke finds the surface pre-wired — without ever leaking the secret to a workflow log or to ${{ steps.vercel.outputs.url }}.
Files in scope (do not edit anything else)
.github/workflows/preview-smoke.yml— modified..github/workflows/visual-diff.yml— modified.
Files explicitly out of scope (do not touch even if it seems related):
playwright.config.js— does not exist yet;adopt-playwright-smokeowns it.tests/smoke/app.smoke.spec.ts— already exists as a stub but stays intests/smoke/;adopt-playwright-smokeowns it.tests/visual/— does not exist yet;adopt-playwright-smokeowns it.AGENTS.md§ 7 — the wording correction (header → query param; queued → wired) is the doc-writer pass at convoy close, NOT this brief.package.json— no new deps.@playwright/testis not in scope here.- Any other workflow in
.github/workflows/(e.g.ci.yml) — out of scope.
Conventions to follow
- Decision A (
.convoys/fix-vercel-deployment-protection-in-ci.md§ Decisions post-IA round). The wait-action receives the bypass via query param onpath:. The header form is reserved for the futureplaywright.config.js. - Decision B (same file).
max_timeout: 600→max_timeout: 120in both workflows. Operator may override at gate 1 if Vercel builds have been slow recently. - Decision C (same file). Do NOT place
${{ secrets.* }}inside anyconcurrency:group expression. GitHub Actions YAML parser rejects secret refs inconcurrency:and the workflow fails to load. Theconcurrency:blocks stay unchanged. - Decision D (same file). Extend the existing
gate:step'sDecideshell script to checkgithub.event.pull_request.head.repo.forkFIRST. When the PR comes from a fork, emit::notice::and setshould_run=false. Same shape in both workflows. - No-go zones (
.cursor/rules/no-go-zones.mdc). None of the files in scope are in the no-go list. Do not edit anything outside.github/workflows/preview-smoke.ymland.github/workflows/visual-diff.ymlin this brief. - Secret-handling discipline:
- NEVER
echo,cat, orprintfa URL or env var that contains${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}or$VERCEL_AUTOMATION_BYPASS_SECRET. - NEVER use
set -xin anyrun:step in either workflow (it would echo every command including ones that interpolate secrets). - NEVER assign the constructed URL (the one with the bypass query) to a shell variable that is later printed.
- The wait-action's own logs are safe (see Boot-the-brief finding #2 below). The risk is in YOUR additions, not in the action.
- GitHub Actions auto-masks values that match registered secrets in workflow logs. That is a backstop, not a primary defense. Do not rely on it to redact full URLs.
- NEVER
- Style match. PR #16 (
fix(ci): scoped permissions for preview-smoke + visual-diff workflows, squash commit7e97254) is the precedent for touching both workflow files in the same PR. Follow its diff shape: same change applied to both files, with workflow-specific differences (smoke vs visual-diff naming, thepull-requests: writepermission only on visual-diff) preserved as-is.
Acceptance criteria
.github/workflows/preview-smoke.yml
- Wait-action step (currently lines 60-65) becomes:
- name: Wait for Vercel Preview deployment
id: vercel
uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 120
path: /?x-vercel-protection-bypass=${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}&x-vercel-set-bypass-cookie=true
Three changes relative to the current step:
max_timeout: 600→max_timeout: 120(Decision B).- New
path:input — MUST begin with a leading/.action.js:42parses this vianew URL(path, url); a missing leading/makes the URL resolver produce a path relative to the wrong base. Verbatim/then?then the two query params. - The
x-vercel-set-bypass-cookie=truesegment is REQUIRED, not optional. It causes Vercel to set a_vercel_jwtcookie on the response so any follow-up same-origin requests (e.g. Playwright'spage.gotoredirects) reuse the bypass without needing the query string again.
Do NOT:
-
Move the
path:line abovemax_timeout:(no semantic difference, but match the verbatim order so the diff stays minimal). -
Use single quotes around the
path:value. YAML treats the unquoted form as a plain string; quoting introduces escape-handling questions. Leave it unquoted. -
Add an
env:block to this step. The wait-action does not readprocess.env.VERCEL_AUTOMATION_BYPASS_SECRET; it consumes the input only. -
gate:job'sDecidestep (currently lines 41-49) becomes:
- name: Decide
id: check
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Smoke skipped on fork PR (bypass secret unavailable to forks)"
elif echo "${{ github.event.pull_request.body }}" | grep -qE 'pipeline:.*skip.*\bsmoke\b'; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Smoke skipped via pipeline directive"
else
echo "should_run=true" >> $GITHUB_OUTPUT
fi
Three changes relative to current:
- New
if [[ "${{ ... fork }}" == "true" ]]branch FIRST. It must come before the body-directive check so fork PRs short-circuit out without parsing the PR body. - The shell comparison is
== "true"(a string compare against the literal string"true").github.event.pull_request.head.repo.forkis rendered as the string"true"or"false"in expression context — NOT as a bare boolean (Risk R5 in the convoy's architecture risk list). - The notice mentions "fork PR" explicitly; this surfaces in the GitHub Actions UI summary so a reader scanning a PR can immediately see why smoke didn't run.
Do NOT:
-
Replace the body-directive check (
pipeline:.*skip.*\bsmoke\b). That gate is still useful for non-fork PRs that legitimately want to skip smoke (e.g. doc-only PRs). -
Move the
gate:step'sif:higher —if: needs.gate.outputs.should_run == 'true'on thesmoke:job is the correct gate; it stays. -
Playwright smoke step (currently lines 77-80) becomes:
- name: Run smoke tests
run: npx playwright test --project=smoke
env:
BASE_URL: ${{ steps.vercel.outputs.url }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
One change relative to current:
- New
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}line in the step'senv:block. This is forward-compat plumbing — theplaywright.config.jsthatadopt-playwright-smokeships will readprocess.env.VERCEL_AUTOMATION_BYPASS_SECRETand inject the bypass viaextraHTTPHeadersper Vercel's snippet. - Do NOT modify
BASE_URL. It stays${{ steps.vercel.outputs.url }}. The wait-action'soutputs.urlis the bare deployment URL — the bypass query string is NOT appended (confirmed by readingaction.js:360). Playwright will inject the bypass via headers; the URL must stay clean so the headers actually apply on every request (Playwright re-appliesextraHTTPHeadersper request, including redirects). - Until
adopt-playwright-smokeships, this step will FAIL because@playwright/testis not installed. That's the documented end state of THIS brief (see acceptance criterion #1 at the bottom of this section); do not try to fix it here.
- All other lines in
.github/workflows/preview-smoke.ymlstay byte-for-byte identical to the current file — including:- The
name:line. - The full
on:block (PR types, target branch). - The full
concurrency:block (Decision C — no secret reference). - The full
permissions:block (PR #16 already landed the minimal scope; do not touch). - The
gate:job'sname:,runs-on:,outputs:, the existingactions/checkout@v4step in thesmoke:job,actions/setup-node@v4,npm ci,npx playwright install --with-deps chromium, and theUpload Playwright report on failurestep. - The leading multi-line comment block at the top of the file (lines 1-11). Update text is the doc-writer's job, not the implementer's.
- The
.github/workflows/visual-diff.yml
- Wait-action step (currently lines 56-61) becomes:
- name: Wait for Vercel Preview deployment
id: vercel
uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 120
path: /?x-vercel-protection-bypass=${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}&x-vercel-set-bypass-cookie=true
Identical to the preview-smoke version (same wait-action, same input shape, same secret). The shared shape is intentional — Decision A applies to both workflows.
gate:job'sDecidestep (currently lines 38-45) becomes:
- id: check
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Visual diff skipped on fork PR (bypass secret unavailable to forks)"
elif echo "${{ github.event.pull_request.body }}" | grep -qE 'pipeline:.*skip.*\bvisual\b'; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Visual diff skipped via pipeline directive"
else
echo "should_run=true" >> $GITHUB_OUTPUT
fi
Same shape as preview-smoke's gate, with two text-only differences:
- The body-directive regex is
\bvisual\b(was\bsmoke\bin preview-smoke). Matches the existing convention in the current file. - The two
::notice::strings say "Visual diff" instead of "Smoke" — matches the workflow's name.
Note: the current visual-diff.yml Decide step is missing the name: field (the current file is - id: check directly). Preserve that style — do not add a name: here just because preview-smoke has one. The diff stays minimal.
- Screenshot-capture step (currently lines 71-75) becomes:
- name: Capture screenshots (PR)
run: npx playwright test --project=visual --update-snapshots=none
env:
BASE_URL: ${{ steps.vercel.outputs.url }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
continue-on-error: true
Same change as preview-smoke's smoke step — add VERCEL_AUTOMATION_BYPASS_SECRET to env:. Do NOT remove continue-on-error: true (the visual-diff workflow's design is to upload diffs even when tests fail; that behavior stays).
- All other lines stay byte-for-byte identical — including:
name:,on:(paths-only trigger),concurrency:,permissions:(note: visual-diff haspull-requests: writebecause of the comment-on-PR step; do not change this).- The leading multi-line comment block at the top of the file (lines 1-5).
- The
actions/upload-artifact@v4andactions/github-script@v7(comment-on-PR) steps.
Cross-file checks (apply to both YAML files)
- No
${{ secrets.* }}reference appears inside theconcurrency:block in either file. Grep before committing:rg 'secrets\.' .github/workflows/preview-smoke.yml .github/workflows/visual-diff.ymlshould return exactly two matches per file (one in the wait-action'spath:, one in the Playwright/visual step'senv:). Three or more matches per file means a stray secret reference snuck somewhere; investigate. - No
echo,printf,cat, orset -xreferences the bypass-bearing URL or env var. Grep:rg 'echo|printf|cat|set -x' .github/workflows/preview-smoke.yml .github/workflows/visual-diff.yml— every match should be an existing line untouched by this brief (thegate:job'secho "should_run=..." >> $GITHUB_OUTPUTlines are fine; they don't echo URLs or secrets). - Diff hygiene.
git diff main..HEAD -- .github/workflows/should show only the changes specified above. No whitespace-only changes elsewhere. No unrelated edits. Total diff is expected to be ~50 LOC across both files (~20 LOC per file changed, plus a few context lines).
Acceptance criterion #1 — end-state behavior
After this brief lands on the convoy branch and a Vercel preview deployment is published for the PR:
- The wait-action's healthcheck exits successfully. In the
Playwright smokejob's run log, expect a single line near the end of theWait for Vercel Preview deploymentstep:
This means the bypass query made it through to Vercel and the protected preview returned 2xx.Received success status code - The wait-action step takes < 90 seconds wall time (typical: < 10s if the deployment is already up, which is the usual case based on PR #16's logs).
- The subsequent
Run smoke testsstep (preview-smoke) orCapture screenshots (PR)step (visual-diff) is REACHED, even though it will fail because@playwright/testis not installed. The expected failure mode is roughly:
OR anpm ERR! could not determine executable to run npm error code ENOENT npm error path .../node_modules/@playwright/testnpx-driven download that succeeds but then fails on the missing config. Either is acceptable for this brief — the success metric is "wait-action passed and Playwright step was reached," not "Playwright passed." - Total job runtime is < 5 minutes. The convoy file's success metric. Now-correctly-passing wait step (~10s) + reached-but-failing Playwright step (~30-90s) is well inside 5 minutes.
- The bypass secret value does NOT appear in any line of the run log. Verify after the run:
Expected:gh run download <run-id> -n logs # Then locally — NEVER commit this script — check that the secret's # first 8 chars do not appear in the downloaded logs. GitHub Actions # also auto-masks; this is belt-and-suspenders. rg "$(head -c 8 <<< "$VERCEL_AUTOMATION_BYPASS_SECRET")" logs/ || echo "OK — bypass not in logs"OK — bypass not in logs.
Manual verification (in addition to the workflow run on push)
Run these in order. Paste relevant output (with secrets redacted) into the PR description.
-
Local YAML lint. Install actionlint (one-time):
brew install actionlintThen validate:
actionlint .github/workflows/preview-smoke.yml .github/workflows/visual-diff.ymlExpected: zero output, exit code 0. If actionlint flags anything, read the message — most actionlint warnings are real (shellcheck embedded). Investigate before commit. If
brewis unavailable, the binary is downloadable from the actionlint releases page; recommended but not strictly required by acceptance criterion (the workflow YAML is small enough to eyeball). -
Local
ghdry-run check. Verify the secret is still seeded (operator says it is, but confirm before pushing):gh secret list | grep VERCEL_AUTOMATION_BYPASS_SECRETExpected: one line showing the secret name and an
Updatedtimestamp. -
Push the branch and observe the first workflow run. From the convoy branch (
convoy/fix-vercel-deployment-protection-in-ci):git push -u origin HEADThen watch the Preview-smoke and Visual-diff workflows. The first Vercel preview deploy on this PR is the one to scrutinize. Expect:
- Wait-action step logs
target url » https://<deployment>.vercel.app(note: no query string in this log — that's the action's safe log of the bare deployment URL). - Within a few seconds,
Received success status code. - Step exits 0.
- Next step (
Set up Node,npm ci, etc.) runs. - Eventually fails at the Playwright step — that's the expected end state of THIS brief.
- Wait-action step logs
-
Re-run validation. Click "Re-run jobs" on the same run. Expect identical behavior — the wait-action's healthcheck issues fresh axios GETs on every iteration (no caching), so re-runs are idempotent (Risk R-not-listed-because-confirmed-OK).
-
Bypass-log-leak validation (as documented above under acceptance criterion #1, final bullet).
Boot-the-brief findings (preempted by the architect; do not re-investigate)
Finding 1 — Wait-action source: path: is consumed via new URL(path, url)
patrickedqvist/wait-for-vercel-preview@v1.3.2 at action.js:42:
let checkUri = new URL(path, url);
await axios.get(checkUri.toString(), { headers });
Parsed as a URL relative to url (the deployment URL). Query strings
work verbatim. MUST begin with / or the URL resolver produces
unexpected paths.
Finding 2 — Wait-action source: bypass secret never appears in the action's logs
Action source emits exactly three console.log calls that include URL
content:
action.js:357—console.log('target url »', targetUrl).targetUrlisstatus.target_url(the bare deployment URL from the GitHub Deployments API). Nopath:is appended. ✅ Safe.action.js:363—console.log('Waiting for a status code 200 from: ${targetUrl}'). SametargetUrl. ✅ Safe.action.js:53-54—console.log('GET status: ${e.response.status}. Attempt ${i} of ${iterations}'). Only the HTTP status code, no URL. ✅ Safe.
The bypass query string lives ONLY in the internal checkUri axios
call. This means: passing the bypass via path: is structurally
safe from log leakage by the action itself. Your remaining job is to
not add any echo/print step in the workflow YAML that constructs a URL
with the bypass.
Finding 3 — outputs.url is the bare URL (no bypass query)
action.js:360: core.setOutput('url', targetUrl) — targetUrl does
NOT include path:. So ${{ steps.vercel.outputs.url }} downstream is
clean. This is why the Playwright BASE_URL env var stays bare: the
future playwright.config.js will inject the bypass via
extraHTTPHeaders, NOT by reconstructing a URL with the query string.
Finding 4 — Empty/unset secret on fork PRs
GitHub Actions silently omits repo secrets on pull_request-event runs
from forks. If the fork-PR gate (Decision D) is NOT added, fork PRs
would build https://<deployment>/?x-vercel-protection-bypass=&x-vercel-set-bypass-cookie=true,
get 401, and time out at max_timeout (120s after Decision B, but
still 4 minutes total wasted per fork PR across both workflows). The
fork-PR gate in Decision D prevents this entirely.
Finding 5 — max_timeout: 600 is excessive
PR #16's failed run (gh run view 26370087240) shows the wait-action
retrieved the deployment URL within 1 second (target url » ... at
T+1s relative to job start). The healthcheck then 401-looped for
~600s. With a working bypass, the first axios GET would have succeeded
within 2 seconds. 120s is plenty of headroom. Decision B applies.
Finding 6 — Concurrency expression is safe
Current concurrency: blocks:
- preview-smoke:
group: preview-smoke-${{ github.event.pull_request.number }} - visual-diff:
group: visual-diff-${{ github.event.pull_request.number }}
No secret reference. Adding the secret via the wait-action's with:
and the Playwright step's env: does NOT touch concurrency:. The
cancel-stale-runs behavior is preserved. Decision C applies.
Finding 7 — tests/smoke/app.smoke.spec.ts is a .ts file in a JS-only repo
Out of scope for this brief — flagged for adopt-playwright-smoke,
which will own both the Playwright config and the JS/TS decision for
its test files. Do NOT rename or edit it here.
Finding 8 — actionlint is not installed locally for the implementer
Not a blocker. The brief recommends installing it for local validation
(see Manual verification), but the absence of actionlint in CI today
means it's a recommended-not-required check. A future adopt-actionlint
convoy can add it to CI; this brief stays focused on the bypass plumb.
Out of scope (do not do these)
- No new Playwright tests or
playwright.config.js. - No
@playwright/testinstall. - No
package.jsonorpackage-lock.jsonchanges. - No edits to any other workflow YAML (
ci.yml, etc.). - No edits to
AGENTS.md§ 7 (the wording correction header → query param is the doc-writer pass after this convoy closes). - No edits to
.cursor/rules/*.mdc. - No new vitest tests (the existing 16-test suite remains green and is unrelated to this convoy).
- No replacement of
patrickedqvist/wait-for-vercel-previewwith another action or a hand-rolledgh api+curlpoll loop. That'sreplace-wait-for-vercel-preview(queued, separate scope) and is explicitly out of scope per the convoy file. - No tightening or loosening of the
permissions:blocks in either workflow — PR #16 landed the minimal scope. - No
setup-node@v4version bump, noactions/checkout@v4bump, noactions/upload-artifact@v4bump. Those are general dependency-bump scope, not this convoy's. - No
max_timeoutchange beyond the 600 → 120 specified by Decision B. - No echo of the constructed URL or BASE_URL in any step. Even for "debugging." If a debug echo is needed during local iteration, remove it before committing.
Rationale (≤3 sentences)
The wait-action's healthcheck loop is the de-facto bypass-works assertion; injecting the bypass via path: query is the only mechanism the action exposes (its action.yml has no custom-header input), and the action's source confirms the query never leaks to logs or outputs.url. Combining query-param-on-wait with header-form-on-Playwright (deferred to adopt-playwright-smoke via the plumb-the-env-var step) keeps both call sites idiomatic for their respective HTTP clients. The fork-PR gate and the max_timeout reduction are small operator-quality-of-life refinements that make a 10-minute failure into a 2-minute (or zero-minute) failure when something does go wrong.