Skip to content

fix(sections): send no empty Pages create, and keep the readable interaction limit in a snapshot - #595

Merged
Vivswan merged 1 commit into
mainfrom
fix/pages-and-interaction-limits
Nov 12, 2026
Merged

Vivswan merged 1 commit into
mainfrom
fix/pages-and-interaction-limits

Conversation

@Vivswan

@Vivswan Vivswan commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Before

pages: {https_enforced: true}, no site          -> POST {} (GitHub requires build_type or source) -> PUT
snapshot, read-only Administration, warn        -> creation-cap GET 403 -> whole section skipped
snapshot, live cap carries include_drafts       -> field dropped
drift: pages: declared in the settings file but GitHub Pages is not enabled on the repo; apply will enable it
drift: pages: the create call takes only build_type and source, so apply will then set the remaining configuration (https_enforced)
result: drift

apply sent:
POST /repos/e2e-owner/e2e-repo/pages -> 201  body {}
PUT /repos/e2e-owner/e2e-repo/pages -> 204  body {"https_enforced":true}
::warning::interaction_limits: skipped - the token was denied GET /repos/e2e-owner/e2e-repo/interaction-limits/pulls/creation-cap (reading the pull request creation cap): 403 Resource not accessible by personal access token. To fix, grant "Administration" (read and write) under the PAT's Repository permissions
result: partial

snapshot.yml:
{}
interaction_limits:
  pull_request_creation_cap:
    enabled: true
    max_open_pull_requests: 5

After

pages: {https_enforced: true}, no site          -> no write, drift names the fix
snapshot, read-only Administration, warn        -> cap and bypass each left out with a note, limit written
snapshot, live cap carries include_drafts       -> field kept
::notice::pages: pages: GitHub reports no Pages site. A fine-grained token missing the Pages permission gets the same answer; if this repo does have a Pages site, grant the token Pages read and write
drift: pages: declared in the settings file but GitHub Pages is not enabled on the repo, and GitHub enables a site only from build_type or source, so apply changes nothing - declare build_type or source to enable the site, or remove the pages section
result: drift

apply sent: no write
Run Before Now
apply, the Pages file sends a create GitHub's schema rejects applied, both lines as notices
snapshot, read-only token, warn partial, section skipped snapshot, limit written
::notice::interaction_limits.pull_request_creation_bypass: left out of the snapshot - the token was denied GET /repos/e2e-owner/e2e-repo/interaction-limits/pulls/bypass-list: 403 Resource not accessible by personal access token. To fix, grant "Administration" (read and write) under the PAT's Repository permissions. Note: a 403 or 404 here can also mean the pull request creation cap is not available on this repository
::notice::interaction_limits.pull_request_creation_cap: left out of the snapshot - the token was denied GET /repos/e2e-owner/e2e-repo/interaction-limits/pulls/creation-cap (reading the pull request creation cap): 403 Resource not accessible by personal access token. To fix, grant "Administration" (read and write) under the PAT's Repository permissions
result: snapshot

snapshot.yml:
interaction_limits:
  limit: contributors_only
interaction_limits:
  pull_request_creation_cap:
    enabled: true
    max_open_pull_requests: 5
    include_drafts: true

include_drafts is named, not typed: a non-boolean still rides the PATCH, and typing it would newly refuse such a file.

With no limit set and both reads denied, the snapshot still ends with the engine's nothing exists on the repository line, untrue there as it already was on main for an inherited limit.

How

  • createBody() alone mints the create body, whose type has no empty form.
  • No body to send means drift plus a note.
  • The cap and bypass reads go through readOrNote.
  • Library: zod and readOrNote, both in the repository.

Proof

  • Tests: 74 pass in both section directories, 37 in the round-trip suite.
  • e2e: both sections' slice passes, 27 of 27.
  • Gates: the six static gates pass.
Technical details
  • Seen failing first: the three Pages cases, the denied-snapshot case, the snapshot row, and the three scenarios each failed on main.
  • Static gates: typecheck, lint, lint:arch, knip, check:compat, and build:check.
  • Not a parse refusal: whether a site exists is live state, so the Pages file still parses.
  • Model: src/sections/workflows/index.ts reports a declared workflow with no file the same way.
  • Both readings: no write follows to surface a denied token, so the note names the 404's other reading, as pages: null already does.
  • The 201 above: it is the e2e mock's answer on main.
  • OpenAPI check: on main the e2e runner's contract validator flags that create as [request-body]: (root) must match a schema in anyOf, and no scenario sent one.
  • Under fail: a denied cap or bypass read still fails the section, as docs/operate/snapshot.md states.
  • 405 unchanged: a cap answering 405 still omits both keys under one note, and the bypass list is not read.
  • Engine line: NOTHING_TO_DECLARE in src/engine/snapshot.ts follows any section that returns no value, and on main it already follows the note for an inherited limit.
  • Projection: a snapshot keeps only the keys a slice names, so naming include_drafts in the cap slice keeps it.
  • Hint: the cap PATCH's 422 hint names include_drafts beside enabled.
  • Helper: readOrNote lives in src/sections/shared/snapshot-helpers.ts, and repository, actions, and environments already use it.
  • Payload type: the section reads its validated input as PlainTyped, so the untyped field is plain data in the PATCH body.
  • Published schema: include_drafts appears under the cap with its description and no type.
  • Extended, not re-expected: the interaction_limits snapshot row and the interaction-limits-snapshot-roundtrip scenario now seed and expect include_drafts.
  • New scenarios: pages-enable-needs-build-type-or-source and interaction-limits-snapshot-cap-denied-warn.
  • Docs: the two docs/sections/*.docs.yml pages state the new behavior, and docs/reference/coverage.md is their regenerated region.
  • Capture command: bun capture.mts /repo scenario.yml [snapshot-path], the script below, once on a copy of origin/main and once on this branch.
  • Pages scenario: pages: {https_enforced: true} against pages: null live, once in check mode and once in apply.
  • Denied scenario: a snapshot of interaction_limits under administration: read, denial_style: 403, and on_missing_permission: warn.
  • Cap scenario: a snapshot against the live cap {enabled: true, max_open_pull_requests: 5, include_drafts: true}.
  • Request lines: apply sent lists the apply run's writes from the mock's request log, and the reads are left out.
  • Left out of stdout: debug request lines, the snapshot taken timestamp, the expiry note, and the snapshot written line.
  • Left out of each snapshot.yml: its comment header, which holds the schema line and the same notes stdout shows.
  • Files: src/sections/pages/index.ts, src/sections/interaction_limits/{index,schema}.ts, test/sections/pages/pages.test.ts, test/sections/interaction_limits/{interaction_limits.test,snapshot-row}.ts, the three scenario files, docs/sections/{pages,interaction_limits}.docs.yml, docs/reference/coverage.md.
// capture.mts: one scenario file through the action bundle against the mock API
import { existsSync, readFileSync } from "node:fs";

const [root, scenarioPath, snapshotOut] = process.argv.slice(2);
const { parseScenario } = await import(`${root}/test/e2e/scenario.ts`);
const { runScenario, stripMaskLines, stripDebugLines } = await import(`${root}/test/e2e/runner/runner.ts`);
const scenario = parseScenario(Bun.YAML.parse(readFileSync(scenarioPath, "utf8")), scenarioPath);
if (snapshotOut !== undefined) {
  scenario.inputs.snapshot_file = snapshotOut;
}
const report = await runScenario(scenario);
console.log(stripDebugLines(stripMaskLines(report.stdout)));
for (const request of report.requests) {
  const body = request.method === "GET" ? "" : `  body ${JSON.stringify(request.body ?? null)}`;
  console.log(`${request.method} ${request.pathname} -> ${request.status}${body}`);
}
if (snapshotOut !== undefined && existsSync(snapshotOut)) {
  console.log(readFileSync(snapshotOut, "utf8"));
}

…raction limit in a snapshot

Pages: a declaration with neither build_type nor source, against a repository without a site, planned a create with an empty body, which GitHub's create schema rejects, and then an update of a site that does not exist.

The plan is now drift with no operation that names the fix, beside a note naming the 404's other reading. createBody() is the only mint of the create body, and its type has no empty form.

interaction_limits snapshot: the creation-cap and bypass-list reads are write-gated, so a read-only Administration grant reads the limit and is denied both, and one denied read dropped the whole section.

Each read now goes through readOrNote: under on-missing-permission: warn the denied key is left out with its note and the limit still reads back, and under fail the section fails as before.

interaction_limits cap: the slice names include_drafts, which GitHub documents on the cap's GET and PATCH, so a snapshot keeps it. The key stays untyped: any value of it has always parsed through the open mapping and ridden the PATCH. The PATCH's 422 hint names it beside enabled.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 02:03
@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:13 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

2 finding(s).

File Line Rule
src/sections/pages/index.ts 71 one-line-comment
test/sections/contract/validate-hook.test.ts 80 all-or-none

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 reported for the Pages creation and interaction-limit snapshot changes.

No open issues 🎉

What changed in this PR

This PR fixes Pages creation and interaction-limit snapshots in the declarative repository settings action. It avoids invalid empty Pages requests and preserves readable settings when optional snapshot reads are denied.

Changes:

  • Prevents Pages creation without build_type or source, reporting drift and actionable notices instead.
  • Omits denied cap and bypass reads under the warn policy while retaining readable limits; preserves include_drafts in snapshots without narrowing accepted input.
  • Adds regression coverage and updates section documentation and generated coverage notes.
File Description
test/​sections/​pages/​scenarios/​pages-enable-needs-build-type-or-source.yml Verifies no writes and explanatory notices when creation fields are absent.
test/​sections/​pages/​pages.test.ts Tests missing-site plans with update-only declarations.
test/​sections/​interaction_limits/​snapshot-row.ts Adds include_drafts to snapshot expectations.
test/​sections/​interaction_limits/​scenarios/​interaction-limits-snapshot-roundtrip.yml Extends round-trip coverage to include_drafts.
test/​sections/​interaction_limits/​scenarios/​interaction-limits-snapshot-cap-denied-warn.yml Covers readable-limit snapshots with denied cap and bypass reads.
test/​sections/​interaction_limits/​interaction_limits.test.ts Tests selective omission under warn and failure under fail.
src/​sections/​pages/​index.ts Restricts creation bodies and handles missing creation fields without writes.
src/​sections/​interaction_limits/​schema.ts Names include_drafts for snapshot projection while preserving passthrough acceptance.
src/​sections/​interaction_limits/​index.ts Handles denied snapshot subreads individually and updates the cap validation hint.
docs/​sections/​pages.docs.yml Documents creation requirements and no-write behavior.
docs/​sections/​interaction_limits.docs.yml Documents permission-aware snapshots and include_drafts.
docs/​reference/​coverage.md Regenerates coverage notes for both sections.

Review effort: Balanced

@Vivswan
Vivswan marked this pull request as ready for review November 12, 2026 02:26
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Nov 12, 2026
@Vivswan
Vivswan merged commit 7338300 into main Nov 12, 2026
34 checks passed
@Vivswan
Vivswan deleted the fix/pages-and-interaction-limits branch November 12, 2026 04:43
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