Skip to content

feat(ai-elements): add Convex UI parts adapter #372

feat(ai-elements): add Convex UI parts adapter

feat(ai-elements): add Convex UI parts adapter #372

Workflow file for this run

name: Tier B regression (preview)
# The single highest-leverage check in this repo. Runs the full
# Tier B Playwright suite (tests/e2e/exact-kit-parity-prod.spec.ts)
# against the Vercel PR preview URL BEFORE merge.
#
# Why this matters: A9 took 4 separate PRs to land on prod earlier
# today (#166, #167, #168, #170-retrigger), because each fix was
# only verified against the live prod URL post-merge. Each fix
# unblocked the next assertion, which we only saw after deploy.
# Running Tier B against the preview URL on every PR catches that
# class of failures as a single CI check on the FIRST PR.
#
# Strategy:
# 1. Wait for Vercel's PR preview deployment to be Ready (poll
# Vercel's deployments API, filter by commit SHA).
# 2. Extract the deployment URL.
# 3. Run Playwright with BASE_URL=$PREVIEW_URL.
#
# Required secrets:
# - VERCEL_TOKEN: a Vercel access token with Read access to the
# team. Create at https://vercel.com/account/tokens.
# - VERCEL_TEAM_ID, VERCEL_PROJECT_ID: already in the project's
# .vercel/project.json — duplicated here as secrets for the
# CI runner which doesn't have a linked Vercel CLI.
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
merge_group:
workflow_dispatch:
inputs:
pr_number:
description: "PR number to inspect for changed files when running manually"
required: false
head_ref:
description: "Head branch to poll in Vercel when running manually"
required: false
head_sha:
description: "Head SHA to prefer when running manually"
required: false
preview_url:
description: "Override preview URL (skip Vercel polling)"
required: false
permissions:
contents: read
pull-requests: write
concurrency:
group: tier-b-preview-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
wait-and-test:
name: Tier B vs preview URL
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
package-manager-cache: false
node-version-file: ".nvmrc"
- name: Install dependencies
run: npm install --no-audit --no-fund
- name: Detect Vercel-relevant changes
id: relevant
run: |
# Mirror scripts/vercel-ignore-build.sh: if the PR's diff doesn't
# touch any path Vercel actually serves, no preview will ever exist.
# Don't block such PRs (docs-only, .github/-only, etc.).
BUILD_RELEVANT=(src convex packages apps public server shared api scripts/vercel-build.sh vercel.json package.json package-lock.json tsconfig.json tsconfig.node.json vite.config.ts vite.config.mjs index.html)
if [ -n "${OVERRIDE_URL:-}" ]; then
echo "has_relevant=true" >> "$GITHUB_OUTPUT"
echo "Manual preview URL supplied; running Tier B against it."
exit 0
fi
if [ -z "${PR_NUMBER:-}" ]; then
echo "has_relevant=true" >> "$GITHUB_OUTPUT"
echo "No PR number available for changed-file detection; assuming relevant."
exit 0
fi
# Get changed files for this PR
CHANGED=$(gh pr diff "$PR_NUMBER" --name-only 2>/dev/null || echo "")
if [ -z "$CHANGED" ]; then
# Fallback: if we can't get the diff, assume relevant
echo "has_relevant=true" >> "$GITHUB_OUTPUT"
echo "Could not determine changed paths — assuming relevant."
exit 0
fi
# Strip test-only / non-shipping files BEFORE relevance detection. Vitest
# tests, __tests__/__mocks__ fixtures, and *.stories never reach the Vercel
# bundle, so a PR that ONLY touches them cannot change the preview — and
# must NOT burn the ~10-min preview-resolve poll. Without this, a
# convex/__tests__/*.test.ts PR matches the broad `convex` path and waits
# the full 10 min to skip-green (the "why is Tier B taking forever" case).
SHIPPING=$(echo "$CHANGED" | grep -vE '(\.test\.[cm]?tsx?$|\.spec\.[cm]?tsx?$|(^|/)__tests__/|(^|/)__mocks__/|\.stories\.[cm]?tsx?$)' || true)
if [ -z "$SHIPPING" ]; then
echo "has_relevant=false" >> "$GITHUB_OUTPUT"
echo "::notice::PR touches only test/non-shipping files — no preview can change. Tier B skipped (instant)."
exit 0
fi
for path in "${BUILD_RELEVANT[@]}"; do
if echo "$SHIPPING" | grep -qE "(^|/)${path}(/|$)"; then
echo "has_relevant=true" >> "$GITHUB_OUTPUT"
echo "Detected change in build-relevant path: ${path}"
exit 0
fi
done
echo "has_relevant=false" >> "$GITHUB_OUTPUT"
echo "::notice::PR touches no build-relevant paths — Vercel will not build a preview, Tier B skipped."
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
OVERRIDE_URL: ${{ github.event.inputs.preview_url }}
- name: Resolve Vercel preview URL
id: preview
if: steps.relevant.outputs.has_relevant == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
# GITHUB_SHA and GITHUB_REF are reserved runner variables and cannot be
# overridden reliably. Keep the Vercel lookup inputs under distinct names.
VERCEL_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha || github.sha }}
VERCEL_HEAD_REF: ${{ github.event.pull_request.head.ref || github.event.inputs.head_ref || github.ref_name }}
OVERRIDE_URL: ${{ github.event.inputs.preview_url }}
run: |
if [ -n "$OVERRIDE_URL" ]; then
echo "Using override URL: $OVERRIDE_URL"
echo "preview_status=ready" >> "$GITHUB_OUTPUT"
echo "url=$OVERRIDE_URL" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "$VERCEL_TOKEN" ]; then
echo "::error::VERCEL_TOKEN secret missing — Tier B preview workflow can't poll Vercel."
exit 1
fi
# Poll for up to ~10 min for a Vercel preview deployment matching this PR.
#
# The query strategy went through two iterations:
# v1 (broken): filter by the runner's merge SHA. After
# update-branch / merge-commit injection, the SHA Vercel records
# can drift from the PR head SHA → false-fail timeouts.
# v2 (this version): filter by meta-githubCommitRef using the explicit
# PR head ref, then require the deployment's metadata to match the
# explicit PR head SHA exactly.
#
# Only READY for the exact PR head SHA is acceptable here. The changed-
# file step already handles proven non-shipping diffs, so CANCELED,
# ERROR, API errors, and resolver timeouts fail closed.
deadline=$(( $(date +%s) + 600 ))
# URL-encode the branch ref. Branches with "/" in the name (refactor/X,
# fix/Y, test/Z) require encoding — without it, Vercel's API treats
# the query as an empty match, returns no deployments, and the loop
# below burns the full 10-min deadline before failing. With encoding,
# the API returns the actual deployment within seconds.
# (jq is preinstalled on ubuntu-latest GitHub runners.)
encoded_ref=$(printf '%s' "$VERCEL_HEAD_REF" | jq -sRr @uri)
while [ "$(date +%s)" -lt "$deadline" ]; do
# Query by branch name (stable) AND limit=20 to catch the latest few
# deployments on this branch even if Vercel created multiple builds.
payload=$(curl -fsS -H "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v6/deployments?teamId=$VERCEL_TEAM_ID&projectId=$VERCEL_PROJECT_ID&meta-githubCommitRef=$encoded_ref&limit=20&target=preview")
result=$(VERCEL_HEAD_SHA="$VERCEL_HEAD_SHA" node -e "
let s=''; process.stdin.on('data',c=>s+=c).on('end',()=>{
try {
const j = JSON.parse(s);
const sha = process.env.VERCEL_HEAD_SHA || '';
if (!Array.isArray(j.deployments)) {
throw new Error(j.error?.message || 'deployments array missing');
}
const deployments = j.deployments;
const state = (d) => d.readyState || d.state;
const exactReady = deployments.find(d => {
const m = d.meta || {};
return (m.githubCommitSha === sha || m.commitSha === sha) && state(d) === 'READY';
});
const exactError = deployments.find(d => {
const m = d.meta || {};
return (m.githubCommitSha === sha || m.commitSha === sha) && state(d) === 'ERROR';
});
const exactCanceled = deployments.find(d => {
const m = d.meta || {};
return (m.githubCommitSha === sha || m.commitSha === sha) && state(d) === 'CANCELED';
});
const dep = exactReady || exactError || exactCanceled;
if (dep) {
const st = state(dep);
if (st === 'READY') console.log('READY:https://' + dep.url);
else if (st === 'CANCELED') console.log('CANCELED:vercel-marked-ignored');
else if (st === 'ERROR') console.log('ERROR:vercel-build-failed');
}
} catch(e) {
console.error('Invalid Vercel deployments response:', e instanceof Error ? e.message : e);
process.exitCode = 2;
}
});
" <<< "$payload")
if [[ "$result" == READY:* ]]; then
url="${result#READY:}"
echo "Preview ready: $url"
echo "preview_status=ready" >> "$GITHUB_OUTPUT"
echo "url=$url" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$result" == CANCELED:* ]]; then
echo "::error::Vercel canceled the exact build-relevant head SHA. Tier B cannot verify this shipping change."
exit 1
fi
if [[ "$result" == ERROR:* ]]; then
echo "::error::Vercel preview build failed for this branch. Tier B can't run against a failed deploy. Investigate the Vercel build log."
exit 1
fi
sleep 15
done
# A build-relevant PR without an exact-head deployment is unverifiable.
echo "::error::No exact-head preview deployment found for branch '$VERCEL_HEAD_REF' at SHA '$VERCEL_HEAD_SHA' after 10min."
exit 1
- name: Install Playwright browsers
if: steps.preview.outputs.preview_status == 'ready'
run: npx playwright install --with-deps chromium
- name: Tier B summary
if: always()
run: |
echo "preview_status=${{ steps.preview.outputs.preview_status || 'not_resolved' }}" >> "$GITHUB_STEP_SUMMARY"
echo "preview_url=${{ steps.preview.outputs.url || 'none' }}" >> "$GITHUB_STEP_SUMMARY"
- name: Require resolved preview for shipping changes
if: always() && steps.relevant.outputs.has_relevant == 'true' && steps.preview.outputs.preview_status != 'ready'
run: |
echo "::error::Build-relevant change did not produce a real READY preview; Tier B fails closed."
exit 1
- name: Run Tier B regression + one-flow regression against preview
if: steps.preview.outputs.preview_status == 'ready'
env:
BASE_URL: ${{ steps.preview.outputs.url }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
run: |
echo "BASE_URL=$BASE_URL"
if [ -z "$VERCEL_AUTOMATION_BYPASS_SECRET" ]; then
echo "::error::VERCEL_AUTOMATION_BYPASS_SECRET is required to test the protected READY preview."
exit 1
fi
# exact-kit-parity-prod: visual selector parity (per-PR canonical-component check)
# one-flow-regression: full surface tour + A9 fallback + interactive chip behavior
npx playwright test tests/e2e/vercel-preview-security.spec.ts tests/e2e/exact-kit-parity-prod.spec.ts tests/e2e/one-flow-regression.spec.ts --project=chromium --reporter=line
- name: Upload Playwright artifacts on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: tier-b-preview-${{ github.run_id }}
path: |
playwright-report/
test-results/
retention-days: 14
- name: Comment failure on PR
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v9
with:
script: |
const url = "${{ steps.preview.outputs.url }}" || "(preview URL unresolved)";
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
`## Tier B regression failed against preview`,
``,
`**Preview URL:** ${url}`,
`**Run:** [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
``,
`Likely a kit-parity DOM regression. Check the Playwright report artifact attached to the run for the failing selector + screenshot.`,
``,
`*This check would have caught all 4 A9 sessionRows iterations as a single PR — that's why it exists.*`,
].join('\n'),
});