-
Notifications
You must be signed in to change notification settings - Fork 0
225 lines (186 loc) · 8.05 KB
/
deploy.yml
File metadata and controls
225 lines (186 loc) · 8.05 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
name: Deploy to polymer@archlinux
on:
workflow_run:
workflows: [ "CI" ]
types: [ completed ]
branches: [ "main" ]
concurrency:
group: polymer-production-deploy
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }}
runs-on: self-hosted
timeout-minutes: 30
env:
DEPLOY_ROOT: /var/www/polymer
SHARED_DIR: /var/www/polymer/shared
MEDIA_DIR: /var/www/polymer-media
RELEASES_DIR: /var/www/polymer/releases
RELEASE_ID: ${{ github.event.workflow_run.head_sha }}-${{ github.run_attempt }}
RELEASE_DIR: /var/www/polymer/releases/${{ github.event.workflow_run.head_sha }}-${{ github.run_attempt }}
BASE_URL: ${{ vars.BASE_URL != '' && vars.BASE_URL || 'http://127.0.0.1:3000' }}
HEALTHCHECK_URL: ${{ vars.HEALTHCHECK_URL != '' && vars.HEALTHCHECK_URL || 'http://127.0.0.1:3000/api/health' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Prepare Release Directory
run: |
set -euo pipefail
mkdir -p "$RELEASES_DIR" "$SHARED_DIR" "$MEDIA_DIR"
mkdir -p "$RELEASE_DIR"
# Build in a fresh release directory so the live app is not mutated in place.
rsync -av --delete \
--exclude '.git' \
--exclude 'node_modules' \
--exclude '.next' \
--exclude 'media' \
./ "$RELEASE_DIR/"
- name: Create Shared Runtime Files
run: |
set -euo pipefail
printf '%s\n' \
"DATABASE_URL=${{ secrets.DATABASE_URL }}" \
"PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET }}" \
"LEGACY_DATABASE_URI=${{ secrets.LEGACY_DATABASE_URI }}" \
"NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" \
"NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" \
"NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" \
> "$SHARED_DIR/.env"
# Runtime may not use the same UNIX user as the deploy runner.
# Keep the file readable by the app process user to avoid startup
# failures like "missing secret key" caused by EACCES on .env.
chmod 644 "$SHARED_DIR/.env"
- name: Install Dependencies
working-directory: ${{ env.RELEASE_DIR }}
run: pnpm install --frozen-lockfile
- name: Link Shared Runtime Assets
run: |
set -euo pipefail
# Link runtime state only after install so dependency lifecycle scripts
# do not receive production secrets by default.
rm -f "$RELEASE_DIR/.env"
ln -sfn "$SHARED_DIR/.env" "$RELEASE_DIR/.env"
ln -sfn "$MEDIA_DIR" "$RELEASE_DIR/media"
- name: Backup Database
env:
DATABASE_URL: "${{ secrets.DATABASE_URL }}"
run: |
set -euo pipefail
mkdir -p "$SHARED_DIR/backups"
pg_dump "$DATABASE_URL" > "$SHARED_DIR/backups/pre-deploy-$(date +%Y%m%d-%H%M%S).sql"
- name: Run migrations via SQL
working-directory: ${{ env.RELEASE_DIR }}
env:
DATABASE_URL: "${{ secrets.DATABASE_URL }}"
run: |
set -euo pipefail
# For zero-downtime deploys, DB changes in this block must remain
# backward compatible with the currently serving release until reload.
./scripts/run_deploy_sql_migrations.sh
- name: Build App
working-directory: ${{ env.RELEASE_DIR }}
env:
DATABASE_URL: "${{ secrets.DATABASE_URL }}"
PAYLOAD_SECRET: "${{ secrets.PAYLOAD_SECRET }}"
LEGACY_DATABASE_URI: "${{ secrets.LEGACY_DATABASE_URI }}"
run: pnpm run build
- name: Record Previous Release
run: |
set -euo pipefail
if [ -L "$DEPLOY_ROOT/current" ]; then
PREVIOUS_RELEASE="$(readlink -f "$DEPLOY_ROOT/current")"
echo "PREVIOUS_RELEASE=$PREVIOUS_RELEASE" >> "$GITHUB_ENV"
fi
- name: Activate Release
run: |
set -euo pipefail
ln -sfn "$RELEASE_DIR" "$DEPLOY_ROOT/current.next"
mv -Tf "$DEPLOY_ROOT/current.next" "$DEPLOY_ROOT/current"
- name: Reload App
run: |
set -euo pipefail
CONFIG_PATH="$RELEASE_DIR/ecosystem.config.cjs"
# Rewrite the cwd in the ecosystem config from the symlink path to the
# literal release path so PM2 doesn't have to follow /current at start
# time (which fails when PM2 runs as a different user than the symlink owner).
sed -i "s|/var/www/polymer/current|$RELEASE_DIR|g" "$CONFIG_PATH"
# Replace any stale process definition before starting from the
# ecosystem file. A previous "pm2 start \"pnpm start\"" style app
# can keep an incompatible script path/interpreter combination that
# survives reloads and leaves the process crash-looping.
pm2 delete polymer || true
pm2 start "$CONFIG_PATH" --only polymer --env production
pm2 save
- name: Verify Release
working-directory: ${{ env.RELEASE_DIR }}
run: |
set -euo pipefail
curl --fail --silent --show-error \
--retry 20 \
--retry-delay 2 \
--retry-all-errors \
"$HEALTHCHECK_URL" >/dev/null
- name: Verify DB Schema
env:
DATABASE_URL: "${{ secrets.DATABASE_URL }}"
run: |
set -euo pipefail
# Validate that columns the app queries actually exist in the live DB.
# LIMIT 0 touches no rows but will error if any column is missing,
# causing this step to fail and triggering the rollback below.
psql "$DATABASE_URL" \
-c "SELECT one_liner, major FROM users LIMIT 0;" \
-c "SELECT opinion_type, image_caption, is_photofeature, gradient_opacity FROM articles LIMIT 0;" \
-c "SELECT id FROM submissions LIMIT 0;" \
-c "SELECT id FROM event_submissions LIMIT 0;" \
-c "SELECT id FROM features_page_layout LIMIT 0;" \
-c "SELECT id FROM staff_page_layout LIMIT 0;" \
-c "SELECT id FROM theme LIMIT 0;" \
-c "SELECT id FROM seo LIMIT 0;"
- name: Roll Back Release
if: failure()
run: |
set -euo pipefail
if [ -z "${PREVIOUS_RELEASE:-}" ] || [ ! -d "$PREVIOUS_RELEASE" ]; then
echo "Rollback requested, but no previous release is available." >&2
exit 1
fi
ln -sfn "$PREVIOUS_RELEASE" "$DEPLOY_ROOT/current.next"
mv -Tf "$DEPLOY_ROOT/current.next" "$DEPLOY_ROOT/current"
CONFIG_PATH="$DEPLOY_ROOT/current/ecosystem.config.cjs"
pm2 delete polymer || true
pm2 start "$CONFIG_PATH" --only polymer --env production
pm2 save
- name: Prune Old Releases
if: success()
run: |
set -euo pipefail
if [ ! -d "$RELEASES_DIR" ]; then
exit 0
fi
mapfile -t releases < <(find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -nr | awk '{print $2}')
if [ "${#releases[@]}" -gt 5 ]; then
for release in "${releases[@]:5}"; do
rm -rf "$release"
done
fi
# Prune backups older than 30 days, keeping at least 5.
BACKUP_DIR="$SHARED_DIR/backups"
if [ -d "$BACKUP_DIR" ]; then
mapfile -t backups < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' | sort -nr | awk '{print $2}')
cutoff=$(date -d '30 days ago' +%s)
for i in "${!backups[@]}"; do
if [ "$i" -lt 5 ]; then
continue
fi
mtime=$(stat -c '%Y' "${backups[$i]}")
if [ "$mtime" -lt "$cutoff" ]; then
rm -f "${backups[$i]}"
fi
done
fi