Skip to content

feat(apollo-wind): apply Future theme styling to Checkbox and CodeBlock #2853

feat(apollo-wind): apply Future theme styling to Checkbox and CodeBlock

feat(apollo-wind): apply Future theme styling to Checkbox and CodeBlock #2853

Workflow file for this run

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));
}
}