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
5 changes: 2 additions & 3 deletions .github/workflows/cron-rebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ name: cron-rebuild
#

# Cadence: twice daily at 02:00 and 14:00 UTC — roughly 7am and 7pm PT
# during PDT, 6am and 6pm during PST. 60 builds/month, well under
# Cloudflare Pages Free's 500/month ceiling. Tighten the cron if events
# start changing more often than morning/evening.
# during PDT, 6am and 6pm during PST. Tighten the cron if events start
# changing more often than morning/evening.
#
# Pipeline doc: docs/explanation/events-pipeline.md

Expand Down
4 changes: 2 additions & 2 deletions docs/explanation/events-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ If you rename a calendar event, the URL changes. That's working as intended —

## Rebuild cadence

A GitHub Actions (GitHub's built-in automation that runs scripts on a schedule or on each push) scheduled workflow — a "cron" job, meaning it runs on a fixed timetable — at [`.github/workflows/cron-rebuild.yml`](../../.github/workflows/cron-rebuild.yml) fires twice a day (roughly morning and evening PT) and dispatches the `Deploy production` workflow. The Pages build re-fetches the calendar on its way through.
A GitHub Actions (GitHub's built-in automation that runs scripts on a schedule or on each push) scheduled workflow — a "cron" job, meaning it runs on a fixed timetable — at [`.github/workflows/cron-rebuild.yml`](../../.github/workflows/cron-rebuild.yml) fires twice a day (roughly morning and evening PT) and dispatches the `Deploy production` workflow. The build re-fetches the calendar before the Worker version is deployed.

Why twice a day: event metadata changes a few times a week at most; a morning and an evening rebuild keep the site current without burning Cloudflare Pages Free's 500-builds/month budget (twice daily = 60/month). Tighten the cron in the workflow file if events start moving faster than that.
Why twice a day: event metadata changes a few times a week at most; morning and evening rebuilds keep the site current without unnecessary deployments. Tighten the cron in the workflow file if events start moving faster than that.

Why GitHub Actions and not a Cloudflare Worker: the trigger needs zero long-lived credentials this way. The workflow uses the auto-issued `GITHUB_TOKEN`, scope-limited to `actions: write` on this repo. No PATs, no Worker secrets, no API token rotation. Logs surface in the Actions UI alongside every other deploy.

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/add-a-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ Steps:

5. **Goals render automatically** at the end of the page from the frontmatter `goals:` array via `src/components/ProjectGoals.astro`. Do **not** add a `## Goals` section in the body — it would duplicate the auto-render.
6. **Three statuses, kept simple.** `planned` (committed but not started), `in-progress` (working on it), `done` (achieved). If a goal genuinely changes scope, edit the text or remove it. Misses and scope changes go in `## Updates`, not in new status types.
7. Commit. Push to `main`. Cloudflare Pages deploys in ~60 seconds.
7. Commit. Push to `main`. GitHub Actions builds the site and deploys the verified Worker version.
2 changes: 1 addition & 1 deletion docs/guides/add-an-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Events live in the LVBT (Las Vegans for Better Transit) Google Calendar, not in
- **Date / time** — in Pacific Time (Las Vegas's time zone, UTC−8/−7). Always set an end time.
- **Location** — a meeting URL (virtual), a full physical address (in-person), or both (hybrid — put the address in Location and the meeting URL in Description). For physical events, use the full Google-style address so the site can publish a real postal address in structured data.
- **Description** — the first paragraph becomes the card / lede summary (keep it to one sentence). Everything after that paragraph becomes the body on the detail page; format it however you want with GCal's rich-text editor (the toolbar for bold, lists, links — like a mini word processor). Add `RSVP: https://…` if registration goes through an external sign-up form. Add `ADMISSION: https://…` or `TICKETS: https://…` only when the link is where people get admission or tickets, even for a free event.
3. Save. The next scheduled rebuild (within ~1 hour) picks it up. To rush it, trigger a redeploy from the Cloudflare Pages dashboard.
3. Save. The scheduled rebuild runs twice a day. For a same-day correction, run the `cron-rebuild` workflow from GitHub Actions; it dispatches the production build.

## When an event needs more than what GCal can hold

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/add-transit-news.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Claude will search, show you a list, ask for confirmation, then run the script o
## Path 3 — Notion form (anyone, no CLI)

A public Notion form where anyone — volunteers, the public — pastes an article
URL. A Cloudflare Pages Function (a small backend script that runs on Cloudflare — see [glossary](../reference/glossary.md#pages-function)) (`/api/transit-news-intake`) enriches the
URL. An API function compiled into the production Worker (`/api/transit-news-intake`) enriches the
submission automatically: it fetches the URL and fills in headline, date,
publication, topics, location, and the full article body.

Expand Down Expand Up @@ -104,7 +104,7 @@ A _webhook_ is an automated message one service sends another when something hap

- Run `pnpm bootstrap --phase secrets`. If `LVBT_TRANSIT_NEWS_INTAKE_SECRET`
is not set anywhere yet, bootstrap generates a random value, stores it on the
Pages project, the Worker and GitHub, and offers to show it once. Copy it for
production Worker, Pages fallback and GitHub, and offers to show it once. Copy it for
step 3. It also asks for `LVBT_NOTION_API_KEY` if that is missing.
- If the secret is already set but nobody has the value, run
`pnpm bootstrap --phase secrets --rotate LVBT_TRANSIT_NEWS_INTAKE_SECRET` to
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/connect-the-membership-form.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ submission fails, is in
> **Before you start.** You need edit access to the form and the value of
> `LVBT_MEMBERSHIP_INTAKE_SECRET` that the live site uses. If the form is
> being connected for the first time, make a new value (see [membership
> intake](../reference/membership-intake.md#required-cloudflare-pages-secrets))
> intake](../reference/membership-intake.md#required-cloudflare-secrets))
> and store it with `pnpm bootstrap --phase secrets`. Never use the value in
> your own `.env.local`; it is a local test value only.

Expand All @@ -26,7 +26,7 @@ submission fails, is in
| Property | Value |
| ------------------------------- | ------------------------------------------------------ |
| `LVBT_MEMBERSHIP_INTAKE_URL` | `https://lasvegasfortransit.org/api/membership-intake` |
| `LVBT_MEMBERSHIP_INTAKE_SECRET` | same value as the Cloudflare Pages secret |
| `LVBT_MEMBERSHIP_INTAKE_SECRET` | same value as the production Worker secret |

6. Open **Triggers** (clock icon) → **Add Trigger** and set:

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/edit-a-long-form-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ These are intentional, considered documents — meaning each was deliberately wr
1. **Required:** read [explanation/voice-and-tone.md](../explanation/voice-and-tone.md) before drafting, so your edit matches the house voice.
2. **Required when you touch numbers** (ridership, dates, dollar amounts): cross-reference [reference/key-facts.md](../reference/key-facts.md) — the same numbers anchor multiple files, so they must stay in sync.
3. Commit messages should explain **why** the change was made, not what changed.
4. Push to `main`. Cloudflare Pages deploys in ~60 seconds.
4. Push to `main`. GitHub Actions builds the site and deploys the verified Worker version.
54 changes: 20 additions & 34 deletions docs/guides/test-the-workers-candidate.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Test the Workers candidate

The Workers candidate runs beside the Pages production site. These checks establish equivalence without changing DNS or the production route.
A candidate is a version of the production Worker that receives a versioned preview URL before deployment. It uses the production bindings, so browser tests use non-destructive data. The public hostnames stay on the previously deployed version until candidate checks pass.

## Check locally

Expand Down Expand Up @@ -31,12 +31,13 @@ Under the `worker-preview` environment's **Environment secrets**, click **Add en

Set the repository variable `CLOUDFLARE_WORKERS_PREVIEW_ENABLED` to `true` after `pnpm worker:upload --env preview` succeeds for `lvbt-website-preview`, the separate Worker that pull request previews use. Re-run the pull request workflow and open the `Worker candidate` link in its comment.

The preview workflow compares the Worker with the Pages production origin, then runs the complete Playwright suite against the Worker URL. The same checks run locally against an uploaded candidate:
The preview workflow runs the complete Playwright suite against the Worker URL. The same checks run locally against an uploaded candidate:

```sh
pnpm worker:test:live \
--pages https://lasvegasfortransit.org \
--worker https://<version>-lvbt-website-preview.<account>.workers.dev
--worker https://<version>-lvbt-website-preview.<account>.workers.dev \
--skip-api
PLAYWRIGHT_BASE_URL=https://<version>-lvbt-website-preview.<account>.workers.dev \
pnpm worker:test:browser
```
Expand All @@ -47,43 +48,28 @@ Inspect the navigation at phone and desktop widths. Check the browser console, r

Create a separate `worker-candidate` GitHub environment the same way: **Settings → Environments → New environment**, type `worker-candidate`, **Configure environment**. It needs its own `CLOUDFLARE_WORKERS_API_TOKEN` environment secret — create a second custom token exactly as above (**Account · Workers Scripts · Edit**, scoped to the LVBT account; name it `lvbt-website candidate (GitHub Actions)` so it reads differently from the preview one in the token list) and add it under this environment's **Environment secrets**. It reads the same `CLOUDFLARE_ACCOUNT_ID` repository variable created above — do not make a second copy. Set the repository variable `CLOUDFLARE_WORKERS_CANDIDATE_ENABLED` to `true` only after the preview workflow passes.

Each successful Pages production run then starts `Deploy Worker candidate` for the same commit. The
workflow uploads a version with the stable `candidate` preview alias, compares it with Pages, and
runs the browser suite. It does not attach a route. Use **Run workflow** on `main` to repeat the check
without publishing Pages again.
Each successful `Deploy production` build starts `Deploy Worker candidate` for the same commit. The workflow uploads a version with the stable `candidate` preview alias and runs the browser suite. With `LVBT_WORKERS_PRODUCTION_ENABLED=true`, it deploys that version only after confirming that `main` still points at the tested commit. Use **Run workflow** on `main` to repeat the release without another build trigger.

Copy the commit, version, and preview URL from the workflow summary into the cutover change.
Record the commit, version, and preview URL from the workflow summary with the release.

## Switch production
## Check production

Confirm the current `main` commit has a passing candidate run and record the Pages deployment ID.
Keep the Pages custom domains and DNS records in place during the first switch. Set the repository
variable `LVBT_WORKERS_PRODUCTION_ENABLED` to `true`, then run `Deploy Worker candidate` on `main`.
The workflow deploys the verified Worker version and compares it with the production hostname.
After `Deploy Worker candidate` succeeds, compare `https://lasvegasfortransit.org` with the version URL recorded in its workflow summary. Check `https://www.lasvegasfortransit.org` redirects to the apex over valid TLS. Follow a content link, refresh a nested page, inspect an unknown path, and check an event calendar file. Confirm that production includes the Cloudflare Web Analytics beacon while version previews do not.

Attach `lasvegasfortransit.org/*` and `www.lasvegasfortransit.org/*` to `lvbt-website` as Worker
routes. Check both hostnames over HTTPS, including `/`, a content page, an unknown path, redirects,
calendar files, and the intake endpoints. Confirm analytics appears on the production hostname and
not on the version preview. Keep the Pages project available as the fallback until these checks
pass. Remove either route to send that hostname back to Pages if the Worker fails live checks.

After the route overlay proves stable, replace the Pages CNAMEs and domain attachments with Worker
custom domains. Check TLS and the same HTTP contract again before retiring the Pages deployment.
Cloudflare requires the Pages CNAME to be removed before a Worker custom domain can use that
hostname.
The `lvbt-website` Worker owns both public hostnames as custom domains. The Pages project has no custom-domain attachment. Its `lvbt-website-5zh.pages.dev` address remains available for emergency rollback; an ordinary release regression rolls back the Worker version without changing DNS. The [deployment pipeline](../reference/deployment-pipeline.md#rollback) records the recovery path.

## Record acceptance

Record the commit, Worker version, Pages deployment, preview URL, and check time in the cutover change. Compare these behaviors before attaching the production hostname:
Record the commit, Worker version, preview URL, and check time in the release record. Compare these behaviors before and after deployment:

| Surface | Required result |
| --------------------- | ------------------------------------------------------------- |
| `/` and content pages | Status, HTML, canonical metadata, and navigation match Pages |
| unknown path | Branded `404.html` with status 404 |
| `_headers` | Security and cache headers match Pages |
| `_redirects` | Every permanent redirect returns 301 to the same location |
| `/events/*.ics` | `text/calendar; charset=utf-8` |
| `/api/*` | Status, CORS, validation, and downstream behavior match Pages |
| analytics | Production hostname included; preview hostname excluded |
| Surface | Required result |
| --------------------- | --------------------------------------------------------------------- |
| `/` and content pages | Status, HTML, canonical metadata, and navigation match the candidate |
| unknown path | Branded `404.html` with status 404 |
| `_headers` | Security and cache headers match the candidate |
| `_redirects` | Every permanent redirect returns 301 to the same location |
| `/events/*.ics` | `text/calendar; charset=utf-8` |
| `/api/*` | Status, CORS, validation, and downstream behavior match the candidate |
| analytics | Production hostname included; preview hostname excluded |

Keep the Pages deployment and its hostname attachment intact until the post-cutover production check passes.
Keep the previous Worker version and the Pages fallback available until the production check passes.
Loading
Loading