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: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Format, lint, type checks, unit tests, a production build and a secret scan for every push to main and every
# live-fix pull request. No secrets, no deploys.
# Format, lint, type checks, unit tests, a production build, a dependency audit and a secret scan for every push to
# main and every pull request. No secrets, no deploys.
name: CI
on:
pull_request:
Expand Down Expand Up @@ -28,6 +28,8 @@ jobs:
- run: bun install --frozen-lockfile
- run: bun run check
- run: bunx cf build
# Fails on a high or critical advisory in any installed package.
- run: bun run audit

# The whole history is scanned (a few seconds), so a secret committed and then removed still fails the run.
# The binary is pinned by checksum; gitleaks-action needs a paid license on organization repos.
Expand Down
59 changes: 30 additions & 29 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Use SQLite DOs only, and nothing outside Cloudflare Free.
```
bun run check # format:check + lint + typecheck + test:unit (pre-commit hook, CI, deploy.yml)
gitleaks git --config .gitleaks.toml --redact . # secret scan; CI scans the whole history on every push
bun run audit # bun audit --audit-level=high; CI fails on a high or critical advisory
bun run format # oxfmt (.oxfmtrc.json); format:check only reports
bun run lint # oxlint --deny-warnings, type-aware (.oxlintrc.json)
bun run typecheck # tsc over the JS: tsconfig.worker.json, tsconfig.web.json, tsconfig.node.json
Expand Down
6 changes: 5 additions & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ bun run dev

`bun run check` runs everything below plus the unit tests. The pre-commit hook runs it, and CI
(`.github/workflows/ci.yml`) runs it on every push to `main` and every pull request, next to a
gitleaks scan of the whole history. The deploy workflow runs it before uploading a version.
gitleaks scan of the whole history and `bun run audit`. The deploy workflow runs it before uploading a version.

- `bun run format` formats the code with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) (`.oxfmtrc.json`,
120 columns); `bun run format:check` only reports. HTML, Markdown and `public/assets` are left alone.
Expand All @@ -47,6 +47,10 @@ gitleaks scan of the whole history. The deploy workflow runs it before uploading
`tsconfig.node.json` (the build configs). Tests and scripts are linted but not type checked.
Where inference falls short, add JSDoc (`/** @param {{ … }} opts */`) rather than casting the
problem away; the code stays `.js`.
- `bun run audit` checks every installed package against the advisory database and fails on a high or
critical one. When the fix sits behind a dependency that pins the old version, add an exact version to
`overrides` in `package.json` (as for `sharp`) once that release is 7 days old. Before bumping a package,
`bun pm diff <pkg>@<old> <pkg>@<new>` shows what its code changed.
- The one whole-repo format commit is listed in `.git-blame-ignore-revs`; run
`git config blame.ignoreRevsFile .git-blame-ignore-revs` once so `git blame` skips it.

Expand Down
2 changes: 1 addition & 1 deletion docs/LIVE_FIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ to `production` for a second confirmation. The jobs already use `environment: <t
`scripts/release.mjs` checks that the uploaded version has `AUTH_SECRET` and `INTERNAL_SECRET`,
and refuses to move traffic if either is missing.
- `.github/workflows/ci.yml` runs on every push to `main` and every pull request: `bun run check`,
`bunx cf build` without credentials, and a gitleaks scan of the whole history.
`bunx cf build` without credentials, `bun run audit` and a gitleaks scan of the whole history.

## Security limits

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"format:check": "oxfmt --check",
"lint": "oxlint --deny-warnings",
"typecheck": "tsc -p tsconfig.worker.json && tsc -p tsconfig.web.json && tsc -p tsconfig.node.json",
"audit": "bun audit --audit-level=high",
"check": "bun run format:check && bun run lint && bun run typecheck && bun run test:unit",
"test": "node tests/smoke.mjs",
"test:unit": "node --import ./tests/register.mjs --test \"tests/*.test.mjs\"",
Expand Down Expand Up @@ -41,6 +42,7 @@
"vite": "8.3.0"
},
"overrides": {
"sharp": "0.35.5",
"undici": "7.30.0"
},
"trustedDependencies": [
Expand Down
38 changes: 18 additions & 20 deletions tests/dev-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,26 +183,21 @@ test("rollback points all traffic at the previous version; dry run changes nothi
);
});

function loadYaml(t, file) {
const py = spawnSync(
"python",
["-c", 'import json,sys,yaml; print(json.dumps(yaml.safe_load(open(sys.argv[1], encoding="utf-8"))))', file],
// Bun parses the YAML (YAML 1.2, so `on` stays a string). Bun runs every script here, so a missing Bun fails the
// test instead of skipping it.
function loadYaml(file) {
const out = spawnSync(
"bun",
["-e", "console.log(JSON.stringify(Bun.YAML.parse(await Bun.file(process.argv[1]).text())))", file],
{ encoding: "utf8" },
);
if (py.error || py.status !== 0) {
if (/No module named|ENOENT|not found/i.test(String(py.error?.message || "") + py.stderr)) {
t.skip("python with PyYAML is not available");
return null;
}
assert.fail("YAML did not parse: " + py.stderr);
}
return JSON.parse(py.stdout);
if (out.error || out.status !== 0) assert.fail("YAML did not parse: " + (out.error?.message || out.stderr));
return JSON.parse(out.stdout);
}

test("deploy workflow parses and exposes the inputs /api/dev dispatches", (t) => {
const wf = loadYaml(t, ".github/workflows/deploy.yml");
if (!wf) return;
const on = wf.on ?? wf[true]; // YAML 1.1 reads a bare `on` key as boolean true
test("deploy workflow parses and exposes the inputs /api/dev dispatches", () => {
const wf = loadYaml(".github/workflows/deploy.yml");
const on = wf.on;
const inputs = on.workflow_dispatch.inputs;
assert.deepEqual(Object.keys(inputs).sort(), [
"hotfix",
Expand Down Expand Up @@ -234,12 +229,15 @@ test("deploy workflow parses and exposes the inputs /api/dev dispatches", (t) =>
assert.ok(check < steps.findIndex((r) => r.includes("release.mjs deploy")), "checks run before the upload");
});

test("CI workflow checks every push to main, scans for secrets and never sees secrets", (t) => {
const wf = loadYaml(t, ".github/workflows/ci.yml");
if (!wf) return;
const on = wf.on ?? wf[true];
test("CI workflow checks every push to main, scans for secrets and never sees secrets", () => {
const wf = loadYaml(".github/workflows/ci.yml");
const on = wf.on;
assert.ok(on.push.branches.includes("main"));
assert.ok(wf.jobs.test.steps.some((s) => s.run === "bun run check"));
assert.ok(
wf.jobs.test.steps.some((s) => s.run === "bun run audit"),
"dependencies are audited",
);
const scan = wf.jobs.secrets;
assert.equal(scan.steps[0].with["fetch-depth"], 0, "the secret scan sees the whole history");
assert.ok(
Expand Down
Loading