Repository navigation
143 lines (132 loc) · 5.9 KB
/
Copy pathmigrate.yml
File metadata and controls
143 lines (132 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
name: migrate
on:
workflow_call:
inputs:
environment:
description: Target environment (production, staging, or dev)
required: true
type: string
secrets:
DATABASE_URL:
required: false
STAGING_DATABASE_URL:
required: false
DEV_DATABASE_URL:
required: false
MIGRATION_DATABASE_URL:
required: false
STAGING_MIGRATION_DATABASE_URL:
required: false
TS_OAUTH_CLIENT_ID:
required: false
TS_AUDIENCE:
required: false
workflow_dispatch:
inputs:
environment:
description: Target environment
required: true
type: choice
options:
- production
- staging
- dev
permissions:
contents: read
jobs:
migrate:
name: migrate
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
# Bulk projection loads and concurrent index builds can outlast ordinary schema changes.
timeout-minutes: 300
# One migrate job per environment at a time, so a manual dispatch queues
# behind a CI run instead of racing it for migrate.ts's advisory lock, which
# gives up after 30 minutes. Job-level because a called workflow evaluates
# workflow-level concurrency with the caller's github context; the group
# must also never equal ci.yml's ci-<ref> group or the two deadlock.
concurrency:
group: migrate-${{ inputs.environment }}
cancel-in-progress: false
permissions:
contents: read
# Tailscale workload identity federation exchanges this job's OIDC token.
id-token: write
steps:
# An environment only migrates from its own branch, so a manual dispatch
# from a feature branch cannot apply that branch's migrations to a shared
# database. Under workflow_call github.ref is the caller's ref (ci.yml's
# push to main/staging/dev). This fails rather than skips: ci.yml's
# promote gates on migrate succeeding, and a skipped run must never read
# as a migration that happened. It runs before any secret is read.
- name: Require the environment's own branch
env:
ENVIRONMENT: ${{ inputs.environment }}
REF: ${{ github.ref }}
run: |
case "${ENVIRONMENT}" in
production) expected=refs/heads/main ;;
staging) expected=refs/heads/staging ;;
dev) expected=refs/heads/dev ;;
*)
echo "::error::Unknown migrate environment '${ENVIRONMENT}'"
exit 1
;;
esac
if [ "${REF}" != "${expected}" ]; then
echo "::error::Refusing to migrate ${ENVIRONMENT} from ${REF}; ${ENVIRONMENT} migrates only from ${expected}"
exit 1
fi
# Database IP restrictions admit the tailnet's app connector, never a
# runner's address. The action already passes --accept-routes, which
# Linux nodes need to use app connector routes; repeating it in args
# makes `tailscale up` reject the duplicate flag.
- name: Join the tailnet
if: vars.CI_DB_TAILNET == 'true'
uses: tailscale/github-action@d1b6cd204f8dceda5b3eaad7f1f767be390056cd # v4.2.0
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
audience: ${{ secrets.TS_AUDIENCE }}
tags: tag:ci
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- name: Setup workspace
uses: ./.github/actions/setup
with:
provider: ${{ vars.CI_PROVIDER }}
node-version: ''
- name: Validate Project migration database prerequisites
if: inputs.environment != 'dev'
working-directory: ./packages/db
env:
DATABASE_URL: ${{ inputs.environment == 'production' && secrets.DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_DATABASE_URL || '' }}
MIGRATION_DATABASE_URL: ${{ inputs.environment == 'production' && secrets.MIGRATION_DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_MIGRATION_DATABASE_URL || '' }}
run: bun --no-env-file scripts/project-contract-required.ts
# The expression maps the explicit environment input to exactly one repo
# secret, so the job never holds another environment's database URL. An
# unknown environment resolves to empty and the guard below fails the job.
# MIGRATION_DATABASE_URL is the optional direct (non-pooled) DSN preferred
# by migrate.ts; when the secret is unset it resolves to empty and the
# script falls back to DATABASE_URL.
- name: Apply database schema changes
working-directory: ./packages/db
env:
DATABASE_URL: ${{ inputs.environment == 'production' && secrets.DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_DATABASE_URL || inputs.environment == 'dev' && secrets.DEV_DATABASE_URL || '' }}
MIGRATION_DATABASE_URL: ${{ inputs.environment == 'production' && secrets.MIGRATION_DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_MIGRATION_DATABASE_URL || '' }}
ENVIRONMENT: ${{ inputs.environment }}
run: |
set -euo pipefail
if [ -z "$DATABASE_URL" ]; then
echo "ERROR: no database URL secret resolved for environment '${ENVIRONMENT}'" >&2
exit 1
fi
if [ "${ENVIRONMENT}" = "dev" ]; then
echo "Dev environment — pushing schema directly (db:push)"
# Dev deliberately forces direct schema reconciliation; staging and
# production use guarded versioned migrations in the other branch.
SIM_DEV_DB_PUSH=1 bun run db:push --force < /dev/null
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
fi