Skip to content

website-comment: add per-pull-request website preview actions - #1332

Open
potiuk wants to merge 14 commits into
apache:mainfrom
potiuk:pr-preview
Open

potiuk wants to merge 14 commits into
apache:mainfrom
potiuk:pr-preview

Conversation

@potiuk

@potiuk potiuk commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Composite actions that publish an armed pull request's built site to <site>-pr<N>.staged.apache.org, with a review overlay that turns a marked region into a screenshot and a link to its diff line. Extracted from apache/magpie-site (in sync up to #219), with generic names.

  • preview-annotate stamps source lines for Jekyll and Astro builds.
  • preview-artifact packages any static site, plus the diff-line anchors computed in the unprivileged build.
  • preview-publish publishes, retires and cleans up. A PR is armed by the preview label (added directly, by a /show-preview comment, or by dispatch), and the publisher runs on workflow_run / issue_comment, never pull_request_target. Every GitHub response is projected, so it never reads PR content.
  • preview-main-overlay (optional) puts the same overlay on the published site, where a comment opens an issue.

Test plan

  • npm test in website-comment/ (163 tests, including boundary.test.mjs: a full publisher run against baited responses and an endpoint allowlist)
  • website-comment-test.yml: Jekyll adapter end to end (on ubuntu-slim, Ruby from apt), preview-main-overlay on a small site, and preview-publish refusing a pull_request event
  • uv run pytest scripts/test_release_actions.py

🤖 Generated with Claude Code

@potiuk

potiuk commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

Extracted it from Magpie work - while working on new website, I developed this action/tooling that makes PR to the website possible to render on staging system of ASF and select areas of the website and get the comments back to the actual lines of code in PR that created it.

Screenshot 2026-09-27 at 18 48 44

How it looks like after selecting the part of the website:

Screenshot 2026-09-27 at 21 14 06

And then you get redirected to the line in the PR that produced the selected part -> and you can ctrl+v the screenshot with the area selected.

@potiuk

potiuk commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

Updated screenshots from Magpie new site - also works on published site - not only on PR:

Screenshot 2026-09-29 at 06 05 22

@potiuk potiuk changed the title pr-preview: add per-pull-request website preview actions website-comment: add per-pull-request website preview actions Sep 29, 2026
@potiuk

potiuk commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Gentle ping @dfoulks1 @ppkarwasz - this has been waiting for review for a while; CI is green. A quick look would be appreciated.


Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting

@potiuk

potiuk commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

@dfoulks1 @ppkarwasz @dave2wave @raboof second ping: this has been open since 2026-09-27 with green CI and still no review. Could one of you review it this week? If you can't get to it, please say so and I'll find another reviewer.


Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting

@raboof

raboof commented Oct 19, 2026

Copy link
Copy Markdown
Member

A quick look would be appreciated.

Wouldn't a PR introducing a whole new action deserve more than a 'quick look'?

If you can't get to it, please say so and I'll find another reviewer.

I don't expect to find time to review this action.

@sebbASF

sebbASF commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Does the website-comment directory tree vary between websites, or could it be shared from a common location?

@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Wouldn't a PR introducing a whole new action deserve more than a 'quick look'?

Absolutely.

Does the website-comment directory tree vary between websites, or could it be shared from a common location?

The website-comment tree is the reusable action. All that is needed by the site that wants to use is to install the action from apache/infrastructure-actions/website-comment. This tree is not needed at all by the website that uses it.

The plugin works on two levels:

a) it modlfiles generated HTML to inject the "comment" functionality in generated static HTML - this is totally independent on how the HTML is generated or which static page generator is used. It just works on any generated HTML.

b) It adds support for linking the "source" of the HTML page - depending on which static page generator is used - using different plugins or custom code - but this is mostly in documentation on how to do it.

I don't expect to find time to review this action.

If I may suggest in general -> I generaly stopped doing line-by-line reviews except having a quick glance at a code to see if it is readable, has good structure, and if I understand what it is supposed to do) - of course except some really critical areas. Line-by-line review of everything are a bit relic of a past - instead adding some guardrails and doing adversarial reviews (several of them) serves me to check if:

a) claimed functionality is there
b) no security issues are there
c) no performance issues/ edge cases can be found
d) architecture is good and readable

(and you can

I am recently using roborev - https://www.roborev.io/docs/ -> which does adversarial reviews using different LLMs and several times in a loop and it finds all kinds of issues - until subsequent reviews do not find any more any issue (it happens automaticallly). I will re-run it now on this one and see if I can fix some of those issues - the PR has been created quite a long time ago so maybe I will find something. And I also heartily recommend this kind of review. When backed by unit tests, functionality tests - this approach greatly speeds up review time and actually increases it's quality IMHO - the number of issues I found this way and quality of code that gets as a result is rather impressive.

potiuk added a commit to potiuk/infrastructure-actions that referenced this pull request Oct 19, 2026
Port magpie-site's switch from the Babel to a Vite Astro adapter (apache#220);
the site now hands the plugin its own vite module. Bump setup-node to
v7.1.0 as magpie-site did.

Fix the roborev findings on apache#1332: encode branch names in ref deletes,
delete stale head branches under a lease, read the zip size from the
trailer only, lift the unzip listing buffer, release on website-comment
pushes, re-arm over a non-writer's label, make the publisher login
configurable, pick pull_request builds only, keep Jekyll stamping out of
quoted attributes, skip no-newline diff markers, and inject the overlay
into pages with an uppercase or omitted </body>.

Generated-by: Claude Opus 5
Three composite actions that publish an armed pull request's built site
to <site>-pr<N>.staged.apache.org, with a review overlay that turns a
marked region into a screenshot and a link to its diff line.

preview-annotate stamps source lines for Jekyll and Astro builds;
preview-artifact packages any static site; preview-publish publishes,
retires and cleans up. Extracted from apache/magpie-site, with
generic names.

Generated-by: Claude Opus 5
Ports apache/magpie-site apache#208, apache#210, apache#212 and apache#214 onto the extracted
actions, keeping the generic names:

- Arm with the `preview` label (a /show-preview comment or a dispatch
  adds it); publish on workflow_run / issue_comment, not a schedule.
- Project every GitHub response so the publisher never reads PR
  content; the build computes the diff anchors (write-meta.mjs).
- Post a new comment on each publish, and lead with the preview URL.
- New preview-main-overlay action: the overlay on the published site,
  where a comment opens an issue.

Generated-by: Claude Opus 5
The actions keep their names and tag prefixes; only the path changes,
to website-comment/<action>.

Generated-by: Claude Opus 5
Ports apache/magpie-site apache#211: ubuntu-slim cannot start a job
container, so Ruby, Bundler and a C toolchain come from apt and gems go
to a writable user gem home instead of the pinned ruby:3.3 image.

Generated-by: Claude Opus 5
Port magpie-site's switch from the Babel to a Vite Astro adapter (apache#220);
the site now hands the plugin its own vite module. Bump setup-node to
v7.1.0 as magpie-site did.

Fix the roborev findings on apache#1332: encode branch names in ref deletes,
delete stale head branches under a lease, read the zip size from the
trailer only, lift the unzip listing buffer, release on website-comment
pushes, re-arm over a non-writer's label, make the publisher login
configurable, pick pull_request builds only, keep Jekyll stamping out of
quoted attributes, skip no-newline diff markers, and inject the overlay
into pages with an uppercase or omitted </body>.

Generated-by: Claude Opus 5
Matches the pin main adopted in apache#1407.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Ran an adversarial review of this PR (roborev, codex agent) and checked every finding against the code; all 11 held up. Fixes are in c7d0318, rebased onto current main, plus ce5623c pinning the test runner to ubuntu-26.04 as #1407 did.

Review findings, fixed

# Finding Fix
1 deleteBranch put the raw name in the ref path, so fix#123 deleted fix Each path segment is URL-encoded
2 Head-branch reap deleted on tips read before the PR lookups, so a push in between could be lost Delete via git push --force-with-lease=<ref>:<sha>, so a moved branch is kept
3 Zip size regex could match an entry named 0 bytes uncompressed Size read only from the unzip -Z trailer line
4 release-actions.yml push paths missed website-comment/**, so no auto-releases Path added, plus a test that every watched path is covered by the trigger
5 /show-preview over a triager's label armed one run only, then the preview was retired A command or dispatch replaces a label not backed by write access
6 A non-default token was never recognised as the publisher (reactions, labels, status comments) New publisher-login input; status comments are now edited only when the publisher's own login wrote them
7 Build lookup accepted push/dispatch runs that carry no preview-site Filter on event=pull_request
8 Jekyll stamping matched tags inside quoted attributes (title="<b>x</b>") Quote-aware scan that carries state across Liquid and lines
9 \ No newline at end of file advanced the line cursor Marker skipped
10 No overlay on pages with </BODY> or an omitted </body> Case-insensitive, with fallback before </html> or at the end
11 unzip listings hit execFile's 1 MiB buffer at about 10k files 256 MiB buffer, 100k-entry cap, clear error on overflow

Synced from apache/magpie-site

Compared against magpie-site main: the library, overlay and vendored code had no logic changes since the #214 sync. Ported what changed:

  • The Astro adapter is now a Vite plugin (magpie-site chore: update erisu/apache-rat-action #220). The site passes in its own vite, since the action's checkout has no node_modules. preview-annotate outputs vite-plugin / PREVIEW_VITE_PLUGIN in place of babel-plugin; the README shows the new astro.config.mjs wiring.
  • setup-node bumped to v7.1.0.

Tests: 180 node tests and 40 script tests pass locally, and new tests cover each fix. The Jekyll suite runs in CI only: the quote-aware stamping was checked locally against stubbed Jekyll classes.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Try each successful build of a shared head commit until one is this PR's,
look up preview-site by name instead of one page of artifacts, remove
every extracted artifact after use, and repost a publish's comments when
the push landed but they did not. The production check now flags only
real data-preview-src attributes, and the Pelican example uploads the
tempdir the action builds into.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Second roborev pass (codex) on the rebased branch found 7 more issues in the original preview code, not in the first round's fixes. Six are fixed in 3f29409; the seventh is deferred (below).

# Finding Fix
1 Two PRs sharing a head commit got the same newest build, whose metadata names only one of them Up to 5 successful pull_request builds are tried, newest first, until one's metadata names this PR; metadata validation unchanged
2 If the push landed but the status or announcement comment failed, later runs saw the branch as current and never posted them When the branch is current, the run checks for its own comments for that SHA and posts whichever is missing, without pushing
3 Artifact lookup read one page of results, so preview-site could be missed Requested by name (?name=preview-site)
4 Pelican example: with publish: false the action leaves the site in tempdir, so path: output did not exist Example sets tempdir: ${{ runner.temp }}/site and uploads that
5 Production check failed on any data-preview-src text, including escaped docs examples Fails only on an HTML element carrying the attribute; a new CI step checks a docs-style page passes
7 Extracted artifacts were never removed, so a run over many PRs could fill the runner fetchArtifact returns cleanup(), called for every artifact fetched, published or not

Deferred: html2canvas-pro's default 100-resource cache can drop images from screenshots of image-heavy pages. Not reproduced yet; it needs a test page with more than 100 images.

Tests: 184 node tests and 40 script tests pass locally; new tests cover the shared head commit, comment repair, cleanup and the named-artifact lookup.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Report an archive the fetcher refuses and try older builds, instead of
failing the run on every trigger; failed downloads still fail it. Keep
artifact-controlled names and values out of refusal comments. Stamp
Astro paths relative to the repository root, and pin setup-node to
v7.1.0 everywhere.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Third roborev pass, this time with Claude, on 3f29409: 5 findings. Four are fixed in 969b650; the screenshot cache stays deferred as noted above.

# Finding Fix
1 A refused archive (too big, too many entries, unsafe names, symlinks, unreadable) threw out of the publish loop: no status comment, older builds not tried, and a red run that re-downloaded the archive on every trigger Refusals are reported as "could not be published" and the next older build is tried. Failed downloads still fail the run, so they are retried. A zip that unzip cannot read now counts as refused
3 Refusal comments repeated text the PR controls (archive entry names, the artifact's pr value), letting a PR post @-mentions or links in the bot's voice Reasons are fixed text and counts; entry names go to the Actions log only. New test plants a link-shaped symlink name and a bogus pr value and checks neither reaches the comment
4 The Astro plugin stamped paths relative to process.cwd(), so a site in a subdirectory never matched the diff anchors Paths are relative to the repository root: PREVIEW_SRC_ROOT, else the nearest directory holding .git, as the Jekyll adapter does
5 setup-node pinned at v7.0.0 in preview-artifact and preview-main-overlay, v7.1.0 elsewhere All four pinned to v7.1.0

Tests: 188 node tests and 40 script tests pass locally.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Trust a label from the publisher's login only next to its arming record,
since github-actions[bot] is shared by every workflow in the repository.
Size the screenshot cache to the page, encode source-link paths, inject
the overlay into .htm pages and inside one function scope, refuse Jekyll
sites on the github-pages gem, detect entry scripts by real path, and
check out the PR head for annotated builds in the README example.

Generated-by: Claude Opus 5
Accept a publisher-login label only with an arming record made after it,
so an old arming cannot vouch for a label re-added after a disarm. Inject
the overlay without lowercasing and as latin1, refuse malformed metadata
per build, keep tombstoned branches for 30 minutes, skip re-screening an
artifact already refused for the head, and tighten the github-pages
check in prepare.sh.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Rounds 4 and 5 of roborev review (codex and Claude each time) found 15 issues between them, several in the earlier fixes themselves. All are fixed: round 4 in f7c3108, round 5 in c4d0f06. That includes the screenshot cache deferred earlier.

Arming (the important one)

  • The default github-actions[bot] is shared by every workflow in the repo, so a preview label added by an auto-labeller used to count as the publisher's own and publish unreviewed content to staging. A label from the publisher's login now counts only next to the publisher's own arming record for that label event: its acknowledgement of a writer's /show-preview, or its dispatch comment (now marked), created at or after the label was added. A record from an earlier arming, before a disarm, no longer vouches for a re-added label. Tested: never armed, arm -> remove -> bot re-add, dispatch. The README recommends a dedicated App token with publisher-login where other workflows post as that login.

Publisher

  • Tombstoned branches are deleted only once the tombstone is 30 minutes old. Runs are event-driven and can be seconds apart, so deleting earlier could strand the old preview on staging.
  • Malformed metadata (such as {"pr": {"toString": null}}) is refused per build like any other refusal; the next older build is tried and every rejected one is cleaned up. Fields are type-checked before coercion.
  • A refusal comment names the head commit, and later runs skip re-downloading while it still matches the current head. A new commit or a dispatch tries again.
  • Entry scripts detect "run directly" by real path, so a symlinked work directory or a path with a space no longer exits 0 having done nothing.

Overlay

  • Insertion searches the page as-is: lowercasing first shifted the index after any U+0130 and broke </body>. Pages are read and written as latin1, so non-UTF-8 pages keep every byte.
  • Inlined helpers sit in one function scope, so they no longer become page globals (relevant on the live site via preview-main-overlay).
  • Injected into .htm and any-case .html pages.
  • Source links encode the path (docs/C#.md -> C%23.md).
  • The screenshot cache is sized to the page, 100 to 10,000 entries, so image-heavy pages no longer lose images.

Jekyll adapter and docs

  • prepare.sh refuses a site using the github-pages gem (which forces safe mode without _config.yml). Only live gem "github-pages" lines or lockfile specs count, so the jekyll new template's commented-out line passes, and the path is quoted, so a source directory with a space is handled.
  • The README build example checks out the PR head, so stamped lines match the diff anchors.

Tests: 203 node tests and 40 script tests pass locally.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Bind the publisher's arming record to the label event's id rather than
its time, recorded hidden in the explainer comment (an edit notifies
nobody), the dispatch comment, or a short comment on a bot's PR. Treat
another PR's build of a shared head as not this PR's, remember refused
build ids so a new build is still screened, skip unchanged status
writes, and match gem("github-pages") in prepare.sh.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

Round 6 of roborev review (codex and Claude, on c4d0f06): 4 findings, all edge cases in the latest fixes. Claude's verdict was Pass; all four are fixed anyway in a4b75c8.

# Finding Fix
1 Arming records were matched to the label by time, so a label removed and re-added by another workflow in the seconds before the publisher's acknowledgement could inherit the old arming Bound by identity: after adding the label, the publisher reads back that label event's id and records it hidden (event=<id>) in its explainer comment (edited in place, no notification), its dispatch comment, or a short "Preview armed" comment on a bot's PR. A publisher-login label arms only when the latest label event's id is recorded. If the events API has not caught up after a few reads, the run fails before acknowledging, so the next run retries. The rocket reaction stays as the "command handled" signal
2 Refusals were remembered per head SHA, so another PR's build of a shared head commit, or a refused build later rebuilt, blocked this PR's own build A build whose metadata names another PR is "not this PR's build", so the status is waiting, not refused. The refusal comment carries the refused build ids hidden, and downloads are skipped only while no newer build of the head exists
3 gem("github-pages") was missed when there is no lockfile Parenthesised form matched; the commented-out template line and github-pages-health-check still pass
4 The waiting status was rewritten on every run Status writes are skipped when the text is unchanged

Tests: 208 node tests and 40 script tests pass locally. Another review pass is running on a4b75c8.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

@sebbASF

sebbASF commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Does the website-comment directory tree vary between websites, or could it be shared from a common location?

The website-comment tree is the reusable action. All that is needed by the site that wants to use is to install the action from apache/infrastructure-actions/website-comment. This tree is not needed at all by the website that uses it.

If the tree is an action, why is the full code included in the PR?
Surely the point of actions is that they can be invoked from a shared source?

Record an arming only when the label changed exactly once while being
added, and accept the record only where the publisher writes it. Make
sure a retired PR says so before its tombstone branch goes, remember
passed-over artifacts by artifact id one by one, and match a split
data-preview-src tag in the production check.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026 •

Copy link
Copy Markdown
Member Author

This is now reviewed adversarialy - with codes

Does the website-comment directory tree vary between websites, or could it be shared from a common location?

The website-comment tree is the reusable action. All that is needed by the site that wants to use is to install the action from apache/infrastructure-actions/website-comment. This tree is not needed at all by the website that uses it.

If the tree is an action, why is the full code included in the PR? Surely the point of actions is that they can be invoked from a shared source?

Yes. This is that shared code - same as sibling actions in this repository already (see the README change in this PR) - https://github.com/apache/infrastructure-actions/blob/main/README.md#available-github-actions

This repository (infra-owned) serves dual purpose:

a) manages list of allowed actions that can be used in ASF
b) exposes shared infrastructure-approved actions that can be used by the PMCs

This PR is adding new action to be used alongside those three already present here and used by PMCs:

Steps run under bash -e -o pipefail, and the per-file loop ended on a
non-matching grep, so find returned non-zero and the clean-site case
aborted the step.

Generated-by: Claude Opus 5
@sebbASF

sebbASF commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

If the tree is an action, why is the full code included in the PR? Surely the point of actions is that they can be invoked from a shared source?

Yes. This is that shared code - same as sibling actions in this repository already (see the README change in this PR) - https://github.com/apache/infrastructure-actions/blob/main/README.md#available-github-actions

Duh! Sorry, my bad.

Replacing a non-writer's label removes it first, and a lagging events
API can show that removal next to the new label event, which the strict
read-back refused on every run, so such a PR never armed.

Generated-by: Claude Opus 5
@potiuk

potiuk commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

OK. I've done a series of adversarial reviews with different models, finding > 20 things to fix - moslty small edge cases, but few quite interesting bugs.

Unless I hear otherwise - I will merge it in a day or two - I incorporated all the changes that I implemented in magpie-site where it came from - and switching magpie-site - to this shared action will be the ultimate test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants