Skip to content

ci: parallelize CI into per-package jobs with dependency caching #63

ci: parallelize CI into per-package jobs with dependency caching

ci: parallelize CI into per-package jobs with dependency caching #63

Workflow file for this run

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