Skip to content

fix(github-actions-grafana-jump): read a PR page's jump-links config from the PR's own branch - #55

Open
nsheaps-oura wants to merge 2 commits into
mainfrom
n8bot/pr-page-head-branch-config
Open

fix(github-actions-grafana-jump): read a PR page's jump-links config from the PR's own branch#55
nsheaps-oura wants to merge 2 commits into
mainfrom
n8bot/pr-page-head-branch-config

Conversation

@nsheaps-oura

Copy link
Copy Markdown
Collaborator

The bug

Branch-aware repo config (#51) reads .github/jump-links.config.yaml at the ref you're looking at, but it deliberately left the PR page out: a PR's head branch isn't in its URL, and resolving it looked like it needed an API round-trip. So a PR page always read the default branch's config — on the one page you're most likely to be on while trying a config change out before merging it.

That's now a live problem: two open draft PRs (jouzen/android#28338, jouzen/ios#32201) add a config file that only exists on their own branch, and their PR pages showed nothing.

Reproduced live on jouzen/android#28338: the URL the old code builds is .../jouzen/android/HEAD/.github/jump-links.config.yaml, and that path doesn't exist on main yet (gh api ... ?ref=mainNot Found), while the branch's own copy has a pr page entry ("CI traces for this PR").

What it does now

No API call needed — GitHub's PR header already states the whole merge, with each branch as a link to its own tree. That gets read from the live DOM the same way a workflow run's branch already is (#53), reusing that change's tree-href parsing. branchFromRunTreeHref is therefore now branchFromTreeHref, shared by both headers.

flowchart TD
    A["/org/repo/pull/123"] --> B["head-branch link in the PR header"]
    B --> C{"is its href a branch<br/>of this same repo?"}
    C -->|yes| D["read the config at that branch"]
    C -->|"no (fork PR)"| E["read the default branch's, as before"]
    C -->|"header hasn't rendered yet"| F["default branch; re-checked on<br/>the next DOM mutation"]
    D --> G{"does that branch<br/>have a config?"}
    G -->|yes| H["use it"]
    G -->|"no (e.g. deleted after merge)"| E
Loading

Which element, and why that one

The head branch is one whole href, so unlike the /blob/<ref>/<path> URLs #53 had to disambiguate, a slashed branch name needs no special handling here. What the header actually renders, captured from #54's own page:

<div class="d-flex flex-items-center overflow-hidden gap-1">
  <a href="/nsheaps/greasemonkey-scripts/tree/n8bot/tree-view-repohome"
     data-component="BranchName" ...>n8bot/tree-view-repohome</a>
  <span data-component="Tooltip" ...>nsheaps/greasemonkey-scripts:n8bot/tree-view-repohome</span>
  <button data-component="IconButton" ...>   <!-- "Copy head branch name to clipboard" -->
</div>

The base branch is the same component rendered right next to it, so which one is the head is decided structurally, not by position: only the head branch has the "copy branch name" button beside it, as an element sibling of the link. Matched on data-component, Primer React's own attribute — the class names around it (PullRequestBranchName-module__branchName__SCtl2) are per-build hashes and aren't used.

Fork PRs and merged/closed PRs

Case Live check Behavior
Open PR, same repo jouzen/android#28338 (draft), #54 config read at the head branch
Fork PR cli/cli#14108, head /loganrosen/cli-1/tree/... no override — href is a different repo
Merged PR, head branch deleted #51 head branch still resolvable; its config 404s, so falls back to the default branch
PR sub-tabs #51 Files changed (/pull/51/changes) same header, same answer

A fork PR is skipped for the same two reasons the run page already skips a fork-triggered run: that ref doesn't exist in the repo being browsed, and a fork's copy of the config isn't this repo's config to read — anyone can open a fork PR, so honoring one would let any contributor decide what buttons the page offers.

A merged or closed PR's head branch is usually deleted, and insisting on it would leave those pages with no repo config where before they had the default branch's. So a PR target is the one target marked fallBackToDefaultBranch: if its own ref has no config, the default branch's is read instead. Both reads share the existing per-{org, repo, ref} cache, so the fallback costs one extra request per repo, not one per pass. Pages whose own URL names a ref keep #51's stricter rule — that ref's version is the answer, including when it hasn't got one.

Verification

Live, in the browser (real github.com pages, logged in): ran the compiled DOM-reading logic (prHeadBranchHrefbranchFromTreeHrefrepoConfigUrl) against each page in the table above and confirmed the resolved branch and the resulting raw URL. On jouzen/android#28338 it resolves n8bot/jump-links-config and builds https://raw.githubusercontent.com/jouzen/android/n8bot/jump-links-config/.github/jump-links.config.yaml, where the old code built the HEAD URL that 404s. Also confirmed the header renders the pair twice (page header + the sticky one when scrolled) and both state the same branch.

Code-correct: yarn test — 83/83 pass, including new coverage for a PR head href (slashed name), a fork PR's href, and the three fetchRepoConfigForTarget fallback cases (falls back on a 404, doesn't when the ref has its own config, doesn't at all for a ref the URL itself named). yarn install --immutable, yarn build, yarn lint clean.

Not verified: the finished userscript running as an installed script on a PR page. Same two blockers #54 documents — github.com's CSP blocks both loading the built script into the page and any page-context fetch to raw.githubusercontent.com (which is exactly why the script uses GM.xmlHttpRequest), and installing a dev build into the browser's script manager would change local browser config. So the config-fetch half was checked out of band (gh api for the branch's file and its absence on main, curl for the 404 a deleted branch gives) rather than through the script itself.

Co-Authored-By: Claude noreply@anthropic.com

https://claude.ai/code/session_01LeMSMf28QJFLTiQ25sVM3S

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Userscript Version Preview

Preview only — versions and CHANGELOGs are bumped automatically on merge to main, not in this PR. Manual bumps to a higher version are preserved.

Package Base New Action
github-actions-grafana-jump 0.2.12 0.2.13 will-bump

henry-nsheaps[bot]

This comment was marked as outdated.

nsheaps-oura and others added 2 commits August 12, 2026 00:30
…from the PR's own branch

A PR page kept reading .github/jump-links.config.yaml from the default
branch, so a config added on a branch didn't show up on the pull request
proposing it - the one page you're most likely to be looking at while
trying a config change out before merging it. Branch-aware config
(#51) skipped the PR
page because a PR's head branch isn't in its URL, and resolving it looked
like it needed an API call.

It doesn't: GitHub's PR header already states the merge in full ("wants to
merge N commits into <base> from <head>"), with each branch as a link to
its own tree. That's read from the live DOM the same way a workflow run's
branch already is
(#53), reusing that
change's tree-href parsing - which is why branchFromRunTreeHref is now
branchFromTreeHref, shared by both headers.

- A fork PR's head branch is in the contributor's own repo, so the shared
  href check rejects it and the config keeps coming from the default
  branch, matching what a fork-triggered run page already does.
- A merged or closed PR's head branch is usually deleted, so a PR target
  falls back to the default branch when its own ref has no config, rather
  than leaving the page with fewer links than before.

Verified live against the PR headers on jouzen/android#28338 (open draft,
same-repo), #51 (merged, head branch deleted)
and #54, and cli/cli#14108 (fork).
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeMSMf28QJFLTiQ25sVM3S
… on fallBackToDefaultBranch too

currentRepoConfigKey was built from repoConfigCacheKey(), which only names
org/repo/branch. A file view on branch X (strict, no fallback) and a PR whose
head branch is also X (fallback allowed) produced the same key, so navigating
between them skipped the required re-resolve and left the wrong config
displayed in both directions. Add repoConfigTargetKey() to fold
fallBackToDefaultBranch into the gate key, and use it in checkLocation()
instead of the bare cache key.

Also tighten prHeadBranchHref()'s head-vs-base check: match the "copy branch
name" sibling by its data-component="IconButton" rather than any <button>,
and walk only the link's later siblings via nextElementSibling instead of
treating the link as one of its own siblings.

Flagged by an automated review on #55 (P1 correctness bug, plus two P2 nits
addressed here).
@nsheaps-oura
nsheaps-oura force-pushed the n8bot/pr-page-head-branch-config branch from 00d059d to 5f03090 Compare August 12, 2026 04:40

@henry-nsheaps henry-nsheaps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

All three findings from v1 addressed in 5f03090, cleanly and tightly-scoped. Approving.

Findings (0 open)

No new findings. All prior findings resolved — see the follow-up commit 5f03090 and the "Verification of the fix" section below.

Verification of the fix

P1 — currentRepoConfigKey shared across strict/fallback targets (prior comment):

  • New repoConfigTargetKey() (src/index.ts:1256-1259) folds fallBackToDefaultBranch into the gate key by appending +fallback — the two Scenario A/B page transitions from the prior comment now hit the repoKey !== currentRepoConfigKey path and re-resolve as required.
  • checkLocation() (src/index.ts:2548) uses repoConfigTargetKey(repoCtx) instead of the bare cache key.
  • The repoConfigTargetKey's own docstring cross-references checkLocation() and spells out the exact file-view-vs-PR scenario, so the entanglement is on the record where a future reader will see it (resolving prior note #4 too).
  • Tests: the new repoConfigTargetKey tells apart a strict target from a fallback one at the same branch test pins the key contract, and cross-navigation between a file view and a same-branch PR fetches config differently each way exercises the module-level repoConfigCache state that made the leak reachable — walking file → PR → file at the same branch and checking each direction returns the correct result. Both tests explicitly name Scenario A/B in comments, which will save the next reviewer a hop.
  • Fetch-behavior guard: the checkLocation() in-flight-response check (if (currentRepoConfigKey !== repoKey) return;) still closes over the new fallback-aware repoKey, so a mid-flight navigation from strict→fallback (or vice versa) at the same branch drops the stale result rather than applying it — no new race introduced.

P2 — head-vs-base heuristic hardening (prior comment):

  • prHeadBranchHref() (src/index.ts:2262-2270) now matches the sibling on data-component="IconButton" rather than any <button>, so a future unrelated <button> GitHub adds beside the link (toolbar, kebab, etc.) won't false-positive.
  • The document-wide a[data-component="BranchName"] selector is untouched — a reasonable defer, given "first match wins" still lands on the header (page + sticky both state the same branch, and no other BranchName-with-IconButton-sibling exists on a PR page today). The narrower sibling check is the higher-value half of the pair.

P2 nit — sibling loop (prior comment):

  • Replaced with the nextElementSibling walk. As a bonus, only later siblings are checked, which correctly mirrors the DOM order shown in the comment above the function (button always follows the anchor) — tighter than what I originally suggested.
How I evaluated
  • Read the second commit's diff (5f03090) end-to-end against the v1 findings, then re-read the surrounding context (RepoConfigTarget docstring, fetchRepoConfigForTarget, checkLocation()'s in-flight-fetch guard) to check the fix doesn't create a new gap.
  • Traced the two Scenario A/B page transitions from the prior P1 comment against the new repoConfigTargetKey() to confirm the gate now sees them as different targets.
  • Read the two new tests (repoConfigTargetKey tells apart…, cross-navigation between a file view and a same-branch PR…) and traced the module-level repoConfigCache state across the three sub-fetches to confirm the third-call assertion (null again) actually depends on the fix rather than passing by coincidence.
  • Confirmed nothing else in the diff changed since v1 (only the second commit's four localized edits + two tests), so the v1 strengths still stand — not repeating them.
  • Did not run the test suite; findings are from static reading of the diff and the PR's declared test results.

🤖 Automated review · Claude Opus 4.7 · job

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.

1 participant