diff --git a/.env.pages.prod.example b/.env.worker.prod.example similarity index 62% rename from .env.pages.prod.example rename to .env.worker.prod.example index f0300eb..6ccc49a 100644 --- a/.env.pages.prod.example +++ b/.env.worker.prod.example @@ -1,4 +1,4 @@ -# Copy to .env.pages.prod and fill in the values for production deploys. +# Copy to .env.worker.prod and fill in the values for production deploys. PUBLIC_SITE_URL=https://swansea-airport.wales PUBLIC_PPR_API_BASE=https://ppr.swansea-airport.wales/api/v1 PUBLIC_WEATHER_BASE=https://wx.swansea-airport.wales @@ -9,7 +9,10 @@ DIRECTUS_PUBLIC_URL=https://cms.swansea-airport.wales DIRECTUS_ADMIN_TOKEN=replace-with-production-directus-token DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners -CF_PAGES_PROJECT_NAME=swansea-airfield -CF_PAGES_BRANCH=main +CF_WORKER_NAME=swansea-airfield +CF_WORKER_COMPATIBILITY_DATE=2026-06-21 +# Optional. Use one of these if you want the deploy command to attach a route or custom domain. +# CF_WORKER_ROUTE=swansea-airport.wales/* +# CF_WORKER_DOMAIN=swansea-airport.wales CLOUDFLARE_ACCOUNT_ID=replace-with-cloudflare-account-id CLOUDFLARE_API_TOKEN=replace-with-cloudflare-api-token diff --git a/.env.pages.test.example b/.env.worker.test.example similarity index 50% rename from .env.pages.test.example rename to .env.worker.test.example index 8ab54d9..04ee7d4 100644 --- a/.env.pages.test.example +++ b/.env.worker.test.example @@ -1,6 +1,6 @@ -# Copy to .env.pages.test and fill in the values for test/preview deploys. -PUBLIC_SITE_URL=https://test.example.pages.dev -PUBLIC_PPR_API_BASE=https://test-ppr.example.com/api/v1 +# Copy to .env.worker.test and fill in the values for test deploys. +PUBLIC_SITE_URL=https://new.swansea-airport.wales +PUBLIC_PPR_API_BASE=https://pprdev.pattinson.org/api/v1 PUBLIC_WEATHER_BASE=https://wx.swansea-airport.wales PUBLIC_WEATHER_MQTT_HOST=https://wx.swansea-airport.wales/mqtt @@ -9,7 +9,10 @@ DIRECTUS_PUBLIC_URL=https://egfhcmstest.pattinson.org DIRECTUS_ADMIN_TOKEN=replace-with-test-directus-token DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners -CF_PAGES_PROJECT_NAME=swansea-airfield -CF_PAGES_BRANCH=test +CF_WORKER_NAME=egfh +CF_WORKER_COMPATIBILITY_DATE=2026-06-21 +# Optional. Use one of these if you want the deploy command to attach a route or custom domain. +# CF_WORKER_ROUTE=test.swansea-airport.wales/* +# CF_WORKER_DOMAIN=test.swansea-airport.wales CLOUDFLARE_ACCOUNT_ID=replace-with-cloudflare-account-id CLOUDFLARE_API_TOKEN=replace-with-cloudflare-api-token diff --git a/.gitignore b/.gitignore index 5130dd9..a0e06e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ node_modules dist .astro .env -.env.pages.test -.env.pages.prod +.env.worker.test +.env.worker.prod .DS_Store diff --git a/README.md b/README.md index 53ffc3b..23dabd7 100644 --- a/README.md +++ b/README.md @@ -23,29 +23,38 @@ Production-ready airfield website stack built with Astro, Directus, PostgreSQL, - The frontend service bind-mounts the project into `/app`, keeps `node_modules` and `.astro` in named volumes, and serves the site with `astro dev` on the published frontend port. - Layout and page structure are controlled entirely by Astro. - Frontend source edits should appear without rebuilding the container image. -- Cloudflare Pages deployment uses Direct Upload with Wrangler. See `docs/cloudflare-pages.md` for the test/prod URL workflow. +- Cloudflare Worker deployment uses Wrangler static assets. See `docs/cloudflare-worker.md` for the test/prod URL workflow. -## Cloudflare Pages deployment +## Cloudflare Worker deployment + +This project is normally developed through Docker Compose, so run the Worker build/deploy scripts inside the `web` service. + +Copy the Worker env files first: + +```bash +cp .env.worker.test.example .env.worker.test +cp .env.worker.prod.example .env.worker.prod +``` Build static files with the test URL set: ```bash -npm run build:pages:test +docker compose exec web npm run build:worker:test ``` -Deploy the test build to the Cloudflare Pages `test` branch: +Deploy the test build to the test Cloudflare Worker: ```bash -npm run deploy:pages:test +docker compose exec web npm run deploy:worker:test ``` Deploy production with the production URL set: ```bash -npm run deploy:pages:prod +docker compose exec web npm run deploy:worker:prod ``` -Copy `.env.pages.test.example` and `.env.pages.prod.example` before using these commands. +The deploy scripts build first, then upload `dist/` with Wrangler. The `test` command uses `.env.worker.test`; the production command uses `.env.worker.prod`. ## Programmatic Directus schema bootstrap diff --git a/docs/cloudflare-pages.md b/docs/cloudflare-pages.md deleted file mode 100644 index 270d75a..0000000 --- a/docs/cloudflare-pages.md +++ /dev/null @@ -1,82 +0,0 @@ -# Cloudflare Pages Direct Upload - -This project uses Astro static output, so Cloudflare Pages only needs the generated `dist/` directory. - -Because the source repository is hosted in Gitea, use Cloudflare Pages Direct Upload rather than Cloudflare's GitHub/GitLab integration. The build happens in your local machine, Docker container, or Gitea CI runner, then Wrangler uploads the finished files to Cloudflare. - -## URL Sets - -Keep three sets of URLs separate: - -- Local development: `.env`, used by Docker Compose and `astro dev`. -- Test/preview deployment: `.env.pages.test`, used by `npm run build:pages:test` and `npm run deploy:pages:test`. -- Production deployment: `.env.pages.prod`, used by `npm run build:pages:prod` and `npm run deploy:pages:prod`. - -The important distinction is that Astro fetches Directus content during the build. The URLs in the env file selected for the build are baked into the generated static files. - -## First-Time Setup - -Copy the example files and fill in the real values: - -```bash -cp .env.pages.test.example .env.pages.test -cp .env.pages.prod.example .env.pages.prod -``` - -Do not commit `.env.pages.test` or `.env.pages.prod`; they contain API tokens. - -Create a Cloudflare API token with permission to deploy Pages projects, then set these values in both env files: - -```env -CF_PAGES_PROJECT_NAME=swansea-airfield -CLOUDFLARE_ACCOUNT_ID=... -CLOUDFLARE_API_TOKEN=... -``` - -For Directus, use public HTTPS URLs in Pages env files. Do not use Docker-only hostnames such as `http://directus:8055` outside Docker Compose. - -## Build Locally For Test - -```bash -npm run build:pages:test -``` - -This loads `.env.pages.test`, fetches content from the test Directus URL, and writes static files to `dist/`. - -## Deploy To Cloudflare Test Branch - -```bash -npm run deploy:pages:test -``` - -This builds with `.env.pages.test`, then uploads `dist/` to the `test` Pages branch. Cloudflare will serve it on the matching branch preview URL. - -## Deploy To Production - -```bash -npm run deploy:pages:prod -``` - -This builds with `.env.pages.prod`, then uploads `dist/` to the `main` Pages branch. - -## Gitea CI Shape - -In Gitea Actions, store the same values as CI secrets and run: - -```bash -npm ci -npm run deploy:pages:prod -``` - -The deploy script reads from the env file first, but existing CI environment variables win. That means secrets injected by Gitea can override placeholder values from a committed example or generated env file. - -## Content Updates - -Static deploys are snapshots. If Directus content changes after deployment, Cloudflare Pages will not update until another build and deploy runs. - -For production, trigger `npm run deploy:pages:prod` from either: - -- a code push to Gitea, -- a manual Gitea Actions workflow, -- a Directus webhook that calls your CI runner, -- or a scheduled CI job. diff --git a/docs/cloudflare-worker.md b/docs/cloudflare-worker.md new file mode 100644 index 0000000..540dd1f --- /dev/null +++ b/docs/cloudflare-worker.md @@ -0,0 +1,112 @@ +# Cloudflare Worker Static Assets + +This project uses Astro static output, so Cloudflare only needs the generated `dist/` directory. + +The public site is deployed as a Cloudflare Worker serving static assets. Wrangler supports deploying a directory of static assets with `wrangler deploy dist`. + +## URL Sets + +Keep three sets of URLs separate: + +- Local development: `.env`, used by Docker Compose and `astro dev`. +- Test Worker deployment: `.env.worker.test`, used by `npm run build:worker:test` and `npm run deploy:worker:test`. +- Production Worker deployment: `.env.worker.prod`, used by `npm run build:worker:prod` and `npm run deploy:worker:prod`. + +The important distinction is that Astro fetches Directus content during the build. The URLs in the env file selected for the build are baked into the generated static files. + +## First-Time Setup + +Copy the example files and fill in the real values: + +```bash +cp .env.worker.test.example .env.worker.test +cp .env.worker.prod.example .env.worker.prod +``` + +Do not commit `.env.worker.test` or `.env.worker.prod`; they contain API tokens. + +Create a Cloudflare API token that can deploy Workers, then set these values in both env files: + +```env +CLOUDFLARE_ACCOUNT_ID=... +CLOUDFLARE_API_TOKEN=... +``` + +In practice, Wrangler's static asset upload currently works reliably with a user-owned API token scoped to the target account. Account-owned tokens may authenticate successfully with `wrangler whoami` but still fail during `workers/scripts//assets-upload-session` with `Authentication error [code: 10000]`. + +For a user-owned token, start with these permissions: + +```text +Account -> Workers Scripts -> Edit +Account -> Account Settings -> Read +User -> Memberships -> Read +``` + +If the deploy command manages Worker routes or domains, also grant the matching zone/worker route permissions for the target domain. + +Each env file also names the target Worker: + +```env +CF_WORKER_NAME=swansea-airfield-test +``` + +For Directus, use public HTTPS URLs in Worker env files. Do not use Docker-only hostnames such as `http://directus:8055` outside Docker Compose. + +## Build Locally For Test + +When working through Docker Compose: + +```bash +docker compose exec web npm run build:worker:test +``` + +This loads `.env.worker.test`, fetches content from the test Directus URL, and writes static files to `dist/`. + +## Deploy To The Test Worker + +```bash +docker compose exec web npm run deploy:worker:test +``` + +This builds with `.env.worker.test`, then uploads `dist/` to the Worker named by `CF_WORKER_NAME`. + +## Deploy To The Production Worker + +```bash +docker compose exec web npm run deploy:worker:prod +``` + +This builds with `.env.worker.prod`, then uploads `dist/` to the production Worker. + +## Routes And Domains + +The deploy wrapper can optionally attach a route or custom domain if the env file sets one of these: + +```env +CF_WORKER_ROUTE=swansea-airport.wales/* +CF_WORKER_DOMAIN=swansea-airport.wales +``` + +Leave both unset if routes/domains are managed in the Cloudflare dashboard. + +## Gitea CI Shape + +In Gitea Actions, create the selected env file from CI secrets, then run: + +```bash +npm ci +npm run deploy:worker:prod +``` + +The deploy script treats the selected env file as the source of truth. This is deliberate because the Docker Compose `web` service already has local development variables in its environment, and production/test deploys need to override them. + +## Content Updates + +Static deploys are snapshots. If Directus content changes after deployment, the Worker will not update until another build and deploy runs. + +For production, trigger `npm run deploy:worker:prod` from either: + +- a code push to Gitea, +- a manual Gitea Actions workflow, +- a Directus webhook that calls your CI runner, +- or a scheduled CI job. diff --git a/package.json b/package.json index 9544e6e..031db8e 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "scripts": { "dev": "astro dev", "build": "astro build", - "build:pages:test": "node scripts/cloudflare-pages.mjs build .env.pages.test", - "build:pages:prod": "node scripts/cloudflare-pages.mjs build .env.pages.prod", - "deploy:pages:test": "node scripts/cloudflare-pages.mjs deploy .env.pages.test --branch test", - "deploy:pages:prod": "node scripts/cloudflare-pages.mjs deploy .env.pages.prod --branch main", + "build:worker:test": "node scripts/cloudflare-worker.mjs build .env.worker.test", + "build:worker:prod": "node scripts/cloudflare-worker.mjs build .env.worker.prod", + "deploy:worker:test": "node scripts/cloudflare-worker.mjs deploy .env.worker.test", + "deploy:worker:prod": "node scripts/cloudflare-worker.mjs deploy .env.worker.prod", "preview": "astro preview", "check": "astro check", "bootstrap:directus": "node scripts/bootstrap-directus.mjs" diff --git a/scripts/cloudflare-pages.mjs b/scripts/cloudflare-worker.mjs similarity index 64% rename from scripts/cloudflare-pages.mjs rename to scripts/cloudflare-worker.mjs index 89e13b3..c0427a0 100644 --- a/scripts/cloudflare-pages.mjs +++ b/scripts/cloudflare-worker.mjs @@ -2,10 +2,10 @@ import { existsSync, readFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { basename } from 'node:path'; -const [command, envFile, ...args] = process.argv.slice(2); +const [command, envFile] = process.argv.slice(2); if (!['build', 'deploy'].includes(command) || !envFile) { - console.error('Usage: node scripts/cloudflare-pages.mjs [--branch name] [--project name]'); + console.error('Usage: node scripts/cloudflare-worker.mjs '); process.exit(1); } @@ -14,20 +14,6 @@ if (!existsSync(envFile)) { process.exit(1); } -function parseArgs(values) { - const options = {}; - - for (let index = 0; index < values.length; index += 1) { - const value = values[index]; - if (value === '--branch' || value === '--project') { - options[value.slice(2)] = values[index + 1]; - index += 1; - } - } - - return options; -} - function parseEnvFile(path) { const entries = {}; const lines = readFileSync(path, 'utf8').split(/\r?\n/); @@ -57,9 +43,7 @@ function parseEnvFile(path) { function applyEnv(entries) { for (const [key, value] of Object.entries(entries)) { - if (process.env[key] === undefined) { - process.env[key] = value; - } + process.env[key] = value; } } @@ -86,9 +70,10 @@ function run(name, commandName, commandArgs) { const envEntries = parseEnvFile(envFile); applyEnv(envEntries); -const options = parseArgs(args); -const projectName = options.project ?? process.env.CF_PAGES_PROJECT_NAME; -const branch = options.branch ?? process.env.CF_PAGES_BRANCH; +const workerName = process.env.CF_WORKER_NAME; +const workerRoute = process.env.CF_WORKER_ROUTE; +const workerDomain = process.env.CF_WORKER_DOMAIN; +const compatibilityDate = process.env.CF_WORKER_COMPATIBILITY_DATE; console.log(`Using ${basename(envFile)} for ${command}.`); console.log(`PUBLIC_SITE_URL=${process.env.PUBLIC_SITE_URL ?? ''}`); @@ -97,15 +82,24 @@ console.log(`DIRECTUS_URL=${process.env.DIRECTUS_URL ?? ''}`); await run('Astro build', 'npm', ['run', 'build']); if (command === 'deploy') { - if (!projectName) { - console.error('Missing CF_PAGES_PROJECT_NAME. Set it in the env file or pass --project .'); + if (!workerName) { + console.error('Missing CF_WORKER_NAME. Set it in the env file.'); process.exit(1); } - const wranglerArgs = ['wrangler', 'pages', 'deploy', 'dist', '--project-name', projectName]; - if (branch) { - wranglerArgs.push('--branch', branch); + const wranglerArgs = ['wrangler', 'deploy', 'dist', '--name', workerName]; + + if (compatibilityDate) { + wranglerArgs.push('--compatibility-date', compatibilityDate); } - await run('Cloudflare Pages deploy', 'npx', wranglerArgs); + if (workerRoute) { + wranglerArgs.push('--route', workerRoute); + } + + if (workerDomain) { + wranglerArgs.push('--domain', workerDomain); + } + + await run('Cloudflare Worker deploy', 'npx', wranglerArgs); }