Skip to content

chore: upgrade Better Auth 1.7, unify deploys, guard db:push #71

chore: upgrade Better Auth 1.7, unify deploys, guard db:push

chore: upgrade Better Auth 1.7, unify deploys, guard db:push #71

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# A merge queue reports required checks against the queued group rather than
# the pull request, so without this the merge never completes. Inert until
# someone enables one, and the deploy jobs below ignore it.
merge_group:
types: [checks_requested]
workflow_dispatch:
# Manual deploys target production only; staging deploys on push to main.
# This builds `main` fresh rather than promoting the build staging is
# running – nothing carries a release identity between runs.
inputs:
deploy_production:
description: "Deploy current main to production after the build"
type: boolean
default: false
env:
HUSKY: 0
# Keyed on the event and on whether a manual run deploys, so ordinary CI on
# `main`, a manual check and a production release never share a group – GitHub
# keeps one pending run per group, so a build-only dispatch would otherwise
# evict a release waiting its turn.
#
# Only pull-request runs cancel the run in flight. A push may be midway through
# staging and a production dispatch midway through production; killing either
# leaves the database and the workers on mixed versions.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.deploy_production && 'deploy' || 'check' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
# Actions are pinned to commit SHAs, the only immutable reference GitHub
# offers: a tag can be moved to new code. Dependabot updates the SHA and its
# `# vX.Y.Z` comment together; see .github/dependabot.yml.
jobs:
build:
name: "Build"
runs-on: ubuntu-latest
# The default is 6 hours, long enough for a hung step to burn a day of
# runner minutes.
timeout-minutes: 30
steps:
# A job skipped by its `if` reports success, so a production request that
# cannot be honoured would finish green having deployed nothing. Fail it
# where someone is watching; the deploy job keeps its own conditions.
- name: Validate production request
if: ${{ github.event_name == 'workflow_dispatch' && inputs.deploy_production }}
env:
DEPLOY_ENABLED: ${{ vars.DEPLOY_ENABLED }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "::error::Production deploys must run from main, not ${GITHUB_REF#refs/heads/}."
exit 1
fi
if [[ "$DEPLOY_ENABLED" != "true" ]]; then
echo "::error::Deployments are off. Set the DEPLOY_ENABLED repository variable to true."
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Checkout otherwise leaves its token behind for later Git commands.
# Nothing in this job runs one.
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- run: bun install --frozen-lockfile
# Every run, not just PRs: whether `main` only receives reviewed commits
# is a branch-protection setting this repository cannot assert.
- run: bun prettier --check .
- run: bun lint
# Validates both roots without remote state or provider credentials.
#
# The same range the roots and the HCP workspaces declare, each resolved
# independently – they may differ for a day after a release, which is
# harmless while none can leave the range. Raising the floor means editing
# all four: both roots, the HCP workspaces, here, and infra.yml.
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: ">=1.12.0 <2.0.0"
- run: bun infra:check
# `apps/api` references `apps/email`, so `tsc --build` emits the template
# declarations before compiling the code that imports them – and leaves
# `apps/email/dist` in place for the artifact below.
- run: bun typecheck
# tsc cannot parse .astro templates; astro check covers apps/web
- run: bun --filter @repo/web check
- run: bun run test -- --run
# The API build targets the container image (apps/api/Dockerfile) –
# Wrangler bundles `worker.ts` from source – so it runs here as a compile
# check and contributes nothing to the deploy artifact.
- run: bun --filter @repo/web build
- run: bun --filter @repo/api build
- run: bun --filter @repo/app build
# Catches dead relative links and unhandled SSR render errors; see the
# production error setting in docs/.vitepress/theme/index.ts.
- run: bun docs:build
# Exactly what the deploy job consumes. `apps/api` deploys from source,
# but imports `@repo/email`, which resolves through `./dist` – so Wrangler
# cannot bundle the API worker without it on a fresh checkout.
#
# Checked on every run, uploaded only on runs that can deploy, so a pull
# request fails here rather than at the next release. Hence looking for a
# file rather than the directory: an empty `dist` uploads nothing, and
# `if-no-files-found` would only catch it on a deploying run.
- name: Verify build output
run: |
for dir in apps/email/dist apps/web/dist apps/app/dist; do
if [[ -z "$(find "$dir" -type f -print -quit 2>/dev/null)" ]]; then
echo "::error::$dir holds no files – the deploy artifact would be empty."
exit 1
fi
done
# Must stay a superset of both deploy jobs' conditions, or a release
# passes CI and then dies at `download-artifact`.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: >-
vars.DEPLOY_ENABLED == 'true' &&
(github.event_name == 'push' || inputs.deploy_production)
with:
name: build
path: |
apps/email/dist
apps/web/dist
apps/app/dist
if-no-files-found: error
# Staging deploys minutes later. Production may wait on a reviewer,
# and GitHub allows 30 days before failing the job – an artifact that
# expired first would turn a legitimate approval into a download error.
retention-days: ${{ inputs.deploy_production && 31 || 1 }}
# Both deploy jobs are opt-in: set the `DEPLOY_ENABLED` repository variable
# once Cloudflare is provisioned and each environment holds its credentials.
# Until then a clone runs pure CI, with no deployment records implying more.
deploy-staging:
name: "Deploy"
needs: [build]
if: >-
vars.DEPLOY_ENABLED == 'true' &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main'
uses: ./.github/workflows/deploy.yml
with:
environment: staging
url: https://staging.example.com
# No `secrets: inherit` – it would hand the called workflow every secret
# this one can see, where deploy.yml names an environment and gets its
# credentials from there alone. `contents: read` must still be repeated: a
# called workflow never holds more than its caller grants, and naming any
# permission drops the rest to `none`, leaving its checkout unable to read
# a private repository.
permissions:
contents: read
deploy-production:
name: "Deploy"
needs: [build]
# The dispatch UI offers a branch picker, so pin production to `main`
# rather than trusting whatever ref the run was started from.
if: >-
vars.DEPLOY_ENABLED == 'true' &&
github.event_name == 'workflow_dispatch' &&
inputs.deploy_production &&
github.ref == 'refs/heads/main'
uses: ./.github/workflows/deploy.yml
with:
environment: production
url: https://example.com
permissions:
contents: read