-
Notifications
You must be signed in to change notification settings - Fork 1
303 lines (303 loc) · 13.3 KB
/
Copy pathdeploy.yml
File metadata and controls
303 lines (303 loc) · 13.3 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
name: Create Deployment
# API-first deploy invariant:
# migrations -> server -> api_ready_gate (live GraphQL validation) -> client
# Never publish the client until production GraphQL accepts every client operation.
# Client-only deploys are allowed when deploy_server is skipped, but still must
# pass live schema validation against https://api.seasket.ch/graphql.
on:
workflow_dispatch:
inputs:
force:
description: "Force deployment of all services, regardless of changes"
required: true
type: boolean
default: false
environment:
description: "Environment"
required: true
default: "production"
type: choice
options:
- production
jobs:
detect_changes:
name: Determine service updates
runs-on: ubuntu-latest
outputs:
migrations: ${{ steps.changes.outputs.migrations }}
api: ${{ steps.changes.outputs.api }}
client: ${{ steps.changes.outputs.client }}
build_label: ${{ steps.short_sha.outputs.sha }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: short_sha
run: echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- uses: actions/github-script@v9
id: deployments
with:
script: |
const query = `query($owner: String!, $repo: String!, $env: [String!]) {
repository(owner: $owner, name: $repo) {
deployments(environments: $env, last: 100) {
nodes {
commitOid
state
}
}
}
}`;
const variables = {
owner: context.repo.owner,
repo: context.repo.repo,
env: ['production_client', 'production_server', 'production_db_migrations']
}
const data = await github.graphql(query, variables);
console.log(data);
const mostRecentActiveDeployment = data.repository.deployments.nodes.reverse().find(({state}) => state == 'ACTIVE');
if (mostRecentActiveDeployment) {
core.setOutput('active_ref', mostRecentActiveDeployment.commitOid);
console.log('base:', mostRecentActiveDeployment.commitOid);
}
console.log('ref:', '${{github.ref}}');
- uses: dorny/paths-filter@v4
name: get changed packages
id: changes
with:
base: ${{ steps.deployments.outputs.active_ref }}
filters: |
api:
- 'packages/api/**'
client:
- 'packages/client/**'
migrations:
- 'packages/api/migrations/committed/**'
infra:
- 'packages/infra/**'
unmanaged_packages:
- 'packages/!(api|client|infra)/**'
run_migrations:
name: Run DB Migrations
if: github.event.inputs.force == 'true' || needs.detect_changes.outputs.migrations == 'true'
concurrency: ${{github.event.inputs.environment }}_db_migrations
timeout-minutes: 15
runs-on: ubuntu-latest
environment:
name: ${{github.event.inputs.environment }}_db_migrations
url: https://api.seasket.ch/graphiql
needs:
- detect_changes
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Install unbuffer command
run: sudo apt-get install expect
- name: Find current maintenance bastion task
id: maintenance
run: |
set -euo pipefail
cluster=$(aws cloudformation describe-stack-resources \
--stack-name SeaSketchMaintenanceBastion \
--query "StackResources[?ResourceType=='AWS::ECS::Cluster' && contains(LogicalResourceId, 'Maintenance')].PhysicalResourceId | [0]" \
--output text)
if [[ -z "$cluster" || "$cluster" == "None" ]]; then
echo "Maintenance ECS cluster could not be found" 1>&2
exit 1
fi
task=$(aws ecs list-tasks \
--cluster "$cluster" \
--desired-status RUNNING \
--query "taskArns[0]" \
--output text)
if [[ -z "$task" || "$task" == "None" ]]; then
echo "Running maintenance ECS task could not be found" 1>&2
exit 1
fi
echo "cluster=$cluster" >> "$GITHUB_OUTPUT"
echo "task=$task" >> "$GITHUB_OUTPUT"
# This remote command must succeed with the message "database migrations complete"
- name: Run migrations using ecs exec
run: |
unbuffer aws ecs execute-command --cluster "${{steps.maintenance.outputs.cluster}}" --task "${{steps.maintenance.outputs.task}}" --container Default --command "/bin/sh -l /home/migrate.sh ${{github.sha}}" --interactive | tee /dev/stderr | grep "database migrations complete"
deploy_server:
runs-on: ubuntu-latest
name: Deploy Server
if: always() && needs.run_migrations.result != 'failure' && (github.event.inputs.force == 'true' || needs.detect_changes.outputs.api == 'true')
concurrency: ${{github.event.inputs.environment }}_server
timeout-minutes: 25
environment:
name: ${{github.event.inputs.environment }}_server
url: https://api.seasket.ch/graphiql
needs:
- detect_changes
- run_migrations
steps:
- uses: actions/checkout@v7
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "**/package-lock.json"
- name: Install CDK
run: npm install -g aws-cdk typescript@5.5.3
- name: Install dependencies
run: npx lerna@6.6.2 bootstrap --scope=infra
- name: Deploy server
working-directory: ./packages/infra
env:
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
BUILD: ${{ needs.detect_changes.outputs.build_label }}
COMMIT: ${{ needs.detect_changes.outputs.build_label }}
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_FILE_UPLOADS_BUCKET: ${{ secrets.R2_FILE_UPLOADS_BUCKET }}
R2_TILES_BUCKET: ${{ secrets.R2_TILES_BUCKET }}
CLOUDFLARE_IMAGES_ACCOUNT: ${{ secrets.CLOUDFLARE_IMAGES_ACCOUNT }}
CLOUDFLARE_IMAGES_TOKEN: ${{ secrets.CLOUDFLARE_IMAGES_TOKEN }}
CLOUDFLARE_IMAGES_ACCOUNT_HASH: ${{ secrets.CLOUDFLARE_IMAGES_ACCOUNT_HASH }}
SCREENSHOTTER_FUNCTION_ARN: ${{ secrets.SCREENSHOTTER_FUNCTION_ARN }}
UPLOADS_BASE_URL: ${{ secrets.UPLOADS_BASE_URL }}
RESOURCES_REMOTE: ${{ secrets.RESOURCES_REMOTE }}
TILES_REMOTE: ${{ secrets.TILES_REMOTE }}
TILES_BASE_URL: ${{ secrets.TILES_BASE_URL }}
CLOUDFLARE_ACCOUNT_TAG: ${{ secrets.CLOUDFLARE_ACCOUNT_TAG }}
CLOUDFLARE_GRAPHQL_TOKEN: ${{ secrets.CLOUDFLARE_GRAPHQL_TOKEN }}
CLOUDFLARE_SITE_TAG: ${{ secrets.CLOUDFLARE_SITE_TAG }}
PMTILES_SERVER_ZONE: ${{ secrets.PMTILES_SERVER_ZONE }}
SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }}
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
S3_REGION: ${{ secrets.S3_REGION }}
GOOGLE_MAPS_2D_TILE_API_KEY: ${{ secrets.GOOGLE_MAPS_2D_TILE_API_KEY }}
CF_AIG_TOKEN: ${{ secrets.CF_AIG_TOKEN }}
CF_AIG_URL: ${{ secrets.CF_AIG_URL }}
run: cdk deploy --require-approval never -e SeaSketchGraphQLServer
api_ready_gate:
name: API ready for client
runs-on: ubuntu-latest
timeout-minutes: 10
# Fail closed: only proceed toward client publish when detect_changes succeeded
# and (if an API deploy was required) deploy_server succeeded.
if: |
always() &&
needs.detect_changes.result == 'success' &&
(github.event.inputs.force == 'true' || needs.detect_changes.outputs.client == 'true')
needs:
- detect_changes
- deploy_server
steps:
- name: Require successful server deploy when API changed
run: |
set -euo pipefail
force="${{ github.event.inputs.force }}"
api_changed="${{ needs.detect_changes.outputs.api }}"
server_result="${{ needs.deploy_server.result }}"
if [[ "$force" == "true" || "$api_changed" == "true" ]]; then
if [[ "$server_result" != "success" ]]; then
echo "API deploy was required (force=$force, api=$api_changed) but deploy_server result was '$server_result'." 1>&2
echo "Blocking client deploy until the GraphQL server is successfully updated." 1>&2
exit 1
fi
echo "Server deploy succeeded; proceeding to live schema validation."
else
if [[ "$server_result" != "success" && "$server_result" != "skipped" ]]; then
echo "Unexpected deploy_server result '$server_result' for a client-only deploy." 1>&2
exit 1
fi
echo "API deploy not required (deploy_server=$server_result); proceeding to live schema validation."
fi
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: packages/client/graphql-validate/package-lock.json
# Isolated package with only graphql — avoids private @seasketch/* deps in
# packages/client/package.json while still using setup-node npm caching.
- name: Validate client operations against live GraphQL schema
working-directory: ./packages/client/graphql-validate
env:
GRAPHQL_ENDPOINT: https://api.seasket.ch/graphql
run: |
set -euo pipefail
npm ci
# validate.js lives in this package so require("graphql") resolves here,
# not via packages/client/node_modules (NODE_PATH is not enough for that).
node ./validate.js
deploy_client:
name: Deploy Client
runs-on: ubuntu-latest
timeout-minutes: 15
if: |
always() &&
needs.api_ready_gate.result == 'success' &&
(github.event.inputs.force == 'true' || needs.detect_changes.outputs.client == 'true')
concurrency: ${{github.event.inputs.environment }}_client
environment:
name: ${{github.event.inputs.environment}}_client
url: https://www.seasketch.org/
needs:
- detect_changes
- api_ready_gate
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "**/package-lock.json"
- name: Install dependencies
run: npx lerna@6.6.2 bootstrap --ci --scope=client --include-dependencies
- name: Build client
working-directory: ./packages/client
env:
SKIP_PREFLIGHT_CHECK: true
REACT_APP_AUTH0_CLIENT_ID: ${{ secrets.REACT_APP_AUTH0_CLIENT_ID }}
REACT_APP_AUTH0_DOMAIN: ${{ secrets.REACT_APP_AUTH0_DOMAIN }}
REACT_APP_AUTH0_SCOPE: "openid profile email permissions"
REACT_APP_AUTH0_AUDIENCE: https://api.seasketch.org
REACT_APP_MAPBOX_ACCESS_TOKEN: ${{ secrets.REACT_APP_MAPBOX_ACCESS_TOKEN }}
REACT_APP_GRAPHQL_ENDPOINT: ${{ secrets.REACT_APP_GRAPHQL_ENDPOINT }}
REACT_APP_BUILD: ${{ needs.detect_changes.outputs.build_label }}
REACT_APP_SENTRY_DSN: ${{ secrets.REACT_APP_SENTRY_DSN }}
REACT_APP_CLOUDFRONT_DOCS_DISTRO: ${{ secrets.REACT_APP_CLOUDFRONT_DOCS_DISTRO }}
REACT_APP_CLOUDFLARE_IMAGES_ENDPOINT: ${{ secrets.REACT_APP_CLOUDFLARE_IMAGES_ENDPOINT }}
REACT_APP_GOOGLE_MAPS_2D_TILE_API_KEY: ${{ secrets.REACT_APP_GOOGLE_MAPS_2D_TILE_API_KEY }}
run: CI=false npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: deploy to cloudflare workers
working-directory: ./packages/client
env:
CLOUDFLARE_ACCOUNT_ID: ${{secrets.CLOUDFLARE_ACCOUNT_ID}}
CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_API_TOKEN}}
run: npx wrangler@4 deploy
- name: Create Sentry release
uses: getsentry/action-release@v3
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: production
release: ${{ needs.detect_changes.outputs.build_label }}
sourcemaps: packages/client/build/static/js/