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
71 changes: 71 additions & 0 deletions .github/actions/setup-bun/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Set up bun
description: >-
Install the exact bun that package.json's packageManager field names, read
from that field rather than repeated per workflow. One action because the
version is a pin every job has to agree on.

# WHY A LOCAL ACTION RATHER THAN `oven-sh/setup-bun`.
#
# Every third-party action in this repository is SHA-pinned, and CodeQL's
# advanced setup asserts that on every PR. Adding one more supply-chain edge to
# download a binary from a URL we could curl ourselves is the wrong trade when
# the whole job is four lines of shell.
#
# THE VERSION IS THE PIN, and it is enough now in a way it was not before.
# During the 1.4 canary era this repo verified a SHA-256, because the `canary`
# release tag ROLLS and the binary under it changed daily, which is intolerable
# for a build whose output is content-addressed. A released tag is immutable, so
# the version string is the guarantee the digest used to provide.
#
# It reads packageManager instead of taking an input so there is ONE declaration
# of which bun this repo runs. A contract test asserts every workflow reaches
# this action rather than installing bun some other way.

runs:
using: composite
steps:
- name: Install the pinned bun
shell: bash
run: |
set -euo pipefail
want=$(node -e "process.stdout.write(require('./package.json').packageManager)")
case "$want" in
bun@*) version="${want#bun@}" ;;
*) echo "::error title=packageManager is not bun::package.json says '$want'"; exit 1 ;;
esac

curl -fsSL -o /tmp/bun.zip \
"https://github.com/oven-sh/bun/releases/download/bun-v${version}/bun-linux-x64.zip"
unzip -qo /tmp/bun.zip -d /tmp/bun-install
bin=$(find /tmp/bun-install -type f -name bun | head -1)
install -m 0755 "$bin" "$HOME/.bun/bin/bun" 2>/dev/null \
|| { mkdir -p "$HOME/.bun/bin" && install -m 0755 "$bin" "$HOME/.bun/bin/bun"; }
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"

got=$("$HOME/.bun/bin/bun" --version)
if [ "$got" != "$version" ]; then
echo "::error title=bun version mismatch::asked for $version, the asset reports $got"
exit 1
fi
echo "bun $got, from packageManager"

- name: Probe the capability the build depends on
shell: bash
run: |
set -euo pipefail
# zstdCompressSync's `dictionary` option is the ONE api this build cannot
# ship without, and every bun before 1.4 ACCEPTS it and silently ignores
# it: the frame still decodes, nothing throws, and the only signal is a
# byte count that never shrank. build.mjs feature-detects it too, but 40
# seconds into a build. This is the same control, run first and named.
bun -e '
const { constants, zstdCompressSync } = require("node:zlib");
const p = Buffer.from("the quick brown fox jumps over the lazy dog ".repeat(200));
const at = (o) => zstdCompressSync(p, { ...o, params: { [constants.ZSTD_c_compressionLevel]: 19 } }).length;
const none = at({}), dict = at({ dictionary: p });
if (dict >= none * 0.5) {
console.error(`::error title=zstd dictionary ignored::none=${none} dict=${dict}; this bun would ship no-op dcz deltas`);
process.exit(1);
}
console.log(`zstd dictionary honoured: none=${none} dict=${dict}`);
'
70 changes: 33 additions & 37 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,36 +57,33 @@ jobs:
# Only a manual run can name a ref, and then it is the point.
ref: ${{ inputs.ref || '' }}

- name: Install pnpm
# No action, no digest to maintain: package.json's packageManager
# field is the source of truth and this pins the same version.
# Must precede setup-node, whose `cache: pnpm` needs pnpm on PATH.
run: npm install -g pnpm@11.21.0
- name: Set up bun
# config/... no longer applies: the version lives in packageManager and the
# action reads it, so there is one declaration of which bun this repo runs.
uses: ./.github/actions/setup-bun

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install homepage dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile

- name: Check Wrangler versions and lockfiles
run: pnpm run check-wrangler
run: bun run check-wrangler

- name: Build homepage
run: pnpm run build
run: bun run build

- name: Check photo asset invariants
run: pnpm run photos:check
run: bun run photos:check

- name: Enforce homepage performance budget
run: pnpm run perf-budget
run: bun run perf-budget

- name: Validate site Worker config
run: pnpm exec wrangler deploy --dry-run
run: bun x --no-install wrangler deploy --dry-run

# Lint, syntax AND type-aware, in one pass. tsgolint (the Go type checker
# behind `--type-aware`) tracks TypeScript 7.0.2, which is the version
Expand All @@ -98,18 +95,18 @@ jobs:
# against 1.1s for the typecheck it precedes) and because a dead binding is
# a cheaper thing to read than whatever it causes further down.
- name: Lint
run: pnpm run lint
run: bun run lint

# Type checking WITHOUT TypeScript: tsc reads the JSDoc already in these
# files, emits nothing, and never touches a served byte. Scoped to cal/src
# today (see tsconfig.json for the measured error counts of the scopes it
# is meant to grow into). It runs BEFORE the tests because a type error is
# cheaper to read than the failure it would cause downstream.
- name: Typecheck annotated JavaScript
run: pnpm run typecheck
run: bun run typecheck

- name: Test homepage representation contracts
run: pnpm test
run: bun run test

# The route oracle, pre-merge. createTestHarness() boots the site Worker
# in-process (honouring wrangler.jsonc's build.command, so it serves the
Expand All @@ -118,48 +115,47 @@ jobs:
# local KV/R2/D1 come up empty. See MAINTENANCE.md "Verify the whole route
# surface" for what the `remote: true` rows sit out and why.
- name: Verify route surface against a local Worker
run: pnpm run routes:check
run: bun run routes:check

- name: Validate LWE and Garage page contracts
run: pnpm run pages:check
run: bun run pages:check

# Only the DECLARATION tier gates here. It reads shell source and needs no
# binary present, so it runs anywhere. The presence tier prints what is
# absent and passes, because a hosted runner carries none of the photo
# toolchain and never runs the pipeline that wants it.
- name: Check system tool declaration
run: pnpm run tools:check
run: bun run tools:check

- name: Test coffee Worker
run: pnpm --filter cal-aadhar-sh test
run: bun run --filter cal-aadhar-sh test

- name: Validate Cloudflare garage Worker config
working-directory: cf-garage
run: |
pnpm test
pnpm exec wrangler deploy --dry-run -c wrangler.toml
bun test
bun x --no-install wrangler deploy --dry-run -c wrangler.toml

- name: Validate LWE ask Worker config
working-directory: lwe-ask
run: pnpm exec wrangler deploy --dry-run -c wrangler.toml
run: bun x --no-install wrangler deploy --dry-run -c wrangler.toml

# The Reader lens Worker. It installs its OWN deps rather than riding the
# root workspace: defuddle + linkedom are ~22 MB of node_modules that only
# this Worker bundles, and keeping them out of the root install is what
# stops them drifting onto the site Worker's dependency surface.
#
# --ignore-workspace is what preserves that under pnpm, and it is load
# bearing rather than decoration. A bare `pnpm install` here walks UP to
# the root pnpm-workspace.yaml, installs the five workspace projects, and
# never creates lens-reader/node_modules at all, so the tests then die on
# ERR_MODULE_NOT_FOUND with a `node_modules missing` warning as the only
# clue. npm had no such behaviour: it installed the local package.json.
# pnpm needed --ignore-workspace here and bun needs NOTHING, measured on
# the parked branch: a bare `bun install` in this directory resolved this
# package.json alone and created lens-reader/node_modules locally. Do not
# add a flag looking for parity with the pnpm era; there is nothing left to
# preserve, and the flag pnpm wanted does not exist here.
- name: Validate Reader lens Worker config and run its tests
working-directory: lens-reader
run: |
pnpm install --ignore-workspace
pnpm test
pnpm exec wrangler deploy --dry-run -c wrangler.toml
bun install
bun test
bun x --no-install wrangler deploy --dry-run -c wrangler.toml

# infra.json against reality. The tree and DNS tiers need no credential;
# the account tier runs only when the read-only token secret exists and
Expand All @@ -185,7 +181,7 @@ jobs:
# a PR whose whole purpose is to FIX production drift cannot turn its own check
# green before it deploys, because the thing it asserts against is the thing it
# repairs. That happened the same day (#195) and the documented way out is the
# local fallback, `pnpm run deploy`, followed by a re-run.
# local fallback, `bun run deploy`, followed by a re-run.
# The account id is read out of wrangler.jsonc rather than from a repo
# variable. This step said `vars.CLOUDFLARE_ACCOUNT_ID` until 2026-08-12 and
# NO repository variable of that name has ever existed, so it expanded to the
Expand Down Expand Up @@ -276,8 +272,8 @@ jobs:
chmod +x /tmp/osv-scanner
set +e
/tmp/osv-scanner scan source \
--lockfile pnpm-lock.yaml \
--lockfile lens-reader/pnpm-lock.yaml \
--lockfile bun.lock \
--lockfile lens-reader/bun.lock \
--lockfile tools/photos/zenc/Cargo.lock \
--lockfile tools/photos/requirements.txt
status=$?
Expand All @@ -303,7 +299,7 @@ jobs:
# advisory rather than a failure either way. It does NOT unlock the
# CodeQL assertion; see the permissions note at the top of this file.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run infra:check
run: bun run infra:check

# TRIPWIRE: a ramp parked on the reviewer gate. It lives HERE, outside
# ramp.yml's concurrency group, because a jam inside that group cannot be
Expand Down Expand Up @@ -396,4 +392,4 @@ jobs:
echo "::notice::no CLOUDFLARE_API_TOKEN (fork PR) — skipping the D1 changelog comparison"
exit 0
fi
pnpm run checkpoints:check
bun run checkpoints:check
18 changes: 8 additions & 10 deletions .github/workflows/dictionary-roll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ name: Dictionary roll
# 2026-07-30, 161 commits earlier, and every dictionary-carrying shell asset
# production was serving (nav, luna, hoist, tooltip, lens) was absent from
# src/dict/a-dict. Returning visitors were taking plain brotli on the render-blocking
# path: 13.7 KB where the deltas are 1.3 KB. `pnpm run dcz:check` reported PASS
# path: 13.7 KB where the deltas are 1.3 KB. `bun run dcz:check` reported PASS
# throughout, because its shell probe offers a COMMITTED candidate and so could
# only ever agree with itself. That probe now has a coverage assertion beside it,
# and this job is what acts on it.
Expand Down Expand Up @@ -64,20 +64,18 @@ jobs:
# is meaningless). That needs real history, not a shallow clone.
fetch-depth: 0

- name: Install pnpm
# package.json's packageManager field is the source of truth; this pins the
# same version so no self-switch is attempted (gotcha 30).
run: npm install -g pnpm@11.21.0
- name: Set up bun
# config/... no longer applies: the version lives in packageManager and the
# action reads it, so there is one declaration of which bun this repo runs.
uses: ./.github/actions/setup-bun

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile

- name: Build
# The page half reads `.build/public` for the page LIST (it fetches the bytes
Expand All @@ -91,7 +89,7 @@ jobs:
# THIS checkout builds, so it would have to run from the deployed commit, and
# on an unramped main it would adopt bytes no browser holds while evicting one
# still in use. The script refuses outright if production reads empty.
run: pnpm run dict:roll
run: bun run dict:roll

- name: Anything to propose?
id: diff
Expand Down Expand Up @@ -143,7 +141,7 @@ jobs:
Sourced from production over the wire, so every candidate is bytes a browser can really offer. Verify with:

\`\`\`bash
pnpm run dcz:check
bun run dcz:check
\`\`\`

Merging is a human call, because \`main\` takes no bypass actors."
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/og-cards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,18 @@ jobs:
- name: Check out main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install pnpm
run: npm install -g pnpm@11.21.0
- name: Set up bun
# config/... no longer applies: the version lives in packageManager and the
# action reads it, so there is one declaration of which bun this repo runs.
uses: ./.github/actions/setup-bun

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile

# playwright-core drives the INSTALLED Chrome (`channel: "chrome"`) rather
# than downloading a browser, which is why there is no `playwright install`
Expand All @@ -94,7 +94,7 @@ jobs:
env:
OG_ONLY: ${{ inputs.only }}
OG_BASE: ${{ inputs.base }}
run: pnpm run og-cards
run: bun run og-cards

# Idempotent, and separate from the bake on purpose: a new page needs its
# <head> meta as well as its PNG, and a re-bake of an existing card needs
Expand Down
Loading
Loading