fix: keep PR walkthrough links canonical (#3438)

This commit is contained in:
Carlos Florêncio
2026-09-06 15:14:28 +01:00
committed by GitHub
parent 7cb5d8c0b3
commit 039142db80
22 changed files with 1793 additions and 106 deletions
@@ -0,0 +1,216 @@
name: PR Walkthrough Reconcile
# A description edit must be able to repair a stale callout without starting
# the model-backed generation workflow or executing pull request code.
on:
pull_request_target:
types: [edited]
permissions: {}
concurrency:
group: pr-walkthrough-reconcile-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr-walkthrough-reconcile:
if: >
vars.PR_WALKTHROUGH_ENABLED == 'true' &&
github.event_name == 'pull_request_target' &&
github.event.action == 'edited' &&
github.event.pull_request.draft == false &&
(
github.event.pull_request.head.repo.full_name == github.repository ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
(
contains(github.event.pull_request.labels.*.name, 'safe-to-review') ||
(vars.CLAUDE_REVIEW_ALLOWLIST != '' &&
contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login))
)
)
)
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: pr-description-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout trusted reconciliation helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- name: Repair stale walkthrough callout
env:
GH_TOKEN: ${{ github.token }}
TRUSTED_SHA: ${{ github.workflow_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
WALKTHROUGH_BASE_URL: https://walkthrough.kandev.ai
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$TRUSTED_SHA"
test -f scripts/pr-walkthrough-pr-body
test "$PR_NUMBER" -gt 0
[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
SHORT_HEAD_SHA="${HEAD_SHA:0:12}"
PUBLIC_URL="${WALKTHROUGH_BASE_URL%/}/pr/${PR_NUMBER}/${SHORT_HEAD_SHA}.html"
RESPONSE_META_PATH=pr-walkthrough-public.response
PUBLIC_HTML_PATH=pr-walkthrough-public.html
if ! curl --fail --silent --show-error --location --max-time 30 \
--retry 3 --retry-all-errors --retry-delay 2 --retry-max-time 30 \
--output "$PUBLIC_HTML_PATH" \
--write-out '%{http_code}\t%{content_type}\t%{url_effective}' \
> "$RESPONSE_META_PATH" \
"$PUBLIC_URL"; then
echo "Canonical walkthrough is not publicly available; skipping repair." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
IFS=$'\t' read -r response_status response_content_type response_effective_url < "$RESPONSE_META_PATH"
if [ "$response_status" != "200" ] ||
[ "$response_effective_url" != "$PUBLIC_URL" ] ||
! printf '%s\n' "$response_content_type" | grep -Eiq '^text/html(;|[[:space:]]|$)' ||
! test -s "$PUBLIC_HTML_PATH"; then
echo "Canonical walkthrough did not return a non-empty HTML response; skipping repair." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
for attempt in 1 2 3; do
rm -f \
pr-response.json \
pr-latest-response.json \
pr-verified-response.json \
pr-update.json \
pr-verify-update.json \
pr-body-before \
pr-body-latest \
pr-body-expected \
pr-body-verified
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-response.json ||
! python3 -c 'import json; json.load(open("pr-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to fetch the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
set +e
python3 scripts/pr-walkthrough-pr-body \
--github-response pr-response.json \
--output pr-update.json \
--url "$PUBLIC_URL" \
--pr-number "$PR_NUMBER" \
--head-sha "$HEAD_SHA" \
--require-existing
update_status=$?
set -e
if [ "$update_status" -eq 4 ]; then
echo "No existing walkthrough callout found; skipping repair." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$update_status" -eq 3 ]; then
echo "PR description already links the canonical walkthrough." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$update_status" -ne 0 ]; then
echo "Existing walkthrough markers are malformed; refusing to rewrite the PR description." >&2
exit 1
fi
jq -j '.body // ""' pr-update.json > pr-body-expected
jq -j '.body // ""' pr-response.json > pr-body-before
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-latest-response.json ||
! python3 -c 'import json; json.load(open("pr-latest-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to refresh the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
current_head_sha="$(jq -r '.head.sha // ""' pr-latest-response.json)"
if [ "$current_head_sha" != "$HEAD_SHA" ]; then
echo "PR head advanced to ${current_head_sha}; skipping stale repair." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -j '.body // ""' pr-latest-response.json > pr-body-latest
if ! cmp -- pr-body-before pr-body-latest; then
echo "Pull request description changed before repair PATCH; retrying." >&2
sleep 2
continue
fi
if ! gh api --method PATCH \
"repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--input pr-update.json \
--silent; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to repair the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-verified-response.json ||
! python3 -c 'import json; json.load(open("pr-verified-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to verify the repaired pull request description." >&2
exit 1
fi
sleep 2
continue
fi
verified_head_sha="$(jq -r '.head.sha // ""' pr-verified-response.json)"
if [ "$verified_head_sha" != "$HEAD_SHA" ]; then
echo "PR head advanced to ${verified_head_sha}; skipping stale repair." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -j '.body // ""' pr-verified-response.json > pr-body-verified
if ! cmp -- pr-body-expected pr-body-verified; then
if [ "$attempt" -eq 3 ]; then
echo "Pull request description changed before repair readback verification." >&2
exit 1
fi
echo "Pull request description changed after repair PATCH; retrying." >&2
sleep 2
continue
fi
set +e
python3 scripts/pr-walkthrough-pr-body \
--github-response pr-verified-response.json \
--output pr-verify-update.json \
--url "$PUBLIC_URL" \
--pr-number "$PR_NUMBER" \
--head-sha "$HEAD_SHA" \
--require-existing
verify_status=$?
set -e
if [ "$verify_status" -eq 3 ]; then
echo "Repaired the stale walkthrough link in the PR description." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Pull request description readback did not contain the canonical walkthrough." >&2
exit 1
fi
echo "Pull request description readback changed; retrying repair." >&2
sleep 2
done
echo "Pull request description repair did not converge." >&2
exit 1
+101 -32
View File
@@ -367,6 +367,9 @@ jobs:
needs: pr-walkthrough-publish
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: pr-description-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
@@ -392,47 +395,113 @@ jobs:
test -f scripts/pr-walkthrough-pr-body
test -n "$PUBLIC_URL"
pr_response_ok=false
for attempt in 1 2 3; do
if gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-response.json &&
python3 -c 'import json; json.load(open("pr-response.json", encoding="utf-8"))'; then
pr_response_ok=true
break
fi
if [ "$attempt" -lt 3 ]; then
rm -f \
pr-response.json \
pr-latest-response.json \
pr-verified-response.json \
pr-update.json \
pr-verify-update.json \
pr-body-before \
pr-body-latest \
pr-body-expected \
pr-body-verified
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-response.json ||
! python3 -c 'import json; json.load(open("pr-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to fetch the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
done
test "$pr_response_ok" = true
set +e
python3 scripts/pr-walkthrough-pr-body \
--github-response pr-response.json \
--output pr-update.json \
--url "$PUBLIC_URL" \
--pr-number "$PR_NUMBER" \
--head-sha "$HEAD_SHA"
update_status=$?
set -e
set +e
python3 scripts/pr-walkthrough-pr-body \
--github-response pr-response.json \
--output pr-update.json \
--url "$PUBLIC_URL" \
--pr-number "$PR_NUMBER" \
--head-sha "$HEAD_SHA"
update_status=$?
set -e
if [ "$update_status" -eq 3 ]; then
echo "PR description already links the published walkthrough." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
test "$update_status" -eq 0
if [ "$update_status" -eq 3 ]; then
echo "PR description already links the published walkthrough." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
test "$update_status" -eq 0
jq -j '.body // ""' pr-update.json > pr-body-expected
patch_ok=false
for attempt in 1 2 3; do
if gh api --method PATCH \
jq -j '.body // ""' pr-response.json > pr-body-before
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-latest-response.json ||
! python3 -c 'import json; json.load(open("pr-latest-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to refresh the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
jq -j '.body // ""' pr-latest-response.json > pr-body-latest
if ! cmp -- pr-body-before pr-body-latest; then
echo "Pull request description changed before PATCH; retrying." >&2
sleep 2
continue
fi
if ! gh api --method PATCH \
"repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--input pr-update.json \
--silent; then
patch_ok=true
break
fi
if [ "$attempt" -lt 3 ]; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to update the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-verified-response.json ||
! python3 -c 'import json; json.load(open("pr-verified-response.json", encoding="utf-8"))'; then
if [ "$attempt" -eq 3 ]; then
echo "Unable to verify the pull request description." >&2
exit 1
fi
sleep 2
continue
fi
jq -j '.body // ""' pr-verified-response.json > pr-body-verified
if ! cmp -- pr-body-expected pr-body-verified; then
if [ "$attempt" -eq 3 ]; then
echo "Pull request description changed before readback verification." >&2
exit 1
fi
echo "Pull request description changed after PATCH; retrying." >&2
sleep 2
continue
fi
set +e
python3 scripts/pr-walkthrough-pr-body \
--github-response pr-verified-response.json \
--output pr-verify-update.json \
--url "$PUBLIC_URL" \
--pr-number "$PR_NUMBER" \
--head-sha "$HEAD_SHA"
verify_status=$?
set -e
if [ "$verify_status" -eq 3 ]; then
echo "Added the walkthrough link to the top of the PR description." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Pull request description readback did not contain the published walkthrough." >&2
exit 1
fi
echo "Pull request description readback changed; retrying." >&2
sleep 2
done
test "$patch_ok" = true
echo "Added the walkthrough link to the top of the PR description." >> "$GITHUB_STEP_SUMMARY"
echo "Pull request description update did not converge." >&2
exit 1
+142 -9
View File
@@ -29,9 +29,10 @@ jobs:
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
outputs:
preview_url: ${{ steps.deploy.outputs.preview_url }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -56,14 +57,20 @@ jobs:
run: pnpm -C apps install --frozen-lockfile
- name: Deploy preview environment
id: deploy
working-directory: apps/backend
run: go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install
run: |
set -euo pipefail
deploy_output="$(go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install --skip-description)"
printf '%s\n' "$deploy_output"
preview_url="$(printf '%s\n' "$deploy_output" | sed -n 's/^preview URL: //p' | tail -n 1)"
test -n "$preview_url"
printf 'preview_url=%s\n' "$preview_url" >> "$GITHUB_OUTPUT"
env:
PR: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# -----------------------------------------------------------------------
# Deploy / update preview (fork PRs, gated by allowlist)
@@ -90,18 +97,19 @@ jobs:
(vars.PREVIEW_ENV_ALLOWLIST != '' && contains(fromJSON(vars.PREVIEW_ENV_ALLOWLIST), github.event.pull_request.user.login)) ||
(vars.CLAUDE_REVIEW_ALLOWLIST != '' && contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)))
runs-on: ubuntu-latest
outputs:
preview_url: ${{ steps.deploy.outputs.preview_url }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
# pull_request_target checks out base by default; explicitly use PR head.
# Safe: the preview CLI only reads files to build artifacts and posts comments.
# Safe: the preview CLI only reads files to build artifacts.
# NOTE: The allowlist gate (safe-to-review, PREVIEW_ENV_ALLOWLIST,
# CLAUDE_REVIEW_ALLOWLIST)
# authorizes running the fork's cmd/preview code with SPRITES_API_TOKEN and GH_TOKEN.
# authorizes running the fork's cmd/preview code with SPRITES_API_TOKEN.
# The safe-to-review label remains an explicit maintainer approval for
# follow-up pushes until the maintainer removes it.
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -125,14 +133,106 @@ jobs:
run: pnpm -C apps install --frozen-lockfile
- name: Deploy preview environment
id: deploy
working-directory: apps/backend
run: go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install
run: |
set -euo pipefail
deploy_output="$(go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install --skip-description)"
printf '%s\n' "$deploy_output"
preview_url="$(printf '%s\n' "$deploy_output" | sed -n 's/^preview URL: //p' | tail -n 1)"
test -n "$preview_url"
printf 'preview_url=%s\n' "$preview_url" >> "$GITHUB_OUTPUT"
env:
PR: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
# -----------------------------------------------------------------------
# Update the PR description after deployment.
# This short trusted job owns the shared description-write lock. Keeping it
# separate prevents dependency installation and sprite health checks from
# starving walkthrough or reconciliation writers.
# -----------------------------------------------------------------------
update-description-same-repo:
if: needs.deploy-same-repo.result == 'success'
needs: deploy-same-repo
runs-on: ubuntu-latest
concurrency:
group: pr-description-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: apps/backend/go.mod
cache-dependency-path: apps/backend/go.sum
- name: Update preview description
working-directory: apps/backend
run: |
set -euo pipefail
test -n "$PREVIEW_URL"
go run ./cmd/preview update-description \
--pr "$PR" \
--sha "$SHA" \
--repo "$REPO" \
--url "$PREVIEW_URL"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
PREVIEW_URL: ${{ needs.deploy-same-repo.outputs.preview_url }}
update-description-fork:
if: needs.deploy-fork.result == 'success'
needs: deploy-fork
runs-on: ubuntu-latest
concurrency:
group: pr-description-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: apps/backend/go.mod
cache-dependency-path: apps/backend/go.sum
- name: Update preview description
working-directory: apps/backend
run: |
set -euo pipefail
test -n "$PREVIEW_URL"
go run ./cmd/preview update-description \
--pr "$PR" \
--sha "$SHA" \
--repo "$REPO" \
--url "$PREVIEW_URL"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
PREVIEW_URL: ${{ needs.deploy-fork.outputs.preview_url }}
# -----------------------------------------------------------------------
# Cleanup (all closed PRs — no allowlist check)
@@ -149,7 +249,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -163,9 +262,43 @@ jobs:
- name: Destroy preview environment
working-directory: apps/backend
run: go run ./cmd/preview cleanup --pr "$PR" --repo "$REPO"
run: go run ./cmd/preview cleanup --pr "$PR" --repo "$REPO" --skip-description
env:
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
cleanup-preview-description:
if: >
always() &&
github.event.action == 'closed' &&
github.event_name == 'pull_request_target' &&
needs.cleanup-preview.result != 'cancelled'
needs: cleanup-preview
runs-on: ubuntu-latest
concurrency:
group: pr-description-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: apps/backend/go.mod
cache-dependency-path: apps/backend/go.sum
- name: Remove preview description
working-directory: apps/backend
run: go run ./cmd/preview remove-description --pr "$PR" --repo "$REPO"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}