diff --git a/.github/workflows/pr-preview-cleanup.yml b/.github/workflows/pr-preview-cleanup.yml index a3ce6112..ee8061c4 100644 --- a/.github/workflows/pr-preview-cleanup.yml +++ b/.github/workflows/pr-preview-cleanup.yml @@ -1,9 +1,11 @@ name: PR Preview Cleanup on: - pull_request: + pull_request_target: types: [closed] +# Write permissions are safe here — this workflow runs from the base repository +# context and never checks out fork code (it only deletes a directory by PR number). permissions: contents: write pull-requests: write @@ -18,7 +20,6 @@ jobs: cleanup: name: Remove Preview runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.full_name == github.repository steps: - name: Checkout gh-pages branch @@ -39,11 +40,13 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git rm -rf "${PREVIEW_DIR}" git commit -m "chore: remove preview for PR #${PR_NUMBER}" - for attempt in 1 2 3; do git fetch origin gh-pages - git rebase origin/gh-pages 2>/dev/null || { git rebase --abort; true; } - git push origin gh-pages && break + if git rebase origin/gh-pages; then + git push origin gh-pages && break + else + git rebase --abort + fi echo "Push attempt ${attempt} failed, retrying..." sleep $((attempt * 5)) done @@ -64,7 +67,6 @@ jobs: const marker = ''; - // Paginate to handle PRs with more than 30 comments const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, }); diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml new file mode 100644 index 00000000..15433922 --- /dev/null +++ b/.github/workflows/pr-preview-deploy.yml @@ -0,0 +1,153 @@ +name: PR Preview Deploy + +on: + workflow_run: + workflows: ["PR Preview"] + types: [completed] + +# Write permissions are safe here — this workflow always runs from the base +# repository context (main branch), never from fork code. +permissions: + contents: write + pull-requests: write + +# Serialize all gh-pages pushes to prevent non-fast-forward failures +# when two PRs deploy at the same time. +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +jobs: + deploy: + name: Deploy Preview + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + + steps: + - name: Download preview artifact + uses: actions/download-artifact@v4 + with: + name: pr-preview-site + path: /tmp/pr-preview-site + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Read PR number from artifact + id: pr + run: | + PR_NUMBER=$(cat /tmp/pr-preview-site/.pr-number) + echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" + rm /tmp/pr-preview-site/.pr-number + + - name: Get PR head SHA + id: sha + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ steps.pr.outputs.number }}, + }); + core.setOutput('sha', pr.data.head.sha.substring(0, 7)); + + - name: Checkout repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: true + + - name: Checkout or initialise gh-pages branch + run: | + git fetch origin gh-pages 2>/dev/null || true + if git show-ref --verify --quiet refs/remotes/origin/gh-pages; then + git checkout -B gh-pages origin/gh-pages + else + git checkout --orphan gh-pages + git rm -rf . --quiet + echo "GitHub Pages for mta-documentation" > README.md + git add README.md + git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" commit -m "chore: initialise gh-pages branch" + git push origin gh-pages + fi + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy preview to gh-pages + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + COMMIT_SHA: ${{ steps.sha.outputs.sha }} + run: | + PREVIEW_DIR="pr-previews/${PR_NUMBER}" + rm -rf "${PREVIEW_DIR}" + mkdir -p "${PREVIEW_DIR}" + cp -r /tmp/pr-preview-site/. "${PREVIEW_DIR}/" + + git add "${PREVIEW_DIR}" + if git diff --staged --quiet; then + echo "No changes to deploy." + else + git commit -m "preview: PR #${PR_NUMBER} @ ${COMMIT_SHA}" + for attempt in 1 2 3; do + git fetch origin gh-pages + if git rebase origin/gh-pages; then + git push origin gh-pages && break + else + git rebase --abort + fi + echo "Push attempt ${attempt} failed, retrying..." + sleep $((attempt * 5)) + done + fi + + - name: Post or update preview comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = ${{ steps.pr.outputs.number }}; + const sha = '${{ steps.sha.outputs.sha }}'; + const owner = context.repo.owner; + const repo = context.repo.repo; + + const previewUrl = + `https://${owner}.github.io/${repo}/pr-previews/${prNumber}/`; + + const body = [ + `## 📄 Documentation Preview`, + ``, + `| | |`, + `|---|---|`, + `| **Preview URL** | ${previewUrl} |`, + `| **Commit** | \`${sha}\` |`, + `| **Updated** | ${new Date().toUTCString()} |`, + ``, + `> Preview updates automatically on every push to this PR.`, + `> It will be removed when the PR is closed.`, + ].join('\n'); + + const marker = ''; + const markedBody = marker + '\n' + body; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, + comment_id: existing.id, + body: markedBody, + }); + } else { + await github.rest.issues.createComment({ + owner, repo, + issue_number: prNumber, + body: markedBody, + }); + } diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index b316884e..57aa3fd5 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -16,29 +16,20 @@ on: - 'index.md' - '.github/workflows/pr-preview.yml' +# Read-only is sufficient — this job only builds and uploads an artifact. +# The deploy job (pr-preview-deploy.yml) runs separately with write permissions +# via workflow_run, which works for fork PRs where this token would be read-only. permissions: - contents: write - pull-requests: write - -# Serialize all gh-pages pushes across both workflows to prevent -# non-fast-forward failures when two PRs deploy at the same time. -concurrency: - group: gh-pages-deploy - cancel-in-progress: false + contents: read jobs: - build-and-deploy: - name: Build & Deploy Preview + build: + name: Build Preview runs-on: ubuntu-latest - # Skip fork PRs — GITHUB_TOKEN is read-only for forks and the push would fail. - if: github.event.pull_request.head.repo.full_name == github.repository steps: - name: Checkout PR branch uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -58,97 +49,12 @@ jobs: --baseurl "/mta-documentation/pr-previews/${PR_NUMBER}" \ --destination _site - - name: Deploy preview to gh-pages - env: - PR_NUMBER: ${{ github.event.number }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - mkdir -p /tmp/pr-preview - cp -r _site/. /tmp/pr-preview/ - - git fetch origin gh-pages 2>/dev/null || true - if git show-ref --verify --quiet refs/remotes/origin/gh-pages; then - git checkout -B gh-pages origin/gh-pages - else - git checkout --orphan gh-pages - git rm -rf . --quiet - echo "GitHub Pages for mta-documentation" > README.md - git add README.md - git commit -m "chore: initialise gh-pages branch" - fi - - PREVIEW_DIR="pr-previews/${PR_NUMBER}" - rm -rf "${PREVIEW_DIR}" - mkdir -p "${PREVIEW_DIR}" - cp -r /tmp/pr-preview/. "${PREVIEW_DIR}/" - - git add "${PREVIEW_DIR}" - if git diff --staged --quiet; then - echo "No changes to deploy." - else - COMMIT_SHA="${{ github.event.pull_request.head.sha }}" - git commit -m "preview: PR #${PR_NUMBER} @ ${COMMIT_SHA::7}" + - name: Save PR number for deploy workflow + run: echo "${{ github.event.number }}" > _site/.pr-number - # Retry push up to 3 times in case of a concurrent non-fast-forward - for attempt in 1 2 3; do - git fetch origin gh-pages - git rebase origin/gh-pages 2>/dev/null || { git rebase --abort; true; } - git push origin gh-pages && break - echo "Push attempt ${attempt} failed, retrying..." - sleep $((attempt * 5)) - done - fi - - - name: Post or update preview comment - uses: actions/github-script@v7 - env: - PR_NUMBER: ${{ github.event.number }} + - name: Upload preview artifact + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = process.env.PR_NUMBER; - const sha = context.payload.pull_request.head.sha.substring(0, 7); - const owner = context.repo.owner; - const repo = context.repo.repo; - - const previewUrl = - `https://${owner}.github.io/${repo}/pr-previews/${prNumber}/`; - - const body = [ - `## 📄 Documentation Preview`, - ``, - `| | |`, - `|---|---|`, - `| **Preview URL** | ${previewUrl} |`, - `| **Commit** | \`${sha}\` |`, - `| **Updated** | ${new Date().toUTCString()} |`, - ``, - `> Preview updates automatically on every push to this PR.`, - `> It will be removed when the PR is closed.`, - ].join('\n'); - - const marker = ''; - const markedBody = marker + '\n' + body; - - // Paginate to handle PRs with more than 30 comments - const comments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number: prNumber, - }); - - const existing = comments.find(c => c.body.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body: markedBody, - }); - } else { - await github.rest.issues.createComment({ - owner, repo, - issue_number: prNumber, - body: markedBody, - }); - } + name: pr-preview-site + path: _site/ + retention-days: 1