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
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Normalise line endings on anything committed from here on. It does not
# rewrite files already stored with CRLF — `git add --renormalize .` does that,
# in a commit of its own — but it stops new ones joining them.
* text=auto eol=lf

# Binary-ish assets git should not touch.
*.png binary
*.jpg binary
*.ico binary
73 changes: 73 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Check

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

# `npm ci` in both packages rather than `install:all`: CI should install
# exactly what the lockfiles say, not resolve fresh.
- name: Install
run: npm ci && npm --prefix backend ci

# No `format:check` step yet: seven files predating the Husky/lint-staged
# hook still carry CRLF line endings, so it fails on `main` today. Fixing
# that is `git add --renormalize .` plus one commit — worth doing on its
# own, not folded into an architecture change.
- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck

# Frontend unit tests and backend tests both.
- name: Tests
run: npm test

# Catches the failure mode a typecheck cannot: a build that trips over
# the shared/ imports crossing the package boundary.
- name: Build the application
run: npm run build

- name: Build the documentation
run: npm run build:docs

# The documentation site has no Worker behind it, so shipping /app,
# /auth or /i there would publish a copy of the product that looks real
# and fails at sign-in. astro.config.mjs strips them; this checks it.
- name: Documentation build must contain no application
run: |
failed=0
for route in app auth i; do
if [ -e "dist/$route" ]; then
echo "::error::dist/$route is in the documentation build — it needs the Worker, and GitHub Pages has none"
failed=1
fi
done
exit $failed

# These pages exist on both hosts. Without a canonical they are duplicate
# content on two domains, and a missing one fails silently — nothing in
# the build or the browser complains.
- name: Documentation pages must point their canonical at the app
run: |
failed=0
while IFS= read -r page; do
if ! grep -q '<link rel="canonical" href="https://bottlecount.pages.dev' "$page"; then
echo "::error::$page has no canonical pointing at the application host"
failed=1
fi
done < <(find dist -name '*.html')
exit $failed
shell: bash
96 changes: 96 additions & 0 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Cloudflare — the app's real home.
#
# Two deployments in order, and the order matters: the Pages project binds to
# the Worker by service name, so the Worker has to exist before Pages can
# resolve the binding on a first deploy.
#
# Requires two repository secrets, CLOUDFLARE_API_TOKEN and
# CLOUDFLARE_ACCOUNT_ID. Without them the workflow stops at the guard below
# rather than failing every push on a fork or a clone that has no Cloudflare
# account behind it.
name: Deploy to Cloudflare

on:
push:
branches: [main]
workflow_dispatch:

jobs:
# `secrets` is not readable from a job-level `if`, so the check is a job of
# its own that publishes a plain output the others can gate on.
configured:
runs-on: ubuntu-latest
outputs:
ready: ${{ steps.check.outputs.ready }}
steps:
- id: check
env:
TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [ -n "$TOKEN" ] && [ -n "$ACCOUNT" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "::notice::Cloudflare secrets are not set — skipping deploy."
fi

worker:
needs: configured
if: needs.configured.outputs.ready == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- run: npm --prefix backend ci

# Migrations before the deploy: a Worker that is live against a schema it
# expects and does not have is a broken sign-in, and D1 migrations here
# only ever add.
- name: Apply D1 migrations
run: npm --prefix backend run db:migrate:remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Deploy Worker
run: npm --prefix backend run deploy:production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

pages:
needs: [configured, worker]
if: needs.configured.outputs.ready == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- run: npm ci

# No BUILD_TARGET and no BASE_PATH: this is the application build, served
# at the root, and it is the default in astro.config.mjs. It carries
# everything — the free tier and the paid one both run from here.
- run: npm run build

- name: Deploy Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# --functions-directory is explicit: the proxy in functions/ lives at
# the repo root, not inside dist/, and losing it would deploy a
# frontend that quietly falls back to the free tier.
command: >-
pages deploy dist
--project-name=bottlecount
--branch=main
--functions-directory=functions
32 changes: 28 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
name: Deploy to GitHub Pages
# GitHub Pages — the documentation site.
#
# This is *not* the application. The app lives on Cloudflare Pages, where the
# Worker is (see deploy-cloudflare.yml), and both tiers are served from there:
# free users get it without an account, paying users sign in.
#
# What this deploys is the landing page, the docs, pricing and the legal pages,
# under the repository's path prefix. `npm run build:docs` deletes /app, /auth
# and /i from the output, because every one of them needs the Worker and there
# is none behind this host — a half-working copy of the product is worse than no
# copy. Those pages also exist on the application host, so each one carries a
# canonical link pointing there.
#
# The build settings live in that npm script rather than here, so a local
# `npm run build:docs` produces exactly what this publishes. That is also why
# these are explicit steps: `withastro/action` runs `npm run build` and cannot
# be pointed at another script.
name: Deploy docs to GitHub Pages

on:
push:
Expand All @@ -19,10 +36,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: withastro/action@v3
- uses: actions/setup-node@v4
with:
package-manager: 'npm'
node-version: 22
node-version-file: .nvmrc
cache: npm

- run: npm ci
- run: npm run build:docs

- uses: actions/upload-pages-artifact@v3
with:
path: dist

deploy:
needs: build
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ pnpm-debug.log*
# environment variables
.env
.env.production
# Wrangler secrets for local dev — never commit these.
.dev.vars
backend/.dev.vars

# wrangler local state (its D1 sqlite file lives here)
.wrangler/

# macOS-specific files
.DS_Store
Expand Down
11 changes: 6 additions & 5 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
dist
.astro
coverage

node_modules
dist
.astro
coverage
.wrangler
backend/node_modules
Loading
Loading