Add wrangler

This commit is contained in:
2026-06-21 05:04:26 -04:00
parent 4498fa2611
commit ed7597f879
8 changed files with 2153 additions and 5 deletions
+15
View File
@@ -0,0 +1,15 @@
# Copy to .env.pages.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
PUBLIC_WEATHER_MQTT_HOST=https://wx.swansea-airport.wales/mqtt
DIRECTUS_URL=https://cms.swansea-airport.wales
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
CLOUDFLARE_ACCOUNT_ID=replace-with-cloudflare-account-id
CLOUDFLARE_API_TOKEN=replace-with-cloudflare-api-token
+15
View File
@@ -0,0 +1,15 @@
# 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
PUBLIC_WEATHER_BASE=https://wx.swansea-airport.wales
PUBLIC_WEATHER_MQTT_HOST=https://wx.swansea-airport.wales/mqtt
DIRECTUS_URL=https://egfhcmstest.pattinson.org
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
CLOUDFLARE_ACCOUNT_ID=replace-with-cloudflare-account-id
CLOUDFLARE_API_TOKEN=replace-with-cloudflare-api-token
+2
View File
@@ -2,4 +2,6 @@ node_modules
dist dist
.astro .astro
.env .env
.env.pages.test
.env.pages.prod
.DS_Store .DS_Store
+23
View File
@@ -23,6 +23,29 @@ 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. - 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. - Layout and page structure are controlled entirely by Astro.
- Frontend source edits should appear without rebuilding the container image. - 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 Pages deployment
Build static files with the test URL set:
```bash
npm run build:pages:test
```
Deploy the test build to the Cloudflare Pages `test` branch:
```bash
npm run deploy:pages:test
```
Deploy production with the production URL set:
```bash
npm run deploy:pages:prod
```
Copy `.env.pages.test.example` and `.env.pages.prod.example` before using these commands.
## Programmatic Directus schema bootstrap ## Programmatic Directus schema bootstrap
+82
View File
@@ -0,0 +1,82 @@
# 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.
+1898 -4
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -5,6 +5,10 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "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",
"preview": "astro preview", "preview": "astro preview",
"check": "astro check", "check": "astro check",
"bootstrap:directus": "node scripts/bootstrap-directus.mjs" "bootstrap:directus": "node scripts/bootstrap-directus.mjs"
@@ -13,7 +17,9 @@
"astro": "^5.6.2" "astro": "^5.6.2"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.9",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"typescript": "^5.8.3" "typescript": "^5.8.3",
"wrangler": "^4.103.0"
} }
} }
+111
View File
@@ -0,0 +1,111 @@
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);
if (!['build', 'deploy'].includes(command) || !envFile) {
console.error('Usage: node scripts/cloudflare-pages.mjs <build|deploy> <env-file> [--branch name] [--project name]');
process.exit(1);
}
if (!existsSync(envFile)) {
console.error(`Missing ${envFile}. Copy the matching .example file and fill in the values first.`);
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/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const separator = trimmed.indexOf('=');
if (separator === -1) continue;
const key = trimmed.slice(0, separator).trim();
let value = trimmed.slice(separator + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
entries[key] = value;
}
return entries;
}
function applyEnv(entries) {
for (const [key, value] of Object.entries(entries)) {
if (process.env[key] === undefined) {
process.env[key] = value;
}
}
}
function run(name, commandName, commandArgs) {
return new Promise((resolve, reject) => {
const child = spawn(commandName, commandArgs, {
env: process.env,
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${name} exited with code ${code}`));
});
});
}
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;
console.log(`Using ${basename(envFile)} for ${command}.`);
console.log(`PUBLIC_SITE_URL=${process.env.PUBLIC_SITE_URL ?? '<unset>'}`);
console.log(`DIRECTUS_URL=${process.env.DIRECTUS_URL ?? '<unset>'}`);
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 <name>.');
process.exit(1);
}
const wranglerArgs = ['wrangler', 'pages', 'deploy', 'dist', '--project-name', projectName];
if (branch) {
wranglerArgs.push('--branch', branch);
}
await run('Cloudflare Pages deploy', 'npx', wranglerArgs);
}