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
46 changes: 46 additions & 0 deletions .github/workflows/pwa-bun.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# app.moshcode.sh runs on Bun in production (see Dockerfile). Its dependencies are
# still installed with npm from apps/pwa/package-lock.json, as the image does.
name: pwa on bun

on:
pull_request:
paths: ["apps/pwa/**", "Dockerfile", ".github/workflows/pwa-bun.yml"]
push:
branches: [main]
paths: ["apps/pwa/**", "Dockerfile", ".github/workflows/pwa-bun.yml"]

permissions:
contents: read

jobs:
test:
name: PWA tests + boot under Bun
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci --no-audit --no-fund
working-directory: apps/pwa
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
# One process per file, as `node --test` does: each file points DATABASE_URL
# at its own scratch database before importing config. --timeout: bun test
# stops a test after 5 s by default; node --test has no limit, and the pit
# tests seed 5000 names first.
- name: Tests (one Bun process per file)
working-directory: apps/pwa
run: |
failed=0
for f in test/*.test.mjs; do bun test --timeout 120000 "./$f" > /tmp/t.log 2>&1 || { echo "FAIL $f"; cat /tmp/t.log; failed=1; }; done
exit $failed
- name: Server boots under Bun
run: |
DATABASE_URL="file:/tmp/pwa-ci.db" SESSION_SECRET=ci PORT=3000 bun apps/pwa/src/server.mjs > /tmp/server.log 2>&1 &
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:3000/healthz && exit 0
sleep 1
done
cat /tmp/server.log; exit 1
23 changes: 21 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
# app.moshcode.sh on dev2: the web app lives in apps/pwa with its own dependencies
# (Railway built it with rootDirectory=apps/pwa; from the repo root nixpacks installs
# only the CLI's). The repo root is copied so relative imports keep resolving.
FROM node:22-slim
#
# Runs on Bun. The CLI in this repo is a Node/pnpm product and the PWA keeps its
# package-lock, so dependencies are still installed by `npm ci` exactly as before;
# only the runtime changes: Debian 12 (node:22-slim's base) with Bun's single
# binary and no node. Contract with dev2 unchanged: listens on $PORT (3000), answers
# /healthz, reads its secrets from app.env at run time.
FROM node:22-slim AS deps
WORKDIR /app
COPY . .
RUN cd apps/pwa && (npm ci --omit=dev 2>/dev/null || npm install --omit=dev)

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=oven/bun:1.4.0-slim /usr/local/bin/bun /usr/local/bin/bun
# The non-root runtime user, uid 1000 like the oven/bun images.
RUN groupadd --gid 1000 bun && useradd --uid 1000 --gid bun --create-home --shell /bin/sh bun
WORKDIR /app
# --chown: dev2's checkout is group-only (660/2770) and COPY keeps those modes.
COPY --from=deps --chown=bun:bun /app /app
USER bun
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "apps/pwa/src/server.mjs"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD bun -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/healthz').then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))"
CMD ["bun", "apps/pwa/src/server.mjs"]
2 changes: 2 additions & 0 deletions apps/pwa/src/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ async function main() {
// Unref'd so it never holds the process open on its own.
setInterval(sweep, SWEEP_MS).unref();
app.listen(config.port, () => console.log(`🤘 app.moshcode.sh on :${config.port} (${config.env}) — ${config.origin}`));
// PID 1 in its container: without a handler `docker stop` waits 10 s for SIGKILL.
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => process.exit(0));
}
main().catch((e) => { console.error("boot failed:", e); process.exit(1); });

Expand Down
7 changes: 6 additions & 1 deletion apps/pwa/test/moshpit-gateway-host.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ test("the origin is asked for the Moshpit name, not the target", async () => {
} finally { server.close(); }
});

test("fetch() would have sent the wrong Host — the bug this replaces", async () => {
// Pins undici's behaviour: Node's fetch() drops a caller-supplied Host header.
// Bun's fetch() honours it, so there is no defect to pin when the suite runs on
// Bun (production does); the gateway's own http.request path is covered above.
test("fetch() would have sent the wrong Host — the bug this replaces", {
skip: process.versions.bun ? "Bun's fetch() keeps a caller-supplied Host" : false,
}, async () => {
const { server, seen, port } = await virtualHost();
try {
const res = await fetch(`http://127.0.0.1:${port}/`, {
Expand Down
Loading