Skip to content

chore(actions): bump the github-actions group with 3 updates #198

chore(actions): bump the github-actions group with 3 updates

chore(actions): bump the github-actions group with 3 updates #198

Workflow file for this run

name: Deploy
# Continuous deployment to Cloudflare (ADR 0029 (all-cloudflare-hosting), issue #111).
#
# On merge to main: build and deploy the stacker frontend to Cloudflare Pages
# and the retriever + petdata backends to Cloudflare Containers (Worker-fronted).
# A gated Alembic migration runs against Supabase before each backend revision
# takes traffic; a migration failure fails the job and the cutover step never
# runs. On pull requests: preview deploys only (no production traffic, no
# migration against the production database).
#
# Manual runs (workflow_dispatch) behave as a production deploy: the operator
# triggers one for the first cutover and for redeploys after setting secrets,
# without needing a code change. A manual run deploys all three services by
# default; the `service` input narrows it to a single service. Like a push, a
# manual run runs the gated migration and cutover (never a preview).
#
# Secrets (never committed) come from Actions secrets:
# CLOUDFLARE_API_TOKEN scoped token: Pages + Workers + Containers deploy
# CLOUDFLARE_ACCOUNT_ID Cloudflare account
# RETRIEVER_DATABASE_URL retriever Supabase pooler URL (alembic DATABASE_URL)
# PETDATA_DATABASE_URL petdata Supabase pooler URL
#
# Until an operator configures CLOUDFLARE_API_TOKEN (the issue's operator step),
# the preflight job below reports the secret as absent and every deploy job is
# skipped, not failed. The same skip covers pull requests from forks, which do
# not receive repository secrets, and manual runs before the token is set. Once
# the token is set, deploys and previews run.
#
# Hyperdrive (issue #109) is deferred: the migration and the containers connect
# directly to the Supabase pooler via the *_DATABASE_URL secrets. When #109
# lands, only the connection string behind these secrets changes.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
service:
description: "Service to deploy"
type: choice
default: all
options:
- all
- petdata
- retriever
- stacker
# Never cancel a production deploy mid-migration/cutover; PR previews are cheap
# to leave running.
concurrency:
group: deploy-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
deployments: write
pull-requests: write
# wrangler-action installs an old default Wrangler (3.90) when no local install
# exists, and that version predates wrangler.jsonc support, so it cannot read the
# backends' container config. Pin a modern Wrangler for every deploy step.
env:
WRANGLER_VERSION: "4.105.0"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
petdata: ${{ steps.resolve.outputs.petdata }}
retriever: ${{ steps.resolve.outputs.retriever }}
stacker: ${{ steps.resolve.outputs.stacker }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# paths-filter needs a diff base, which a manual dispatch has no commit
# range for, so run it only for push/pull_request and resolve manual runs
# from the `service` input below.
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
if: ${{ github.event_name != 'workflow_dispatch' }}
with:
filters: |
petdata:
- 'services/petdata/**'
- '.github/workflows/deploy.yml'
retriever:
- 'services/retriever/**'
- '.github/workflows/deploy.yml'
stacker:
- 'apps/stacker/**'
- '.github/workflows/deploy.yml'
# On manual dispatch, select services from the `service` input ('all' picks
# all three); otherwise pass the paths-filter results through unchanged.
- id: resolve
env:
SERVICE: ${{ github.event.inputs.service }}
FILTER_PETDATA: ${{ steps.filter.outputs.petdata }}
FILTER_RETRIEVER: ${{ steps.filter.outputs.retriever }}
FILTER_STACKER: ${{ steps.filter.outputs.stacker }}
run: |
{
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
for svc in petdata retriever stacker; do
if [ "$SERVICE" = "all" ] || [ "$SERVICE" = "$svc" ]; then
echo "$svc=true"
else
echo "$svc=false"
fi
done
else
echo "petdata=$FILTER_PETDATA"
echo "retriever=$FILTER_RETRIEVER"
echo "stacker=$FILTER_STACKER"
fi
} >> "$GITHUB_OUTPUT"
# Secrets cannot be read in a job-level `if`, so detect the Cloudflare token in
# a step and expose the result as an output the deploy jobs gate on. When it is
# absent (operator has not set it yet, or a fork PR), the deploy jobs skip.
preflight:
runs-on: ubuntu-latest
outputs:
configured: ${{ steps.check.outputs.configured }}
steps:
- id: check
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::notice::CLOUDFLARE_API_TOKEN is not set; skipping all deploy jobs."
fi
deploy-stacker:
needs: [changes, preflight]
if: ${{ needs.changes.outputs.stacker == 'true' && needs.preflight.outputs.configured == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/stacker
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: apps/stacker/package-lock.json
# Inline the production public env at build time. SvelteKit bakes
# $env/static/public values (PUBLIC_SUPABASE_URL/PUBLISHABLE_KEY, the backend
# URLs) into the bundle during `npm run build`, so they must be the
# production values here rather than the localhost defaults in
# .env.example. Supabase values come from Actions secrets (the publishable key
# is browser-safe but managed as a secret); the backend URLs default to
# the production subdomains and can be overridden by Actions variables.
- name: Write production env
env:
PUBLIC_SUPABASE_URL: ${{ secrets.STACKER_PUBLIC_SUPABASE_URL }}
PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.STACKER_PUBLIC_SUPABASE_PUBLISHABLE_KEY }}
PUBLIC_RETRIEVER_API_URL: ${{ vars.STACKER_PUBLIC_RETRIEVER_API_URL || 'https://retriever.evermore.backchain.app' }}
PUBLIC_PETDATA_API_URL: ${{ vars.STACKER_PUBLIC_PETDATA_API_URL || 'https://petdata.evermore.backchain.app' }}
run: |
{
echo "PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL"
echo "PUBLIC_SUPABASE_PUBLISHABLE_KEY=$PUBLIC_SUPABASE_PUBLISHABLE_KEY"
echo "PUBLIC_RETRIEVER_API_URL=$PUBLIC_RETRIEVER_API_URL"
echo "PUBLIC_PETDATA_API_URL=$PUBLIC_PETDATA_API_URL"
echo "PUBLIC_ENABLED_MODULES=retriever"
} > .env
- run: npm ci
- run: npm run build
# push or manual dispatch -> production branch deploy; PR head branch ->
# preview deploy (Cloudflare Pages treats any non-production branch as a
# preview). Project name and build-output dir come from
# apps/stacker/wrangler.toml. DEPLOY_BRANCH is passed as an env var and
# referenced with quoted shell expansion (not GitHub template
# interpolation) so an attacker-controlled PR branch name cannot inject
# into the deploy command.
- name: Deploy to Cloudflare Pages
# wrangler-action does not shell-expand its `command` string, so a branch
# passed as --branch="$DEPLOY_BRANCH" reached wrangler as the literal
# "$DEPLOY_BRANCH", which Cloudflare sanitized to a phantom "deploy-branch"
# preview: the production slot never filled. A direct run step lets the
# shell expand DEPLOY_BRANCH from the environment (injection-safe: an env
# var, not a ${{ }} interpolation of an attacker-controlled branch name),
# so the real branch reaches wrangler and a push or manual dispatch on the
# production branch produces a production deployment. Project name and
# build-output dir come from apps/stacker/wrangler.toml; the job already
# runs in apps/stacker via the job-level working-directory default.
env:
DEPLOY_BRANCH: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref_name || github.head_ref }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: npx --yes wrangler@${{ env.WRANGLER_VERSION }} pages deploy --branch="$DEPLOY_BRANCH"
deploy-retriever:
needs: [changes, preflight]
if: ${{ needs.changes.outputs.retriever == 'true' && needs.preflight.outputs.configured == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/retriever
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
cache-dependency-glob: services/retriever/uv.lock
- run: uv sync
# The Worker entry (worker/index.ts) imports @cloudflare/containers from the
# Worker's isolated npm project (worker/package.json). wrangler's esbuild
# bundler resolves it from worker/node_modules, so install those deps before
# any wrangler step or the build fails with "Could not resolve".
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: services/retriever/worker/package-lock.json
- name: Install Worker dependencies
working-directory: services/retriever/worker
run: npm ci
# Gated migration (production only): must succeed before the new container
# revision takes traffic (ADR 0029 (all-cloudflare-hosting)). A failure here fails the job, so the
# wrangler deploy step below never runs and production keeps the old revision.
# Push and manual dispatch are production; pull requests are not.
- name: Migrate database (gated, production only)
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
env:
DATABASE_URL: ${{ secrets.RETRIEVER_DATABASE_URL }}
run: uv run alembic upgrade head
# Production cutover on merge to main or manual dispatch: builds the
# container image from the Dockerfile and deploys the Worker + Container,
# shifting traffic.
- name: Deploy container (production)
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: ${{ env.WRANGLER_VERSION }}
workingDirectory: services/retriever
command: deploy
# Preview on PRs: upload a new version (with its container image) that gets
# a preview URL and takes no production traffic. No migration runs against
# the production database for previews.
- name: Upload preview version (pull request)
if: ${{ github.event_name == 'pull_request' }}
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: ${{ env.WRANGLER_VERSION }}
workingDirectory: services/retriever
command: versions upload
deploy-petdata:
needs: [changes, preflight]
if: ${{ needs.changes.outputs.petdata == 'true' && needs.preflight.outputs.configured == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/petdata
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
cache-dependency-glob: services/petdata/uv.lock
- run: uv sync --extra dev
# The Worker entry (worker/index.ts) imports @cloudflare/containers from the
# Worker's isolated npm project (worker/package.json). wrangler's esbuild
# bundler resolves it from worker/node_modules, so install those deps before
# any wrangler step or the build fails with "Could not resolve".
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: services/petdata/worker/package-lock.json
- name: Install Worker dependencies
working-directory: services/petdata/worker
run: npm ci
# Gated migration (production only): see deploy-retriever for rationale.
# petdata's alembic env reads PETDATA_DATABASE_URL. Push and manual
# dispatch are production; pull requests are not.
- name: Migrate database (gated, production only)
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
env:
PETDATA_DATABASE_URL: ${{ secrets.PETDATA_DATABASE_URL }}
run: uv run alembic upgrade head
- name: Deploy container (production)
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: ${{ env.WRANGLER_VERSION }}
workingDirectory: services/petdata
command: deploy
- name: Upload preview version (pull request)
if: ${{ github.event_name == 'pull_request' }}
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: ${{ env.WRANGLER_VERSION }}
workingDirectory: services/petdata
command: versions upload