ci: parallelize CI into per-package jobs with dependency caching #63
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: PR Preview | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| concurrency: | |
| group: "preview-${{ github.head_ref }}" | |
| cancel-in-progress: true | |
| jobs: | |
| deploy-preview: | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| pages: read | |
| steps: | |
| - name: Post "updating" status to PR | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request.number; | |
| const sha = context.payload.pull_request.head.sha; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr, | |
| }); | |
| const existing = comments.find( | |
| c => c.user.login === 'github-actions[bot]' && c.body.includes('PR Preview Deployed') | |
| ); | |
| if (existing) { | |
| // Strip any prior "(deploying ...)" marker rather than stacking, in case | |
| // a previous run was cancelled (via concurrency) before it could clean up. | |
| const body = existing.body | |
| .replace(/ _\(deploying .+?\)_$/, '') | |
| .trimEnd(); | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: body + ` _(deploying ${sha}...)_`, | |
| }); | |
| } | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Install dependencies | |
| uses: ./.github/actions/setup | |
| - name: Build site (preview base URL) | |
| run: npm run deploy | |
| env: | |
| BASE_URL: /evo-web/previews/pr-${{ github.event.pull_request.number }}/ | |
| - name: Resolve GitHub Pages base URL | |
| id: pages | |
| run: | | |
| set -euo pipefail | |
| # Ask the API where this repo's Pages site is actually served | |
| # (custom domain on the real repo, <owner>.github.io on forks) so the | |
| # liveness check below and the posted link agree with reality. | |
| base=$(curl -fsS --max-time 15 \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/${{ github.repository }}/pages" \ | |
| | jq -r '.html_url') \ | |
| || { echo "GitHub Pages is not enabled for ${{ github.repository }}; enable it (deploy from the gh-pages branch) to publish previews."; exit 1; } | |
| base="${base%/}/" | |
| echo "Pages base URL: ${base}" | |
| echo "base=${base}" >> "$GITHUB_OUTPUT" | |
| - name: Deploy to gh-pages branch (preview subdirectory) | |
| id: deploy | |
| run: | | |
| set -euo pipefail | |
| # Stamp this deploy so the wait step below can verify the preview | |
| # that is actually being served is this run's build. | |
| echo "${{ github.sha }}" > _site/public/deploy-stamp.txt | |
| repo_url="https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" | |
| # gh-pages commits in its own temporary clone, so set the identity globally | |
| # rather than via --user (whose parser rejects the github-actions[bot] | |
| # brackets). Retry on a non-fast-forward rejection in case a concurrent | |
| # gh-pages write (another preview, the cleanup job, or the production deploy) | |
| # lands first; each run re-clones the branch, so clear the cache between attempts. | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| pushed=false | |
| for attempt in 1 2 3; do | |
| rm -rf node_modules/.cache/gh-pages | |
| if npx gh-pages \ | |
| --dist _site/public \ | |
| --dest previews/pr-${{ github.event.pull_request.number }} \ | |
| --branch gh-pages \ | |
| --nojekyll \ | |
| --add \ | |
| --message "preview: pr-${{ github.event.pull_request.number }} @ ${{ github.sha }}" \ | |
| --repo "$repo_url"; then | |
| pushed=true | |
| break | |
| fi | |
| echo "gh-pages push failed (attempt ${attempt}); retrying." | |
| sleep $((attempt * 3)) | |
| done | |
| $pushed || { echo "Failed to deploy preview after 3 attempts."; exit 1; } | |
| - name: Wait for preview to be live | |
| run: | | |
| set -euo pipefail | |
| expected="${{ github.sha }}" | |
| stamp_url="${{ steps.pages.outputs.base }}previews/pr-${{ github.event.pull_request.number }}/deploy-stamp.txt" | |
| # Poll the deployed site itself rather than the deployments API, | |
| # which does not record these pushes on every repo. The unique query | |
| # string keeps the CDN from serving a cached stamp. | |
| for attempt in $(seq 1 40); do | |
| live=$(curl -fsS --max-time 15 "${stamp_url}?cb=${expected}-${attempt}" 2>/dev/null || true) | |
| if [ "$live" = "$expected" ]; then | |
| echo "Preview is live (deploy stamp ${expected})." | |
| exit 0 | |
| fi | |
| echo "Preview is not live yet (attempt ${attempt}/40; serving stamp: ${live:-none})..." | |
| sleep 10 | |
| done | |
| echo "Preview did not go live within ~7 minutes; not posting the link." | |
| exit 1 | |
| - name: Post preview URL to PR | |
| uses: actions/github-script@v7 | |
| env: | |
| PAGES_BASE: ${{ steps.pages.outputs.base }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request.number; | |
| const sha = context.payload.pull_request.head.sha.slice(0, 7); | |
| const url = `${process.env.PAGES_BASE}previews/pr-${pr}/`; | |
| // Find and update an existing bot comment, or create a new one | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr, | |
| }); | |
| const existing = comments.find( | |
| c => c.user.login === 'github-actions[bot]' && c.body.includes('PR Preview Deployed') | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: existing.body | |
| .replace('### PR Preview Deployed _(removed)_', '### PR Preview Deployed') | |
| .replace(/ _\(deploying .+?\)_$/, '') | |
| .replace(/###### commit .+$/, `###### commit ${sha}`), | |
| }); | |
| } else { | |
| const body = [ | |
| `### PR Preview Deployed`, | |
| ``, | |
| `[Website](${url}) • [\`evo-marko\`](${url}marko/) • [\`evo-react\`](${url}react/) • [\`ebayui-core\`](${url}ebayui-core/) • [\`ebayui-core-react\`](${url}ebayui-core-react/) • [\`skin\`](${url}skin-storybook/)`, | |
| ``, | |
| `###### commit ${sha}`, | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr, | |
| body, | |
| }); | |
| } |