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 .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ jobs:
node-version: 22
cache: pnpm

- name: Setup OpenTofu
uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.11.5
tofu_wrapper: false

- name: Install dependencies
run: pnpm install --frozen-lockfile

Expand All @@ -57,6 +63,9 @@ jobs:
- name: Check workspace
run: npm run check:workspace

- name: Check infra formatting
run: npm run infra:fmt:check

- name: Test packages with coverage
run: npm run test:coverage

Expand Down
148 changes: 148 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Deploy

on:
workflow_dispatch:
inputs:
environment:
description: "Target deployment environment"
required: true
type: choice
options:
- staging
- production
deploy_worker:
description: "Deploy the Cloudflare Worker"
required: true
type: boolean
default: true
deploy_pages:
description: "Deploy the Cloudflare Pages app"
required: true
type: boolean
default: true
run_hosted_smoke:
description: "Run hosted smoke checks after deploy"
required: true
type: boolean
default: false

permissions:
contents: read

concurrency:
group: deploy-${{ github.workflow }}-${{ inputs.environment }}
cancel-in-progress: false

jobs:
deploy:
name: Deploy ${{ inputs.environment }}
runs-on: ubuntu-latest
timeout-minutes: 20
environment: ${{ inputs.environment }}
env:
CI: true
WRANGLER_SEND_METRICS: false
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_PAGES_PROJECT_NAME: ${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME || 'forage-web' }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.23.0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Setup OpenTofu
uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.11.5
tofu_wrapper: false

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Resolve deployment environment
id: resolve
shell: bash
run: |
set -euo pipefail

environment_name="${{ inputs.environment }}"
case "$environment_name" in
production)
web_origin="${{ vars.FORAGE_PRODUCTION_WEB_ORIGIN }}"
worker_origin="${{ vars.FORAGE_PRODUCTION_WORKER_ORIGIN }}"
pages_branch="${{ vars.FORAGE_PRODUCTION_PAGES_BRANCH }}"
default_pages_branch="main"
;;
staging)
web_origin="${{ vars.FORAGE_STAGING_WEB_ORIGIN }}"
worker_origin="${{ vars.FORAGE_STAGING_WORKER_ORIGIN }}"
pages_branch="${{ vars.FORAGE_STAGING_PAGES_BRANCH }}"
default_pages_branch="staging"
;;
*)
echo "::error::Unsupported deployment environment: $environment_name"
exit 1
;;
esac

if [ -z "$worker_origin" ]; then
echo "::error::Set FORAGE_${environment_name^^}_WORKER_ORIGIN as a GitHub environment variable."
exit 1
fi

if [ "${{ inputs.run_hosted_smoke }}" = "true" ] && [ -z "$web_origin" ]; then
echo "::error::Set FORAGE_${environment_name^^}_WEB_ORIGIN as a GitHub environment variable before hosted smoke checks."
exit 1
fi

if [ -z "$pages_branch" ]; then
pages_branch="$default_pages_branch"
fi

{
echo "environment_name=$environment_name"
echo "web_origin=$web_origin"
echo "worker_origin=$worker_origin"
echo "pages_branch=$pages_branch"
} >> "$GITHUB_OUTPUT"

- name: Check infra formatting
run: npm run infra:fmt:check

- name: Build Worker
run: pnpm --filter @forage/worker build

- name: Build web app
env:
PUBLIC_WORKER_ORIGIN: ${{ steps.resolve.outputs.worker_origin }}
run: pnpm --filter @forage/web build

- name: Deploy Worker
if: ${{ inputs.deploy_worker }}
working-directory: apps/worker
run: pnpm exec wrangler deploy --env "${{ steps.resolve.outputs.environment_name }}"

- name: Deploy Pages
if: ${{ inputs.deploy_pages }}
working-directory: apps/worker
run: |
pnpm exec wrangler pages deploy ../../apps/web/dist \
--project-name "$CLOUDFLARE_PAGES_PROJECT_NAME" \
--branch "${{ steps.resolve.outputs.pages_branch }}"

- name: Hosted smoke check
if: ${{ inputs.run_hosted_smoke }}
env:
FORAGE_WEB_ORIGIN: ${{ steps.resolve.outputs.web_origin }}
FORAGE_WORKER_ORIGIN: ${{ steps.resolve.outputs.worker_origin }}
run: pnpm smoke:hosted
5 changes: 3 additions & 2 deletions docs/17-ci-and-quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Current `npm run check` coverage:
- Analysis review fixture check
- Documentation checks
- Workspace structure checks
- OpenTofu formatting for `infra/opentofu`
- Package tests with coverage
- Worker API contract tests
- Biome lint and format checks for JavaScript, TypeScript, CSS, and JSON
Expand Down Expand Up @@ -74,5 +75,5 @@ Future gates:
- Raise package source coverage from 75% to 90% after the MVP worker/import state boundaries stabilize.
- Add package-level unit tests for IndexedDB migrations.
- Add browser smoke tests once the app has stable flows beyond the current import slice.
- Add deployment preview checks when Cloudflare environment bindings are defined.
- Add a scheduled or manual CI job for `pnpm smoke:hosted` once Cloudflare domains are configured.
- Expand deployment preview checks after Cloudflare environment bindings are proven.
- Enable hosted smoke checks by default once Cloudflare domains are configured.
12 changes: 11 additions & 1 deletion docs/21-hosting-ui-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,20 @@ Required repository settings:
Recommended repository secrets for deployment CI:
- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_ACCOUNT_ID`
- Any project-specific deploy token required by the chosen deployment workflow

Do not store GitHub App client secrets as public repository variables.

Recommended GitHub environment variables for the manual deploy workflow:
- `CLOUDFLARE_PAGES_PROJECT_NAME`
- `FORAGE_STAGING_WEB_ORIGIN`
- `FORAGE_STAGING_WORKER_ORIGIN`
- `FORAGE_STAGING_PAGES_BRANCH`
- `FORAGE_PRODUCTION_WEB_ORIGIN`
- `FORAGE_PRODUCTION_WORKER_ORIGIN`
- `FORAGE_PRODUCTION_PAGES_BRANCH`

Use GitHub environments named `staging` and `production`. Production should require manual approval before `.github/workflows/deploy.yml` can deploy to it.

## Post-Deploy Verification

Run the automated hosted smoke check:
Expand Down
2 changes: 2 additions & 0 deletions docs/22-infrastructure-as-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Set `manage_worker_custom_domains = false` until the Worker service has been dep

After apply, copy the `settings_kv_namespaces` output into the Worker binding configuration as `SETTINGS_KV` for each environment.

Use [Deployment Automation](./23-deployment-automation.md) for the GitHub Actions workflow, repository secrets, GitHub environment variables, and first deployment order.

## Sources Checked

- [Cloudflare Terraform overview](https://developers.cloudflare.com/terraform/)
Expand Down
91 changes: 91 additions & 0 deletions docs/23-deployment-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Deployment Automation

Status:
Manual GitHub Actions workflow scaffolded

Forage deploys through `.github/workflows/deploy.yml`. The workflow is manual-only for now so staging and production deploys remain operator-controlled while the Cloudflare hosting path is still being proven.

## Workflow Inputs

- `environment`: `staging` or `production`
- `deploy_worker`: deploy `apps/worker` with Wrangler
- `deploy_pages`: deploy `apps/web/dist` to Cloudflare Pages with Wrangler direct upload
- `run_hosted_smoke`: run `pnpm smoke:hosted` after deployment

## GitHub Repository Secrets

Configure these as repository or environment secrets:

- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_ACCOUNT_ID`

The Cloudflare API token should be scoped narrowly to the Forage account and resources needed for Worker and Pages deployment. Keep GitHub App client secrets in Cloudflare Worker secrets, not GitHub repository variables.

## GitHub Environment Variables

Configure these on the `staging` and `production` GitHub environments:

- `CLOUDFLARE_PAGES_PROJECT_NAME`
- `FORAGE_STAGING_WEB_ORIGIN`
- `FORAGE_STAGING_WORKER_ORIGIN`
- `FORAGE_STAGING_PAGES_BRANCH`
- `FORAGE_PRODUCTION_WEB_ORIGIN`
- `FORAGE_PRODUCTION_WORKER_ORIGIN`
- `FORAGE_PRODUCTION_PAGES_BRANCH`

Defaults:

- Pages project defaults to `forage-web` when `CLOUDFLARE_PAGES_PROJECT_NAME` is unset.
- Staging Pages branch defaults to `staging` when `FORAGE_STAGING_PAGES_BRANCH` is unset.
- Production Pages branch defaults to `main` when `FORAGE_PRODUCTION_PAGES_BRANCH` is unset.

`FORAGE_*_WORKER_ORIGIN` is required because the Astro build compiles `PUBLIC_WORKER_ORIGIN` into the web app and CSP. `FORAGE_*_WEB_ORIGIN` is required when hosted smoke checks are enabled.

## First Deploy Order

1. Apply `infra/opentofu` with `manage_worker_custom_domains = false`.
2. Configure Worker secrets in Cloudflare.
3. Deploy the Worker once with Wrangler or the manual deploy workflow.
4. Enable Worker custom domains in OpenTofu if the Worker service now exists.
5. Apply OpenTofu again.
6. Deploy Pages through the manual deploy workflow.
7. Run the hosted smoke check.

This order avoids the Cloudflare custom-domain dependency where the Worker service must exist before OpenTofu can attach Worker domains.

## Workflow Behavior

Worker deploy:

```sh
pnpm --filter @forage/worker build
cd apps/worker
pnpm exec wrangler deploy --env staging
```

Pages deploy:

```sh
PUBLIC_WORKER_ORIGIN=https://api-staging.forage.example.com pnpm --filter @forage/web build
cd apps/worker
pnpm exec wrangler pages deploy ../../apps/web/dist --project-name forage-web --branch staging
```

Hosted smoke:

```sh
FORAGE_WEB_ORIGIN=https://staging.forage.example.com \
FORAGE_WORKER_ORIGIN=https://api-staging.forage.example.com \
pnpm smoke:hosted
```

## Quality Gates

The main Check workflow installs OpenTofu and runs `npm run infra:fmt:check`. The root `npm run check` also includes the same infra formatting check so local and CI gates stay aligned.

## Sources Checked

- [Cloudflare Pages direct upload](https://developers.cloudflare.com/pages/get-started/direct-upload/)
- [Cloudflare Workers GitHub Actions](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/)
- [Wrangler GitHub Action](https://github.com/cloudflare/wrangler-action)
- [OpenTofu setup action](https://github.com/opentofu/setup-opentofu)
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This directory contains the planning and technical decision material for Forage.
- [Hosting And Security Plan](./20-hosting-and-security.md)
- [Hosting UI Setup](./21-hosting-ui-setup.md)
- [Infrastructure As Code](./22-infrastructure-as-code.md)
- [Deployment Automation](./23-deployment-automation.md)

## Architecture Decisions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"analysis:review": "node --import tsx scripts/analysis/review-export.ts",
"build": "pnpm --recursive --if-present build",
"check": "npm run check:scripts && npm run check:pre-mvp && npm run check:analysis && npm run check:docs && npm run check:workspace && npm run test:coverage && npm run check:web-tests && npm run check:worker-tests && npm run lint && npm run format:astro:check && npm run typecheck && npm run build && npm run check:web-smoke",
"check": "npm run check:scripts && npm run check:pre-mvp && npm run check:analysis && npm run check:docs && npm run check:workspace && npm run infra:fmt:check && npm run test:coverage && npm run check:web-tests && npm run check:worker-tests && npm run lint && npm run format:astro:check && npm run typecheck && npm run build && npm run check:web-smoke",
"check:analysis": "pnpm analysis:review scripts/analysis/fixtures/forage-export.sample.json > /dev/null",
"check:docs": "node scripts/check-docs.mjs",
"check:reporting": "pnpm --filter @forage/reporting test",
Expand Down
Loading