From f70476bbe9accb2dcbd0968a85591c84781e9ce8 Mon Sep 17 00:00:00 2001 From: gas2own Date: Thu, 16 Jul 2026 23:15:03 +0000 Subject: [PATCH 1/2] workspace: make Quarto PR preview robust (un-failable comment + correct URL on private repos) Two latent bugs in the reusable Quarto deploy workflow made a freshly scaffolded repo's first PR look broken (red deploy check, no/wrong preview link) even when the site published fine: 1. The preview-comment gh api read was unguarded under set -e; a transient 5xx HTML body breaks --jq and abORTS the deploy after publish. Now the block runs under set +e and closes on a guaranteed-success command. 2. The preview URL was hardcoded to .github.io/, dead on private/internal repos that serve from an obfuscated *.pages.github.io domain. Now resolved from the Pages API with fallback; deploy job and scaffold docs.yml template grant pages: read. Supersedes #91, #92, #93. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/workspace-quarto-site.yml | 24 ++++++++++++++++--- .../skills/workspace/assets/docs.yml | 3 +++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workspace-quarto-site.yml b/.github/workflows/workspace-quarto-site.yml index 871779b..cfcdadd 100644 --- a/.github/workflows/workspace-quarto-site.yml +++ b/.github/workflows/workspace-quarto-site.yml @@ -87,6 +87,10 @@ jobs: permissions: contents: write pull-requests: write + # `pages: read` lets us resolve the site's real served URL from the Pages + # API (private/internal repos serve from an obfuscated *.pages.github.io + # domain, not .github.io/). Degrades gracefully if absent. + pages: read concurrency: group: pages-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -145,12 +149,25 @@ jobs: # For a PR, ensure exactly one preview-link comment (create or update), # independent of whether the content changed. Best-effort — a comment - # hiccup shouldn't fail an otherwise-successful publish. + # hiccup shouldn't fail an otherwise-successful publish, so the whole + # block runs with errexit disabled and closes on a guaranteed success. if [ "${{ github.event_name }}" = "pull_request" ]; then - url="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr-preview/pr-$PR_NUMBER/" + set +e + # Resolve the site's real base URL from the Pages config. Private and + # internal repos serve from an obfuscated .pages.github.io + # domain, NOT .github.io/, so hardcoding the latter + # yields a dead preview link there. Fall back to the canonical form + # if the Pages API is unavailable (e.g. token lacks `pages: read`). + base=$(gh api "repos/${{ github.repository }}/pages" --jq '.html_url' 2>/dev/null) + base="${base%/}" + [ -n "$base" ] || base="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" + url="$base/pr-preview/pr-$PR_NUMBER/" body="📄 Preview: $url" + # Guard the read: it runs the same set of transient failure modes as + # the writes below (a 5xx returns an HTML body that --jq can't parse), + # so an unguarded assignment would abort the step mid-publish. existing=$(gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \ - --jq 'map(select(.body | startswith("📄 Preview:"))) | .[0].id // empty') + --jq 'map(select(.body | startswith("📄 Preview:"))) | .[0].id // empty' 2>/dev/null) if [ -n "$existing" ]; then gh api "repos/${{ github.repository }}/issues/comments/$existing" -X PATCH -f body="$body" >/dev/null \ || echo "::warning::could not update preview comment" @@ -158,6 +175,7 @@ jobs: gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null \ || echo "::warning::could not create preview comment" fi + set -e fi # Remove a PR's preview when the PR closes. Shares that PR's concurrency group diff --git a/plugins/asta-tools/skills/workspace/assets/docs.yml b/plugins/asta-tools/skills/workspace/assets/docs.yml index 2df18ae..4b61eea 100644 --- a/plugins/asta-tools/skills/workspace/assets/docs.yml +++ b/plugins/asta-tools/skills/workspace/assets/docs.yml @@ -13,9 +13,12 @@ on: # # The token needs write access for gh-pages pushes and PR preview comments; # the called workflow's jobs individually downscope to least privilege. +# `pages: read` lets the deploy job resolve the real served URL for the preview +# link (private/internal repos use an obfuscated *.pages.github.io domain). permissions: contents: write pull-requests: write + pages: read jobs: docs: From 198a61d8a694eed4df451eaa1ee2b7b0e75044ea Mon Sep 17 00:00:00 2001 From: gas2own agent Date: Fri, 17 Jul 2026 05:01:15 +0000 Subject: [PATCH 2/2] workspace: guard preview base URL against gh api error bodies When Pages isn't configured on a repo, GET /repos/{repo}/pages returns 404 and gh prints the raw error JSON to stdout instead of applying --jq, so the previous non-empty check let that error text flow into the preview link. Validate that the resolved base is an https URL before using it, falling back to the canonical .github.io/ form otherwise. --- .github/workflows/workspace-quarto-site.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workspace-quarto-site.yml b/.github/workflows/workspace-quarto-site.yml index cfcdadd..9791092 100644 --- a/.github/workflows/workspace-quarto-site.yml +++ b/.github/workflows/workspace-quarto-site.yml @@ -159,8 +159,15 @@ jobs: # yields a dead preview link there. Fall back to the canonical form # if the Pages API is unavailable (e.g. token lacks `pages: read`). base=$(gh api "repos/${{ github.repository }}/pages" --jq '.html_url' 2>/dev/null) - base="${base%/}" - [ -n "$base" ] || base="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" + # On failure (e.g. Pages not configured yet -> 404, or a 5xx), + # `gh api` prints the raw error body to stdout and never applies + # --jq, so $base is a non-empty blob of error JSON. Guard on it + # actually being an https URL, not merely non-empty, before trusting + # it -- otherwise the error text lands inside the preview link. + case "$base" in + https://*) base="${base%/}" ;; + *) base="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" ;; + esac url="$base/pr-preview/pr-$PR_NUMBER/" body="📄 Preview: $url" # Guard the read: it runs the same set of transient failure modes as