-
Notifications
You must be signed in to change notification settings - Fork 3
376 lines (333 loc) · 15.2 KB
/
vercel-deploy.yml
File metadata and controls
376 lines (333 loc) · 15.2 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
name: Vercel Deployments
on:
pull_request:
push:
branches: [main]
concurrency:
group: vercel-${{ github.ref }}
cancel-in-progress: true
jobs:
pre-deploy:
name: Initialize Deployment Status
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Post initial deployment status
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const identifier = '<!-- vercel-deployments-comment -->';
const timestamp = new Date().toLocaleString('en-US', {
timeZone: 'America/Los_Angeles',
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
});
const projects = [
'apollo-design',
'apollo-docs',
'apollo-landing',
'apollo-ui-react',
'apollo-vertex'
];
const tableRows = projects.map(projectName => {
const logsLink = `[Logs](https://github.com/${ context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
return `| ${projectName} | 🟡 Deploying... | ${logsLink} | ${timestamp} |`;
}).join('\n');
const comment = [
identifier,
'<!-- updated-packages: -->',
'The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.com/docs/deployments/git).',
'',
'| Project | Deployment | Review | Updated (PT) |',
'|---------|------------|--------|---------------|',
tableRows
].join('\n');
// Check for existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = comments.find(c => c.body?.includes(identifier));
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
deploy:
name: Deploy ${{ matrix.project_name }}
runs-on: ubuntu-latest
needs: pre-deploy
if: ${{ !cancelled() && (github.event_name == 'push' || needs.pre-deploy.result != 'cancelled') }}
continue-on-error: true
permissions:
contents: read
pull-requests: write
strategy:
matrix:
include:
- project_name: apollo-design
vercel_project_id_secret: VERCEL_PROJECT_ID_CANVAS
- project_name: apollo-docs
vercel_project_id_secret: VERCEL_PROJECT_ID_DOCS
- project_name: apollo-landing
vercel_project_id_secret: VERCEL_PROJECT_ID_LANDING
- project_name: apollo-ui-react
vercel_project_id_secret: VERCEL_PROJECT_ID_UI_REACT
- project_name: apollo-vertex
vercel_project_id_secret: VERCEL_PROJECT_ID_VERTEX
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cache Vercel CLI
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.npm
key: ${{ runner.os }}-vercel-cli
restore-keys: |
${{ runner.os }}-vercel-cli
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Set deployment variables
id: vars
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
echo "prod_flag=" >> $GITHUB_OUTPUT
else
echo "prod_flag=--prod" >> $GITHUB_OUTPUT
fi
- name: Set Vercel Project ID
id: set-project-id
run: |
# Map matrix secret name to actual secret value
case "${{ matrix.vercel_project_id_secret }}" in
VERCEL_PROJECT_ID_CANVAS)
echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_CANVAS }}" >> $GITHUB_ENV
;;
VERCEL_PROJECT_ID_DOCS)
echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_DOCS }}" >> $GITHUB_ENV
;;
VERCEL_PROJECT_ID_LANDING)
echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_LANDING }}" >> $GITHUB_ENV
;;
VERCEL_PROJECT_ID_UI_REACT)
echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_UI_REACT }}" >> $GITHUB_ENV
;;
VERCEL_PROJECT_ID_VERTEX)
echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_VERTEX }}" >> $GITHUB_ENV
;;
*)
echo "Error: Unknown vercel_project_id_secret value '${{ matrix.vercel_project_id_secret }}'. Please update the case statement in .github/workflows/vercel-deploy.yml." >&2
exit 1
;;
esac
- name: Deploy to Vercel
id: deploy
continue-on-error: true
run: |
# Deploy from repo root - Vercel uses Root Directory from dashboard settings
# Explicitly passing token via --token flag to ensure authentication
ERROR_MSG=""
DEPLOY_URL=""
set +e # Don't exit on error
DEPLOY_OUTPUT=$(vercel deploy --token "$VERCEL_TOKEN" --yes \
--build-env GH_NPM_REGISTRY_TOKEN="$GH_NPM_REGISTRY_TOKEN" \
${{ steps.vars.outputs.prod_flag }} 2>&1)
DEPLOY_EXIT_CODE=$?
set -e
# Defensive: redact known secrets from captured output before any
# downstream consumer (step summary, PR comment, logs) sees it.
# GH Actions masks secrets in live runner logs, but masking does not
# extend to values we re-emit through outputs or external APIs.
for __secret in "$GH_NPM_REGISTRY_TOKEN" "$VERCEL_TOKEN" "$VERCEL_ORG_ID"; do
if [ -n "$__secret" ]; then
DEPLOY_OUTPUT="${DEPLOY_OUTPUT//$__secret/***}"
fi
done
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
# Extract or construct the deployment URL
if [ "${{ steps.vars.outputs.prod_flag }}" == "--prod" ]; then
# For production: use the clean production URL format
DEPLOY_URL="https://${{ matrix.project_name }}.vercel.app"
else
# For preview: extract the preview URL from output (with hash)
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -oP 'https://[^\s]+\.vercel\.app[^\s]*' | head -n 1)
# Fallback: if grep didn't find URL, try last line
if [ -z "$DEPLOY_URL" ]; then
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | tail -n 1)
echo "⚠️ Warning: URL extraction fallback used for ${{ matrix.project_name }}"
fi
fi
echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT
echo "project=${{ matrix.project_name }}" >> $GITHUB_OUTPUT
echo "error_message=" >> $GITHUB_OUTPUT
echo "✅ Deployed ${{ matrix.project_name }} to $DEPLOY_URL" >> $GITHUB_STEP_SUMMARY
else
echo "url=" >> $GITHUB_OUTPUT
echo "project=${{ matrix.project_name }}" >> $GITHUB_OUTPUT
ERROR_MSG=$(echo "$DEPLOY_OUTPUT" | tail -n 5 | tr '\n' ' ')
# Truncate error message if too long (max 500 chars for output safety)
if [ ${#ERROR_MSG} -gt 500 ]; then
ERROR_MSG="${ERROR_MSG:0:500}..."
fi
echo "error_message=$ERROR_MSG" >> $GITHUB_OUTPUT
echo "❌ Failed to deploy ${{ matrix.project_name }}: $ERROR_MSG" >> $GITHUB_STEP_SUMMARY
exit 1
fi
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
GH_NPM_REGISTRY_TOKEN: ${{ secrets.GH_NPM_REGISTRY_TOKEN }}
CI: true
NODE_ENV: production
- name: Update PR comment
if: always() && github.event_name == 'pull_request'
env:
PROJECT_NAME: ${{ matrix.project_name }}
DEPLOY_OUTCOME: ${{ steps.deploy.outcome }}
DEPLOY_URL: ${{ steps.deploy.outputs.url }}
ERROR_MESSAGE: ${{ steps.deploy.outputs.error_message }}
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const identifier = '<!-- vercel-deployments-comment -->';
const projectName = process.env.PROJECT_NAME;
const outcome = process.env.DEPLOY_OUTCOME;
const deployUrl = process.env.DEPLOY_URL;
const errorMsg = process.env.ERROR_MESSAGE;
const timestamp = new Date().toLocaleString('en-US', {
timeZone: 'America/Los_Angeles',
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
});
// Truncate error message at word boundary
const truncateError = (msg, maxLength = 100) => {
if (!msg || msg.length <= maxLength) return msg;
const truncated = msg.substring(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
return lastSpace > 0 ? truncated.substring(0, lastSpace) + '...' : truncated + '...';
};
// Determine status
let status = '⚠️ Unknown';
if (outcome === 'success') {
status = '🟢 Ready';
} else if (outcome === 'failure') {
status = errorMsg ? `❌ Failed: ${truncateError(errorMsg)}` : '❌ Failed';
} else {
status = '⚠️ Skipped';
}
// Build this project's row
const projectLink = deployUrl ? `[${projectName}](${deployUrl})` : projectName;
const previewLink = deployUrl ? `[Preview](${deployUrl})` : 'N/A';
const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
const newRow = `| ${projectLink} | ${status} | ${previewLink}, ${logsLink} | ${timestamp} |`;
// Retry logic for concurrent updates with exponential backoff + jitter
// Random jitter prevents multiple jobs from retrying simultaneously
const getRetryDelay = (attempt) => {
const baseDelay = Math.min(1000 * Math.pow(2, attempt), 15000); // 1s, 2s, 4s, 8s, max 15s
const jitter = Math.random() * 1000; // 0-1s random jitter
return baseDelay + jitter;
};
const maxRetries = 5;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Always fetch fresh comment state to avoid overwriting concurrent updates
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = comments.find(c => c.body?.includes(identifier));
if (!existingComment) {
console.log('No existing comment found, skipping update');
break;
}
// Extract updated packages list (consistent regex pattern)
const updatedMatch = existingComment.body.match(/<!-- updated-packages: (.*?) -->/);
const updatedPackages = updatedMatch ? updatedMatch[1].split(',').map(p => p.trim()).filter(Boolean) : [];
// Check if row for this project already shows final state
const lines = existingComment.body.split('\n');
const currentRow = lines.find(line =>
line.includes(`| ${projectName} |`) || line.includes(`| [${projectName}](`)
);
// If row already matches our target state - we're done
if (currentRow === newRow) {
console.log(`Comment was successfully updated for ${projectName}`);
break;
}
// Build updated comment body atomically
const updatedLines = lines.map(line => {
// Match either plain project name or linked project name
if (line.includes(`| ${projectName} |`) || line.includes(`| [${projectName}](`)) {
return newRow;
}
return line;
});
// Update the packages list only after successful row update
const newUpdatedPackages = updatedPackages.includes(projectName)
? updatedPackages
: [...updatedPackages, projectName];
const newPackagesTag = `<!-- updated-packages: ${newUpdatedPackages.join(', ')} -->`;
const updatedBody = updatedLines.join('\n').replace(
/<!-- updated-packages: (.*?) -->/,
newPackagesTag
);
// Update comment (will throw on API errors)
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: updatedBody
});
} catch (error) {
// Only catch actual API errors here
if (attempt === maxRetries - 1) {
console.error(`Failed to update comment after ${maxRetries} attempts:`, error.message);
throw error;
}
const delay = getRetryDelay(attempt);
console.log(`API error on attempt ${attempt + 1}, retrying in ${(delay / 1000).toFixed(1)}s... (${error.message})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}