From 2a9df0cb280d16b757ffa4aae3b398cc01dd75fe Mon Sep 17 00:00:00 2001 From: "mark a. foltz" Date: Thu, 29 Oct 2026 12:59:31 -0700 Subject: [PATCH 1/2] Replace .pr-preview.json with multi-spec GitHub Actions PR preview workflow --- .github/workflows/auto-publish.yml | 31 +++ .github/workflows/pr-preview.yml | 323 +++++++++++++++++++++++++++++ .pr-preview.json | 7 - 3 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/pr-preview.yml delete mode 100644 .pr-preview.json diff --git a/.github/workflows/auto-publish.yml b/.github/workflows/auto-publish.yml index c2efbc5..1d27f31 100644 --- a/.github/workflows/auto-publish.yml +++ b/.github/workflows/auto-publish.yml @@ -77,3 +77,34 @@ jobs: W3C_WG_DECISION_URL: https://lists.w3.org/Archives/Public/public-secondscreen/2022Apr/0007.html W3C_BUILD_OVERRIDE: | status: WD + + - name: Build PR preview bundle + if: github.event_name == 'pull_request' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + export PATH="$(python -m site --user-base)/bin:${PATH}" + python -c "import pygments" 2>/dev/null || python -m pip install Pygments + if ! command -v bikeshed >/dev/null 2>&1; then + python -m pip install bikeshed + bikeshed update + fi + python scripts/pygmentize_dir.py + mkdir -p _pr_preview + for spec in index application network; do + bikeshed --print=plain --no-update spec "${spec}.bs" "_pr_preview/${spec}.html" --md-warning="not ready" + done + cp -r images _pr_preview/images + jq -n \ + --argjson pr_number "$PR_NUMBER" \ + --arg head_sha "$HEAD_SHA" \ + '{pr_number: $pr_number, head_sha: $head_sha}' > _pr_preview/metadata.json + + - name: Upload PR preview bundle + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: pr-preview-bundle + path: _pr_preview/ + retention-days: 5 diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..5121dd3 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,323 @@ +# Publishes multi-spec PR previews (index.html, application.html, network.html) +# to pr-preview/pr-/ on gh-pages after "Build, Validate, and Deploy" succeeds +# on a pull request, posts/updates a sticky comment with Preview and Diff links, +# and removes the preview directory when the pull request is closed. + +name: PR Preview + +on: + workflow_run: + workflows: ["Build, Validate, and Deploy"] + types: [completed] + pull_request_target: + types: [closed] + +jobs: + deploy-preview: + name: Deploy PR preview + if: >- + github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + pull-requests: write + steps: + - name: Download PR preview bundle + uses: actions/download-artifact@v4 + with: + name: pr-preview-bundle + path: _pr_preview + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Validate PR metadata and detect modified specs + id: meta + uses: actions/github-script@v7 + with: + script: | + const fs = require("fs"); + const meta = JSON.parse(fs.readFileSync("_pr_preview/metadata.json", "utf8")); + const prNumber = meta.pr_number; + const headSha = meta.head_sha; + + if (!Number.isInteger(prNumber) || prNumber <= 0) { + core.setFailed(`Invalid pr_number in metadata.json: ${prNumber}`); + return; + } + if (headSha !== context.payload.workflow_run.head_sha) { + core.setFailed( + `Artifact head_sha (${headSha}) does not match workflow_run.head_sha (${context.payload.workflow_run.head_sha}).` + ); + return; + } + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + if (pr.state !== "open") { + core.info(`PR #${prNumber} is ${pr.state}; skipping preview deployment.`); + core.setOutput("skip", "true"); + return; + } + if (pr.head.sha !== headSha) { + core.info(`PR #${prNumber} head moved to ${pr.head.sha}; skipping stale run for ${headSha}.`); + core.setOutput("skip", "true"); + return; + } + if (//i.test(pr.body || "")) { + core.info(`PR #${prNumber} opted out of previews via .`); + core.setOutput("skip", "true"); + return; + } + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + per_page: 100, + }); + const changed = new Set(files.map((f) => f.filename)); + const touchesPrefix = (prefix) => [...changed].some((f) => f.startsWith(prefix)); + const sharedChanged = + changed.has("code-style.html") || + changed.has("biblio.json") || + touchesPrefix("scripts/"); + const imagesChanged = touchesPrefix("images/"); + + const modified = { + application: + sharedChanged || + changed.has("application.bs") || + changed.has("application_messages.cddl"), + network: + sharedChanged || + imagesChanged || + changed.has("network.bs") || + changed.has("network_messages.cddl"), + index: + sharedChanged || + imagesChanged || + changed.has("index.bs") || + changed.has("messages_appendix.cddl"), + }; + + core.setOutput("skip", "false"); + core.setOutput("pr_number", String(prNumber)); + core.setOutput("head_sha", headSha); + core.setOutput("modified", JSON.stringify(modified)); + + - name: Publish preview to gh-pages + if: steps.meta.outputs.skip != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.meta.outputs.pr_number }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + git clone --depth 1 --branch gh-pages "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _gh_pages + + python3 - <<'PY' + import os + import pathlib + import shutil + + pr = int(os.environ["PR_NUMBER"]) + src = pathlib.Path("_pr_preview") + dst = pathlib.Path("_gh_pages") / "pr-preview" / f"pr-{pr}" + if dst.exists(): + shutil.rmtree(dst) + dst.mkdir(parents=True, exist_ok=True) + + for name in ("index.html", "application.html", "network.html"): + f = src / name + if not f.is_file() or f.is_symlink(): + raise SystemExit(f"Missing or invalid expected file: {f}") + shutil.copyfile(f, dst / name) + + img_src = src / "images" + if img_src.is_dir() and not img_src.is_symlink(): + img_dst = dst / "images" + img_dst.mkdir(exist_ok=True) + for item in sorted(img_src.iterdir()): + if item.is_file() and not item.is_symlink(): + shutil.copyfile(item, img_dst / item.name) + PY + + cd _gh_pages + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "pr-preview/pr-${PR_NUMBER}" + if git diff --cached --quiet; then + echo "Preview files unchanged; nothing to commit." + exit 0 + fi + git commit -m "Deploy PR #${PR_NUMBER} preview (${HEAD_SHA::7})" + for attempt in 1 2 3; do + git pull --rebase origin gh-pages && git push origin gh-pages && exit 0 + sleep $((attempt * 2)) + done + echo "Failed to push PR preview to gh-pages after 3 attempts." >&2 + exit 1 + + - name: Post or update PR preview comment + if: steps.meta.outputs.skip != 'true' + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ steps.meta.outputs.pr_number }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + MODIFIED_JSON: ${{ steps.meta.outputs.modified }} + with: + script: | + const prNumber = Number(process.env.PR_NUMBER); + const headSha = process.env.HEAD_SHA; + const shortSha = headSha.slice(0, 7); + const modified = JSON.parse(process.env.MODIFIED_JSON); + + const owner = context.repo.owner.toLowerCase(); + const repo = context.repo.repo; + const baseRoot = `https://${owner}.github.io/${repo}`; + const previewRoot = `${baseRoot}/pr-preview/pr-${prNumber}`; + + const specs = [ + { + key: "application", + label: "Open Screen Application Protocol", + src: "application.bs", + file: "application.html", + }, + { + key: "network", + label: "Open Screen Network Protocol", + src: "network.bs", + file: "network.html", + }, + { + key: "index", + label: "Open Screen Protocol (Discontinued)", + src: "index.bs", + file: "index.html", + }, + ]; + + const rows = specs.map((s) => { + const baseUrl = `${baseRoot}/${s.file}`; + const previewUrl = `${previewRoot}/${s.file}`; + const diffUrl = + `https://services.w3.org/htmldiff?doc1=${encodeURIComponent(baseUrl)}` + + `&doc2=${encodeURIComponent(previewUrl)}`; + const status = modified[s.key] ? "**Modified**" : "Unchanged"; + return `| **${s.label}** (\`${s.src}\`) | ${status} | [Preview](${previewUrl}) \\| [Diff](${diffUrl}) |`; + }); + + const marker = ""; + const body = [ + marker, + `### PR Preview (commit \`${shortSha}\`)`, + "", + "| Specification | Status | Links |", + "| :--- | :--- | :--- |", + ...rows, + ].join("\n"); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + const existing = comments.find( + (c) => c.user?.type === "Bot" && (c.body || "").includes(marker) + ); + + 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: prNumber, + body, + }); + } + + cleanup-preview: + name: Clean up closed PR preview + if: github.event_name == 'pull_request_target' && github.event.action == 'closed' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Remove preview from gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid PR_NUMBER: $PR_NUMBER" >&2 + exit 1 + fi + git clone --depth 1 --branch gh-pages "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _gh_pages + + python3 - <<'PY' + import os + import pathlib + import shutil + + pr = int(os.environ["PR_NUMBER"]) + dst = pathlib.Path("_gh_pages") / "pr-preview" / f"pr-{pr}" + if dst.exists(): + shutil.rmtree(dst) + PY + + cd _gh_pages + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A pr-preview + if git diff --cached --quiet; then + echo "No preview directory found for PR #${PR_NUMBER}; nothing to remove." + exit 0 + fi + git commit -m "Remove PR #${PR_NUMBER} preview" + for attempt in 1 2 3; do + git pull --rebase origin gh-pages && git push origin gh-pages && exit 0 + sleep $((attempt * 2)) + done + echo "Failed to push preview cleanup to gh-pages after 3 attempts." >&2 + exit 1 + + - name: Update PR preview comment after cleanup + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const prNumber = Number(process.env.PR_NUMBER); + const marker = ""; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + const existing = comments.find( + (c) => c.user?.type === "Bot" && (c.body || "").includes(marker) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: `${marker}\n_PR preview for #${prNumber} has been removed now that this pull request is closed._`, + }); + } diff --git a/.pr-preview.json b/.pr-preview.json deleted file mode 100644 index 41eb560..0000000 --- a/.pr-preview.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "src_file": "index.bs", - "type": "bikeshed", - "params": { - "force": 1 - } -} From 618c1bcdfccc49142b2eb5f8a5759429ba383541 Mon Sep 17 00:00:00 2001 From: "mark a. foltz" Date: Thu, 29 Oct 2026 13:13:22 -0700 Subject: [PATCH 2/2] Reuse spec-prod artifacts from auto-publish.yml for PR previews --- .github/workflows/auto-publish.yml | 31 ---- .github/workflows/pr-preview.yml | 278 ++++++++--------------------- 2 files changed, 73 insertions(+), 236 deletions(-) diff --git a/.github/workflows/auto-publish.yml b/.github/workflows/auto-publish.yml index 1d27f31..c2efbc5 100644 --- a/.github/workflows/auto-publish.yml +++ b/.github/workflows/auto-publish.yml @@ -77,34 +77,3 @@ jobs: W3C_WG_DECISION_URL: https://lists.w3.org/Archives/Public/public-secondscreen/2022Apr/0007.html W3C_BUILD_OVERRIDE: | status: WD - - - name: Build PR preview bundle - if: github.event_name == 'pull_request' - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - export PATH="$(python -m site --user-base)/bin:${PATH}" - python -c "import pygments" 2>/dev/null || python -m pip install Pygments - if ! command -v bikeshed >/dev/null 2>&1; then - python -m pip install bikeshed - bikeshed update - fi - python scripts/pygmentize_dir.py - mkdir -p _pr_preview - for spec in index application network; do - bikeshed --print=plain --no-update spec "${spec}.bs" "_pr_preview/${spec}.html" --md-warning="not ready" - done - cp -r images _pr_preview/images - jq -n \ - --argjson pr_number "$PR_NUMBER" \ - --arg head_sha "$HEAD_SHA" \ - '{pr_number: $pr_number, head_sha: $head_sha}' > _pr_preview/metadata.json - - - name: Upload PR preview bundle - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 - with: - name: pr-preview-bundle - path: _pr_preview/ - retention-days: 5 diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 5121dd3..e0e8d39 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -1,7 +1,7 @@ # Publishes multi-spec PR previews (index.html, application.html, network.html) -# to pr-preview/pr-/ on gh-pages after "Build, Validate, and Deploy" succeeds -# on a pull request, posts/updates a sticky comment with Preview and Diff links, -# and removes the preview directory when the pull request is closed. +# to pr-preview/pr-/ on gh-pages using the artifacts already built by +# auto-publish.yml, posts/updates a sticky PR comment with Preview and Diff +# links, and removes the preview directory when the PR is closed. name: PR Preview @@ -25,53 +25,21 @@ jobs: contents: write pull-requests: write steps: - - name: Download PR preview bundle - uses: actions/download-artifact@v4 - with: - name: pr-preview-bundle - path: _pr_preview - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Validate PR metadata and detect modified specs - id: meta + - name: Find open PR and modified specs + id: pr uses: actions/github-script@v7 with: script: | - const fs = require("fs"); - const meta = JSON.parse(fs.readFileSync("_pr_preview/metadata.json", "utf8")); - const prNumber = meta.pr_number; - const headSha = meta.head_sha; - - if (!Number.isInteger(prNumber) || prNumber <= 0) { - core.setFailed(`Invalid pr_number in metadata.json: ${prNumber}`); - return; - } - if (headSha !== context.payload.workflow_run.head_sha) { - core.setFailed( - `Artifact head_sha (${headSha}) does not match workflow_run.head_sha (${context.payload.workflow_run.head_sha}).` - ); - return; - } - - const { data: pr } = await github.rest.pulls.get({ + const headSha = context.payload.workflow_run.head_sha; + const openPRs = await github.paginate(github.rest.pulls.list, { owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, + state: "open", + per_page: 100, }); - - if (pr.state !== "open") { - core.info(`PR #${prNumber} is ${pr.state}; skipping preview deployment.`); - core.setOutput("skip", "true"); - return; - } - if (pr.head.sha !== headSha) { - core.info(`PR #${prNumber} head moved to ${pr.head.sha}; skipping stale run for ${headSha}.`); - core.setOutput("skip", "true"); - return; - } - if (//i.test(pr.body || "")) { - core.info(`PR #${prNumber} opted out of previews via .`); + const pr = openPRs.find((p) => p.head.sha === headSha); + if (!pr || //i.test(pr.body || "")) { + core.info(`No active previewable PR at ${headSha}; skipping.`); core.setOutput("skip", "true"); return; } @@ -79,139 +47,80 @@ jobs: const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, + pull_number: pr.number, per_page: 100, }); const changed = new Set(files.map((f) => f.filename)); - const touchesPrefix = (prefix) => [...changed].some((f) => f.startsWith(prefix)); - const sharedChanged = - changed.has("code-style.html") || - changed.has("biblio.json") || - touchesPrefix("scripts/"); - const imagesChanged = touchesPrefix("images/"); - - const modified = { - application: - sharedChanged || - changed.has("application.bs") || - changed.has("application_messages.cddl"), - network: - sharedChanged || - imagesChanged || - changed.has("network.bs") || - changed.has("network_messages.cddl"), - index: - sharedChanged || - imagesChanged || - changed.has("index.bs") || - changed.has("messages_appendix.cddl"), - }; + const touches = (prefix) => [...changed].some((f) => f.startsWith(prefix)); + const shared = changed.has("code-style.html") || changed.has("biblio.json") || touches("scripts/"); core.setOutput("skip", "false"); - core.setOutput("pr_number", String(prNumber)); - core.setOutput("head_sha", headSha); - core.setOutput("modified", JSON.stringify(modified)); + core.setOutput("number", String(pr.number)); + core.setOutput("modified", JSON.stringify({ + application: shared || changed.has("application.bs") || changed.has("application_messages.cddl"), + network: shared || touches("images/") || changed.has("network.bs") || changed.has("network_messages.cddl"), + index: shared || touches("images/") || changed.has("index.bs") || changed.has("messages_appendix.cddl"), + })); + + - name: Download spec-prod artifacts from build workflow + if: steps.pr.outputs.skip != 'true' + uses: actions/download-artifact@v6 + with: + pattern: spec-prod-result* + path: _artifacts + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} - - name: Publish preview to gh-pages - if: steps.meta.outputs.skip != 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.meta.outputs.pr_number }} - HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + - name: Stage GitHub Pages preview files + if: steps.pr.outputs.skip != 'true' run: | - git clone --depth 1 --branch gh-pages "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _gh_pages - - python3 - <<'PY' - import os - import pathlib - import shutil - - pr = int(os.environ["PR_NUMBER"]) - src = pathlib.Path("_pr_preview") - dst = pathlib.Path("_gh_pages") / "pr-preview" / f"pr-{pr}" - if dst.exists(): - shutil.rmtree(dst) - dst.mkdir(parents=True, exist_ok=True) - - for name in ("index.html", "application.html", "network.html"): - f = src / name - if not f.is_file() or f.is_symlink(): - raise SystemExit(f"Missing or invalid expected file: {f}") - shutil.copyfile(f, dst / name) - - img_src = src / "images" - if img_src.is_dir() and not img_src.is_symlink(): - img_dst = dst / "images" - img_dst.mkdir(exist_ok=True) - for item in sorted(img_src.iterdir()): - if item.is_file() and not item.is_symlink(): - shutil.copyfile(item, img_dst / item.name) - PY - - cd _gh_pages - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add "pr-preview/pr-${PR_NUMBER}" - if git diff --cached --quiet; then - echo "Preview files unchanged; nothing to commit." - exit 0 - fi - git commit -m "Deploy PR #${PR_NUMBER} preview (${HEAD_SHA::7})" - for attempt in 1 2 3; do - git pull --rebase origin gh-pages && git push origin gh-pages && exit 0 - sleep $((attempt * 2)) + mkdir -p _preview + for pair in "spec-prod-result:index.html" "spec-prod-result-applicationbs:application.html" "spec-prod-result-networkbs:network.html"; do + dir="_artifacts/${pair%%:*}" + file="${pair#*:}" + src=$(find "$dir" \( -path "*.gh/${file}" -o -path "${dir}/${file}" \) -type f | head -n 1) + cp "$src" "_preview/${file}" done - echo "Failed to push PR preview to gh-pages after 3 attempts." >&2 - exit 1 + img_dir=$(find _artifacts \( -path "*.gh/images" -o -path "*/spec-prod-result/images" \) -type d | head -n 1) + if [ -n "$img_dir" ]; then + cp -r "$img_dir" _preview/images + fi + + - name: Deploy preview to gh-pages + if: steps.pr.outputs.skip != 'true' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: _preview + destination_dir: pr-preview/pr-${{ steps.pr.outputs.number }} + keep_files: true - name: Post or update PR preview comment - if: steps.meta.outputs.skip != 'true' + if: steps.pr.outputs.skip != 'true' uses: actions/github-script@v7 env: - PR_NUMBER: ${{ steps.meta.outputs.pr_number }} - HEAD_SHA: ${{ steps.meta.outputs.head_sha }} - MODIFIED_JSON: ${{ steps.meta.outputs.modified }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + MODIFIED_JSON: ${{ steps.pr.outputs.modified }} with: script: | const prNumber = Number(process.env.PR_NUMBER); - const headSha = process.env.HEAD_SHA; - const shortSha = headSha.slice(0, 7); + const shortSha = process.env.HEAD_SHA.slice(0, 7); const modified = JSON.parse(process.env.MODIFIED_JSON); - - const owner = context.repo.owner.toLowerCase(); - const repo = context.repo.repo; - const baseRoot = `https://${owner}.github.io/${repo}`; + const baseRoot = `https://${context.repo.owner.toLowerCase()}.github.io/${context.repo.repo}`; const previewRoot = `${baseRoot}/pr-preview/pr-${prNumber}`; const specs = [ - { - key: "application", - label: "Open Screen Application Protocol", - src: "application.bs", - file: "application.html", - }, - { - key: "network", - label: "Open Screen Network Protocol", - src: "network.bs", - file: "network.html", - }, - { - key: "index", - label: "Open Screen Protocol (Discontinued)", - src: "index.bs", - file: "index.html", - }, + ["application", "Open Screen Application Protocol", "application.bs", "application.html"], + ["network", "Open Screen Network Protocol", "network.bs", "network.html"], + ["index", "Open Screen Protocol (Discontinued)", "index.bs", "index.html"], ]; - - const rows = specs.map((s) => { - const baseUrl = `${baseRoot}/${s.file}`; - const previewUrl = `${previewRoot}/${s.file}`; - const diffUrl = - `https://services.w3.org/htmldiff?doc1=${encodeURIComponent(baseUrl)}` + - `&doc2=${encodeURIComponent(previewUrl)}`; - const status = modified[s.key] ? "**Modified**" : "Unchanged"; - return `| **${s.label}** (\`${s.src}\`) | ${status} | [Preview](${previewUrl}) \\| [Diff](${diffUrl}) |`; + const rows = specs.map(([key, label, src, file]) => { + const baseUrl = `${baseRoot}/${file}`; + const previewUrl = `${previewRoot}/${file}`; + const diffUrl = `https://services.w3.org/htmldiff?doc1=${encodeURIComponent(baseUrl)}&doc2=${encodeURIComponent(previewUrl)}`; + const status = modified[key] ? "**Modified**" : "Unchanged"; + return `| **${label}** (\`${src}\`) | ${status} | [Preview](${previewUrl}) \\| [Diff](${diffUrl}) |`; }); const marker = ""; @@ -230,24 +139,11 @@ jobs: issue_number: prNumber, per_page: 100, }); - const existing = comments.find( - (c) => c.user?.type === "Bot" && (c.body || "").includes(marker) - ); - + const existing = comments.find((c) => c.user?.type === "Bot" && (c.body || "").includes(marker)); if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); + 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: prNumber, - body, - }); + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body }); } cleanup-preview: @@ -258,43 +154,17 @@ jobs: contents: write pull-requests: write steps: - - name: Remove preview from gh-pages + - name: Remove preview directory from gh-pages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | - if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then - echo "Invalid PR_NUMBER: $PR_NUMBER" >&2 - exit 1 - fi git clone --depth 1 --branch gh-pages "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _gh_pages - - python3 - <<'PY' - import os - import pathlib - import shutil - - pr = int(os.environ["PR_NUMBER"]) - dst = pathlib.Path("_gh_pages") / "pr-preview" / f"pr-{pr}" - if dst.exists(): - shutil.rmtree(dst) - PY - cd _gh_pages git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A pr-preview - if git diff --cached --quiet; then - echo "No preview directory found for PR #${PR_NUMBER}; nothing to remove." - exit 0 - fi - git commit -m "Remove PR #${PR_NUMBER} preview" - for attempt in 1 2 3; do - git pull --rebase origin gh-pages && git push origin gh-pages && exit 0 - sleep $((attempt * 2)) - done - echo "Failed to push preview cleanup to gh-pages after 3 attempts." >&2 - exit 1 + git rm -rf --ignore-unmatch "pr-preview/pr-${PR_NUMBER}" + git diff --cached --quiet || (git commit -m "Remove PR #${PR_NUMBER} preview" && git push origin gh-pages) - name: Update PR preview comment after cleanup uses: actions/github-script@v7 @@ -310,9 +180,7 @@ jobs: issue_number: prNumber, per_page: 100, }); - const existing = comments.find( - (c) => c.user?.type === "Bot" && (c.body || "").includes(marker) - ); + const existing = comments.find((c) => c.user?.type === "Bot" && (c.body || "").includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner,