Skip to content

feat(web): issue badge route + share/OG card on issue permalinks (#54 PR2b)#63

Open
lukaso-bot wants to merge 1 commit into
mainfrom
feat/issue-badge-og
Open

feat(web): issue badge route + share/OG card on issue permalinks (#54 PR2b)#63
lukaso-bot wants to merge 1 commit into
mainfrom
feat/issue-badge-og

Conversation

@lukaso-bot

Copy link
Copy Markdown
Collaborator

What

Completes the deferred share story of PR2 (#62) for issue permalinks — the third slice of #54.

  • New /i/:owner/:repo/:number/badge.svg route + federated /h/:host/i/:path/:number/badge.svg. badgeRoute disambiguates issue-vs-PR on the path (both carry :number) and reuses the issue#N cache key the permalink already warms.
  • Badge states mirror the PR badge:
    • released → version tag
    • fix-merged-not-shipped → "not yet"
    • still-open → "not yet" (gold; flips once the fix ships, mirrors an open PR)
    • closed-without-fix → "unknown"
  • Resolved + not-yet issue permalink pages now carry the same share actions + per-commit OG card that PR pages already have. Dropped the hideShare deferral from PR2 and removed the now-dead prop from ResultCard.

Why

PR2 (#62) shipped issue permalinks but deferred the badge + share/OG because the PR badge route keys off :number and would mis-read an issue as a PR. This adds the dedicated issue routes so issue permalinks reach parity with PR permalinks.

Tests

Tests-first: 5 new integration tests (calm-state badges, federated registration, fully-resolved issue page asserting share actions + og:image = per-commit card). Full validate.sh green locally (build / typecheck / 207✓ web tests / a11y / lint / shellcheck / actionlint / gitleaks / publint / osv). Pre-push gate green.

After merge + deploy

Verify a resolved issue permalink shows the "as Badge" action and /i/<o>/<r>/<n>/badge.svg returns the version SVG.

Follow-ups (not in this PR)

  • PR2c: issue-aware OG card that says "Issue #N" instead of the commit card (optional polish).
  • PR3: CLI parity (git-released <issue-url>).

…PR2b)

PR2 (#62) shipped the issue permalink but deferred its badge and the
in-page share actions, because the embedded badge needs its own route:
the PR badge route keys off `:number` and would mis-resolve an issue as
a PR.

- New `/i/:owner/:repo/:number/badge.svg` + federated
  `/h/:host/i/:projectPath/:number/badge.svg`. badgeRoute disambiguates
  issue-vs-PR on the path (both carry `:number`) and reuses the exact
  `issue#N` cache key the permalink page warms. States: released → tag,
  fix-merged-not-shipped → "not yet" (gold), still-open → "not yet"
  (gold, so it flips once fixed+shipped, mirroring an open PR),
  closed-without-fix → "unknown" (never flips).
- Resolved + not-yet issue pages now carry the same share actions +
  dynamic per-commit OG card as PR/commit pages (drop the deferral).
- Remove the now-unused `hideShare` prop from ResultCard (its only
  purpose was this deferral).

Tests-first: issue badge calm-states (open/closed-without-fix/invalid),
federated route registration, and a fully-resolved issue page asserting
the share actions + per-commit og:image. Full validate.sh green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

In-house adversarial review (Copilot quota-blocked here → no automated review will land)

Per review-policy.md, Copilot is perpetually quota-blocked on this repo, so I ran a skeptical, correctness-first pass with a distinct lens. The stated risk is the badge disambiguation on the shared :number path — I verified that first, against source.

Correctness — issue-vs-PR disambiguation on the shared :number path ✅

badgeRoute is one handler for all six routes, so it must re-derive which literal matched. isIssueBadgePath does it on new URL(req.url).pathname with two anchored regexes: ^\/i\/ and ^\/h\/[^/]+\/i\/. Checked against every route:

  • /i/:owner/:repo/:number/badge.svg^\/i\/ matches → issue ✅
  • /p/... → neither matches → pr ✅
  • /h/:host/i/:projectPath/... → second regex matches (host is one slash-free segment, then /i/) → issue ✅
  • /h/:host/p/... → requires /i/ after host → no match → pr ✅

projectPath is encodeURIComponent'd into a single %2F-joined segment, and URL#pathname does not decode %2F, so no embedded slash can shift the marker position or fake an /i/. The ^ anchors defeat any later-in-path /i/. No false-positive or false-negative across the route set.

Cache key reuse ✅

Badge sets keyPart = issue#${n}, which is exactly the key the issue permalink page warms (issue.tsx:95), mirroring pr#${n} (pr.tsx:85). So a page visit (or a prior badge fetch) warms the badge and vice-versa. Verified both keys.

Error-state mapping ✅

  • Still-open issue → IssueNotClosedError → new branch returns "not yet" gold + SHORT_CACHE so the proxy re-fetches and it flips once the fix ships. Correct analog of the open-PR (PrNotMergedError, open) branch right above it.
  • Closed-without-fix → IssueClosedWithoutFixErrornot special-cased, so it correctly falls through to the "unknown" fallthrough (never flips on its own).
  • Fixed-but-not-yet-released issue (closer merged, no release contains it yet) → handled by the pre-existing generic path (status==='ok' with no firstRelease, or status==='not_yet') → "not yet" + SHORT_CACHE. No new code needed, and none added — correct.

Both new error branches are covered by tests (open → gold/300, closed-without-fix → unknown/300), plus non-numeric :number short-circuits to "unknown" without an upstream fetch.

Share re-enable is end-to-end ✅

The client permaPath (layout.tsx) was already kind-aware and emits /i/... (and federated /h/host/i/enc/N) for issue results; the badge markup is perma + '/badge.svg'. So the only thing that made hideShare necessary in PR2 was the missing server route — which this PR adds. Dropping hideShare now resolves instead of 404ing, for GitHub and federated alike. The dead hideShare prop is fully removed from ResultCard/NotYetReleased (typecheck green confirms no dangling caller).

Non-blocking observation (not a merge blocker)

The not-yet issue page enables share but does not pass ogResult to Layout, so its unfurl uses the text ogFallbackTitle, not the dynamic per-commit card. That actually matches the PR pages — pr.tsx sets ogResult only on the resolved page, not renderPrNotYetReleased — so parity with PR behavior holds. The only imprecision is the issue.tsx header comment, which groups "Resolved and not-yet issue pages carry the same ... per-commit OG card"; the per-commit OG card lands on the resolved page only. Doc-precision nit; behavior is correct and consistent with PR pages.

Verdict: correctness verified against source (disambiguation, cache-key reuse, and error mapping in particular); no blocking issues; mergeable with CI green (8/8). Posted as a comment, not an approve — GitHub won't let the PR author self-approve. Real proof is post-merge + deploy: a resolved issue permalink shows the "as Badge" action and /i/<o>/<r>/<n>/badge.svg returns the version SVG.

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