Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .claude/skills/contributing-to-loopover/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ for maintainer approval (CI shows unverified → the engine **holds**, never clo
## 1. Every CI check → local command → what fails it

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

| 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) |
| ui → extension typecheck | `tsc --noEmit` (extensions) | `npm run extension:typecheck && npm run miner-extension:typecheck` | extension type error (same `push \|\| ui==true` trigger) |
| security (PR only) | dependency-review (moderate+) | `npm audit --audit-level=moderate` | a **newly added** dep has a moderate+ advisory |
| 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 |

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

Expand Down
175 changes: 175 additions & 0 deletions .github/actions/deploy-ui-preview/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Deploy UI preview
description: >-
The trusted deploy half of the per-PR preview pipeline, shared by its two call sites so they can
never drift (the setup-workspace precedent): ui-preview.yml deploys same-repo PRs inline in the
build job, and ui-preview-deploy.yml deploys fork PRs from the workflow_run trust boundary. It
validates a built dist bundle, writes the preview Wrangler config HERE (never taken from the PR --
a fork cannot control bindings, routes, or vars), uploads a 0%-traffic preview version (a
workers.dev URL; wrangler only uploads the bundle, it never executes it), and records the GitHub
Deployment + success status that Reviewbot reads for the "after" screenshot. Callers must have
Node available (actions/setup-node) before invoking, and keep their own failure()-gated step that
records a `failure` deployment_status (that step is caller-specific: it must also catch failures
of the caller's OWN earlier steps, which this action cannot see).
inputs:
pr-number:
description: The pull request number this preview belongs to (Reviewbot re-reviews it on success).
required: true
head-sha:
description: The PR head SHA the deployment record points at. Pass only GitHub-set values, never fork-supplied data.
required: true
dist-dir:
description: Path to the built UI bundle (server/ + client/), relative to the workspace.
required: true
cloudflare-api-token:
description: Cloudflare API token with "Workers Scripts:Edit" on the account.
required: true
cloudflare-account-id:
description: The Cloudflare account id that owns loopover-ui.
required: true
github-token:
description: Token with deployments:write, used to record the Deployment for Reviewbot.
required: true
outputs:
preview_url:
description: The workers.dev preview URL the upload produced.
value: ${{ steps.upload.outputs.preview_url }}
runs:
using: composite
steps:
- name: Install trusted Wrangler
shell: bash
run: npm install --global wrangler@4.95.0

# The bundle may have been produced by an UNTRUSTED build (fork code; harmless for the trusted
# same-repo call site, where the same checks still catch build anomalies). Validate it before
# handing it to wrangler: reject symlinks (a path-traversal / exfil vector when the bundle is
# processed), require the expected SSR build structure, and allowlist file extensions so a
# malicious build can't smuggle scripts/binaries/unexpected paths into the deploy.
- name: Validate dist bundle
shell: bash
run: |
set -euo pipefail
cd "${{ inputs.dist-dir }}"
# 1) No symlinks anywhere in the bundle.
symlinks="$(find . -type l)"
if [ -n "$symlinks" ]; then
echo "::error::Bundle contains symlinks — refusing to deploy:"
printf '%s\n' "$symlinks"
exit 1
fi
# 2) Required SSR build structure (server worker entry + client assets dir).
test -f server/index.mjs || { echo "::error::bundle missing server/index.mjs"; exit 1; }
test -d client || { echo "::error::bundle missing client/ assets dir"; exit 1; }
# 3) Allowlist file extensions — fail on anything that isn't a normal web/build output (blocks
# smuggled scripts/binaries). A few extensionless CF asset files are explicitly permitted.
# `zip` covers the served downloads (e.g. /downloads/loopover-extension.zip) — a passive
# static asset wrangler only uploads (never executes), so allowing it doesn't run fork code.
unexpected="$(find . -regextype posix-extended -type f \
-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)$' \
-not -name '_headers' -not -name '_redirects' -not -name '_routes.json' -not -name '.assetsignore')"
if [ -n "$unexpected" ]; then
echo "::error::Bundle contains unexpected file types — refusing to deploy:"
printf '%s\n' "$unexpected"
exit 1
fi
echo "Bundle validated: no symlinks, expected SSR structure, allowlisted file types only."

# The Wrangler config is written HERE (trusted) — never taken from the PR — so a fork cannot
# control bindings, routes, or vars. It points at the validated bundle. It deliberately OMITS the
# production custom-domain route: `wrangler versions upload` creates a 0%-traffic preview version
# (a workers.dev URL) and applies no routes, so the route is unused here — and omitting it
# guarantees that even a future switch to `wrangler deploy` could never point fork-built code at
# the production domain. Do not add a `routes` block to this preview config.
- name: Write trusted preview Wrangler config
shell: bash
run: |
cat > "${{ inputs.dist-dir }}/server/wrangler.preview.json" <<'JSON'
{
"compatibility_date": "2026-05-28",
"name": "loopover-ui",
"workers_dev": true,
"preview_urls": true,
"compatibility_flags": ["nodejs_compat"],
"placement": {
"mode": "smart"
},
"observability": {
"enabled": true,
"logs": {
"enabled": true,
"head_sampling_rate": 1
},
"traces": {
"enabled": true,
"head_sampling_rate": 1
}
},
"vars": {
"VITE_LOOPOVER_API_ORIGIN": "https://api.loopover.ai"
},
"main": "index.mjs",
"assets": {
"binding": "ASSETS",
"directory": "../client"
},
"no_bundle": true,
"rules": [
{
"type": "ESModule",
"globs": ["**/*.mjs", "**/*.js"]
}
]
}
JSON

- name: Upload preview version
id: upload
shell: bash
env:
CLOUDFLARE_API_TOKEN: ${{ inputs.cloudflare-api-token }}
CLOUDFLARE_ACCOUNT_ID: ${{ inputs.cloudflare-account-id }}
run: |
set -o pipefail
out=$(wrangler versions upload --config "${{ inputs.dist-dir }}/server/wrangler.preview.json" 2>&1 | tee /dev/stderr)
# Take a workers.dev URL that belongs to the loopover-ui worker, so any other URL in the logs
# (or a changed output format) can't be recorded as the preview by mistake. Tolerant of the
# version-alias prefix (`<alias>-loopover-ui.<sub>.workers.dev`).
url=$(printf '%s\n' "$out" | grep -oiE 'https://[a-z0-9.-]+\.workers\.dev' | grep -i 'loopover-ui' | head -n1)
if [ -z "$url" ]; then
echo "::error::Could not parse an expected loopover-ui preview URL from wrangler output"
exit 1
fi
echo "preview_url=$url" >> "$GITHUB_OUTPUT"
echo "Preview: $url"

- name: Record deployment for Reviewbot
if: ${{ steps.upload.outputs.preview_url != '' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ inputs.github-token }}
script: |
const url = ${{ toJSON(steps.upload.outputs.preview_url) }};
const sha = ${{ toJSON(inputs.head-sha) }};
const prNumber = Number(${{ toJSON(inputs.pr-number) }});
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: sha,
environment: `preview/pr-${prNumber}`,
auto_merge: false,
required_contexts: [],
transient_environment: true,
description: "LoopOver UI preview",
// Reviewbot reads `pr` here to re-review this exact PR once the preview is live.
payload: JSON.stringify({ pr: prNumber, head_sha: sha }),
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: "success",
environment: `preview/pr-${prNumber}`,
environment_url: url,
description: "Preview ready",
});
core.notice(`Preview deployment recorded for PR #${prNumber}: ${url}`);
156 changes: 0 additions & 156 deletions .github/workflows/backtest-logic-check.yml

This file was deleted.

Loading