⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
Mirror this fix exactly: #6191 (fix(github): label/assignee writes skip the installation-token retry every sibling GitHub-write helper uses) established the
withInstallationTokenRetry convention for every GitHub write helper in src/github/. This
issue applies the identical fix to src/integrations/project-tracker-adapter.ts, which never
adopted it.
withInstallationTokenRetry (src/github/app.ts:190-206) evicts and re-mints a stale cached
installation token on a bad-credentials/permission-scope error, retrying the wrapped operation
once with a freshly-minted token. Every GitHub-write helper in src/github/ routes its
installation-token GitHub calls through it (assignees.ts:40, labels.ts:33,88,
comments.ts:145, issues.ts:51, milestones.ts:29,48, pr-actions.ts, repo-doc-pr.ts:170).
GitHubMilestonesAdapter and GitHubProjectsAdapter in src/integrations/project-tracker-adapter.ts
implement the same ProjectTrackerAdapter interface as a GitHub-backed tracker, and their four
real (non-inert) methods each call createInstallationToken directly and pass the token straight
to makeInstallationOctokit, with no retry wrapper anywhere in this file:
GitHubMilestonesAdapter.listOpenMilestones (src/integrations/project-tracker-adapter.ts:79-95)
GitHubMilestonesAdapter.attachToMilestone (:104-116, a write — patches the PR's milestone)
GitHubProjectsAdapter.listOpenProjects (:200-226)
GitHubProjectsAdapter.attachToProject (:235-249, a write — adds the PR to a Projects v2
board)
makeInstallationOctokit (src/github/client.ts:708-742) only wraps requests in a
write-suppression hook for self-hosted dry-run/paused modes and routes fetches through
timeoutFetch for caching/rate-limit-retry purposes (fetchWithGitHubRetry) — neither of those
mechanisms retries a bad-credentials (401) response; that is specifically
withInstallationTokenRetry's job, and it is never invoked anywhere in this file (confirmed: zero
references to withInstallationTokenRetry in src/integrations/project-tracker-adapter.ts). A
stale cached installation token on any of these four calls throws straight through to the caller
(resolveTrackerMatches's .catch(() => []) in the list case degrades this to "no suggestion";
the two attach methods propagate the failure to whatever action executed the attach) instead of
self-healing the way the equivalent GitHub write in src/github/ would.
Requirements
- All four call sites listed above (
listOpenMilestones, attachToMilestone, listOpenProjects,
attachToProject) must route their createInstallationToken + Octokit call sequence through
withInstallationTokenRetry (imported from src/github/app.ts), matching the pattern already
used throughout src/github/.
- Preserve each method's existing return contract and existing non-token error handling exactly —
this issue only adds the self-heal-on-stale-token retry, it does not change any other behavior
(pagination, the projectId/milestoneId validation guards, the inert stub methods, etc.).
- Follow this repo's established convention (see
withInstallationTokenRetry's own usage
elsewhere) of wrapping the whole operation — token mint through the Octokit call(s) that use it —
in the retry, not just the token mint itself.
Deliverables
All of the above Deliverables are required in the same PR.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on every changed line in
src/integrations/project-tracker-adapter.ts.
Expected Outcome
A stale cached installation token on any of the four GitHub-backed ProjectTrackerAdapter methods
self-heals via one retry, matching the convention every GitHub write/read call in src/github/
already follows (#6191), instead of a transient token issue silently degrading to "no
suggestion" (list methods) or failing an attach outright (write methods).
Links & Resources
src/github/app.ts:190-206 (withInstallationTokenRetry, the helper to use)
src/integrations/project-tracker-adapter.ts:79-95, :104-116, :200-226, :235-249 (the four
call sites)
#6191 (the prior, closed instance of this exact bug class for src/github/)
test/unit/project-tracker-adapter.test.ts (existing coverage — does not exercise a
stale-token/401 case for any of these four methods)
Context
Mirror this fix exactly:
#6191(fix(github): label/assignee writes skip the installation-token retry every sibling GitHub-write helper uses) established thewithInstallationTokenRetryconvention for every GitHub write helper insrc/github/. Thisissue applies the identical fix to
src/integrations/project-tracker-adapter.ts, which neveradopted it.
withInstallationTokenRetry(src/github/app.ts:190-206) evicts and re-mints a stale cachedinstallation token on a bad-credentials/permission-scope error, retrying the wrapped operation
once with a freshly-minted token. Every GitHub-write helper in
src/github/routes itsinstallation-token GitHub calls through it (
assignees.ts:40,labels.ts:33,88,comments.ts:145,issues.ts:51,milestones.ts:29,48,pr-actions.ts,repo-doc-pr.ts:170).GitHubMilestonesAdapterandGitHubProjectsAdapterinsrc/integrations/project-tracker-adapter.tsimplement the same
ProjectTrackerAdapterinterface as a GitHub-backed tracker, and their fourreal (non-inert) methods each call
createInstallationTokendirectly and pass the token straightto
makeInstallationOctokit, with no retry wrapper anywhere in this file:GitHubMilestonesAdapter.listOpenMilestones(src/integrations/project-tracker-adapter.ts:79-95)GitHubMilestonesAdapter.attachToMilestone(:104-116, a write — patches the PR's milestone)GitHubProjectsAdapter.listOpenProjects(:200-226)GitHubProjectsAdapter.attachToProject(:235-249, a write — adds the PR to a Projects v2board)
makeInstallationOctokit(src/github/client.ts:708-742) only wraps requests in awrite-suppression hook for self-hosted dry-run/paused modes and routes fetches through
timeoutFetchfor caching/rate-limit-retry purposes (fetchWithGitHubRetry) — neither of thosemechanisms retries a bad-credentials (401) response; that is specifically
withInstallationTokenRetry's job, and it is never invoked anywhere in this file (confirmed: zeroreferences to
withInstallationTokenRetryinsrc/integrations/project-tracker-adapter.ts). Astale cached installation token on any of these four calls throws straight through to the caller
(
resolveTrackerMatches's.catch(() => [])in the list case degrades this to "no suggestion";the two attach methods propagate the failure to whatever action executed the attach) instead of
self-healing the way the equivalent GitHub write in
src/github/would.Requirements
listOpenMilestones,attachToMilestone,listOpenProjects,attachToProject) must route theircreateInstallationToken+ Octokit call sequence throughwithInstallationTokenRetry(imported fromsrc/github/app.ts), matching the pattern alreadyused throughout
src/github/.this issue only adds the self-heal-on-stale-token retry, it does not change any other behavior
(pagination, the
projectId/milestoneIdvalidation guards, the inert stub methods, etc.).withInstallationTokenRetry's own usageelsewhere) of wrapping the whole operation — token mint through the Octokit call(s) that use it —
in the retry, not just the token mint itself.
Deliverables
GitHubMilestonesAdapter.listOpenMilestoneswraps its token+Octokit sequence inwithInstallationTokenRetry.GitHubMilestonesAdapter.attachToMilestonewraps its token+Octokit sequence inwithInstallationTokenRetry.GitHubProjectsAdapter.listOpenProjectswraps its token+Octokit sequence inwithInstallationTokenRetry.GitHubProjectsAdapter.attachToProjectwraps its token+Octokit sequence inwithInstallationTokenRetry.test/unit/project-tracker-adapter.test.tsfor all four methods,asserting a bad-credentials/401 on the first Octokit call is retried once with a
freshly-minted token and succeeds, rather than throwing/propagating on the first attempt.
All of the above Deliverables are required in the same PR.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on every changed line in
src/integrations/project-tracker-adapter.ts.Expected Outcome
A stale cached installation token on any of the four GitHub-backed
ProjectTrackerAdaptermethodsself-heals via one retry, matching the convention every GitHub write/read call in
src/github/already follows (
#6191), instead of a transient token issue silently degrading to "nosuggestion" (list methods) or failing an attach outright (write methods).
Links & Resources
src/github/app.ts:190-206(withInstallationTokenRetry, the helper to use)src/integrations/project-tracker-adapter.ts:79-95,:104-116,:200-226,:235-249(the fourcall sites)
#6191(the prior, closed instance of this exact bug class forsrc/github/)test/unit/project-tracker-adapter.test.ts(existing coverage — does not exercise astale-token/401 case for any of these four methods)