Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6f26edd
ci: add runtime gate, security scan, workflow lint and house Renovate…
rishitank Oct 8, 2026
61f45a1
ci: pin runtime-gate to 8040381 (operates every safe control)
rishitank Oct 8, 2026
cad1237
ci: pin runtime-gate to 8e8a197 (fills and submits forms)
rishitank Oct 8, 2026
f28b467
ci: correct the runtime.yml form comment; regenerate bun.lock on a ru…
rishitank Oct 8, 2026
8c30412
ci(deps-refresh-once): fix the commit step
rishitank Oct 8, 2026
90bba3b
chore(deps): refresh lockfile to pick up patched versions
github-actions[bot] Oct 8, 2026
6a9c11a
ci: remove the one-shot deps-refresh-once workflow
rishitank Oct 8, 2026
3df9260
ci(lighthouse): 5 runs, assert on the median run, performance score a…
rishitank Oct 8, 2026
e26e7e1
ci(lighthouse): aggregate with "median", not "median-run"
rishitank Oct 8, 2026
2c6670a
ci(runtime-gate): move to runtime-gate v4 (signed-in crawl, recorded …
rishitank Oct 8, 2026
af62cae
ci(runtime-gate): move to runtime-gate v4.1
rishitank Oct 8, 2026
fcbf2df
perf: slim Motion via LazyMotion, load the report on demand; type the…
rishitank Oct 8, 2026
46ebc68
perf: wire LazyMotion and the lazy report into the routes; type the r…
rishitank Oct 8, 2026
049e2c9
fix(router): type the root error boundary from the router; wrap the o…
rishitank Oct 8, 2026
af568ca
ci: pin shared workflows to the v1 commit; document advisory performa…
rishitank Oct 9, 2026
66d3003
ci: delete a PR's head branch after it merges, with an allowlist
rishitank Oct 9, 2026
3249e8a
ci(branch-cleanup): only delete the merged commit; keep heads of open…
rishitank Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/runtime-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Runtime gate hints: preview-proof

PreviewProof is a single-page tool (TanStack Start on a Cloudflare Worker).
A visitor pastes a public URL; the server fetches it the way link-preview
bots do and shows the Google / X / LinkedIn / Slack / WhatsApp previews it
would produce, a score, and copy-paste fixes. There are no accounts.

Flows that matter most:

1. `/` renders the hero, the URL form, the "Try:" example chips and the
"How it works" landing section. The header's "How it works" links scroll
to it.
2. Theme toggle in the header switches light/dark without layout jumps.
3. Form validation: submitting empty is blocked; an invalid value such as
`not a url` or a private address such as `http://127.0.0.1` or
`http://10.0.0.1` must show a friendly "We couldn't check that link"
message, never a crash or a blank panel.
4. A missing path (e.g. `/nope`) shows the 404 page with a working
"Go home" link.

In CI the server has no reliable outbound network and this production build
does not use the e2e fake network, so a real public URL may come back as
"We couldn't check that link" (fetch failed / timeout). That message is the
correct behaviour, not a bug. A spinner that never ends, an uncaught error
or "Something went wrong" is a bug.

Do NOT: submit more than a handful of checks, or use internal/metadata
addresses other than the loopback/private examples above.
91 changes: 91 additions & 0 deletions .github/workflows/branch-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Deletes a pull request's head branch once the PR has been merged.
#
# Why a workflow and not a setting:
# - GitHub has no account-wide "delete head branches" setting. It is a
# per-repository toggle that is easy to miss on new repos, and it deletes
# every merged head branch without exception.
# - Private repositories on the Free plan cannot protect branches, so nothing
# stops that toggle from deleting a long-lived branch such as `develop` when
# it is merged as the head of a PR.
# - So this keeps an explicit allowlist instead: the default branch, main,
# master, develop, staging, test, gh-pages, deploy-* and release/* are never
# deleted, and neither is a branch that an open PR still uses as its base or
# head, or one that has moved past the merged commit.
#
# Self-contained on purpose (no reusable workflow), and it only acts on
# same-repository heads, never on a fork's branch.
name: branch-cleanup

on:
pull_request:
types: [closed]

permissions: {}

jobs:
delete-merged-branch:
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 2
permissions:
contents: write # delete the merged head branch's ref
pull-requests: read # list open PRs that still use the branch
steps:
- name: Delete the merged head branch
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

if [ "$HEAD_REF" = "$DEFAULT_BRANCH" ]; then
echo "::notice::Keeping $HEAD_REF: it is the default branch."
exit 0
fi

case "$HEAD_REF" in
main | master | develop | staging | test | gh-pages | deploy-* | release/*)
echo "::notice::Keeping $HEAD_REF: it is on the long-lived branch allowlist."
exit 0
;;
esac

bases="$(gh pr list --repo "$REPO" --base "$HEAD_REF" --state open --json number --jq length)"
if [ "$bases" -gt 0 ]; then
echo "::notice::Keeping $HEAD_REF: $bases open PR(s) use it as their base."
exit 0
fi
heads="$(gh pr list --repo "$REPO" --head "$HEAD_REF" --state open --json number --jq length)"
if [ "$heads" -gt 0 ]; then
echo "::notice::Keeping $HEAD_REF: $heads other open PR(s) use it as their head."
exit 0
fi

ref="$(jq -rn --arg r "$HEAD_REF" '$r|@uri')"
if ! current="$(gh api "repos/$REPO/git/ref/heads/$ref" --jq .object.sha 2>&1)"; then
if printf '%s' "$current" | grep -Eq 'Not Found|HTTP 404'; then
echo "::notice::Branch $HEAD_REF was already deleted."
exit 0
fi
printf '%s\n' "$current" >&2
exit 1
fi
# Only delete the commit that was merged. REST has no conditional
# delete, so a push in the moment between this check and the DELETE
# is not caught; anything pushed after the merge before now is.
if [ "$current" != "$HEAD_SHA" ]; then
echo "::notice::Keeping $HEAD_REF: it now points at $current, not the merged head $HEAD_SHA."
exit 0
fi

if out="$(gh api -X DELETE "repos/$REPO/git/refs/heads/$ref" 2>&1)"; then
echo "Deleted branch $HEAD_REF ($HEAD_SHA)."
elif printf '%s' "$out" | grep -Eq 'Reference does not exist|HTTP 404'; then
echo "::notice::Branch $HEAD_REF was already deleted."
else
printf '%s\n' "$out" >&2
exit 1
fi
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ jobs:
retention-days: 7

lighthouse:
# Shared Lighthouse CI workflow; budgets live in lighthouserc.json and block the build.
uses: rishitank/.github/.github/workflows/lighthouse-ci.yml@main
# Shared Lighthouse CI workflow; budgets live in lighthouserc.json. Accessibility,
# best-practices, SEO and CLS budgets block the build; performance is a warning.
uses: rishitank/.github/.github/workflows/lighthouse-ci.yml@5c644123ed93759cf78aa32d9d119150910e113e # v1
permissions:
contents: read
pull-requests: write
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Runtime gate: boots the app and uses it in a real browser before a change
# (a Renovate update included) can merge. Fails on runtime errors: uncaught
# exceptions, console errors, React hydration errors, failed same-origin
# requests, 4xx/5xx, blank pages, crash screens. Advisory on top: screenshot
# diff against the last main run, and an AI explorer (Gemini free
# tier, GEMINI_API_KEY secret) that uses the app like a person for major or
# framework updates, the `ai-explore` label, the weekly schedule and manual
# runs. See docs/runtime-gate.md in rishitank/.github.
name: runtime

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "53 4 * * 5"
workflow_dispatch:

concurrency:
group: runtime-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
runtime:
name: runtime
uses: rishitank/.github/.github/workflows/runtime-gate.yml@f5b19aff6eac60563a34e7a2ddc057e75766235c
permissions:
contents: read
actions: read
pull-requests: write
with:
# No Dockerfile: production is a Cloudflare Worker (nitro
# cloudflare-module). Build with bun exactly as ci.yml does, then serve
# the built worker with `wrangler dev` on 4173, the same command
# lighthouserc.json uses, so the gate tests the bundle that deploys.
mode: command
setup: bun
package-manager: bun
build: bun run build
start: npx --yes wrangler@4 dev --config .output/server/wrangler.json --ip 127.0.0.1 --port 4173
port: 4173
# PREVIEWPROOF_FAKE_NET=1 is the e2e suite's fake internet. It is only
# honoured by `vite dev` builds (import.meta.env.DEV); the production
# bundle served here ignores it. The crawler fills the URL form with
# https://example.com and submits it, so the crawl runs one real audit:
# the worker fetches that URL live from the runner.
env: |
PREVIEWPROOF_FAKE_NET=1
WRANGLER_SEND_METRICS=false
# No login-* inputs: the app is a single public page (paste a URL, get a
# preview audit) with no accounts, sessions or sign-in.
secrets:
gemini-api-key: ${{ secrets.GEMINI_API_KEY }}

# Weekly/manual runs only: saves the paths the explorer completed to
# .github/runtime-gate-flows.json through a reviewed PR; the runtime job then
# replays them on every PR. ci.yml has no workflow_dispatch trigger, so only
# this workflow is started on the flows branch.
flows:
needs: runtime
if: ${{ !cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }}
uses: rishitank/.github/.github/workflows/runtime-gate-flows.yml@f5b19aff6eac60563a34e7a2ddc057e75766235c
permissions:
contents: write
pull-requests: write
actions: write
with:
ci-workflows: runtime.yml
27 changes: 27 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Vulnerable dependencies (osv-scanner), leaked secrets (gitleaks, full
# history) and the ecosystem's own audit, via the shared workflow.
name: security

on:
push:
branches: [main]
pull_request:
schedule:
- cron: "40 4 * * 1"
workflow_dispatch:

concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
security:
uses: rishitank/.github/.github/workflows/security.yml@5c644123ed93759cf78aa32d9d119150910e113e # v1
with:
# osv-scanner (always on) reads bun.lock directly. `ecosystem` only picks
# the native audit, and npm audit cannot read bun.lock (it needs
# package-lock.json), so there is no native audit to run here.
ecosystem: none
18 changes: 18 additions & 0 deletions .github/workflows/workflows-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# actionlint over this repo's workflows. Catches bad `uses:` refs, undefined
# contexts and shell bugs in `run:` blocks before they cost a red build.
name: workflows-lint

on:
pull_request:
paths: [".github/workflows/**"]
push:
branches: [main]
paths: [".github/workflows/**"]
workflow_dispatch:

permissions:
contents: read

jobs:
actionlint:
uses: rishitank/.github/.github/workflows/workflows-lint.yml@5c644123ed93759cf78aa32d9d119150910e113e # v1
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ npx @lhci/cli@0.14.x autorun # Lighthouse budgets against the built Worker
| `report.test.ts` | Combined snippet de-duplication (head fixes only), combined prompt, Markdown export (including snippets that contain backticks), share links, recent-checks storage (including corrupt storage) |
| `e2e/app.spec.ts` | The page's own social tags and OG image, input typed before hydration, every validation error in the UI, shareable `?url=` links, dark mode persistence and contrast, reduced motion, empty submit, no horizontal scroll, axe WCAG A/AA. Runs on desktop and mobile |
| `e2e/results.spec.ts` | Every results path against a dev-only fake network (`src/test/fake-net.ts`, excluded from production builds): a perfect page, a blank SPA shell, an older Lovable app, a bot challenge, a name that resolves privately, oversized images, very long URLs; copy, share and download actions; exactly one server call per check; axe on the results page in light and dark mode |
| `lighthouserc.json` | Lighthouse CI against the production Worker build (run locally with Wrangler). The build fails below 90 performance or 100 for accessibility, best practices and SEO, or if CLS goes above 0.05 |
| `lighthouserc.json` | Lighthouse CI against the production Worker build (run locally with Wrangler), median of 5 runs. The build fails below 100 for accessibility, best practices and SEO, or if CLS goes above 0.05. A performance score below 90 is a warning, not a failure: the hosted CI runner scores well below local runs |

The key tests were mutation-checked. Each of the following was re-introduced deliberately, and each one made the suite fail:

Expand Down
Loading
Loading