|
1 | | -name: Build and Preview Site |
| 1 | +name: PR Preview Deployment (Gatsby + GitHub Pages) |
| 2 | + |
2 | 3 | on: |
3 | | - pull_request: |
| 4 | + pull_request_target: |
4 | 5 | branches: [master] |
5 | | - types: [opened, synchronize, reopened] |
| 6 | + types: [opened, synchronize, reopened, closed] |
| 7 | + |
| 8 | +permissions: |
| 9 | + contents: write |
| 10 | + pull-requests: write |
| 11 | + |
| 12 | +concurrency: |
| 13 | + group: preview-${{ github.event.pull_request.number || github.run_id }} |
| 14 | + cancel-in-progress: true |
| 15 | + |
| 16 | +defaults: |
| 17 | + run: |
| 18 | + shell: bash |
6 | 19 |
|
7 | 20 | jobs: |
8 | | - site-preview: |
| 21 | + preview: |
9 | 22 | runs-on: ubuntu-latest |
| 23 | + outputs: |
| 24 | + removed_prs_json: ${{ steps.prune-previews.outputs.removed_prs_json }} |
| 25 | + env: |
| 26 | + PREVIEW_RETENTION_LIMIT: 6 |
| 27 | + |
10 | 28 | steps: |
11 | | - - name: Checkout 🛎️ |
12 | | - uses: actions/checkout@master |
| 29 | + |
| 30 | + - name: Checkout PR |
| 31 | + if: github.event.action != 'closed' |
| 32 | + uses: actions/checkout@v6 |
13 | 33 | with: |
14 | | - persist-credentials: false |
15 | | - fetch-depth: 1 |
| 34 | + ref: ${{ github.event.pull_request.head.sha }} |
16 | 35 |
|
17 | | - - name: Install 🔧 |
18 | | - run: make setup |
| 36 | + - name: Checkout gh-pages for cleanup |
| 37 | + if: github.event.action == 'closed' |
| 38 | + uses: actions/checkout@v6 |
| 39 | + with: |
| 40 | + ref: gh-pages |
19 | 41 |
|
20 | | - - name: Build 🏗️ |
21 | | - # Always run the full production build explicitly. |
22 | | - # Do NOT use `make clean` here — that target is a developer convenience |
23 | | - # command whose recipe has changed multiple times, causing blog posts and |
24 | | - # other collections to be silently excluded from the deployed site when |
25 | | - # it ran a lite/dev build instead of the full production build. |
26 | | - # `npm run build` is the canonical, stable production build command: |
27 | | - # cross-env BUILD_FULL_SITE=true gatsby build |
28 | | - run: npm run build |
| 42 | + - name: Setup Node |
| 43 | + if: github.event.action != 'closed' |
| 44 | + uses: actions/setup-node@v6 |
| 45 | + with: |
| 46 | + node-version: "20" |
29 | 47 |
|
30 | | - - name: Broken Link Check 🔗 |
31 | | - uses: technote-space/broken-link-checker-action@v2 |
| 48 | + - name: Install dependencies |
| 49 | + if: github.event.action != 'closed' |
| 50 | + run: npm ci |
| 51 | + |
| 52 | + - name: Set PATH_PREFIX for preview |
| 53 | + if: github.event.action != 'closed' |
| 54 | + run: | |
| 55 | + echo "PATH_PREFIX=pr-preview/pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV |
| 56 | +
|
| 57 | + - name: Build PR preview |
| 58 | + if: github.event.action != 'closed' |
| 59 | + env: |
| 60 | + GATSBY_PREVIEW: "true" |
| 61 | + GATSBY_SITE_URL: https://${{ github.repository_owner }}.github.io |
| 62 | + run: | |
| 63 | + PATH_PREFIX=$PATH_PREFIX npm run build:preview |
| 64 | +
|
| 65 | + # Prevent indexing |
| 66 | + echo -e "User-agent: *\nDisallow: /" > public/robots.txt |
| 67 | +
|
| 68 | + - name: Deploy PR preview |
| 69 | + if: github.event.action != 'closed' |
| 70 | + uses: rossjrw/pr-preview-action@v1.6.3 |
32 | 71 | with: |
33 | | - target: ./public/**/*.html |
| 72 | + source-dir: ./public |
| 73 | + preview-branch: gh-pages |
| 74 | + umbrella-dir: pr-preview |
| 75 | + action: auto |
| 76 | + comment: false |
| 77 | + |
| 78 | + - name: Checkout gh-pages for preview retention |
| 79 | + if: github.event.action != 'closed' |
| 80 | + uses: actions/checkout@v6 |
| 81 | + with: |
| 82 | + ref: gh-pages |
| 83 | + fetch-depth: 0 |
| 84 | + filter: blob:none |
| 85 | + sparse-checkout: | |
| 86 | + pr-preview |
| 87 | + path: gh-pages-maintenance |
| 88 | + |
| 89 | + - name: Prune old PR previews |
| 90 | + id: prune-previews |
| 91 | + if: github.event.action != 'closed' |
| 92 | + run: | |
| 93 | + cd gh-pages-maintenance |
| 94 | + mkdir -p pr-preview |
| 95 | + removed_prs=() |
| 96 | +
|
| 97 | + mapfile -t previews < <( |
| 98 | + while IFS= read -r preview; do |
| 99 | + timestamp="$(git log -1 --format=%ct -- "pr-preview/$preview" 2>/dev/null || echo 0)" |
| 100 | + printf '%s %s\n' "$timestamp" "$preview" |
| 101 | + done < <(find pr-preview -mindepth 1 -maxdepth 1 -type d -name 'pr-*' -printf '%f\n') \ |
| 102 | + | sort -nr \ |
| 103 | + | awk '{print $2}' |
| 104 | + ) |
| 105 | +
|
| 106 | + if (( ${#previews[@]} <= PREVIEW_RETENTION_LIMIT )); then |
| 107 | + echo "removed_prs_json=[]" >> "$GITHUB_OUTPUT" |
| 108 | + exit 0 |
| 109 | + fi |
| 110 | +
|
| 111 | + for preview in "${previews[@]:PREVIEW_RETENTION_LIMIT}"; do |
| 112 | + rm -rf "pr-preview/$preview" |
| 113 | + removed_prs+=("${preview#pr-}") |
| 114 | + done |
| 115 | +
|
| 116 | + if git diff --quiet -- pr-preview; then |
| 117 | + echo "removed_prs=" >> "$GITHUB_OUTPUT" |
| 118 | + echo "removed_prs_json=[]" >> "$GITHUB_OUTPUT" |
| 119 | + exit 0 |
| 120 | + fi |
| 121 | +
|
| 122 | + git config user.name "github-actions[bot]" |
| 123 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 124 | + git add pr-preview |
| 125 | + git commit -m "Prune old PR previews" |
| 126 | + git push |
| 127 | +
|
| 128 | + echo "removed_prs=$(IFS=,; echo "${removed_prs[*]}")" >> "$GITHUB_OUTPUT" |
| 129 | + echo "removed_prs_json=$(printf '%s\n' "${removed_prs[@]}" | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" |
| 130 | +
|
| 131 | + - name: Comment PR with Preview URL |
| 132 | + if: github.event.action != 'closed' |
| 133 | + uses: marocchino/sticky-pull-request-comment@v2 |
| 134 | + with: |
| 135 | + header: pr-preview |
| 136 | + message: | |
| 137 | + 🚀 Preview deployment: https://layer5.io/pr-preview/pr-${{ github.event.pull_request.number }}/ |
| 138 | + > *Note: Preview may take a moment (GitHub Pages deployment in progress). Please wait and refresh. Track deployment [here](https://github.com/${{ github.repository }}/actions/workflows/pages/pages-build-deployment)* |
| 139 | +
|
| 140 | + - name: Comment on pruned previews |
| 141 | + if: github.event.action != 'closed' && steps.prune-previews.outputs.removed_prs_json != '[]' |
| 142 | + uses: actions/github-script@v7 |
| 143 | + env: |
| 144 | + REMOVED_PRS_JSON: ${{ steps.prune-previews.outputs.removed_prs_json }} |
| 145 | + PREVIEW_RETENTION_LIMIT: ${{ env.PREVIEW_RETENTION_LIMIT }} |
| 146 | + with: |
| 147 | + script: | |
| 148 | + const removedPrs = JSON.parse(process.env.REMOVED_PRS_JSON); |
| 149 | + const retentionLimit = process.env.PREVIEW_RETENTION_LIMIT; |
| 150 | + const header = "pr-preview"; |
| 151 | + const marker = `<!-- Sticky Pull Request Comment${header} -->`; |
| 152 | +
|
| 153 | + for (const prNumber of removedPrs) { |
| 154 | + const body = |
| 155 | + `Preview deployment for PR #${prNumber} removed.\n\n` + |
| 156 | + `This PR preview was automatically pruned because we keep only the ${retentionLimit} most recently updated previews on GitHub Pages to stay within deployment size limits.\n\n` + |
| 157 | + `If needed, push a new commit to this PR to generate a fresh preview.\n` + |
| 158 | + `${marker}`; |
| 159 | +
|
| 160 | + const { data: comments } = await github.rest.issues.listComments({ |
| 161 | + owner: context.repo.owner, |
| 162 | + repo: context.repo.repo, |
| 163 | + issue_number: Number(prNumber), |
| 164 | + per_page: 100, |
| 165 | + }); |
| 166 | +
|
| 167 | + const existingComment = [...comments].reverse().find((comment) => |
| 168 | + comment.user?.login === "github-actions[bot]" && |
| 169 | + comment.body?.includes(marker) |
| 170 | + ); |
| 171 | +
|
| 172 | + if (existingComment) { |
| 173 | + await github.rest.issues.updateComment({ |
| 174 | + owner: context.repo.owner, |
| 175 | + repo: context.repo.repo, |
| 176 | + comment_id: existingComment.id, |
| 177 | + body, |
| 178 | + }); |
| 179 | + continue; |
| 180 | + } |
34 | 181 |
|
35 | | - - name: Zip Site |
36 | | - run: bash ./script.sh |
| 182 | + await github.rest.issues.createComment({ |
| 183 | + owner: context.repo.owner, |
| 184 | + repo: context.repo.repo, |
| 185 | + issue_number: Number(prNumber), |
| 186 | + body, |
| 187 | + }); |
| 188 | + } |
37 | 189 |
|
38 | | - - name: Upload files |
39 | | - uses: actions/upload-artifact@v6 |
| 190 | + - name: Cleanup PR preview on close |
| 191 | + if: github.event.action == 'closed' |
| 192 | + uses: rossjrw/pr-preview-action@v1.6.3 |
40 | 193 | with: |
41 | | - name: public-dir |
42 | | - path: ./public-dir.zip |
43 | | - retention-days: 1 |
44 | | - - name: Trigger Inner workflow |
45 | | - run: echo "triggering inner workflow" |
| 194 | + preview-branch: gh-pages |
| 195 | + umbrella-dir: pr-preview |
| 196 | + action: remove |
0 commit comments