diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 246a60f..6aa3b52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,32 +77,110 @@ jobs: name: iso-word-document path: build/${{ env.DOC }}.docx + # The site is served from the gh-pages branch (Pages source: "Deploy from a + # branch"): main's Editor's Copy lives at the root, and per-PR previews live + # under PREVIEW-DO-NOT-USE/pr-/ (removed again by preview-cleanup.yml). + # Same model as the openid/publication repo. publish-to-pages: name: Publish Editor's Copy to GitHub Pages if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest - # Serialise Pages deployments so two quick merges to main can't race (one - # failing with "a deployment is already in progress", or publishing stale). + # Serialise gh-pages pushes so two quick merges to main can't race. concurrency: group: "pages" cancel-in-progress: false permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + contents: write steps: + - uses: actions/checkout@v4 - name: Download site artifact uses: actions/download-artifact@v4 with: name: site - path: _site - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 + path: ${{ runner.temp }}/site + - name: Publish to gh-pages branch + # Run a copy from outside the work tree: the script switches the + # checkout to gh-pages, which has no tools/. + run: | + cp tools/publish-gh-pages.sh "$RUNNER_TEMP/" + bash "$RUNNER_TEMP/publish-gh-pages.sh" root "$RUNNER_TEMP/site" \ + "Publish Editor's Copy for ${GITHUB_SHA}" + + # HTML preview of the PR's Editor's Copy, linked from a sticky PR comment. + # Fork PRs get a read-only GITHUB_TOKEN, so previews only run for branches in + # this repo. + deploy-preview: + name: Deploy PR preview to GitHub Pages + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + needs: build + runs-on: ubuntu-latest + concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Download site artifact + uses: actions/download-artifact@v4 with: - path: _site - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + name: site + path: ${{ runner.temp }}/site + - name: Deploy preview to gh-pages branch + run: | + cp tools/publish-gh-pages.sh "$RUNNER_TEMP/" + bash "$RUNNER_TEMP/publish-gh-pages.sh" subdir \ + "PREVIEW-DO-NOT-USE/pr-${{ github.event.pull_request.number }}" \ + "$RUNNER_TEMP/site" \ + "Preview for PR #${{ github.event.pull_request.number }}" + - name: Comment with preview link + uses: actions/github-script@v7 + with: + script: | + const prNum = context.issue.number; + const sha = context.payload.pull_request.head.sha; + // Assumes the default .github.io/ Pages host (no CNAME). + const url = `https://${context.repo.owner}.github.io/${context.repo.repo}/PREVIEW-DO-NOT-USE/pr-${prNum}/${process.env.DOC}-editors-copy.html`; + + // Invisible marker identifying our comment, so the visible heading + // can be reworded without breaking the update-in-place matching. + const marker = ''; + const body = [ + '📄 **Editor\'s Copy preview**', + '', + url, + '', + `_Preview of ${sha}; updated on every push. For review only — ` + + 'the official specifications are published at https://openid.net/specs/_', + marker, + ].join('\n'); + + // Update the existing preview comment rather than adding a new one. + let existing = null; + for await (const response of github.paginate.iterator( + github.rest.issues.listComments, + { owner: context.repo.owner, repo: context.repo.repo, issue_number: prNum, per_page: 100 } + )) { + const found = response.data.find(c => c.body.includes(marker)); + if (found) { existing = found; break; } + } + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNum, + body, + }); + } diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml new file mode 100644 index 0000000..883f015 --- /dev/null +++ b/.github/workflows/preview-cleanup.yml @@ -0,0 +1,34 @@ +name: Clean up PR preview + +# Removes the PR's HTML preview (deployed to the gh-pages branch by the +# deploy-preview job in build.yml) once the PR is closed or merged. +on: + pull_request: + types: [closed] + +permissions: + contents: write + +jobs: + cleanup-preview: + name: Remove PR preview from GitHub Pages + # Fork PRs never get a preview (see deploy-preview in build.yml). + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + # Same group as deploy-preview, so a still-running deployment for this PR + # finishes before we delete its output. + concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: false + steps: + # Check out the default branch for tools/: the PR's merge ref can be + # gone by the time a closed PR's workflow runs. + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + - name: Remove preview + run: | + cp tools/publish-gh-pages.sh "$RUNNER_TEMP/" + bash "$RUNNER_TEMP/publish-gh-pages.sh" delete \ + "PREVIEW-DO-NOT-USE/pr-${{ github.event.pull_request.number }}" \ + "Remove preview for PR #${{ github.event.pull_request.number }}" diff --git a/tools/publish-gh-pages.sh b/tools/publish-gh-pages.sh new file mode 100755 index 0000000..2b042bc --- /dev/null +++ b/tools/publish-gh-pages.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Publish content to the gh-pages branch (the GitHub Pages source). Shared by +# the publish-to-pages and deploy-preview jobs in .github/workflows/build.yml +# and by .github/workflows/preview-cleanup.yml. +# +# publish-gh-pages.sh root +# Replace the site root with 's contents, keeping the +# PREVIEW-DO-NOT-USE/ PR previews. +# publish-gh-pages.sh subdir +# Replace with 's contents. +# publish-gh-pages.sh delete +# Remove ; a no-op if it (or the branch) doesn't exist. +# +# Switches the current checkout to gh-pages, so the workflows run a copy from +# outside the work tree ($RUNNER_TEMP) rather than the checked-out file. + +set -euo pipefail + +mode=$1 + +git config user.name "OIDF GitHub Automation" +git config user.email "github@oidf.org" + +# The fetch doubles as the does-the-branch-exist probe. --depth=1 because the +# branch accumulates a full site snapshot per publish and nothing here needs +# its history. The explicit refspec creates the local branch directly: +# actions/checkout configures origin to fetch only the triggering ref, so a +# bare `git fetch origin gh-pages` wouldn't give `git checkout` anything to +# resolve `gh-pages` against. +if git fetch --depth=1 origin gh-pages:gh-pages; then + git checkout gh-pages +elif [ "$mode" = delete ]; then + echo "No gh-pages branch; nothing to clean up." + exit 0 +else + # Very first publish: start the branch empty. + git checkout --orphan gh-pages + git rm -rf --quiet . +fi + +case $mode in + root) + src=$2 msg=$3 + find . -mindepth 1 -maxdepth 1 \ + ! -name .git ! -name PREVIEW-DO-NOT-USE -exec rm -rf {} + + cp "$src"/* . + ;; + subdir) + dir=$2 src=$3 msg=$4 + rm -rf "$dir" + mkdir -p "$dir" + cp "$src"/* "$dir/" + ;; + delete) + dir=$2 msg=$3 + if [ ! -d "$dir" ]; then + echo "No $dir; nothing to clean up." + exit 0 + fi + rm -rf "$dir" + ;; + *) + echo "usage: $0 root | subdir | delete " >&2 + exit 2 + ;; +esac + +# Serve files as-is (skip Pages' default Jekyll processing). +touch .nojekyll + +git add -A +if git diff --cached --quiet; then + echo "gh-pages unchanged; nothing to push." + exit 0 +fi +git commit -m "$msg" +# Retry around pushes racing another job's gh-pages push (main publish vs. PR +# previews run in different concurrency groups); concurrent commits touch +# disjoint paths, so the rebase is conflict-free. +for _ in 1 2 3; do + git push origin gh-pages && exit 0 + git pull --rebase origin gh-pages +done +exit 1