ci: slash GitHub Actions minutes via paths-ignore + consolidation + caching (#126)
Standalone infrastructure PR (no convoy ceremony needed — single-file scope). Triggered by the GitHub Actions billing block that gated PRs #124 + #125 today. Three layers of savings applied per the user's max-savings option: 1. paths-ignore on ci.yml + preview-smoke.yml - Doc-only PRs (.convoys/**, **/*.md, docs/**, AGENTS.md, .cursor/**, README.md) now trigger ZERO Actions jobs. - Vercel still builds (it's not on the Actions billing). - visual-diff.yml unchanged — it was already cost-conscious via a positive paths: allowlist (pages/**, components/**, styles/**, etc.). 2. Consolidate 6 grep-only forbidden-* jobs into 1 - Previously 6 independent jobs each ran their own actions/checkout (~3s × 6 = 18s of redundant checkout). - Merged into a single forbidden-patterns job with 6 sequential ::group:: sections, one FAIL flag at the bottom — preserves "see all violations in one run" diagnostic behavior. Per-file ::error file=...::msg annotations work the same way. - Removed jobs: forbidden-endpoints, forbidden-cors-headers, forbidden-client-side-llm-keys, forbidden-modal-shell-without-primitive, forbidden-deprecated-color-aliases, forbidden-stale-strings. - pr-health-rollup.yml only looks up "Lint" and "Schema map up to date" by name — unaffected. 3. Cache node_modules + Playwright browsers - actions/cache@v4 for node_modules keyed by package-lock.json hash, applied to lint / test / migrate / preview-smoke / visual-diff. Cuts npm ci from ~30-45s to ~3-5s on cache hit. setup-node@v4's built-in cache: npm stays (caches ~/.npm) — both layered. - actions/cache@v4 for ~/.cache/ms-playwright keyed by the resolved @playwright/test version. Cache invalidates on any Playwright version bump. On cache hit, only system deps install runs (npx playwright install-deps chromium) — saves ~15-25s/run. AGENTS.md updates: - § 6 Testing § CI behavior: appended "CI minute optimizations" subsection documenting all three layers. - § Product vocabulary table caption: updated "CI job forbidden-stale-strings" reference to "CI check Forbidden patterns (6 checks) → Check 6/6" with a historical pointer. - Gotcha #5: updated the standalone forbidden-endpoints reference similarly. Estimated savings per typical convoy mix (~30% doc PRs based on repo history): - Doc-only PRs: 100% reduction (was ~6-7 min, now 0 Actions min). - Code-touching PRs: ~30-50% reduction (cache hits for npm + Playwright + no redundant 6× checkout). - Weighted average: ~50-60% reduction. This is short of the 70-80% I floated in chat — the real ceiling is limited by lint / vitest / migrate / Playwright runtime itself, all of which are kept on code-touching PRs (they're high-signal). Verification: - All 3 workflow YAMLs parse (python3 -c "yaml.safe_load(...)"). - npm run lint passes (1 pre-existing unrelated warning). - npm run test:run: 118/118 tests pass. - forbidden-patterns logic is byte-equivalent to the 6 original jobs' bash bodies — the differences are: per-check ::group::/::endgroup:: framing, a shared FAIL flag instead of per-job exit 1, and renamed local arrays (FOUND → LLM_FOUND / MODAL_FOUND / STRING_FOUND) to avoid clobbering across the single job's scope. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8bb174c7cc
commit
a4dca47642
4 changed files with 197 additions and 107 deletions
224
.github/workflows/ci.yml
vendored
224
.github/workflows/ci.yml
vendored
|
|
@ -16,8 +16,27 @@ name: CI
|
|||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
# Skip CI entirely for doc-only PRs. The `slash-ci-minutes` convoy
|
||||
# (2026-06-04) added this filter because every grep / lint / vitest /
|
||||
# migrate job that fires on a pure-docs PR (e.g. `.convoys/` seeds +
|
||||
# briefs, AGENTS.md edits, `docs/`) costs GitHub Actions minutes for
|
||||
# zero signal. The full job set still runs on code-touching PRs.
|
||||
paths-ignore:
|
||||
- '.convoys/**'
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- 'AGENTS.md'
|
||||
- '.cursor/**'
|
||||
- 'README.md'
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '.convoys/**'
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- 'AGENTS.md'
|
||||
- '.cursor/**'
|
||||
- 'README.md'
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
|
|
@ -36,7 +55,19 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
# Cache node_modules between runs (in addition to setup-node's npm
|
||||
# cache). `setup-node@v4`'s `cache: npm` caches `~/.npm` but `npm
|
||||
# ci` still does a fresh install into `node_modules` — that's the
|
||||
# ~30-45s we want to skip on cache hit. Keyed by package-lock.json
|
||||
# hash so any dep change invalidates correctly.
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||
- run: npm ci
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
- run: npm run lint --if-present
|
||||
|
||||
schema-map-fresh:
|
||||
|
|
@ -74,13 +105,27 @@ jobs:
|
|||
fi
|
||||
echo "OK: schema map and migration scripts are in sync."
|
||||
|
||||
forbidden-endpoints:
|
||||
name: No dev endpoints in pages/api
|
||||
# Consolidated grep-based regression gates. Previously 6 separate jobs
|
||||
# (forbidden-endpoints / -cors-headers / -client-side-llm-keys /
|
||||
# -modal-shell-without-primitive / -deprecated-color-aliases /
|
||||
# -stale-strings); the `slash-ci-minutes` convoy (2026-06-04) merged
|
||||
# them into one job to amortize a single actions/checkout across all
|
||||
# 6 checks. Each check still emits `::error file=...::msg` so the
|
||||
# in-file annotations work the same way. The job uses a FAIL flag so
|
||||
# ALL violations across all 6 checks are reported in a single run
|
||||
# (matches the previous independent-jobs behavior; nicer than
|
||||
# stop-at-first-failure).
|
||||
forbidden-patterns:
|
||||
name: Forbidden patterns (6 checks)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if dev endpoints re-appear under pages/api/
|
||||
- name: Run all forbidden-pattern checks
|
||||
run: |
|
||||
FAIL=0
|
||||
|
||||
# ---------- (1) No dev endpoints in pages/api ----------
|
||||
echo "::group::Check 1/6: No dev endpoints in pages/api"
|
||||
BAD_PATHS=(
|
||||
"pages/api/simple.js"
|
||||
"pages/api/test-auth.js"
|
||||
|
|
@ -93,7 +138,6 @@ jobs:
|
|||
FOUND+=("$path")
|
||||
fi
|
||||
done
|
||||
# Also flag any new pages/api/test-*.js the explicit list missed.
|
||||
while IFS= read -r path; do
|
||||
FOUND+=("$path")
|
||||
done < <(find pages/api -maxdepth 4 -type f -name 'test-*.js' 2>/dev/null || true)
|
||||
|
|
@ -102,27 +146,14 @@ jobs:
|
|||
for path in "${FOUND[@]}"; do
|
||||
echo "::error file=${path}::Forbidden dev endpoint."
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: no forbidden dev endpoints under pages/api/."
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
forbidden-cors-headers:
|
||||
name: No wildcard CORS in pages/api
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if any pages/api/ handler carries Access-Control-Allow-Origin
|
||||
run: |
|
||||
# The tcg-vault frontend and API are served from the same Vercel
|
||||
# deployment (same origin), so CORS headers serve no purpose and
|
||||
# are a documented attack surface (see .convoys/cors-tighten.md
|
||||
# and AGENTS.md Gotcha #5). Brief 4 of fix-auth-bypass cleaned
|
||||
# login.js + register.js; the cors-tighten convoy swept the
|
||||
# remaining 24 files. This job locks the cleanup in.
|
||||
#
|
||||
# If a future cross-origin caller is legitimately needed, design
|
||||
# a proper CORS layer (probably via middleware) rather than
|
||||
# scaffolding wildcards into individual handlers.
|
||||
# ---------- (2) No wildcard CORS in pages/api ----------
|
||||
echo "::group::Check 2/6: No wildcard CORS in pages/api"
|
||||
MATCHES=$(grep -rEn 'Access-Control-Allow-(Origin|Methods|Headers)' pages/api/ 2>/dev/null || true)
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "::error::Forbidden CORS headers present under pages/api/. Remove them — same-origin Vercel deployment does not need CORS."
|
||||
|
|
@ -131,21 +162,17 @@ jobs:
|
|||
lineno=$(echo "$line" | cut -d: -f2)
|
||||
echo "::error file=${file},line=${lineno}::Forbidden CORS header — delete this line."
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: no Access-Control-Allow-* headers under pages/api/."
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
forbidden-client-side-llm-keys:
|
||||
name: No client-side LLM key leakage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail on key-returning config endpoints or new browser LLM URLs
|
||||
run: |
|
||||
# Deleted by secure-scanner-gemini-key — must not return API keys to browsers.
|
||||
# ---------- (3) No client-side LLM key leakage ----------
|
||||
echo "::group::Check 3/6: No client-side LLM key leakage"
|
||||
if [ -f pages/api/config/gemini.js ]; then
|
||||
echo "::error file=pages/api/config/gemini.js::Forbidden config endpoint — do not return API keys to browsers."
|
||||
exit 1
|
||||
FAIL=1
|
||||
fi
|
||||
CONFIG_MATCHES=$(grep -rEn 'apiKey:' pages/api/config/ 2>/dev/null || true)
|
||||
if [ -n "$CONFIG_MATCHES" ]; then
|
||||
|
|
@ -155,7 +182,7 @@ jobs:
|
|||
lineno=$(echo "$line" | cut -d: -f2)
|
||||
echo "::error file=${file},line=${lineno}::Do not return API keys from config endpoints."
|
||||
done
|
||||
exit 1
|
||||
FAIL=1
|
||||
fi
|
||||
AI_OCR_IMPORTS=$(grep -rEn 'from ['\''"].*ai-ocr|import.*ai-ocr' components/ 2>/dev/null || true)
|
||||
if [ -n "$AI_OCR_IMPORTS" ]; then
|
||||
|
|
@ -165,14 +192,13 @@ jobs:
|
|||
lineno=$(echo "$line" | cut -d: -f2)
|
||||
echo "::error file=${file},line=${lineno}::Remove ai-ocr import; call server-side scan API."
|
||||
done
|
||||
exit 1
|
||||
FAIL=1
|
||||
fi
|
||||
# lib/ may hold server-only helpers imported only from pages/api/.
|
||||
SERVER_ONLY=(
|
||||
lib/scan-vision.js
|
||||
)
|
||||
LLM_PATTERN='generativelanguage\.googleapis\.com|api\.openai\.com|ai-gateway\.vercel\.sh'
|
||||
FOUND=()
|
||||
LLM_FOUND=()
|
||||
while IFS= read -r file; do
|
||||
skip=false
|
||||
for so in "${SERVER_ONLY[@]}"; do
|
||||
|
|
@ -185,66 +211,40 @@ jobs:
|
|||
continue
|
||||
fi
|
||||
if grep -qE "$LLM_PATTERN" "$file" 2>/dev/null; then
|
||||
FOUND+=("$file")
|
||||
LLM_FOUND+=("$file")
|
||||
fi
|
||||
done < <(find components lib pages -name '*.js' ! -path 'pages/api/*' 2>/dev/null || true)
|
||||
if [ ${#FOUND[@]} -gt 0 ]; then
|
||||
if [ ${#LLM_FOUND[@]} -gt 0 ]; then
|
||||
echo "::error::Client-side LLM API URLs must not appear outside pages/api/."
|
||||
for path in "${FOUND[@]}"; do
|
||||
for path in "${LLM_FOUND[@]}"; do
|
||||
echo "::error file=${path}::Move LLM calls server-side or add to server-side-scan-pipeline removal list."
|
||||
done
|
||||
exit 1
|
||||
FAIL=1
|
||||
fi
|
||||
echo "OK: no client-side LLM key leakage patterns detected."
|
||||
[ ${#LLM_FOUND[@]} -eq 0 ] && [ -z "$CONFIG_MATCHES" ] && [ -z "$AI_OCR_IMPORTS" ] && [ ! -f pages/api/config/gemini.js ] && echo "OK: no client-side LLM key leakage patterns detected."
|
||||
echo "::endgroup::"
|
||||
|
||||
forbidden-modal-shell-without-primitive:
|
||||
name: No new modal shells without <Modal> primitive
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if ANY file uses the legacy modal-shell pattern
|
||||
run: |
|
||||
# liquid-glass-modal-and-surface-primitive convoy — every modal
|
||||
# MUST compose the `<Modal>` primitive from components/ui/.
|
||||
# The original 9 grandfathered shells were swept in the
|
||||
# finish-liquid-glass-design PR; the gate is now strict and
|
||||
# has zero allow-list entries. If a new file legitimately
|
||||
# needs the legacy pattern (rare — backdrop-blur sheets that
|
||||
# are not modals belong in a different primitive), revive the
|
||||
# allow-list pattern AND open a tracking issue.
|
||||
FOUND=()
|
||||
# ---------- (4) No legacy modal shells ----------
|
||||
echo "::group::Check 4/6: No new modal shells without <Modal> primitive"
|
||||
MODAL_FOUND=()
|
||||
while IFS= read -r file; do
|
||||
FOUND+=("$file")
|
||||
MODAL_FOUND+=("$file")
|
||||
done < <(grep -lE 'fixed inset-0 bg-black bg-opacity-' \
|
||||
pages components -r --include='*.js' 2>/dev/null \
|
||||
| sort -u || true)
|
||||
if [ ${#FOUND[@]} -gt 0 ]; then
|
||||
if [ ${#MODAL_FOUND[@]} -gt 0 ]; then
|
||||
echo "::error::Legacy modal-shell pattern detected. Use the <Modal> primitive from components/ui/."
|
||||
for f in "${FOUND[@]}"; do
|
||||
for f in "${MODAL_FOUND[@]}"; do
|
||||
echo "::error file=${f}::Replace 'fixed inset-0 bg-black bg-opacity-' with <Modal open={…} onClose={…} title=…> from components/ui."
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: no legacy modal shells anywhere in pages/ or components/."
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
forbidden-deprecated-color-aliases:
|
||||
name: No use of deprecated color aliases
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if ANY code uses pre-Deck-Hearth blue/purple/pink aliases
|
||||
run: |
|
||||
# liquid-glass-design-tokens + cleanup-legacy-design-css convoys.
|
||||
# Pre-Deck-Hearth alias utility classes (.gradient-text-blue /
|
||||
# .gradient-text-purple / .glow-blue / .glow-purple / .glow-pink /
|
||||
# .gradient-bg-purple / .gradient-bg-blue / .gradient-bg-pink) were
|
||||
# graduated from WARN to FAIL by the finish-liquid-glass-design PR
|
||||
# after every consumer was swept to --accent-ember equivalents
|
||||
# AND the CSS definitions were deleted from styles/globals.css.
|
||||
#
|
||||
# If a new consumer of one of these classes appears, the build
|
||||
# will fail — use --accent-ember, --accent-flame, --accent-gold
|
||||
# (the canonical warm-palette tokens) instead.
|
||||
# ---------- (5) No deprecated color aliases ----------
|
||||
echo "::group::Check 5/6: No use of deprecated color aliases"
|
||||
PATTERNS=(
|
||||
'gradient-text-purple'
|
||||
'gradient-text-pink'
|
||||
|
|
@ -258,9 +258,9 @@ jobs:
|
|||
)
|
||||
ALL_MATCHES=""
|
||||
for pattern in "${PATTERNS[@]}"; do
|
||||
MATCHES=$(grep -rFn "$pattern" pages components --include='*.js' 2>/dev/null || true)
|
||||
if [ -n "$MATCHES" ]; then
|
||||
ALL_MATCHES=$(printf '%s\n%s' "$ALL_MATCHES" "$MATCHES")
|
||||
P_MATCHES=$(grep -rFn "$pattern" pages components --include='*.js' 2>/dev/null || true)
|
||||
if [ -n "$P_MATCHES" ]; then
|
||||
ALL_MATCHES=$(printf '%s\n%s' "$ALL_MATCHES" "$P_MATCHES")
|
||||
fi
|
||||
done
|
||||
if [ -n "$ALL_MATCHES" ]; then
|
||||
|
|
@ -271,43 +271,47 @@ jobs:
|
|||
lineno=$(echo "$line" | cut -d: -f2)
|
||||
echo "::error file=${file},line=${lineno}::Deprecated color alias — replace with the canonical Deck Hearth warm-palette equivalent."
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: no deprecated color aliases in pages/ or components/."
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
forbidden-stale-strings:
|
||||
name: No stale ownership/collection copy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if forbidden UI strings appear in pages/ or components/
|
||||
run: |
|
||||
# rename-collections-vocabulary convoy — ownership vs curated-list taxonomy.
|
||||
# API/DB literals (e.g. system collection name) live under pages/api/ only.
|
||||
PATTERNS=(
|
||||
# ---------- (6) No stale ownership/collection copy ----------
|
||||
echo "::group::Check 6/6: No stale ownership/collection copy"
|
||||
STRING_PATTERNS=(
|
||||
'Mark Owned'
|
||||
'Owned Cards'
|
||||
'All My Cards'
|
||||
)
|
||||
FOUND=()
|
||||
for pattern in "${PATTERNS[@]}"; do
|
||||
STRING_FOUND=()
|
||||
for pattern in "${STRING_PATTERNS[@]}"; do
|
||||
while IFS= read -r line; do
|
||||
FOUND+=("$line")
|
||||
STRING_FOUND+=("$line")
|
||||
done < <(grep -rFn "$pattern" pages/ components/ \
|
||||
--include='*.js' \
|
||||
--exclude-dir=api 2>/dev/null || true)
|
||||
done
|
||||
if [ ${#FOUND[@]} -gt 0 ]; then
|
||||
if [ ${#STRING_FOUND[@]} -gt 0 ]; then
|
||||
echo "::error::Forbidden stale UI string(s) in pages/ or components/. Use lib/collection-vocabulary.js labels — see AGENTS.md § Product vocabulary."
|
||||
printf '%s\n' "${FOUND[@]}" | sort -u | while IFS= read -r line; do
|
||||
printf '%s\n' "${STRING_FOUND[@]}" | sort -u | while IFS= read -r line; do
|
||||
file=$(echo "$line" | cut -d: -f1)
|
||||
lineno=$(echo "$line" | cut -d: -f2)
|
||||
text=$(echo "$line" | cut -d: -f3-)
|
||||
echo "::error file=${file},line=${lineno}::${text}"
|
||||
done
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: no forbidden stale strings in pages/ or components/."
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
# ---------- Final exit ----------
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
echo "::error::One or more forbidden-pattern checks failed. See annotations above."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: no forbidden stale strings in pages/ or components/."
|
||||
echo "All 6 forbidden-pattern checks passed."
|
||||
|
||||
migrate:
|
||||
name: Migrations apply (node-pg-migrate)
|
||||
|
|
@ -334,7 +338,14 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||
- run: npm ci
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
- name: Write migrate env file
|
||||
run: echo "POSTGRES_URL=${POSTGRES_URL}" >> .env.local
|
||||
- run: npm run migrate up
|
||||
|
|
@ -348,7 +359,14 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||
- run: npm ci
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
- run: npm run test:run
|
||||
env:
|
||||
JWT_SECRET: ci-secret-only-for-tests-do-not-use-in-prod
|
||||
|
|
|
|||
41
.github/workflows/preview-smoke.yml
vendored
41
.github/workflows/preview-smoke.yml
vendored
|
|
@ -14,6 +14,16 @@ on:
|
|||
pull_request:
|
||||
branches: [main]
|
||||
types: [labeled, opened, synchronize, reopened]
|
||||
# Skip the heavy Playwright smoke on doc-only PRs. Same set as
|
||||
# ci.yml's paths-ignore — see `slash-ci-minutes` convoy
|
||||
# (2026-06-04). Code/style/UI PRs still trigger smoke normally.
|
||||
paths-ignore:
|
||||
- '.convoys/**'
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- 'AGENTS.md'
|
||||
- '.cursor/**'
|
||||
- 'README.md'
|
||||
|
||||
concurrency:
|
||||
group: preview-smoke-${{ github.event.pull_request.number }}
|
||||
|
|
@ -89,10 +99,39 @@ jobs:
|
|||
node-version: '20'
|
||||
cache: npm
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- run: npm ci
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
|
||||
# Cache Playwright's browser binaries (~/.cache/ms-playwright) keyed
|
||||
# by the resolved @playwright/test version in the lockfile. Saves
|
||||
# ~15-25s/run of chromium download + system-deps install. Cache
|
||||
# invalidates automatically on any Playwright version bump.
|
||||
- name: Resolve Playwright version
|
||||
id: pw-version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright browsers
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-chromium
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
run: |
|
||||
if [ "${{ steps.cache-playwright.outputs.cache-hit }}" = "true" ]; then
|
||||
npx playwright install-deps chromium
|
||||
else
|
||||
npx playwright install --with-deps chromium
|
||||
fi
|
||||
|
||||
- name: Run smoke tests
|
||||
run: npx playwright test --project=smoke
|
||||
|
|
|
|||
30
.github/workflows/visual-diff.yml
vendored
30
.github/workflows/visual-diff.yml
vendored
|
|
@ -87,8 +87,36 @@ jobs:
|
|||
node-version: '20'
|
||||
cache: npm
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
|
||||
# Cache Playwright browsers — see preview-smoke.yml for rationale.
|
||||
- name: Resolve Playwright version
|
||||
id: pw-version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright browsers
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-chromium
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: |
|
||||
if [ "${{ steps.cache-playwright.outputs.cache-hit }}" = "true" ]; then
|
||||
npx playwright install-deps chromium
|
||||
else
|
||||
npx playwright install --with-deps chromium
|
||||
fi
|
||||
|
||||
- name: Capture screenshots (PR)
|
||||
run: npx playwright test --project=visual --update-snapshots=none
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ User-facing copy distinguishes **ownership** (everything you own) from **curated
|
|||
| Add ownership | **Add to My Collection** | `POST /api/user-cards`, scanner bulk | Replaces "Mark Owned" / "Mark as Owned" |
|
||||
| Add to curated list | **Add to List** | `POST /api/collections/:id/cards` | Replaces "Add to Collection" in scanner/card flows |
|
||||
|
||||
CI job `forbidden-stale-strings` blocks `"Mark Owned"`, `"Owned Cards"`, and `"All My Cards"` in `pages/` + `components/` (API literals exempt).
|
||||
CI check `Forbidden patterns (6 checks)` → Check 6/6 blocks `"Mark Owned"`, `"Owned Cards"`, and `"All My Cards"` in `pages/` + `components/` (API literals exempt). [Formerly the standalone `forbidden-stale-strings` job; merged into `forbidden-patterns` by the `slash-ci-minutes` convoy on 2026-06-04.]
|
||||
|
||||
## Visual language
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
|||
idempotent, UNIQUE-collision-safe (fails loud if `setup-neon-db.js`
|
||||
already ran post-rename — which would indicate an ordering error).
|
||||
Order: migration FIRST, then any subsequent `npm run setup-db`.
|
||||
- **#5 — `pages/api/setup-database.js` public endpoint. RESOLVED** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`). The file is deleted along with the other three dev endpoints (`/api/simple`, `/api/test-auth`, `/api/test-db`), and `.github/workflows/ci.yml`'s new `forbidden-endpoints` job fails the build if any of them are re-introduced (or if a new `pages/api/test-*.js` file appears). Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#5 — `pages/api/setup-database.js` public endpoint. RESOLVED** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`). The file is deleted along with the other three dev endpoints (`/api/simple`, `/api/test-auth`, `/api/test-db`), and `.github/workflows/ci.yml`'s `forbidden-patterns` job (formerly the standalone `forbidden-endpoints` job; consolidated by `slash-ci-minutes` convoy on 2026-06-04) fails the build if any of them are re-introduced (or if a new `pages/api/test-*.js` file appears). Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#6 — Migrations were bare scripts. RESOLVED** by `migration-tool` convoy (2026-05-26). `node-pg-migrate@^8` is the chosen tool (lightweight, raw-SQL-friendly, zero TS surface — matches the repo's JavaScript-only `@vercel/postgres` style). New migrations live under `migrations/` at the repo root and use the default `pgmigrations` tracking table. The initial backfill `migrations/1779853647564_initial-schema.js` reproduces `scripts/setup-neon-db.js`'s 7-table DDL verbatim using `CREATE TABLE IF NOT EXISTS`, so it's idempotent against fresh AND pre-existing envs — first-time `npm run migrate up` on an env that already ran `setup-neon-db.js` pre-convoy is a no-op DDL-wise (only records the `pgmigrations` row). The legacy 27 `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js` jobs are append-only history per the no-go-zones rule — do NOT add new ones. New column / constraint work ships as a `node-pg-migrate` migration. See § 3 Conventions § "Schema changes" above + `.convoys/migration-tool.md`. Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#7 — Dual `is_public` semantics.** Collections and decks both have `is_public` columns; check which controls discovery vs. anonymous read in the relevant route.
|
||||
- **#8 — Layout has hardcoded default user. RESOLVED** by `fix-layout-default-user` convoy (PR #15, squash commit `ca302a8`). `components/Layout.js`'s default prop is now `null`; `UserProfileDropdown` renders a `<Link href="/login">Sign in</Link>` CTA when `user === null`. Brief 2 also swept the 7 pages that needed page-level fixes (`scanner` / `decks` / `deck-builder` / `deck/[id]` now pass `user={user}` to Layout; `profile` / `settings` replaced leaky `useState({email:'me@…'})` with `useState(null)` + null-guards on every sync `user.*` read; `card/[id]` swapped a hardcoded `const user = {...}` for `useAuth()` from `lib/use-auth.js`). `test/components/Layout.test.js` adds 5 regression-lock assertions (no maintainer email when user is null/omitted; "Sign in" link present; supplied email renders; no "Guest" placeholder); vitest 21/21 green at merge. New devDeps: `jsdom@^29` + `@testing-library/react@^16`. See `.convoys/fix-layout-default-user.md` and `.convoys/ship-readiness.md` P0 #7. Entry kept (not renumbered) to preserve cross-references.
|
||||
|
|
@ -165,6 +165,11 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
|||
- **Vitest:** the `test:` job in `.github/workflows/ci.yml` runs `npm run test:run` on every PR and push to `main` and is **blocking** (no `|| true`, no `continue-on-error`). A red test job blocks merge.
|
||||
- **Playwright smoke:** runs on every PR via `preview-smoke.yml`. Gate skip via `pipeline: skip smoke` in the PR body (handled in the `gate:` job's Decide step via env-var routing — see § 7's shell-injection note). Last measured runtime: 59s end-to-end, 3/3 tests pass in 2.9s (PR #18 post-merge run).
|
||||
- **Screenshot diff:** runs only on PRs touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js` via `visual-diff.yml`. **First `Screenshot diff` run after `adopt-playwright-smoke` will fail at the test step** because no baseline exists yet; `continue-on-error: true` swallows the failure and the comment-on-PR step posts "Visual Diff — view run" with empty artifacts. That is the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression — it stays that way until `seed-visual-baselines-on-linux` lands.
|
||||
- **CI minute optimizations (slash-ci-minutes convoy, 2026-06-04):**
|
||||
- **Doc-only PRs skip ALL of ci.yml + preview-smoke.yml.** Both workflows carry `paths-ignore` for `.convoys/**`, `**/*.md`, `docs/**`, `AGENTS.md`, `.cursor/**`, and `README.md`. A pure-docs PR triggers zero GitHub Actions jobs (Vercel still builds — it's not on the Actions billing). `visual-diff.yml` was already cost-conscious via a positive `paths:` allowlist and is unchanged.
|
||||
- **The 6 grep-only forbidden-* jobs collapsed into one.** They previously ran as 6 independent jobs (each with its own `actions/checkout`); the consolidated `forbidden-patterns` job runs all 6 checks as labeled `::group::` sections in a single bash step, with a FAIL flag at the bottom so every violation across all 6 checks still surfaces in one run (same diagnostic behavior, ~5/6 of the per-PR checkout overhead removed). The 6 original job names (`forbidden-endpoints`, `forbidden-cors-headers`, `forbidden-client-side-llm-keys`, `forbidden-modal-shell-without-primitive`, `forbidden-deprecated-color-aliases`, `forbidden-stale-strings`) no longer appear in the checks list — references in this file (e.g. CI job `forbidden-stale-strings` blocks ...) are now informational, not check-name lookups. `pr-health-rollup.yml` was unaffected because it only looks up `Lint` and `Schema map up to date` by name.
|
||||
- **`node_modules` cached between runs** in lint / test / migrate / preview-smoke / visual-diff. Keyed on `package-lock.json` hash so any dep change invalidates correctly. Cuts `npm ci` from ~30-45s to ~3-5s on cache hit. `actions/setup-node@v4`'s built-in `cache: npm` is layered above this (caches `~/.npm`) — both stay because the `setup-node` cache helps on cache-miss days too.
|
||||
- **Playwright browsers cached** in `preview-smoke.yml` + `visual-diff.yml`. Keyed on the resolved `@playwright/test` version from `package-lock.json`. Cache invalidates automatically on any Playwright version bump. On cache hit, only the system deps install (`npx playwright install-deps chromium`) runs — saves ~15-25s/run.
|
||||
- **Manual QA:** `TESTING_GUIDE.md` still applies for flows not yet covered by automated tests (scanner camera path, card-import jobs, multi-step UI wizards). The automated smoke + visual suite is steadily eclipsing it; `TESTING_GUIDE.md` will be renamed to `docs/MANUAL_QA.md` and trimmed to truly-manual-only flows in a future cleanup convoy (see `.convoys/ship-readiness.md` § Role-doc-writer findings).
|
||||
|
||||
## 7. Deployment
|
||||
|
|
|
|||
Loading…
Reference in a new issue