Repository navigation
967 lines (951 loc) · 47.6 KB
/
Copy pathplatform.yml
File metadata and controls
967 lines (951 loc) · 47.6 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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
name: platform
# Checks, images and deploys for dembrane/platform (the Bun rewrite) and the frontend it
# serves. Checks and image builds run in parallel; images are built once and pushed, and a
# deploy only migrates, rolls out and verifies.
#
# A run checks only the side it changed: 00-plan diffs the commit against its base (a PR's base
# branch, or the previous commit of a push) and skips the server checks when only
# dembrane/frontend changed, and the frontend checks the other way round. Each image is tagged
# with a hash of its inputs, and an image whose inputs are already in the registry is tagged
# for the new commit instead of built again (.github/scripts/build-images.sh). A PR preview rolls out
# only the services whose image or settings changed (.github/scripts/deploy-env.sh).
#
# PR previews and staging are each their own GCP project, reached through its own keyless GitHub
# trust, and push images to their own registry: a preview identity has no role in staging.
#
# PR previews (dembrane-web-previews): adding the "preview" label deploys one (echo-pr-<n>-*,
# database echo_pr_<n>, dashboard-<n>, portal-<n> and api-<n>.preview.dembrane.com), each push
# to a labelled PR redeploys it, and removing the label, closing or merging removes it. Every
# deploy seeds the preview admin, the fictional sample project and the accounts demo. At most three
# exist; a fourth tears down the oldest. One comment on the PR carries the links, and one
# message in #alerts-ci shows the preview's current state, with its history in the thread.
#
# staging (dembrane-web-staging) deploys main: by hand, and on every push to main once the
# repository variable STAGING_DEPLOY_ON_MAIN is "true". Each deploy lists the PRs it carried in
# #alerts-ci, under the dashboard's link, and comments on each PR.
#
# echo-next (the dembrane-web-dummy app on the DigitalOcean dev cluster) runs main through the
# path prod releases take: every push to main, and a run by hand with target echo-next, pushes
# the images to registry.digitalocean.com/dbr-cr as dembrane-web-<app> and sets global.imageTag
# in helm/dembrane-web/values-echo-next.yaml of Dembrane/echo-gitops. Argo CD syncs that app by
# itself, and the job waits for api.echo-next.dembrane.com to serve the commit. No tag and no
# approval. Only that app reads values-echo-next.yaml, so this never moves prod.
#
# prod runs on DigitalOcean Kubernetes through GitOps. A release tag vX.Y.Z on main, after the
# prod environment's approval (by hand with the tag, and on pushing the tag once
# PROD_DEPLOY_ON_TAG is "true"), pushes the images the same way and sets global.imageTag in
# helm/dembrane-web/values-prod.yaml of Dembrane/echo-gitops; Argo CD migrates, rolls out and
# smoke-tests from there. The branch both commit to is GITOPS_PROD_BRANCH (prod-v3 until the
# cutover in echo-gitops' CUTOVER.md, then main). Once PROD_WAIT_FOR_ROLLOUT is "true" the prod
# job waits for api.dembrane.com to serve the release. Then it publishes the GitHub Release,
# tells #team-engineering, #alerts-ci and each PR, and sends
# release.published to sam, which drafts the in-app release notes for review.
#
# #alerts-ci says when an environment is being created or updated, in a message that is edited
# when the deploy ends: created, updated, removed, or not updated and why. Checks on PRs
# without a preview stay in the PR.
#
# Caches: pull request runs only restore them. Pushes to main (and runs by hand) check trusted
# code and save them, so code from a PR never writes a cache that another run reads. Images
# have no layer cache: 40-build-images builds each changed image once and pushes that build. A PR can
# only read caches saved on its base branch, so until the cutover, when main starts running
# this workflow, PR runs start cold. feat/bun-migration has no push run: its PR checks every
# commit, and a second run would only duplicate every check.
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled, closed]
paths:
- "dembrane/platform/**"
- "dembrane/frontend/**"
- "dembrane/infra/**"
- ".github/workflows/platform.yml"
- ".github/scripts/**"
push:
branches: [main]
# Tag pushes ignore the path filter.
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
paths:
- "dembrane/platform/**"
- "dembrane/frontend/**"
- "dembrane/infra/**"
- ".github/workflows/platform.yml"
- ".github/scripts/**"
workflow_dispatch:
inputs:
target:
description: What to deploy (PR previews come only from the "preview" label on a PR)
type: choice
options: [staging, echo-next, prod]
default: staging
tag:
description: "prod: the release tag to deploy (vX.Y.Z, on main)"
type: string
default: ""
gitops_branch:
description: "prod: the Dembrane/echo-gitops branch to bump (empty: GITOPS_PROD_BRANCH, else prod-v3)"
type: string
default: ""
hold_data:
description: "staging: deploy the migrate job without running it and keep the workers at 0"
type: boolean
default: true
permissions:
contents: read
concurrency:
group: platform-${{ github.event.pull_request.number || inputs.target || github.ref }}
# A newer push replaces a running check or preview; a staging, echo-next or prod rollout runs
# to the end.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
defaults:
run:
working-directory: dembrane/platform
env:
REGION: europe-west4
REGISTRY: europe-west4-docker.pkg.dev/dembrane-web-previews/echo-preview
# Pull request runs build the merge commit GitHub prepares (github.sha), the code the checks
# test; a prod run builds its release tag.
REF: ${{ inputs.tag || github.sha }}
jobs:
# Decides what this run does: checks only, a PR preview deploy or teardown, or a staging,
# echo-next or prod deploy. A push to main deploys echo-next beside staging.
plan:
name: 00-plan
runs-on: ubuntu-latest
timeout-minutes: 3
defaults:
run:
working-directory: .
outputs:
sha: ${{ steps.plan.outputs.sha }}
pr: ${{ steps.plan.outputs.pr }}
deploy_pr: ${{ steps.plan.outputs.deploy_pr }}
deploy_env: ${{ steps.plan.outputs.deploy_env }}
deploy_next: ${{ steps.plan.outputs.deploy_next }}
release_tag: ${{ steps.plan.outputs.release_tag }}
teardown: ${{ steps.plan.outputs.teardown }}
server: ${{ steps.changes.outputs.server }}
frontend: ${{ steps.changes.outputs.frontend }}
images: ${{ steps.changes.outputs.images }}
steps:
- name: Plan the run
id: plan
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
ACTION: ${{ github.event.action }}
PR_EVENT: ${{ github.event.pull_request.number }}
TARGET: ${{ inputs.target }}
TAG_INPUT: ${{ inputs.tag }}
HAS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'preview') }}
EVENT_LABEL: ${{ github.event.label.name }}
STAGING_ON_MAIN: ${{ vars.STAGING_DEPLOY_ON_MAIN }}
PROD_ON_TAG: ${{ vars.PROD_DEPLOY_ON_TAG }}
run: |
sha=$GITHUB_SHA pr='' deploy_pr=false deploy_env='' deploy_next=false release_tag='' teardown=false
case "$EVENT" in
workflow_dispatch)
# The GCP trusts enforce the same refs; failing here says why before any build.
[ "$GITHUB_REF" = refs/heads/main ] || { echo "::error::$TARGET deploys run from main, not $GITHUB_REF"; exit 1; }
if [ "$TARGET" = echo-next ]; then deploy_next=true; else deploy_env=$TARGET; fi
if [ "$TARGET" = prod ]; then release_tag=$TAG_INPUT
elif [ -n "$TAG_INPUT" ]; then echo "::error::A tag is for prod; $TARGET deploys main"; exit 1; fi
;;
push)
case "$GITHUB_REF" in
refs/tags/*)
if [ "$PROD_ON_TAG" = true ]; then deploy_env=prod release_tag=$GITHUB_REF_NAME
else echo "::notice::Tag pushes deploy prod once the variable PROD_DEPLOY_ON_TAG is true; checks only"; fi ;;
refs/heads/main)
deploy_next=true
if [ "$STAGING_ON_MAIN" = true ]; then deploy_env=staging
else echo "::notice::Pushes to main deploy staging once the variable STAGING_DEPLOY_ON_MAIN is true; checks only"; fi ;;
esac
;;
pull_request)
pr=$PR_EVENT
if [ "$ACTION" = closed ]; then teardown=true
elif [ "$ACTION" = unlabeled ]; then
if [ "$EVENT_LABEL" = preview ]; then teardown=true; fi
# Adding some other label to a preview PR is not a reason to redeploy it.
elif [ "$ACTION" = labeled ] && [ "$EVENT_LABEL" != preview ]; then :
elif [ "$HAS_LABEL" = true ]; then deploy_pr=true; fi
;;
esac
if [ "$deploy_env" = prod ]; then
[[ $release_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::prod deploys a release tag vX.Y.Z, not '$release_tag'"; exit 1; }
sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$release_tag" --jq .sha) || { echo "::error::No tag $release_tag"; exit 1; }
case "$(gh api "repos/$GITHUB_REPOSITORY/compare/main...$sha" --jq .status)" in
identical | behind) ;;
*) echo "::error::$release_tag ($sha) is not on main"; exit 1 ;;
esac
fi
echo "sha=$sha pr=$pr deploy_pr=$deploy_pr deploy_env=$deploy_env deploy_next=$deploy_next release_tag=$release_tag teardown=$teardown"
{
echo "sha=$sha"; echo "pr=$pr"; echo "deploy_pr=$deploy_pr"; echo "deploy_env=$deploy_env"
echo "deploy_next=$deploy_next"
echo "release_tag=$release_tag"; echo "teardown=$teardown"
} >> "$GITHUB_OUTPUT"
# A pull request's merge commit has the base branch tip as its first parent, so two
# commits of history are enough to diff what the PR changes. Trees only, no file contents.
- name: Check out history for the diff
if: steps.plan.outputs.teardown != 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
ref: ${{ env.REF }}
persist-credentials: false
fetch-depth: 2
filter: blob:none
sparse-checkout: .github/scripts
# Which sides this run checks and which images it builds. Runs by hand, tags and anything
# the diff cannot be computed for check and build everything.
- name: Find what changed
id: changes
if: steps.plan.outputs.teardown != 'true'
env:
EVENT: ${{ github.event_name }}
BEFORE: ${{ github.event.before }}
run: |
all() { echo "server=true"; echo "frontend=true"; echo "images=api worker migrate media web"; }
base=''
case "$EVENT/$GITHUB_REF" in
pull_request/*) base=$(git rev-parse HEAD^1 2>/dev/null) || base='' ;;
push/refs/heads/*)
if [[ $BEFORE =~ ^[0-9a-f]{40}$ && $BEFORE != 0000000000000000000000000000000000000000 ]] &&
git fetch -q --depth=1 --filter=blob:none origin "$BEFORE" 2>/dev/null; then base=$BEFORE; fi ;;
esac
if [ -z "$base" ]; then
echo "Checking and building everything (no base to diff against)"
all >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --no-renames --name-only "$base" HEAD)
if grep -qE '^\.github/(workflows/platform\.yml$|scripts/)' <<<"$files"; then
echo "The workflow or its scripts changed: checking and building everything"
all >> "$GITHUB_OUTPUT"
exit 0
fi
server=false frontend=false
grep -qE '^dembrane/(platform|infra)/' <<<"$files" && server=true
grep -q '^dembrane/frontend/' <<<"$files" && frontend=true
images=$(.github/scripts/build-images.sh changed "$base" HEAD)
echo "server=$server frontend=$frontend images=[$images] (against ${base:0:7})"
{ echo "server=$server"; echo "frontend=$frontend"; echo "images=$images"; } >> "$GITHUB_OUTPUT"
check-server:
name: 10-check-server
needs: plan
if: needs.plan.outputs.teardown != 'true' && needs.plan.outputs.server == 'true'
runs-on: ubuntu-latest
timeout-minutes: 8
services:
postgres:
image: pgvector/pgvector:0.8.1-pg16
env: { POSTGRES_USER: echo, POSTGRES_PASSWORD: echo, POSTGRES_DB: postgres }
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U echo -d postgres" --health-interval 1s --health-retries 60
env:
TEST_DATABASE_ADMIN_URL: postgres://echo:echo@localhost:5432/postgres
SAVE_CACHE: ${{ github.event_name != 'pull_request' }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: dembrane/platform/.bun-version
no-cache: ${{ github.event_name == 'pull_request' }}
- name: Restore Bun cache
id: bun-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('dembrane/platform/bun.lock') }}
restore-keys: bun-
# The incremental type-check state: a run after a small change re-checks only it.
- name: Restore type-check cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: dembrane/platform/.cache
key: tsc-${{ github.sha }}
restore-keys: tsc-
# The pipeline and media tests run real ffmpeg: the same static build the media image
# ships, copied out of its image once and cached.
- name: Restore ffmpeg cache
id: ffmpeg
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/ffmpeg
key: ffmpeg-static-7.1.1
- name: Download ffmpeg
if: steps.ffmpeg.outputs.cache-hit != 'true'
run: |
mkdir -p ~/ffmpeg
id=$(docker create mwader/static-ffmpeg:7.1.1)
docker cp "$id:/ffmpeg" ~/ffmpeg/ffmpeg && docker cp "$id:/ffprobe" ~/ffmpeg/ffprobe
docker rm "$id" >/dev/null
# Saved before any repository code runs, so the cache holds only the upstream image's files.
- name: Save ffmpeg cache
if: env.SAVE_CACHE == 'true' && steps.ffmpeg.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/ffmpeg
key: ffmpeg-static-7.1.1
- name: Add ffmpeg to PATH
run: echo ~/ffmpeg >> "$GITHUB_PATH"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint and format
run: bunx biome ci .
- name: Type check
run: bun run typecheck
# Config is declared, valid in every environment, and read.
- name: Check config
run: bun run config check
# Packages import down the layers, and README.md's package map is current.
- name: Check package layers
run: bun run packages check
- name: Check schema matches migrations
working-directory: dembrane/platform/packages/db
run: bunx drizzle-kit generate < /dev/null | tee /dev/stderr | grep -q "No schema changes"
# The frontend keeps generated copies of the accounts contract and the error catalog. The
# scripts need only Node, so a server-only change is held to them without the frontend job.
- name: Check the frontend's API types are in sync
working-directory: dembrane/frontend
run: |
node scripts/sync-accounts-contract.mjs --check
node scripts/sync-error-codes.mjs --check
# Files in parallel, each database test on its own database.
- name: Run tests
run: bun test --parallel
- name: Save Bun cache
if: env.SAVE_CACHE == 'true' && steps.bun-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('dembrane/platform/bun.lock') }}
- name: Save type-check cache
if: env.SAVE_CACHE == 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: dembrane/platform/.cache
key: tsc-${{ github.sha }}
check-frontend:
name: 20-check-frontend
needs: plan
if: needs.plan.outputs.teardown != 'true' && needs.plan.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: dembrane/frontend
env:
SAVE_CACHE: ${{ github.event_name != 'pull_request' }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
- name: Set up pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with: { version: 10 }
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: .tool-versions
- name: Find pnpm store
id: pnpm
run: echo "store=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm cache
id: pnpm-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.pnpm.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
# A Mac's disk ignores case, Linux's doesn't: fails when a path or an import
# would mean a different file (or none) on one of them.
- name: Check path case
run: node scripts/check-path-case.mjs
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm exec tsc
# Fails when extract or compile changes a catalog, or when a locale lacks a message
# English has. Fill gaps locally with `pnpm translations:fill` (Gemini EU; entries land
# in each catalog's machine-translations.json for review) and commit the result.
- name: Check translations
run: |
pnpm messages:extract
pnpm messages:compile
node scripts/check-translations.mjs --strict
# The frontend builds without the platform folder, so it keeps a generated copy of the API types.
- name: Check API types are in sync
run: |
node scripts/sync-accounts-contract.mjs --check
node scripts/sync-error-codes.mjs --check
- name: Save pnpm cache
if: env.SAVE_CACHE == 'true' && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.pnpm.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml') }}
# The test suite runs beside the checks: together they took most of the frontend's time.
test-frontend:
name: 21-test-frontend
needs: plan
if: needs.plan.outputs.teardown != 'true' && needs.plan.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: dembrane/frontend
env:
SAVE_CACHE: ${{ github.event_name != 'pull_request' }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
- name: Set up pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with: { version: 10 }
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: .tool-versions
- name: Find pnpm store
id: pnpm
run: echo "store=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm cache
id: pnpm-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.pnpm.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Vitest's prebundled dependencies and its timing file, which orders slow files first.
- name: Restore Vitest cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: dembrane/frontend/node_modules/.vite/vitest
key: vitest-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml', 'dembrane/frontend/vite.config.ts') }}-${{ github.sha }}
restore-keys: |
vitest-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml', 'dembrane/frontend/vite.config.ts') }}-
vitest-${{ runner.os }}-
# Every vitest file under src; the Playwright specs in e2e/ are excluded in vite.config.ts.
- name: Run tests
run: pnpm exec vitest run
- name: Save pnpm cache
if: env.SAVE_CACHE == 'true' && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.pnpm.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml') }}
- name: Save Vitest cache
if: env.SAVE_CACHE == 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: dembrane/frontend/node_modules/.vite/vitest
key: vitest-${{ runner.os }}-${{ hashFiles('dembrane/frontend/pnpm-lock.yaml', 'dembrane/frontend/vite.config.ts') }}-${{ github.sha }}
# Builds the images whose inputs changed and proves they start. A run that deploys a PR
# preview or staging also pushes them, to that environment's registry, and tags every image
# it did not need to build for this commit, so the deploy job only rolls out. prod pushes in
# its own job, after the prod environment's approval.
images:
name: 40-build-images
needs: plan
if: >-
needs.plan.outputs.teardown != 'true'
&& (needs.plan.outputs.images != '' || needs.plan.outputs.deploy_pr == 'true' || needs.plan.outputs.deploy_env == 'staging')
runs-on: ubuntu-latest
timeout-minutes: 12
# The preview trust accepts a PR's jobs only in this environment. It has no reviewers; it
# marks the jobs that may push and deploy a preview. An empty name is no environment.
environment: ${{ needs.plan.outputs.deploy_pr == 'true' && 'pr-preview' || '' }}
permissions:
contents: read
id-token: write
env:
TAG: ${{ needs.plan.outputs.sha }}
PUSH: ${{ needs.plan.outputs.deploy_pr == 'true' && 'preview' || needs.plan.outputs.deploy_env == 'staging' && 'staging' || '' }}
REGISTRY: ${{ needs.plan.outputs.deploy_env == 'staging' && 'europe-west4-docker.pkg.dev/dembrane-web-staging/echo-staging' || 'europe-west4-docker.pkg.dev/dembrane-web-previews/echo-preview' }}
IMAGES: ${{ needs.plan.outputs.images }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
# The runner is thrown away after the job, and removing the builder took half a minute.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with: { cleanup: false }
- name: Authenticate to Google Cloud (preview)
if: env.PUSH == 'preview'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13
with:
workload_identity_provider: projects/218237812097/locations/global/workloadIdentityPools/echo-preview-github/providers/github
service_account: echo-preview-deployer@dembrane-web-previews.iam.gserviceaccount.com
- name: Authenticate to Google Cloud (staging)
if: env.PUSH == 'staging'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13
with:
workload_identity_provider: projects/1089877593337/locations/global/workloadIdentityPools/echo-staging-github/providers/github
service_account: echo-staging-deployer@dembrane-web-staging.iam.gserviceaccount.com
- name: Set up gcloud
if: env.PUSH != ''
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2.2.1
with: { skip_install: true }
- name: Configure Docker for Artifact Registry
if: env.PUSH != ''
run: gcloud auth configure-docker europe-west4-docker.pkg.dev --quiet
- name: Build images
id: build
run: |
built=$RUNNER_TEMP/built
# shellcheck disable=SC2086
if [ -n "$PUSH" ]; then BUILT_FILE=$built $GITHUB_WORKSPACE/.github/scripts/build-images.sh --push
else BUILT_FILE=$built $GITHUB_WORKSPACE/.github/scripts/build-images.sh --load $IMAGES; fi
echo "built=$(tr '\n' ' ' < "$built")" >> "$GITHUB_OUTPUT"
- name: Smoke test containers
if: steps.build.outputs.built != ''
env:
BUILT: ${{ steps.build.outputs.built }}
run: |
# Each binary checks at boot that its image carries the files it reads and exits
# non-zero naming any that are missing. The worker and migrate job need a database
# to go further, so they stop after the check. Only images built in this run are
# here; a reused one passed this test when it was built.
built() { grep -qw "$1" <<<"$BUILT"; }
if built worker; then docker run --rm "$REGISTRY/worker:$TAG" --check-assets; fi
if built migrate; then docker run --rm "$REGISTRY/migrate:$TAG" --check-assets; fi
if built api; then
docker run -d --name api -p 8080:8080 -e APP_ENV=test -e DATABASE_URL=postgres://u:p@127.0.0.1:1/d \
-e AUTH_SECRET=smoke-test-secret-that-is-long-enough-000 -e INVITE_HASH_SECRET=smoke-test-invite-secret-000 \
"$REGISTRY/api:$TAG"
ok=
for _ in $(seq 1 25); do curl -sf localhost:8080/health && ok=1 && break; sleep 0.2; done
[ -n "$ok" ] || { docker logs api; exit 1; }
# Asset-backed routes that need no database: popcorn's logo and a drawing read from
# /app/assets, the same tree the deck page template comes from.
for f in logo.png illustrations/scan.webp; do
curl -sf "localhost:8080/api/v2/popcorn/public/smoke/$f" -o /dev/null || { docker logs api; exit 1; }
done
fi
if built media; then
# The media image must ship the ffmpeg it runs, not only answer HTTP.
docker run -d --name media -p 8081:8080 -e APP_ENV=test "$REGISTRY/media:$TAG"
for _ in $(seq 1 25); do curl -sf localhost:8081/health && break; sleep 0.2; done
curl -sf -X POST localhost:8081/probe-url -H 'content-type: application/json' -d '{"url":"http://127.0.0.1:9/x.mp3"}' -o /dev/null -w '%{http_code}' | grep -q 422 \
|| { docker logs media; exit 1; }
fi
# The participant portal loads on phones at events, so its first download must not grow.
# Measured on the bundle the web image just built (its manifest sits beside the served
# files), so the frontend is bundled once per commit.
- name: Check portal bundle size
if: contains(steps.build.outputs.built, 'web')
run: |
dist=$RUNNER_TEMP/web-dist
id=$(docker create "$REGISTRY/web:$TAG")
docker cp "$id:/app/dist" "$dist"
docker cp "$id:/app/build-meta/.vite" "$dist/.vite"
docker rm "$id" >/dev/null
node ../frontend/scripts/check-portal-bundle.mjs --dist "$dist"
# A PR preview: its own services and database, from the images 40-build-images pushed.
# Serialized across PRs so two new previews cannot both take the last slot. It runs when a
# check failed too, only to say so in the PR comment. A check the plan skipped counts as
# passed: its side did not change.
deploy-pr:
name: 50-deploy-pr-preview
if: ${{ !cancelled() && needs.plan.outputs.deploy_pr == 'true' }}
needs: [plan, check-server, check-frontend, test-frontend, images]
runs-on: ubuntu-latest
timeout-minutes: 20
environment: pr-preview
concurrency:
group: preview-slots
cancel-in-progress: false
permissions:
contents: read
id-token: write
pull-requests: write
env:
TAG: ${{ needs.plan.outputs.sha }}
PR: ${{ needs.plan.outputs.pr }}
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
- name: Stop when a check failed
id: gate
env:
RESULTS: |
10-check-server=${{ needs.check-server.result }}
20-check-frontend=${{ needs.check-frontend.result }}
21-test-frontend=${{ needs.test-frontend.result }}
40-build-images=${{ needs.images.result }}
run: |
failed=$(awk -F= '$2 != "success" && $2 != "skipped" { printf "%s%s", (n++ ? ", " : ""), $1 }' <<<"$RESULTS")
[ -z "$failed" ] && exit 0
echo "failed=$failed" >> "$GITHUB_OUTPUT"
echo "::error::Not deploying: $failed did not pass"
exit 1
# Only a first deploy is announced as it begins. The preview comment remembers the
# message, so the step that ends this run, or a later run, edits it.
- name: Tell #alerts-ci a preview is being created
continue-on-error: true
env:
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REF: ${{ github.base_ref }}
run: $GITHUB_WORKSPACE/.github/scripts/ci-notify.sh preview starting "$PR"
- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13
with:
workload_identity_provider: projects/218237812097/locations/global/workloadIdentityPools/echo-preview-github/providers/github
service_account: echo-preview-deployer@dembrane-web-previews.iam.gserviceaccount.com
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2.2.1
with: { skip_install: true }
- name: Make room (at most 3 previews)
id: room
env:
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
run: |
set -o pipefail
removed=$($GITHUB_WORKSPACE/.github/scripts/deploy-env.sh make-room "$PR" | tee /dev/stderr | awk '/^removed PR preview / { print $4 }')
for n in $removed; do
$GITHUB_WORKSPACE/.github/scripts/ci-notify.sh preview removed "$n" "it made room for #$PR" || true
done
- name: Deploy and verify
id: deploy
# pipefail: a deploy that fails must fail the step, not be hidden by tail.
run: |
set -o pipefail
out=$($GITHUB_WORKSPACE/.github/scripts/deploy-env.sh deploy "pr-$PR" "$TAG" | tail -n 1)
echo "$out"
echo "Preview for #$PR: ${out// / }" >> "$GITHUB_STEP_SUMMARY"
# The PR comment and #alerts-ci: a failure here is reported here, where the comment that
# remembers the preview's Slack thread can be edited.
- name: Comment on PR and tell #alerts-ci
if: ${{ !cancelled() }}
env:
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REF: ${{ github.base_ref }}
CHECKS_FAILED: ${{ steps.gate.outputs.failed }}
OUTCOMES: |
gate=${{ steps.gate.outcome }}
Authenticate to Google Cloud=${{ steps.auth.outcome }}
Make room=${{ steps.room.outcome }}
Deploy and verify=${{ steps.deploy.outcome }}
run: |
failed=$(awk -F= '$2 == "failure" { print $1; exit }' <<<"$OUTCOMES")
[ "$failed" = gate ] && failed="$CHECKS_FAILED (nothing was deployed)"
if [ -n "$failed" ]; then $GITHUB_WORKSPACE/.github/scripts/ci-notify.sh preview failed "$PR" "$failed"
else $GITHUB_WORKSPACE/.github/scripts/ci-notify.sh preview deployed "$PR"; fi
teardown-pr:
name: 51-teardown-pr-preview
if: needs.plan.outputs.teardown == 'true'
needs: plan
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pr-preview
concurrency:
group: preview-slots
cancel-in-progress: false
permissions:
contents: read
id-token: write
pull-requests: write
env:
PR: ${{ needs.plan.outputs.pr }}
steps:
- name: Check out scripts
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
sparse-checkout: |
.github/scripts
dembrane/infra
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13
with:
workload_identity_provider: projects/218237812097/locations/global/workloadIdentityPools/echo-preview-github/providers/github
service_account: echo-preview-deployer@dembrane-web-previews.iam.gserviceaccount.com
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2.2.1
with: { skip_install: true }
# Only the scripts and infra are checked out here, so there is no dembrane/platform.
- name: Tear down preview
working-directory: .
run: $GITHUB_WORKSPACE/.github/scripts/deploy-env.sh teardown "$PR"
- name: Update PR comment and tell #alerts-ci
working-directory: .
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
WHY: ${{ github.event.action == 'closed' && (github.event.pull_request.merged && 'the PR was merged' || 'the PR was closed') || 'the preview label was removed' }}
run: $GITHUB_WORKSPACE/.github/scripts/ci-notify.sh preview removed "$PR" "$WHY"
# staging rolls out what 40-build-images pushed, from its own project's registry with its own
# identity; prod builds and pushes here, after the approval. A check the plan skipped counts as
# passed, as on a PR preview.
deploy-staging:
name: 60-deploy-staging
if: ${{ !cancelled() && !failure() && needs.plan.outputs.deploy_env == 'staging' }}
needs: [plan, check-server, check-frontend, test-frontend, images]
runs-on: ubuntu-latest
timeout-minutes: 20
# Records each deploy as a GitHub Deployment: the last successful one is where the next
# deploy's list of PRs starts.
environment: staging
concurrency:
group: deploy-staging
cancel-in-progress: false
permissions:
contents: read
deployments: read
id-token: write
pull-requests: write
env:
TAG: ${{ needs.plan.outputs.sha }}
REGISTRY: europe-west4-docker.pkg.dev/dembrane-web-staging/echo-staging
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
# One message in #alerts-ci per deploy: posted here, edited by the step that ends the job.
- name: Tell #alerts-ci the deploy is starting
id: start
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh announce-start staging "$TAG"
- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13
with:
workload_identity_provider: projects/1089877593337/locations/global/workloadIdentityPools/echo-staging-github/providers/github
service_account: echo-staging-deployer@dembrane-web-staging.iam.gserviceaccount.com
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2.2.1
with: { skip_install: true }
- name: Deploy and verify
id: deploy
env:
HOLD_DATA: ${{ inputs.hold_data && '1' || '0' }}
run: $GITHUB_WORKSPACE/.github/scripts/deploy-env.sh deploy staging "$TAG"
# The deploy stands even when a message fails to send.
- name: Announce deploy
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
SLACK_TS: ${{ steps.start.outputs.slack_ts }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh announce-staging "$TAG"
- name: Tell #alerts-ci the deploy stopped
if: ${{ failure() || cancelled() }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
SLACK_TS: ${{ steps.start.outputs.slack_ts }}
STOPPED: ${{ job.status == 'cancelled' && 'cancelled' || steps.auth.outcome == 'failure' && 'Authenticate to Google Cloud' || steps.deploy.outcome == 'failure' && 'Deploy and verify' || '' }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh announce-failed staging "$TAG" "$STOPPED"
# echo-next runs main through prod's release path, with no tag and no approval. The job holds
# the registry login and the echo-gitops token and nothing else: it has no cluster credentials,
# and gitops-bump.sh echo-next writes values-echo-next.yaml, a file the prod app does not read.
# Argo CD on the dev cluster syncs dembrane-web-dummy by itself.
deploy-echo-next:
name: 65-deploy-echo-next
if: ${{ !cancelled() && !failure() && needs.plan.outputs.deploy_next == 'true' }}
needs: [plan, check-server, check-frontend, test-frontend, images]
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: deploy-echo-next
cancel-in-progress: false
permissions:
contents: read
env:
TAG: ${{ needs.plan.outputs.sha }}
REGISTRY: registry.digitalocean.com/dbr-cr
IMAGE_PREFIX: dembrane-web-
GITOPS_BRANCH: ${{ vars.GITOPS_PROD_BRANCH || 'prod-v3' }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
- name: Log in to the DigitalOcean registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: registry.digitalocean.com
username: ${{ secrets.DO_REGISTRY_USERNAME }}
password: ${{ secrets.DO_REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with: { cleanup: false }
# Tags an image already in the registry with the same inputs; builds the rest. A later
# release of this commit to prod finds all five there.
- name: Push images
run: $GITHUB_WORKSPACE/.github/scripts/build-images.sh --push
- name: Check out Dembrane/echo-gitops
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
repository: Dembrane/echo-gitops
ref: ${{ env.GITOPS_BRANCH }}
token: ${{ secrets.GITOPS_REPO_TOKEN }}
path: gitops
- name: Update the image tag in echo-gitops
working-directory: .
run: .github/scripts/gitops-bump.sh gitops "$GITOPS_BRANCH" echo-next "$TAG"
# Argo CD notices the commit within a few minutes, migrates with the contract held, rolls
# out and smoke-tests.
- name: Wait for the rollout
working-directory: .
run: |
api=https://api.echo-next.dembrane.com
end=$((SECONDS + 900))
until [ "$(curl -sf "$api/health" | jq -r .release 2>/dev/null)" = "$TAG" ]; do
[ "$SECONDS" -lt "$end" ] || { echo "::error::$api did not serve $TAG within 15 minutes; check the dembrane-web-dummy sync in Argo CD on the dev cluster"; exit 1; }
sleep 10
done
echo "$api serves $TAG" | tee -a "$GITHUB_STEP_SUMMARY"
# Pushes the release's images to the DigitalOcean registry and points Dembrane/echo-gitops at
# them. Argo CD does the rollout (migrate hook, rollout, smoke hook), so this job never touches
# the cluster.
deploy-prod:
name: 70-deploy-prod
if: ${{ !cancelled() && !failure() && needs.plan.outputs.deploy_env == 'prod' }}
needs: [plan, check-server, check-frontend, test-frontend, images]
runs-on: ubuntu-latest
timeout-minutes: 30
# The prod environment's required reviewers approve each deploy before the job runs.
environment: prod
concurrency:
group: deploy-prod
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
env:
TAG: ${{ needs.plan.outputs.sha }}
RELEASE_TAG: ${{ needs.plan.outputs.release_tag }}
REGISTRY: registry.digitalocean.com/dbr-cr
IMAGE_PREFIX: dembrane-web-
GITOPS_BRANCH: ${{ inputs.gitops_branch || vars.GITOPS_PROD_BRANCH || 'prod-v3' }}
steps:
- name: Check out code
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with: { ref: "${{ env.REF }}", persist-credentials: false }
# One message in #alerts-ci per deploy: posted here, once the deploy is approved, and
# edited by the step that ends the job.
- name: Tell #alerts-ci the deploy is starting
id: start
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh announce-start prod "$RELEASE_TAG"
- name: Log in to the DigitalOcean registry
id: login
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: registry.digitalocean.com
username: ${{ secrets.DO_REGISTRY_USERNAME }}
password: ${{ secrets.DO_REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with: { cleanup: false }
# Tags an image already in the registry with the same inputs; builds the rest.
- name: Push images
id: images
run: $GITHUB_WORKSPACE/.github/scripts/build-images.sh --push
- name: Check out Dembrane/echo-gitops
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
repository: Dembrane/echo-gitops
ref: ${{ env.GITOPS_BRANCH }}
token: ${{ secrets.GITOPS_REPO_TOKEN }}
path: gitops
- name: Update the image tag in echo-gitops
id: bump
working-directory: .
run: .github/scripts/gitops-bump.sh gitops "$GITOPS_BRANCH" prod "$TAG" "$RELEASE_TAG"
# Until the cutover Argo CD syncs dembrane-web-prod by hand and api.dembrane.com is the old
# stack, so there is nothing to wait for. After it, the release is announced once it serves.
- name: Wait for the rollout
id: wait
if: vars.PROD_WAIT_FOR_ROLLOUT == 'true'
working-directory: .
run: |
api=https://api.dembrane.com
end=$((SECONDS + 900))
until [ "$(curl -sf "$api/health" | jq -r .release 2>/dev/null)" = "$TAG" ]; do
[ "$SECONDS" -lt "$end" ] || { echo "::error::$api did not serve $TAG within 15 minutes; check the dembrane-web-prod sync in Argo CD"; exit 1; }
sleep 10
done
until curl -sf "$api/ready/worker?release=$TAG" >/dev/null; do
[ "$SECONDS" -lt "$end" ] || { echo "::error::no worker of $TAG heartbeat within 15 minutes"; exit 1; }
sleep 5
done
echo "$api serves $TAG and a worker of $TAG is running"
# The GitHub Release, #team-engineering, #alerts-ci, a comment on each PR, and release.published to
# sam (skipped with a notice while its secrets are empty). The deploy stands even when
# one of these fails.
- name: Publish release
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
SAM_RELEASE_WEBHOOK_URL: ${{ secrets.SAM_RELEASE_WEBHOOK_URL }}
SAM_RELEASE_WEBHOOK_SECRET: ${{ secrets.SAM_RELEASE_WEBHOOK_SECRET }}
SLACK_TS: ${{ steps.start.outputs.slack_ts }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh publish-release "$RELEASE_TAG"
- name: Tell #alerts-ci the deploy stopped
if: ${{ failure() || cancelled() }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
SLACK_TS: ${{ steps.start.outputs.slack_ts }}
STOPPED: ${{ job.status == 'cancelled' && 'cancelled' || steps.login.outcome == 'failure' && 'Log in to the DigitalOcean registry' || steps.images.outcome == 'failure' && 'Push images' || steps.bump.outcome == 'failure' && 'Update the image tag in echo-gitops' || steps.wait.outcome == 'failure' && 'Wait for the rollout' || '' }}
run: $GITHUB_WORKSPACE/.github/scripts/release.sh announce-failed prod "$RELEASE_TAG" "$STOPPED"
# A failed run tells #alerts-ci which environment was not updated and what stopped it, except
# checks on a PR without a preview (they stay in the PR) and a staging, prod or preview deploy
# job that failed (it has said so itself, in the message it posted when it began). echo-next
# posts nothing when it is updated, so its failed deploy is told here.
notify-failure:
name: 90-notify-failure
if: >-
failure()
&& ((github.event_name != 'pull_request' && needs.deploy-staging.result != 'failure' && needs.deploy-prod.result != 'failure')
|| needs.deploy-echo-next.result == 'failure'
|| (needs.plan.outputs.deploy_pr == 'true' && needs.deploy-pr.result != 'failure')
|| needs.plan.outputs.teardown == 'true')
needs: [plan, check-server, check-frontend, test-frontend, images, deploy-pr, teardown-pr, deploy-staging, deploy-echo-next, deploy-prod]
runs-on: ubuntu-latest
timeout-minutes: 2
permissions:
contents: read
actions: read
steps:
- name: Check out scripts
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Post to #alerts-ci
working-directory: .
env:
GH_TOKEN: ${{ github.token }}
SLACK_TOKEN: ${{ secrets.SLACK_ALERTS_BOT_TOKEN }}
DEPLOY_ENV: ${{ needs.deploy-echo-next.result == 'failure' && 'echo-next' || needs.plan.outputs.deploy_env || (needs.plan.outputs.deploy_next == 'true' && 'echo-next') || '' }}
PR: ${{ needs.plan.outputs.pr }}
TEARDOWN: ${{ needs.plan.outputs.teardown }}
NEEDS: ${{ toJSON(needs) }}
run: $GITHUB_WORKSPACE/.github/scripts/ci-notify.sh failure