-
Notifications
You must be signed in to change notification settings - Fork 0
253 lines (226 loc) · 11.3 KB
/
Copy pathdeploy-Dataspace.yml
File metadata and controls
253 lines (226 loc) · 11.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
name: Deploy DataSpace
on:
push:
branches:
- dev
- main
# Prevent two deploys to the same environment from racing each other and
# corrupting the releases/ directory or the .last_good / .rollback_target markers.
concurrency:
group: deploy-dataspace-${{ github.ref_name }}
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'production' || 'development' }}
outputs:
release: ${{ steps.meta.outputs.release }}
env:
KEYCLOAK_CLIENT_ID: ${{ secrets.KEYCLOAK_CLIENT_ID }}
KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}
AUTH_ISSUER: ${{ secrets.AUTH_ISSUER }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
NEXT_PUBLIC_NEXTAUTH_URL: ${{ vars.NEXT_PUBLIC_NEXTAUTH_URL }}
END_SESSION_URL: ${{ secrets.END_SESSION_URL }}
REFRESH_TOKEN_URL: ${{ secrets.REFRESH_TOKEN_URL }}
NEXT_PUBLIC_BACKEND_URL: ${{ vars.NEXT_PUBLIC_BACKEND_URL }}
BACKEND_URL: ${{ vars.BACKEND_URL }}
NEXT_PUBLIC_BACKEND_GRAPHQL_URL: ${{ vars.NEXT_PUBLIC_BACKEND_GRAPHQL_URL }}
BACKEND_GRAPHQL_URL: ${{ vars.BACKEND_GRAPHQL_URL }}
NEXT_PUBLIC_ENABLE_ACCESSMODEL: ${{ vars.NEXT_PUBLIC_ENABLE_ACCESSMODEL }}
NEXT_PUBLIC_ANALYTICS_URL: ${{ vars.NEXT_PUBLIC_ANALYTICS_URL }}
NEXT_PUBLIC_PLATFORM_URL: ${{ vars.NEXT_PUBLIC_PLATFORM_URL }}
NEXT_PUBLIC_PLATFORM_PROTOCOL: ${{ vars.NEXT_PUBLIC_PLATFORM_PROTOCOL }}
NEXT_PUBLIC_PLATFORM_DOMAIN: ${{ vars.NEXT_PUBLIC_PLATFORM_DOMAIN }}
NEXT_PUBLIC_GA_ID: ${{ secrets.NEXT_PUBLIC_GA_ID }}
FEATURE_SITEMAPS: ${{ vars.FEATURE_SITEMAPS }}
FEATURE_SITEMAP_BACKEND_BASE_URL: ${{ vars.FEATURE_SITEMAP_BACKEND_BASE_URL }}
FEATURE_SITEMAP_ITEMS_PER_PAGE: ${{ vars.FEATURE_SITEMAP_ITEMS_PER_PAGE }}
FEATURE_SITEMAP_CACHE_DURATION: ${{ vars.FEATURE_SITEMAP_CACHE_DURATION }}
FEATURE_SITEMAP_CHILD_CACHE_DURATION: ${{ vars.FEATURE_SITEMAP_CHILD_CACHE_DURATION }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Compute release name
id: meta
run: echo "release=$(date -u +%Y%m%d%H%M%S)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
# npm ci, not npm install: installs exactly what package-lock.json pins.
# npm install can silently resolve a different version than the lockfile
# (this is how a canary/newer `next` ended up in a previous build).
- name: Install dependencies (from lockfile only)
run: npm ci --legacy-peer-deps
- name: Guard - installed Next.js must match package.json
run: |
DECLARED=$(node -p "require('./package.json').dependencies.next")
INSTALLED=$(node -p "require('./node_modules/next/package.json').version")
echo "declared next: $DECLARED / installed next: $INSTALLED"
if [ "$DECLARED" != "$INSTALLED" ]; then
echo "::error::Installed next ($INSTALLED) does not match package.json ($DECLARED). Aborting before build."
exit 1
fi
- name: Generate
run: npm run generate:ci
env:
BACKEND_GRAPHQL_URL: ${{ vars.BACKEND_GRAPHQL_URL }}
NODE_ENV: production
- name: Lint
run: npm run lint
- name: Build
run: npm run build
# next.config.mjs runs again on every `next start` boot (not just at
# build time) and does `jiti('./env')` synchronously - env.ts has to
# exist on disk in the release dir or the server crash-loops with
# "Cannot find module './env'" (found via a live failed deploy).
- name: Package release artifact
run: tar czf release.tar.gz .next public package.json package-lock.json next.config.mjs env.ts
- name: Send release artifact to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: release.tar.gz
target: DataExchange/incoming/${{ steps.meta.outputs.release }}
# Extract into a brand-new releases/<release>/ directory, install deps
# THERE (never touching the currently-live release), verify, and only
# then flip the DataExFrontend symlink. If anything up to and including
# the health check fails, the previous release is still on disk untouched
# and this step rolls the symlink back itself before exiting non-zero.
- name: Extract, install, verify, and activate release on EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
set -euo pipefail
BASE=/home/ubuntu/DataExchange
RELEASE=${{ steps.meta.outputs.release }}
RELEASE_DIR="$BASE/releases/$RELEASE"
NODE_BIN=/home/ubuntu/.nvm/versions/node/v24.13.0/bin
# npm/pm2 are scripts with a `#!/usr/bin/env node` shebang, so
# invoking them by absolute path is not enough - env still
# re-resolves `node` via PATH, which appleboy/ssh-action's
# non-interactive shell leaves pointed at the ancient system
# node (v10) since it never sources nvm. Put NODE_BIN on PATH
# first so that shebang resolution finds the right node too.
export PATH="$NODE_BIN:$PATH"
PM2="$NODE_BIN/pm2"
NPM="$NODE_BIN/npm"
NODE="$NODE_BIN/node"
mkdir -p "$RELEASE_DIR" "$BASE/releases" "$BASE/shared"
tar xzf "$BASE/incoming/$RELEASE/release.tar.gz" -C "$RELEASE_DIR"
rm -rf "$BASE/incoming/$RELEASE"
# Runtime secrets live only in shared/.env.local (NOT the docker-compose .env one level up), never shipped by CI.
ln -sfn "$BASE/shared/.env.local" "$RELEASE_DIR/.env.local"
cd "$RELEASE_DIR"
"$NPM" ci --omit=dev
INSTALLED=$("$NODE" -p "require('./node_modules/next/package.json').version")
DECLARED=$("$NODE" -p "require('./package.json').dependencies.next")
if [ "$INSTALLED" != "$DECLARED" ]; then
echo "Next version mismatch on server ($INSTALLED vs $DECLARED). Not activating." >&2
exit 1
fi
if [ -f "$BASE/releases/.last_good" ]; then
PREVIOUS=$(cat "$BASE/releases/.last_good")
else
PREVIOUS=$(basename "$(readlink -f "$BASE/DataExFrontend")")
fi
echo "$PREVIOUS" > "$BASE/releases/.rollback_target"
ln -sfn "$RELEASE_DIR" "$BASE/DataExFrontend"
"$PM2" restart dataspace
ATTEMPTS=0
until curl -f -s -o /dev/null http://127.0.0.1:3000; do
ATTEMPTS=$((ATTEMPTS+1))
if [ "$ATTEMPTS" -ge 10 ]; then
echo "Health check failed after $ATTEMPTS attempts. Rolling back to $PREVIOUS." >&2
ln -sfn "$BASE/releases/$PREVIOUS" "$BASE/DataExFrontend"
"$PM2" restart dataspace
exit 1
fi
sleep 3
done
echo "Release $RELEASE is live and passed the boot health check."
# Keep the 5 most recent releases plus whatever the rollback target is.
# `grep -v` exits 1 when there's nothing to prune (fewer than 6
# releases so far) - under `set -o pipefail` that would otherwise
# kill the script right after the success message above and report
# this whole deploy as failed despite it actually having succeeded
# (found via a live deploy: app was healthy, CI still said failure).
cd "$BASE/releases"
ls -1dt */ 2>/dev/null | tail -n +6 | grep -v "^${PREVIOUS}/$" | xargs -r rm -rf || true
# Production has no smoke-test job today, so its only quality gate is the
# boot health check above. Mark this release good immediately so a FUTURE
# deploy knows what to roll back to if it fails.
- name: Mark release as last-known-good (prod only)
if: github.ref_name == 'main'
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: echo "${{ steps.meta.outputs.release }}" > /home/ubuntu/DataExchange/releases/.last_good
smoke-tests:
needs: build-and-deploy
if: github.ref_name == 'dev'
uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI
secrets:
HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }}
TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }}
TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }}
TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }}
TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }}
# dev only: smoke tests are the real quality gate here. Only once they pass
# does this release become the thing a future rollback would target.
promote-dev:
needs: [build-and-deploy, smoke-tests]
if: github.ref_name == 'dev' && needs.smoke-tests.result == 'success'
runs-on: ubuntu-latest
# vars.EC2_HOST is an environment-scoped variable (Settings -> Environments
# -> development), only visible to jobs that declare `environment:`.
# Without this, appleboy/ssh-action fails with "missing server host"
# (found via a live run - build-and-deploy/smoke-tests both succeeded but
# this job still failed, which is why `if:` here is already gated to dev).
environment: development
steps:
- name: Mark this release as last-known-good
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: echo "${{ needs.build-and-deploy.outputs.release }}" > /home/ubuntu/DataExchange/releases/.last_good
rollback-dev:
needs: [build-and-deploy, smoke-tests]
if: github.ref_name == 'dev' && needs.smoke-tests.result == 'failure'
runs-on: ubuntu-latest
# Same environment-scoped vars.EC2_HOST issue as promote-dev - see the
# comment there.
environment: development
steps:
- name: Revert to last known-good release
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
set -euo pipefail
BASE=/home/ubuntu/DataExchange
NODE_BIN=/home/ubuntu/.nvm/versions/node/v24.13.0/bin
# See the comment in build-and-deploy's activation step: pm2's
# shebang re-resolves node via PATH, so NODE_BIN must be on it.
export PATH="$NODE_BIN:$PATH"
PM2="$NODE_BIN/pm2"
TARGET=$(cat "$BASE/releases/.rollback_target")
echo "Smoke tests failed. Reverting DataExFrontend -> releases/$TARGET"
ln -sfn "$BASE/releases/$TARGET" "$BASE/DataExFrontend"
"$PM2" restart dataspace
sleep 3
curl -f http://127.0.0.1:3000 -o /dev/null -s -w "post-rollback HTTP %{http_code}\n"