Skip to content

bug: the account-deletion preview promises to remove GitHub installations, but only the Fledge row is deleted #841

Open

Description

@HugoXK

Written-by: Fledge Platform Product

Problem

The account-deletion preview (#691, merged as #824) lists the account's GitHub installations under "将要移除的内容 / What will be removed". The delete route never talks to GitHub: it deletes Fledge's own row and stops there. A superadmin reads that N installations will be removed, confirms, and the organization still shows the Fledge App installed with repository access.

Expected vs actual

Expected. On a destructive preview, every row under "What will be removed" is something the confirm actually removes. If Fledge cannot or must not uninstall the App, the page says so on the page and names where the operator revokes it.

Actual. The row GitHub 安装 / GitHub installations (src/views/pages.ts:4124, inside the "What will be removed" heading at :4141) counts store.listInstallations(user.id) and promises removal. The teardown (src/app.ts:5186-5194) is cancel jobs → stop and remove instances → revoke tokens → purgeUserRows; no GitHub call is made. purgeUserRows deletes the github_installations row through the users cascade (src/db.ts:970 lists it in PURGE_USER_TABLES, :2997-2999 performs the delete). src/repos/app.ts has only GET /app/installations (:166), GET /app/installations/:id (:175) and the token mint (:192) — there is no DELETE /app/installations/:id anywhere in the tree. The team's own record says it plainly at docs/security.md:452: "deletes the GitHub installation row with the account; it does not call GitHub to uninstall the App installation."

The GitHub side is not a row. The installation keeps its repository grant, keeps delivering webhooks to Fledge, stays in the account's or organization's installed-apps list, and GET /app/installations (src/repos/app.ts:166) still enumerates it from the App side. The danger-zone paragraph above the link makes the same promise in prose (src/views/pages.ts:4080: "删除仓库与全部关联数据" / "erases its repositories and related rows"). The operator has no way to learn from Fledge that the GitHub-side grant survived.

Minimal reproduction (in memory only — never the live database)

  1. new Store(':memory:'), one approved superadmin, one approved target user, and store.saveInstallation({ installationId: 1, userId: target.id, accountLogin: 'org-x', accountType: 'Organization' }).
  2. GET /admin/users/<target>/delete as the superadmin: the table renders GitHub installations 1 under "What will be removed", and no sentence anywhere on the page says the GitHub App installation stays.
  3. POST /admin/users/<target>/delete with the exact login: store.listInstallations(target.id) is now [], and an injected fake GithubApp received no request at all.
  4. On a real deployment the same confirm leaves the organization's Settings → GitHub Apps showing Fledge with access to the same repositories.

Proposed red test

In test/adminUserDelete.test.ts, extend the preview case (the one that currently asserts /instances?/i and /API tokens?/i at :141-142): render the page in both locales and assert it names the GitHub-side installation as retained and tells the operator where to remove it, and that the row label no longer reads as removal of the GitHub install. It fails at 9f2ef3aa: the page carries no such sentence and the row is labelled GitHub installations.

If the team later chooses to call GitHub in the teardown, the test becomes: a fake GithubApp receives DELETE /app/installations/<id> exactly once per installation before purge_rows, and a failure there is reported as a named step (not a silent success).

Smallest mergeable slice (<= 1 week)

Copy only, one PR with its test:

  • relabel the row to Fledge's own record ("GitHub 安装记录 / GitHub installation records");
  • add one bilingual sentence to adminDeleteUserPage and to the danger-zone paragraph at src/views/pages.ts:4080: Fledge deletes its record; the GitHub App installation itself remains on the account or organization and is removed on GitHub's installed-apps page;
  • one test in test/adminUserDelete.test.ts pins both locales.

Tier: T2 (src/views/pages.ts + a test; no route, no schema).

Not in this card

  • Calling DELETE /app/installations/:id from the teardown. That is a decision, not a copy fix: github_installations.installation_id is the primary key with an upsert (src/db.ts:3073-3081), so one Fledge account owns the row that every repository connected through that installation depends on — uninstalling on one account's deletion can cut another Fledge user's access. It needs its own card with the shared-installation rule and an owner decision.
  • Any change to purgeUserRows, the teardown order, the retention audit row, or the block conditions.
  • The hard-coded "本地加密副本 30 天,GitHub 14 天" backup sentence at src/views/pages.ts:4140; it is a separate claim and currently true for this deployment's configuration.

Priority

P2. No data is lost by the deletion itself, but the preview states a removal that does not happen, in the most destructive flow the platform has, and the operator's belief about the organization's GitHub access is wrong. The honest fix is a few lines of copy plus one test.

Environment and dedupe

  • Code: live main 9f2ef3aa9b0cb00446f6cd0a25325d2efc2fe3bf (GitHub API Date: Sun, 15 Nov 2026 13:00:59 GMT).
  • Production fledge.hugoxk.online reports version 0.3.6, published before feat: add a superadmin account deletion workflow #824 merged (2026-11-15T09:25:12Z), so the preview is not live yet — this ships with the next release.
  • Dedupe: open and closed issues searched for "uninstall the app installation" (0 hits) and "installation record" (28 hits, none about this). #691 (closed) built the preview and #690 (closed) owns the delete set; this card corrects one claim inside #691, it does not reopen it. No open PR touches adminDeleteUserPage.

Activity

  1. added
    bugSomething isn't working
    area:securitySecrets, authz, data exposure
    area:uiViews, CSS, accessibility, i18n
    needs-reviewWaiting for the Fledge Platform Reviewer's issue review
    on Nov 15, 2026
  2. HugoXK-bot commented on Nov 15, 2026

    @HugoXK-bot
    Collaborator

    Issue-review: PASS at issue #841 body bb242a690591
    Written-by: HugoXK-bot
    Posted-by: HugoXK-bot

    The defect is real and every anchor is exact at live main 9f2ef3aa9b0cb00446f6cd0a25325d2efc2fe3bf (I hashed all 713 blobs of that tree before citing them): the preview's removal table carries countRow(t('GitHub 安装', 'GitHub installations'), counts.installations) at src/views/pages.ts:4124, under the 将要移除的内容 / What will be removed heading at :4141, and the teardown at src/app.ts:5184-5194 is cancel_jobs → stop_instances → revoke_tokens → purge_rows with no GitHub call. src/repos/app.ts has only GET /app/installations (:166), GET /app/installations/:id (:175) and the token mint (:192); grep -rn "app/installations" src | grep -i delete finds nothing, so no code path can uninstall. docs/security.md:452-453 states the omission in the repo's own words, the counts come from the github_installations rows (src/db.ts:2845, :2865), and the existing test at test/adminUserDelete.test.ts:249 asserts only that the local row is gone.

    The card is specific where it must be: the promise rule ("every row under What will be removed is removed, or the page says so and names where the operator revokes it"), an in-memory reproduction, a red test that fails at 9f2ef3aa for the sentence and the label, the smallest slice (relabel + one bilingual sentence on both surfaces + one test), explicit non-goals (no DELETE /app/installations/:id, no purgeUserRows/order/audit change, no touching the backup-window claim), the shared-installation reason for deferring the real uninstall, and dedupe against #691/#690 with no open PR touching adminDeleteUserPage (checked: #780, #837, #531 do not touch src/views/pages.ts; #797, #782, #813 touch other functions in it, none the delete preview).

    Three notes to fold in when the Dev writes it, none of them blocking the card:

    1. src/views/pages.ts:4080 is on /admin/users/:id, but the named test case drives /admin/users/<id>/delete and then nothing else. To keep the second promised surface pinned, the test must also GET /admin/users/<id> and assert the retained-installation sentence there in both locales; otherwise that sentence can land with no test.
    2. src/views/pages.ts:4144 — the empty-state sentence still lists "安装 / installations" among the things the account has. Once the row reads "installation records", that sentence has to match, or the page contradicts itself in the same view.
    3. Anchor drift, cosmetic: the users delete is src/db.ts:3000 (DELETE FROM users WHERE id = ?), not :2997-2999; :2924 is the PURGE_USER_TABLES loop and :970 does name github_installations. The claim "through the cascade" is right; the line number is one off.

    Ran:

    • node scripts/issue-hash.mjs --issue 841 — raw bb242a690591, trimmed d498a673643a
    • node scripts/issue-hash.mjs --issue 841 --match bb242a690591 — match raw
    • GITHUB_TOKEN=… GITHUB_REPOSITORY=HugoXK/fledge node scripts/issue-gate.mjs --issue 841 — no-pass #841 before this verdict
    • grep -n over src/views/pages.ts, src/app.ts, src/db.ts, src/repos/app.ts, docs/security.md, test/adminUserDelete.test.ts for the anchors above, in a tree hashed 713/713 against 9f2ef3aa
      Not run: the red test (it does not exist yet; that is the card), a live install/uninstall against a real GitHub App.

    Conclusion: PASS — one real misstatement in the most destructive flow, bounded to copy plus one test, with the uninstall decision correctly deferred to its own card. Add the two test/label details above in the PR, and this is claimable at T2.

  3. added
    review:approvedIssue reviewed and approved by the Fledge Platform Reviewer
    and removed
    needs-reviewWaiting for the Fledge Platform Reviewer's issue review
    on Nov 15, 2026
  4. HugoXK commented on Nov 15, 2026

    @HugoXK
    OwnerAuthor

    Written-by: HugoXK
    Author: Fledge Platform Security

    Read live this turn: issue body hash bb242a690591 (unchanged from the PASS), live main abfe0439f346668af340affa98598e314533deb4 (2026-11-15T13:05:41Z, the #832 squash).

    The claim is correct, and the copy-only slice is the right fix. Checked at that tip:

    • src/views/pages.ts:4124 renders the GitHub installations row under the "What will be removed" heading (:4141); src/app.ts:5168 fills it with store.listInstallations(user.id).length.
    • POST /admin/users/:id/delete (src/app.ts:5176) runs cancel_jobs → stop_instances → revoke_tokens → purge_rows and makes no GitHub call. purgeUserRows deletes the github_installations row through PURGE_USER_TABLES (src/db.ts:961-977), and src/repos/app.ts has only the two GETs (:166, :175) and the token mint (:192) — there is no DELETE /app/installations/:id anywhere in the tree.
    • docs/security.md:473-474 already says the row goes and GitHub is not called, so the operator page is the only place promising more than the code does.

    One precision that supports the proposed wording rather than the teardown change: the row is accurate about Fledge's own record — the row is removed — so the honest fix is the label ("GitHub installation records") plus the sentence naming where the GitHub-side installation is revoked. Keep the DELETE out of the teardown, and the shared-row argument is if anything stronger than the card states: installation_id is the primary key (src/db.ts:1373) and the upsert on conflict deliberately does not reassign user_id (src/db.ts:3073-3080), so the installation stays owned by whichever account inserted it while other accounts' repositories can still be served through it — deleting that first owner would cut access they never granted Fledge to revoke. That needs its own card and an owner decision, exactly as the "Not in this card" section says.

    Worth one clause in both the preview and the danger-zone paragraph (src/views/pages.ts:4080): GitHub keeps the installation and keeps delivering its webhooks to the deployment after Fledge stops knowing about it. No labels changed; triage is yours.

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

Metadata

Metadata

Assignees

Labels

agent-readyReady for an agent to implementarea:securitySecrets, authz, data exposurearea:uiViews, CSS, accessibility, i18nbugSomething isn't workingreview:approvedIssue reviewed and approved by the Fledge Platform Reviewer

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions