feat(apollo-wind): update ButtonGroup and ToggleGroup for Future themes #2858
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } | |
| } |