Skip to content

Commit a20ddd8

Browse files
authored
chore(ci): four per-PR runner reductions — fold dependency review into changes, make selfhost push-only, one-runner same-repo previews, merge the calibration advisories (#8523)
Follow-up to the validate-tests unsharding (#8514), attacking the remaining per-PR runner fan-out at the source (observed 44-deep queue with 3 running): 1. The standalone `security` job folds into `changes` as a PR-only dependency-review step -- one fewer runner slot on EVERY PR at zero wall-clock cost (the API lockfile diff takes seconds). `validate`'s needs drops it; a dep failure still reddens `validate` via `changes`. 2. selfhost.yml drops its pull_request trigger (~99 x 5.2min/week -- migrations/** made nearly every backend PR pay a docker build+boot). ci.yml still validates migrations/schema/pg suites pre-merge; the boot smoke now catches a break minutes after merge, and self-host users only consume tagged releases, never main. 3. Same-repo UI PRs build AND deploy their preview in ONE job: the deploy half moves to a shared .github/actions/deploy-ui-preview composite (setup-workspace precedent), ui-preview.yml gains the inline build-deploy job, and ui-preview-deploy.yml becomes fork-only (its job skips runner-free for same-repo builds via head_repository comparison). The fork trust boundary is untouched: fork code still builds with zero secrets and deploys only through the trusted workflow_run half. Bonus: the deploy job now has a trusted default-branch checkout, retiring the hand-synced hardcoded Node version. 4. backtest-logic-check.yml + counterfactual-replay-check.yml merge into calibration-advisory.yml -- identical scaffolding, overlapping paths (ai-review.ts fired BOTH), so one job runs both halves for one npm ci and one runner slot. The replay half keeps its budget spend-guard and now self-gates on the canonical prompt actually differing (replacing its ai-review.ts-only path filter). Fail-open posture preserved verbatim on every advisory step. Net per-PR floor drops 5 -> 4 slots, UI PRs 7 -> 5, migration PRs lose the 5-minute docker job. Pinned tests updated (ci-skip-draft-prs, workflow-runner-labels); skill reference.md rows updated.
1 parent 27ea32e commit a20ddd8

12 files changed

Lines changed: 724 additions & 642 deletions

File tree

.claude/skills/contributing-to-loopover/reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ for maintainer approval (CI shows unverified → the engine **holds**, never clo
1515
## 1. Every CI check → local command → what fails it
1616

1717
The **required** status checks on `main` are **`validate`** (it aggregates `changes, lint, test,
18-
workers, mcp, ui, security`; a path-skipped job counts as success) and **`Superagent Security Scan`**
18+
workers, mcp, ui`; a path-skipped job counts as success) and **`Superagent Security Scan`**
1919
(a separate third-party GitHub App check, not part of this repo's own workflow files — confirmed via
2020
`gh api repos/JSONbored/loopover/branches/main/protection/required_status_checks`). **Codecov** posts
2121
`codecov/patch` (the real coverage gate) and `codecov/project` (informational) independently. The
@@ -91,9 +91,9 @@ regression-guards the script itself against reverting to `vite preview`.
9191

9292
| ui → extension lint | `eslint` (VS Code + miner extensions) | `npm run extension:lint && npm run miner-extension:lint` | extension ESLint error (same `push \|\| ui==true` trigger as the `ui →` rows) |
9393
| ui → extension typecheck | `tsc --noEmit` (extensions) | `npm run extension:typecheck && npm run miner-extension:typecheck` | extension type error (same `push \|\| ui==true` trigger) |
94-
| security (PR only) | dependency-review (moderate+) | `npm audit --audit-level=moderate` | a **newly added** dep has a moderate+ advisory |
94+
| changes → dependency review (PR only) | dependency-review (moderate+; a step inside the `changes` job since 2026-07-24, not a separate job) | `npm audit --audit-level=moderate` | a **newly added** dep has a moderate+ advisory |
9595

96-
**One command for *almost* everything except `security`:** `npm run test:ci`. There is **no** CodeQL/Analyze
96+
**One command for *almost* everything except the dependency review:** `npm run test:ci`. There is **no** CodeQL/Analyze
9797
workflow in this repo. There is **no** root-level Prettier gate — Prettier is enforced only inside
9898
`ui:lint` (so it only bites `apps/loopover-ui/**`).
9999

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Deploy UI preview
2+
description: >-
3+
The trusted deploy half of the per-PR preview pipeline, shared by its two call sites so they can
4+
never drift (the setup-workspace precedent): ui-preview.yml deploys same-repo PRs inline in the
5+
build job, and ui-preview-deploy.yml deploys fork PRs from the workflow_run trust boundary. It
6+
validates a built dist bundle, writes the preview Wrangler config HERE (never taken from the PR --
7+
a fork cannot control bindings, routes, or vars), uploads a 0%-traffic preview version (a
8+
workers.dev URL; wrangler only uploads the bundle, it never executes it), and records the GitHub
9+
Deployment + success status that Reviewbot reads for the "after" screenshot. Callers must have
10+
Node available (actions/setup-node) before invoking, and keep their own failure()-gated step that
11+
records a `failure` deployment_status (that step is caller-specific: it must also catch failures
12+
of the caller's OWN earlier steps, which this action cannot see).
13+
inputs:
14+
pr-number:
15+
description: The pull request number this preview belongs to (Reviewbot re-reviews it on success).
16+
required: true
17+
head-sha:
18+
description: The PR head SHA the deployment record points at. Pass only GitHub-set values, never fork-supplied data.
19+
required: true
20+
dist-dir:
21+
description: Path to the built UI bundle (server/ + client/), relative to the workspace.
22+
required: true
23+
cloudflare-api-token:
24+
description: Cloudflare API token with "Workers Scripts:Edit" on the account.
25+
required: true
26+
cloudflare-account-id:
27+
description: The Cloudflare account id that owns loopover-ui.
28+
required: true
29+
github-token:
30+
description: Token with deployments:write, used to record the Deployment for Reviewbot.
31+
required: true
32+
outputs:
33+
preview_url:
34+
description: The workers.dev preview URL the upload produced.
35+
value: ${{ steps.upload.outputs.preview_url }}
36+
runs:
37+
using: composite
38+
steps:
39+
- name: Install trusted Wrangler
40+
shell: bash
41+
run: npm install --global wrangler@4.95.0
42+
43+
# The bundle may have been produced by an UNTRUSTED build (fork code; harmless for the trusted
44+
# same-repo call site, where the same checks still catch build anomalies). Validate it before
45+
# handing it to wrangler: reject symlinks (a path-traversal / exfil vector when the bundle is
46+
# processed), require the expected SSR build structure, and allowlist file extensions so a
47+
# malicious build can't smuggle scripts/binaries/unexpected paths into the deploy.
48+
- name: Validate dist bundle
49+
shell: bash
50+
run: |
51+
set -euo pipefail
52+
cd "${{ inputs.dist-dir }}"
53+
# 1) No symlinks anywhere in the bundle.
54+
symlinks="$(find . -type l)"
55+
if [ -n "$symlinks" ]; then
56+
echo "::error::Bundle contains symlinks — refusing to deploy:"
57+
printf '%s\n' "$symlinks"
58+
exit 1
59+
fi
60+
# 2) Required SSR build structure (server worker entry + client assets dir).
61+
test -f server/index.mjs || { echo "::error::bundle missing server/index.mjs"; exit 1; }
62+
test -d client || { echo "::error::bundle missing client/ assets dir"; exit 1; }
63+
# 3) Allowlist file extensions — fail on anything that isn't a normal web/build output (blocks
64+
# smuggled scripts/binaries). A few extensionless CF asset files are explicitly permitted.
65+
# `zip` covers the served downloads (e.g. /downloads/loopover-extension.zip) — a passive
66+
# static asset wrangler only uploads (never executes), so allowing it doesn't run fork code.
67+
unexpected="$(find . -regextype posix-extended -type f \
68+
-not -iregex '.*\.(mjs|js|cjs|map|json|css|html?|txt|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|wasm|xml|webmanifest|md|csv|zip|wgsl|glb|gltf)$' \
69+
-not -name '_headers' -not -name '_redirects' -not -name '_routes.json' -not -name '.assetsignore')"
70+
if [ -n "$unexpected" ]; then
71+
echo "::error::Bundle contains unexpected file types — refusing to deploy:"
72+
printf '%s\n' "$unexpected"
73+
exit 1
74+
fi
75+
echo "Bundle validated: no symlinks, expected SSR structure, allowlisted file types only."
76+
77+
# The Wrangler config is written HERE (trusted) — never taken from the PR — so a fork cannot
78+
# control bindings, routes, or vars. It points at the validated bundle. It deliberately OMITS the
79+
# production custom-domain route: `wrangler versions upload` creates a 0%-traffic preview version
80+
# (a workers.dev URL) and applies no routes, so the route is unused here — and omitting it
81+
# guarantees that even a future switch to `wrangler deploy` could never point fork-built code at
82+
# the production domain. Do not add a `routes` block to this preview config.
83+
- name: Write trusted preview Wrangler config
84+
shell: bash
85+
run: |
86+
cat > "${{ inputs.dist-dir }}/server/wrangler.preview.json" <<'JSON'
87+
{
88+
"compatibility_date": "2026-05-28",
89+
"name": "loopover-ui",
90+
"workers_dev": true,
91+
"preview_urls": true,
92+
"compatibility_flags": ["nodejs_compat"],
93+
"placement": {
94+
"mode": "smart"
95+
},
96+
"observability": {
97+
"enabled": true,
98+
"logs": {
99+
"enabled": true,
100+
"head_sampling_rate": 1
101+
},
102+
"traces": {
103+
"enabled": true,
104+
"head_sampling_rate": 1
105+
}
106+
},
107+
"vars": {
108+
"VITE_LOOPOVER_API_ORIGIN": "https://api.loopover.ai"
109+
},
110+
"main": "index.mjs",
111+
"assets": {
112+
"binding": "ASSETS",
113+
"directory": "../client"
114+
},
115+
"no_bundle": true,
116+
"rules": [
117+
{
118+
"type": "ESModule",
119+
"globs": ["**/*.mjs", "**/*.js"]
120+
}
121+
]
122+
}
123+
JSON
124+
125+
- name: Upload preview version
126+
id: upload
127+
shell: bash
128+
env:
129+
CLOUDFLARE_API_TOKEN: ${{ inputs.cloudflare-api-token }}
130+
CLOUDFLARE_ACCOUNT_ID: ${{ inputs.cloudflare-account-id }}
131+
run: |
132+
set -o pipefail
133+
out=$(wrangler versions upload --config "${{ inputs.dist-dir }}/server/wrangler.preview.json" 2>&1 | tee /dev/stderr)
134+
# Take a workers.dev URL that belongs to the loopover-ui worker, so any other URL in the logs
135+
# (or a changed output format) can't be recorded as the preview by mistake. Tolerant of the
136+
# version-alias prefix (`<alias>-loopover-ui.<sub>.workers.dev`).
137+
url=$(printf '%s\n' "$out" | grep -oiE 'https://[a-z0-9.-]+\.workers\.dev' | grep -i 'loopover-ui' | head -n1)
138+
if [ -z "$url" ]; then
139+
echo "::error::Could not parse an expected loopover-ui preview URL from wrangler output"
140+
exit 1
141+
fi
142+
echo "preview_url=$url" >> "$GITHUB_OUTPUT"
143+
echo "Preview: $url"
144+
145+
- name: Record deployment for Reviewbot
146+
if: ${{ steps.upload.outputs.preview_url != '' }}
147+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
148+
with:
149+
github-token: ${{ inputs.github-token }}
150+
script: |
151+
const url = ${{ toJSON(steps.upload.outputs.preview_url) }};
152+
const sha = ${{ toJSON(inputs.head-sha) }};
153+
const prNumber = Number(${{ toJSON(inputs.pr-number) }});
154+
const deployment = await github.rest.repos.createDeployment({
155+
owner: context.repo.owner,
156+
repo: context.repo.repo,
157+
ref: sha,
158+
environment: `preview/pr-${prNumber}`,
159+
auto_merge: false,
160+
required_contexts: [],
161+
transient_environment: true,
162+
description: "LoopOver UI preview",
163+
// Reviewbot reads `pr` here to re-review this exact PR once the preview is live.
164+
payload: JSON.stringify({ pr: prNumber, head_sha: sha }),
165+
});
166+
await github.rest.repos.createDeploymentStatus({
167+
owner: context.repo.owner,
168+
repo: context.repo.repo,
169+
deployment_id: deployment.data.id,
170+
state: "success",
171+
environment: `preview/pr-${prNumber}`,
172+
environment_url: url,
173+
description: "Preview ready",
174+
});
175+
core.notice(`Preview deployment recorded for PR #${prNumber}: ${url}`);

.github/workflows/backtest-logic-check.yml

Lines changed: 0 additions & 156 deletions
This file was deleted.

0 commit comments

Comments
 (0)