Skip to content

fix(sections): never plan the owner, hedge a concealed team 404, and advise the grant the writes need - #588

Merged
Vivswan merged 1 commit into
mainfrom
fix/sections-owner-and-teams
Nov 12, 2026
Merged

Vivswan merged 1 commit into
mainfrom
fix/sections-owner-and-teams

Conversation

@Vivswan

@Vivswan Vivswan commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Before

collaborators: entry names the owner -> settled like any entry -> write planned on the owner
teams, check:  list names the team, probe 404 -> "no access; apply will grant"
teams, apply:  list denied -> advice: Administration (read) -> next apply: grant PUT denied
# collaborators: [{username: acme}] on user-owned acme/app, mode check
drift: collaborators[acme]: declared "write" != live "admin"; apply will set the declared permission
result: drift

# teams: [{name: platform, permission: push}], token without Members, mode check
drift: teams[platform]: no access to acme/app; apply will grant "push"
result: drift

# the same teams file, token without Administration, mode apply
::error::preflight: teams: the token was denied GET /repos/acme/app/teams: 403 Resource not accessible by personal access token. To fix, grant "Administration" (read) under the PAT's Repository permissions
result: failed

# the next apply, with Administration (read) granted
::error::teams: not applied - the token was denied PUT /orgs/acme/teams/platform/repos/acme/app (granting team "platform" access): 403 Resource not accessible by personal access token. To fix, grant "Members" (read) under the PAT's Organization permissions and "Administration" (read and write) under its Repository permissions
result: failed

After

collaborators: entry names the owner -> note, no write
teams, check:  list names the team, probe 404 -> drift names both readings of the 404
teams, apply:  list denied -> advice: the grant the section's writes need
# collaborators: [{username: acme}] on user-owned acme/app, mode check
::notice::collaborators: collaborator "acme" is the repository owner, whose access is implicit and never managed; the entry is ignored - remove it from the settings file to silence this note
result: clean

# teams: [{name: platform, permission: push}], token without Members, mode check
drift: teams[platform]: listed with access to acme/app, but the access probe answered 404, read here as no access, so apply will grant "push". A fine-grained token missing the grant gets the same answer; if the team does have access, grant "Members" (read) under the PAT's Organization permissions and "Administration" (read and write) under its Repository permissions, then check again
result: drift

# the same teams file, token without Administration, mode apply
::error::preflight: teams: the token was denied GET /repos/acme/app/teams: 403 Resource not accessible by personal access token. To fix, grant "Members" (read) under the PAT's Organization permissions and "Administration" (read and write) under its Repository permissions
result: failed

A file that declares the owner keeps applying. The entry is skipped with a note, not refused.

Only a listed team is hedged. An unlisted team's 404 still reads no access, since no listing contradicts it.

How

  • The owner check sits in settleDeclared, the one place a declared entry earns its op or note.
  • listedProbe404 words the 404 once, and plan() and snapshot() both call it.
  • coversPermission asks whether the section's grant satisfies the denied endpoint, and if so that grant is advised.
  • Only the team list's advice moves: every other override names a resource its section's grant lacks.
  • LiveCollaborator.permissions is deleted: it was parsed and never read.

Proof

  • Tests: 219 pass in test/sections/{collaborators,teams,contract}, the 4 defect cases seen failing first.
  • e2e: the collaborators and teams slice passes, 31 of 31, with one new scenario per defect.
  • Gates: typecheck, lint, lint:arch, knip, check:compat, and build:check pass.
Technical details
  • Organization owner: username: Acme on an organization repository planned an invitation (collaborators[Acme]: missing - not a collaborator on the repo; apply will send an invitation with "push") and now earns the same note.
  • Grant still planned: a listed team whose probe answers 404 keeps its grant op, so apply either grants or surfaces the denial at the PUT.
  • Inherited listings: a team the list names with access_source: organization is hedged the same way, since the list states its access either way.
  • Snapshot wording unchanged: the snapshot note for the same state reads as before, now through listedProbe404.
  • Coverage direction: coversPermission(grant, asked) holds when every alternative of grant is one asked accepts and asked wants no organization permission grant lacks.
  • overrideAdviceLevel unchanged: a denial on the team list no longer reaches it.
  • Census before, src/sections/collaborators/index.ts: 2 schema keys named permissions and 4 .permissions reads, all 4 on an invitation.
  • Census after: 1 schema key named permissions, the invitation's, and the same 4 reads.
  • Docs, owner: docs/sections/collaborators.docs.yml states the owner rule for a declared entry.
  • Docs, snapshot: the same page said a custom role named "push" leaves the snapshot written as partial, and now says the section fails, which fails the target with no file written.
  • Docs, teams: docs/sections/teams.docs.yml states the listed-team 404 line, and docs/reference/coverage.md is the regenerated region of both pages.
  • New scenarios: collaborators-declared-owner-note, teams-check-concealed-probe, teams-list-denied-advice.
  • Re-expected tests: none, and no assertion was removed.
  • Library: not a library-shaped category: three section rules and a permission comparison.
  • Recorded, not built: partitioning the owner out of the live pool in readLiveAccess would retire the two older isOwner() calls, and no input produces a wrong output today.
  • Recorded, not built: hedging every probe 404, which would cover a child team that inherits access and is absent from a public repository's team list.
  • Capture command: bun capture.mts /repo scenario.yml, the script below, once with src/ at origin/main and once on this branch.
  • Capture scenarios: the settings, token, and mode named on each # line, against the mock API.
  • Capture state: the owner case lists the owner as admin, and the team cases have platform holding write.
  • Redaction: the harness names e2e-owner/e2e-repo are shown as acme/app, and the # lines are labels.
  • Left out of the blocks: debug request lines, the unchanged preflight failed: summary line, and the script's closing status line.
  • Files: src/sections/{collaborators,teams}/index.ts, src/sections/contract/{errors,permissions}.ts, test/sections/{collaborators/collaborators,teams/teams,contract/contract}.test.ts, the three scenario files, the two .docs.yml pages, docs/reference/coverage.md.
// capture.mts: one scenario file through the action bundle against the mock API, printing the run's stdout without debug and mask lines
import { readFileSync } from "node:fs";

const [repo, scenarioPath] = process.argv.slice(2);
if (repo === undefined || scenarioPath === undefined) {
  console.error("usage: capture.mts <repo> <scenario.yml>");
  process.exit(2);
}
const { parseScenario } = await import(`${repo}/test/e2e/scenario.ts`);
const { runScenario, stripMaskLines, stripDebugLines } = await import(`${repo}/test/e2e/runner/runner.ts`);
const scenario = parseScenario(Bun.YAML.parse(readFileSync(scenarioPath, "utf8")), scenarioPath);
const report = await runScenario(scenario);
console.log(stripDebugLines(stripMaskLines(report.stdout)).trimEnd());
console.log(`[exit ${report.exitCode}; scenario expectations ${report.ok ? "met" : "not met"}]`);
process.exit(0);

…advise the grant the writes need

A `collaborators` entry naming the repository owner was settled like any other: on a user-owned repository it planned a PUT re-granting the owner at the declared role, and an organization's own login planned an invitation. The declared walk now skips the owner with a note saying to remove the entry, so no write is ever planned for it.

Check mode read a 404 on a declared team's access probe as "no access" even when the repository's team list named the team, the state a fine-grained token without the Members organization permission produces. The drift line now names both readings of the 404 and the grant that settles it, in the words the snapshot's note already used. The grant is still planned.

A denied team list advised "Administration" (read), the list's own gate. That passes the next apply preflight and is denied again at the grant PUT. A denial on an endpoint whose permission the section's grant covers now advises the section's grant, so the first advice names what the writes need.

`LiveCollaborator.permissions` was parsed and never read, and is gone. The collaborators page said a custom role named "push" leaves a partial snapshot: the section fails, which fails the target with no file written, and the page now says so.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 01:22
@github-actions

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 6 warning(s).

File Size Tier Cap
src/engine/diff.ts:56 11 comment lines warn 10
src/github/api-error.ts:9 11 comment lines warn 10
src/problem.ts:73 18 comment lines warn 10
src/sections/contract/errors.ts:18 11 comment lines warn 10
src/sections/contract/module.ts:55 12 comment lines warn 10
src/sections/shared/list/decl.ts:265 14 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

@github-actions

Copy link
Copy Markdown
Contributor

Member shape check

9 finding(s).

File Line Rule
test/sections/contract/validate-hook.test.ts 80 all-or-none
test/sections/teams/teams.test.ts 20 all-or-none
test/sections/teams/teams.test.ts 23 all-or-none
test/sections/teams/teams.test.ts 24 all-or-none
test/sections/teams/teams.test.ts 25 all-or-none
test/sections/teams/teams.test.ts 26 all-or-none
test/sections/teams/teams.test.ts 28 all-or-none
test/sections/teams/teams.test.ts 31 all-or-none
test/sections/teams/teams.test.ts 34 constant-spacing

Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in .member-shape-allow.local with a # reason.

4 generated file(s) skipped.

Copilot AI 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.

🟢 Approval recommended

No active review issues are identified for the collaborator and team handling changes.

No open issues 🎉

What changed in this PR

This PR corrects collaborator and team handling in the settings action: declared repository owners are ignored with a note, listed-team probe 404s receive qualified advice, and permission errors recommend the grant needed for the section's writes.

Changes:

  • Prevent writes for declared owners and remove unused collaborator permission parsing.
  • Share listed-team 404 wording between planning and snapshots, and improve permission-grant advice.
  • Add regression tests and end-to-end scenarios, and update coverage documentation.
File Description
test/​sections/​teams/​teams.test.ts Tests listed-team 404 wording and denial advice.
test/​sections/​teams/​scenarios/​teams-list-denied-advice.yml Verifies full-grant advice when listing teams is denied.
test/​sections/​teams/​scenarios/​teams-check-concealed-probe.yml Verifies check-mode wording for concealed team access.
test/​sections/​contract/​contract.test.ts Tests permission coverage direction and alternatives.
test/​sections/​collaborators/​scenarios/​collaborators-declared-owner-note.yml Verifies declared owners receive a note without writes.
test/​sections/​collaborators/​collaborators.test.ts Tests user and organization owner declarations.
src/​sections/​teams/​index.ts Shares 404 wording and checks whether teams are listed.
src/​sections/​contract/​permissions.ts Adds permission coverage comparison.
src/​sections/​contract/​errors.ts Prefers the section grant when it covers the denied endpoint.
src/​sections/​collaborators/​index.ts Skips declared owners and removes unused schema parsing.
docs/​sections/​teams.docs.yml Documents listed-team 404 behavior.
docs/​sections/​collaborators.docs.yml Documents owner handling and corrects snapshot failure behavior.
docs/​reference/​coverage.md Regenerates collaborator and team coverage documentation.

Review effort: Balanced

@Vivswan
Vivswan marked this pull request as ready for review November 12, 2026 01:35
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Nov 12, 2026
@Vivswan
Vivswan merged commit 8dd68e1 into main Nov 12, 2026
33 checks passed
@Vivswan
Vivswan deleted the fix/sections-owner-and-teams branch November 12, 2026 04:41
This was referenced Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants