Skip to content

Commit c57e222

Browse files
V48 Gate 3 (specification-implementation): surface why GitHub token regeneration failed instead of a bare Invalid badge (V48-Gate3-F34)
Retested after the F33 fix (deployed and confirmed live by timestamp): Refresh still shows "Invalid." With env vars confirmed present and no way to pull historical Vercel logs for this project, the remaining causes (installation actually revoked on GitHub's side vs. some other bug in the regeneration chain) were indistinguishable from the UI alone. Rather than keep guessing blind, make the failure diagnosable: persist a source-safe last_regeneration_error (GitHub's own API error text, or a plain reason like missing app credentials) on the connection row every time installation-token regeneration is attempted, cleared on success. It flows through the existing metadata sanitizer with no other route changes needed. VCSConnectionCard now shows this reason under the Invalid badge, with a plain-language note that a 401/403/404 means the GitHub App installation was removed — which only a reinstall (not Refresh) can fix. This turns the next occurrence of "Refresh doesn't fix Invalid" into something checkable at a glance instead of another investigation. Also noted (not fixed here): components/base/bitcode/vcs/__tests__/ VCSConnectionCard.test.tsx tests a superseded version of this component and isn't in jest.config.cjs's testMatch, so it silently never runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dbef719 commit c57e222

5 files changed

Lines changed: 186 additions & 7 deletions

File tree

BITCODE_V48_QA.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,13 @@ Track 3-4 scripts (BTD ledger, settlement, pack journaling) get added when those
533533
- Cause (a real bug, found by inspection): the expiry check that decides whether to regenerate reads `connectionData.installation_token_expires_at` (line ~193) — the field `_callback-handler.ts` writes at initial install. But the regeneration's own persistence call, `this.updateTokens(...)`, only ever wrote `token_expires_at` — a DIFFERENT field, never read by that check. So every regeneration attempt "succeeded" (fresh token minted, `validateToken` passes for that one request) yet the field the NEXT check reads never moved off its original, already-past value — forcing a full re-regeneration on every single subsequent call (every Refresh click, every repo/branch/commit fetch that reaches this path) forever. Separately, `GET /api/vcs/[provider]/connection` built the returned status from the `connection` row fetched BEFORE `validateStoredConnection` ran, so even a successful in-request regeneration wouldn't show its fresh `expiresAt`/metadata until a subsequent call.
534534
- Repair: `VCSConnections.updateTokens` now writes `installation_token_expires_at` alongside `token_expires_at` (its only caller is this one GitHub-App regeneration path, so this is safe with no other call sites to consider). `GET /api/vcs/[provider]/connection` now re-fetches the connection after a successful `validateStoredConnection` before building the response, so a same-request regeneration is reflected immediately instead of one Refresh click behind. Note: if the GitHub App installation itself was actually revoked/uninstalled on GitHub's side (not just an expired token), regeneration will keep failing and Disconnect+reinstall genuinely is required then — check https://github.com/settings/installations (or the org's) to tell the two apart. Verified: `tsc --noEmit` 0 (both packages); new `packages/vcs/src/__tests__/connections.test.ts` (2/2, pins both fields moving together and that a fresh token skips re-regeneration); full uapi jest suite 639/640 green (1 pre-existing skip).
535535

536+
### V48-Gate3-F34 — Refreshing GitHub still showed bare "Invalid" after the F33 fix, no way to tell why (FIXED)
537+
538+
- Severity: high (live-confirmed, 2026-07-08: clicked Refresh after the F33 deploy went live — same "Invalid" badge in Auxillaries, same reconnect-required banner on `/deposits`. Confirmed deploy timing: commit `dbef719e` (F33) pushed 11:24:50, its Preview build was `Ready` by ~11:27, the retest screenshot is 11:46 — the fix WAS live when retested, so F33's fix, while a real and necessary bug, was not the reason THIS specific connection stayed Invalid).
539+
- Investigation: with F33 confirmed deployed and env vars confirmed present (F33), the only two remaining explanations are (a) the GitHub App installation was genuinely revoked/uninstalled on GitHub's side (only a real reinstall fixes that), or (b) a different bug in the regeneration call chain. Neither was distinguishable from the UI or from `vercel logs` (historical logs aren't retrievable after the fact for this project's plan/log-drain setup — confirmed by trying).
540+
- Repair (turns the black box into a source-safe diagnostic instead of resolving blind): `VCSConnections` now persists a `last_regeneration_error` (and `last_regeneration_at`) onto the connection row every time installation-token regeneration is attempted — the actual reason on failure (GitHub's own API error text, e.g. a 404 meaning the installation was removed; or `github_app_credentials_not_configured` if the env gate itself was skipped), or cleared to `null` on success. This flows through the existing `sanitizeConnectionMetadata` pass-through into `connectionStatus.metadata` with no other route changes needed (the new keys aren't in the token/secret blocklist). `VCSConnectionCard.tsx` now renders this reason under the badge when `!valid`, with a plain-language hint that a 401/403/404 means reinstalling the GitHub App is the only fix Refresh can't provide. This makes the NEXT occurrence of "Refresh doesn't fix Invalid" immediately diagnosable from the card itself — for this user or anyone — without needing server log access. Verified: `tsc --noEmit` 0 (both packages); new `uapi/tests/vcsConnectionCard.test.tsx` (4/4: no extra UI when valid, reason + reinstall hint on a 404, missing-credentials reason mapped to plain language with no reinstall hint, no extra UI when invalid but no diagnostic recorded yet); full uapi jest suite 643/644 green (1 pre-existing skip); `packages/vcs` suite 7/7 green.
541+
- Noted but out of scope here: `uapi/components/base/bitcode/vcs/__tests__/VCSConnectionCard.test.tsx` is a pre-existing test file for an entirely superseded implementation of this component (mocks `@supabase/auth-helpers-nextjs` directly, expects copy/icons that no longer exist) — it isn't in `jest.config.cjs`'s `testMatch` allowlist, so it silently never runs and was not caught by this drift. Left as-is; a follow-up should either delete it or rewrite it against the current component.
542+
536543
## Track 1 — Identity / Authentication / Auxillaries — COMPLETE 2026-06-12 (email deferred by decision; F2/F9 and legacy eradication queued for gates)
537544

538545
- [x] Sign up / sign in via Connect Wallet (nav CTA → SignUpWindow → wallet signature on testnet4 → `custom:bitcode-bitcoin` session → `/tps/supabase/callback`) — verified 2026-06-12 after F5 fix; lands on `/packs`. Re-verified from fully nuked state (purged user + cleared site data): created 19:29:21 → session 19:29:25 → binding auto-written 19:29:29 by the bridge on `/packs` mount with no Auxillaries visit; UI consistent across nav, Wallet, and Profile panes.

packages/vcs/src/connections.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ interface PersistedConnectionData {
3535
installation_token_expires_at?: unknown;
3636
oauth_token?: unknown;
3737
instance_url?: unknown;
38+
/** V48-Gate3-F34: source-safe reason the last regeneration attempt failed, or null on success. */
39+
last_regeneration_error?: unknown;
40+
last_regeneration_at?: unknown;
3841
[key: string]: unknown;
3942
}
4043

@@ -206,40 +209,60 @@ export class VCSConnections {
206209
const { GitHubAppAuth } = await import('@bitcode/github');
207210
const appId = process.env.GITHUB_APP_ID;
208211
const privateKey = process.env.GITHUB_PRIVATE_KEY;
209-
212+
210213
if (appId && privateKey) {
211214
const githubApp = new GitHubAppAuth({
212215
appId,
213216
privateKey,
214217
clientId: process.env.GITHUB_APP_CLIENT_ID,
215218
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET
216219
});
217-
220+
218221
// Don't request specific permissions - use whatever the installation has granted
219222
// This avoids 422 errors when requesting permissions not available to the installation
220223
const tokenData = await githubApp.generateInstallationToken(
221224
Number(installationConnectionId)
222225
// Omitting permissions parameter to use installation's granted permissions
223226
);
224-
227+
225228
accessToken = tokenData.token;
226-
229+
227230
// Update the connection with new token
228231
await this.updateTokens(connection.id, {
229232
accessToken: tokenData.token,
230233
expiresAt: tokenData.expiresAt
231234
});
232-
235+
// Clear any prior failure diagnostic now that regeneration succeeded.
236+
await this.recordRegenerationDiagnostic(connection.id, null);
237+
233238
log('GitHub installation token regenerated successfully', 'info', {
234239
connectionId,
235240
expiresAt: tokenData.expiresAt
236241
});
242+
} else {
243+
// V48-Gate3-F34: env misconfiguration (missing GITHUB_APP_ID/
244+
// GITHUB_PRIVATE_KEY on this deployment target) silently no-op'd
245+
// here before — the stale token then failed live validation with
246+
// no trace of WHY, indistinguishable from a genuinely revoked
247+
// installation. Persist a source-safe reason so Invalid is
248+
// diagnosable without server log access.
249+
await this.recordRegenerationDiagnostic(
250+
connection.id,
251+
'github_app_credentials_not_configured',
252+
);
237253
}
238254
} catch (error) {
255+
const reason = error instanceof Error ? error.message : 'unknown_regeneration_error';
239256
log('Failed to regenerate GitHub installation token', 'error', {
240257
connectionId,
241258
error
242259
});
260+
// V48-Gate3-F34: persist WHY regeneration failed (source-safe —
261+
// GitHub's own API error text, e.g. installation suspended/
262+
// uninstalled — no tokens/secrets) so a stuck-Invalid connection
263+
// is diagnosable from the connection status alone, without
264+
// needing server logs.
265+
await this.recordRegenerationDiagnostic(connection.id, reason);
243266
// Fall back to OAuth token if available
244267
accessToken = readString(connectionData.oauth_token);
245268
}
@@ -344,7 +367,36 @@ export class VCSConnections {
344367
);
345368
}
346369
}
347-
370+
371+
/**
372+
* V48-Gate3-F34: persist the outcome of the last GitHub App installation
373+
* token regeneration attempt (source-safe reason text only — never a
374+
* token/secret) so a connection stuck on "Invalid" is diagnosable from the
375+
* stored connection alone. Best-effort: a failure here must never mask the
376+
* original regeneration error, so it's swallowed.
377+
*/
378+
private async recordRegenerationDiagnostic(
379+
connectionId: string,
380+
reason: string | null
381+
): Promise<void> {
382+
try {
383+
const connection = await this.connections.getById(connectionId);
384+
if (!connection) return;
385+
const connectionData = asConnectionData(connection.connection_data);
386+
const nextConnectionData = {
387+
...connectionData,
388+
last_regeneration_error: reason,
389+
last_regeneration_at: new Date().toISOString()
390+
} as Database['public']['Tables']['user_connections']['Update']['connection_data'];
391+
392+
await this.connections.update(connectionId, {
393+
connection_data: nextConnectionData
394+
});
395+
} catch (error) {
396+
log('Failed to record regeneration diagnostic', 'warn', { error, connectionId });
397+
}
398+
}
399+
348400
/**
349401
* Delete a connection
350402
*/

uapi/components/base/bitcode/vcs/VCSConnectionCard.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,30 @@ export function VCSConnectionCard({
227227
</div>
228228
)}
229229
</div>
230-
230+
231+
{/* V48-Gate3-F34: Refresh already retries installation-token
232+
regeneration silently — if it still fails, surface WHY
233+
(source-safe: GitHub's own API error text, no tokens) instead
234+
of leaving "Invalid" with no explanation. A 404/"Not Found"
235+
here means the GitHub App installation itself was removed,
236+
which Refresh can never fix — only reinstalling the app can. */}
237+
{!status.valid && status.metadata?.last_regeneration_error && (
238+
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
239+
<p className="font-medium">Last reconnect attempt failed:</p>
240+
<p className="mt-1 break-words">
241+
{status.metadata.last_regeneration_error === 'github_app_credentials_not_configured'
242+
? 'The Bitcode GitHub App credentials are not configured on this deployment.'
243+
: String(status.metadata.last_regeneration_error)}
244+
</p>
245+
{/\b40[134]\b/.test(String(status.metadata.last_regeneration_error)) && (
246+
<p className="mt-1 text-destructive/80">
247+
This usually means the GitHub App installation was removed —
248+
Refresh can&apos;t fix that; reinstalling the app will.
249+
</p>
250+
)}
251+
</div>
252+
)}
253+
231254
<div className="flex gap-2">
232255
<Button
233256
variant="outline"

uapi/jest.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ module.exports = {
153153
'<rootDir>/tests/productAnalytics.test.ts',
154154
'<rootDir>/tests/searchableSelect.test.tsx',
155155
'<rootDir>/tests/vcsFileTreePicker.test.tsx',
156+
'<rootDir>/tests/vcsConnectionCard.test.tsx',
156157
'<rootDir>/tests/bitcodeInlineExplainerAriaLabel.test.tsx',
157158
'<rootDir>/tests/readRouteModel.test.ts',
158159
'<rootDir>/tests/readPageClient.test.tsx',
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import "@testing-library/jest-dom";
2+
import React from "react";
3+
import { render, screen, waitFor } from "@testing-library/react";
4+
5+
import { VCSConnectionCard } from "@/components/base/bitcode/vcs/VCSConnectionCard";
6+
7+
// V48-Gate3-F34: when a stored GitHub connection is Invalid, Refresh already
8+
// silently retries installation-token regeneration (V48-Gate3-F33) — but if
9+
// regeneration itself keeps failing, the card used to show only "Invalid"
10+
// with no way to tell why, indistinguishable from a genuinely revoked
11+
// installation. This suite pins the new diagnostic surfacing.
12+
13+
function mockConnectionFetch(payload: Record<string, unknown>) {
14+
global.fetch = jest.fn().mockResolvedValue({
15+
ok: true,
16+
status: 200,
17+
headers: { get: () => "application/json" },
18+
json: async () => payload,
19+
}) as unknown as typeof fetch;
20+
}
21+
22+
describe("VCSConnectionCard — regeneration-failure diagnostic (V48-Gate3-F34)", () => {
23+
afterEach(() => {
24+
jest.restoreAllMocks();
25+
});
26+
27+
it("shows nothing extra when the connection is valid", async () => {
28+
mockConnectionFetch({
29+
connected: true,
30+
valid: true,
31+
username: "bitcode",
32+
metadata: {},
33+
});
34+
35+
render(<VCSConnectionCard provider="github" />);
36+
37+
await screen.findByText("bitcode");
38+
expect(screen.queryByText(/Last reconnect attempt failed/i)).not.toBeInTheDocument();
39+
});
40+
41+
it("surfaces the source-safe regeneration failure reason when invalid", async () => {
42+
mockConnectionFetch({
43+
connected: true,
44+
valid: false,
45+
username: "advancedengineeredsoftware",
46+
metadata: {
47+
last_regeneration_error: "Failed to generate installation token: 404 Not Found",
48+
},
49+
});
50+
51+
render(<VCSConnectionCard provider="github" />);
52+
53+
await screen.findByText(/Last reconnect attempt failed/i);
54+
expect(
55+
screen.getByText(/Failed to generate installation token: 404 Not Found/i),
56+
).toBeInTheDocument();
57+
expect(
58+
screen.getByText(/GitHub App installation was removed/i),
59+
).toBeInTheDocument();
60+
});
61+
62+
it("maps the missing-credentials reason to a clear message without the reinstall hint", async () => {
63+
mockConnectionFetch({
64+
connected: true,
65+
valid: false,
66+
username: "advancedengineeredsoftware",
67+
metadata: {
68+
last_regeneration_error: "github_app_credentials_not_configured",
69+
},
70+
});
71+
72+
render(<VCSConnectionCard provider="github" />);
73+
74+
await screen.findByText(/Last reconnect attempt failed/i);
75+
expect(
76+
screen.getByText(/GitHub App credentials are not configured/i),
77+
).toBeInTheDocument();
78+
expect(
79+
screen.queryByText(/GitHub App installation was removed/i),
80+
).not.toBeInTheDocument();
81+
});
82+
83+
it("shows nothing extra when invalid but no diagnostic has been recorded yet", async () => {
84+
mockConnectionFetch({
85+
connected: true,
86+
valid: false,
87+
username: "advancedengineeredsoftware",
88+
metadata: {},
89+
});
90+
91+
render(<VCSConnectionCard provider="github" />);
92+
93+
await waitFor(() => expect(screen.getByText("Invalid")).toBeInTheDocument());
94+
expect(screen.queryByText(/Last reconnect attempt failed/i)).not.toBeInTheDocument();
95+
});
96+
});

0 commit comments

Comments
 (0)