diff --git a/.github/workflows/pr-preview-cleanup.yml b/.github/workflows/pr-preview-cleanup.yml new file mode 100644 index 00000000..a3ce6112 --- /dev/null +++ b/.github/workflows/pr-preview-cleanup.yml @@ -0,0 +1,79 @@ +name: PR Preview Cleanup + +on: + pull_request: + types: [closed] + +permissions: + contents: write + pull-requests: write + +# Use the same concurrency group as the deploy workflow so cleanup and +# deploy pushes to gh-pages are always serialized. +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +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 + uses: actions/checkout@v4 + with: + ref: gh-pages + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 1 + persist-credentials: true + + - name: Delete preview directory + env: + PR_NUMBER: ${{ github.event.number }} + run: | + PREVIEW_DIR="pr-previews/${PR_NUMBER}" + if [ -d "${PREVIEW_DIR}" ]; then + git config user.name "github-actions[bot]" + 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 + echo "Push attempt ${attempt} failed, retrying..." + sleep $((attempt * 5)) + done + else + echo "No preview directory found for PR #${PR_NUMBER}, nothing to do." + fi + + - name: Update preview comment to reflect closure + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ github.event.number }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = process.env.PR_NUMBER; + const owner = context.repo.owner; + const repo = context.repo.repo; + + 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, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, + comment_id: existing.id, + body: marker + '\n## 📄 Documentation Preview\n\n> This preview has been removed because the PR was closed.', + }); + } diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 00000000..b316884e --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,154 @@ +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'docs/**' + - 'assemblies/**' + - 'topics/**' + - '_includes/**' + - '_layouts/**' + - '_sass/**' + - 'assets/**' + - '_config.yml' + - 'Gemfile' + - 'index.md' + - '.github/workflows/pr-preview.yml' + +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 + +jobs: + build-and-deploy: + name: Build & Deploy 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 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Install system dependencies + run: sudo apt-get install -y asciidoctor + + - name: Build site with preview base URL + env: + JEKYLL_ENV: production + PR_NUMBER: ${{ github.event.number }} + run: | + bundle exec jekyll build \ + --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}" + + # 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 }} + 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, + }); + }