Skip to content

fix(report): keep a clean check clean under the issue channel, and deliver a long report - #598

Merged
Vivswan merged 2 commits into
mainfrom
fix/report-check-and-cap
Nov 12, 2026
Merged

Vivswan merged 2 commits into
mainfrom
fix/report-check-and-cap

Conversation

@Vivswan

@Vivswan Vivswan commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Before

target under an issue channel, settings declare `labels` -> marker label declared, always
  repository without the label -> "missing" -> check: drift, exit 1 -> label and issue created
report composed whole -> body over 65536 characters -> GitHub answers 422, on every run
$ bun test/e2e/run.ts --scenario multi-report-issue-on-failure-healthy-quiet --print-stdout
report: created label "settings-as-code-report" in private repository #1
report: created issue #1 in private repository #1
::warning::private repository #1: drift - labels. details hidden: the repository is private or internal. Set private-repos: show to reveal them, or run the action inside that repository
result: drift

$ bun capture-limit.ts /repo
issue body: 82191 characters
report: created label "settings-as-code-report" in private repository #1
warning: private repository #1: could not deliver the private report (HTTP 422). Re-run, or set private-report: none if it persists

After

target under an issue channel -> GET marker label
  -> 404:         not declared -> labels planned as written
  -> 200:         declared     -> label kept, never deleted
  -> read failed: declared     -> label kept where it exists
                                  no such label -> check: drift, apply: label created
report composed under 65536 -> too long? -> yes: lines left out and counted -> delivered
                                         -> no:  delivered whole
$ bun test/e2e/run.ts --scenario multi-report-issue-on-failure-healthy-quiet --print-stdout
report: nothing to deliver for private repository #1
result: clean

$ bun capture-limit.ts /repo
issue body: 65492 characters
**Cut to fit 65536 characters.** Lines left out: 119 of 600; each cut is marked where it was made.
(lines left out: 119)
report: created label "settings-as-code-report" in private repository #1
report: created issue #1 in private repository #1
  • Apply: under issue-on-failure it no longer creates the marker label on a healthy private repository, unless the marker read fails.
  • Cost: the issue channels send one more GET per private target per run.
  • Re-expected: two flow tests pinned an apply creating the label, and healthy-quiet no longer seeds it.

How

  • The read lives in issue-report.ts and prints nothing.
  • Only a 404 leaves the marker undeclared, so a failed read never lets an apply delete it.
  • The cut keeps section detail first, one line per section in turn, then the transcript.
  • Library: searched npm (markdown-truncate, mdast-excerpt, remark-excerpt) and this repository, and none keeps whole lines per section or counts the fence.

Proof

  • Tests: 156 pass in test/report and test/flows/{deliver,multi}.test.ts, and 10 of them fail on origin/main.
  • e2e: the 9 multi-report-issue-* scenarios pass, one of them new.
  • Fuzz: FUZZ_SEED=20261111, 25 of 25 iterations ok.
  • Gates: the six static gates pass.
Technical details
  • First capture: the repository's own runner, on a copy of origin/main holding this PR's scenario file, then on this branch.
  • Second capture: bun capture-limit.ts /repo, the script below, on a copy of origin/main, then on this branch.
  • Left out of the blocks: the ::debug:: request lines and the runner's own PASS and FAIL lines.
  • Red run: this PR's multi, deliver, and delivery test files on a copy of origin/main give 80 pass and 10 fail.
  • Red run, composer: composer.test.ts cannot load on origin/main, since it imports the cut.
  • Fuzz command: FUZZ_SEED=20261111 bun test/e2e/fuzz.ts --iterations 25, after bun run build:schema.
  • The script's client double: it answers 422 to an issue body over 65536 characters, as GitHub does, since the e2e mock accepts any length.
  • Limit source: GitHub's REST reference states no limit, so the number is the one in its 422 message, "body is too long (maximum is 65536 characters)".
  • Length unit: a JavaScript string's length counts UTF-16 units, never fewer than GitHub's characters.
  • The read: GET /repos/{owner}/{repo}/labels/{name}, one request per proven-private target, whether or not its settings declare labels.
  • When it is asked: withDelivery asks the channel before each target's work, and the artifact channel answers no.
  • Static gates: typecheck, lint, lint:arch, knip, check:compat, and build:check.
  • Failed read: a 403, a 5xx, a transport failure, and a client that throws all declare the marker, as every target did before.
  • Failed read without the label: check reads drift and apply creates the label under "nothing to deliver", and the flow table pins both.
  • Flow table: eight cells, the read answering 404, the label, a 500 over a label that exists, and a 500 over no label, each in check and in apply.
  • New scenario: multi-report-issue-on-failure-marker-kept applies, then checks, a private target that holds the label without declaring it.
  • That scenario on origin/main: it fails only for the missing read, since main declares the marker for every target.
  • That scenario's control: with the marker never declared it fails on DELETE /repos/e2e-owner/svc-kept/labels.
  • Its channel: issue-on-failure, since under issue every run writes the report issue and the check could not show zero writes.
  • Public signature: composeReport(input) is as on main, and the cut is composeReportWithin, which src/index.ts does not export.
  • Cut contract: detail and transcript lines never push the body past the limit, and the fixed part (heading, notice, run table, section statuses, marks) is never cut.
  • Hidden target: the read follows the slug's masking, and a flow test runs the real client against the mock server to find its trace line as GET <redacted> -> 404.
  • Artifact channel: it is not cut, and test/report/delivery.test.ts pins a 700-line report uploaded whole.
  • Mock: test/e2e/mock/core-paths.ts serves the read for a delivery target, under the fault key core.reportLabelRead.
  • Fuzzer: a raw target may now receive a labels GET, since the read precedes the parse, and iteration seeds 148 and 817 fail without that line.
  • Re-expected test: "the marker label of a target that holds it is declared, and the notice lands in the report only" ran against a repository without the label, and now seeds it.
  • Re-expected test: "the marker label of a target that holds it is declared; its notice lands in the closing report only" asserted the apply created the marker, and now asserts the apply's one label write is bug.
  • Re-expected test: the withDelivery injection table routes and counts the read, and the ordered-events case gains it as the first request.
  • Re-expected scenario: multi-report-issue-on-failure-healthy-quiet declared and seeded the marker label to stay clean, and now does neither.
  • Recorded, not built: the cut reserves each mark at its longest, so at an exact boundary one line that would fit is left out.
  • Recorded, not built: a fixed part over 65536 characters stays over the limit and still says "Cut to fit".
  • Its reach: a library caller's field of that size, such as runUrl, and no action input.
  • Recorded, not built: a long backtick run in a transcript line widens the fence reserve by twice its length.
  • Its reach: the labels schema refuses a description over 100 characters, so a realistic run costs a few characters.
  • Unchanged: the 422 advice text, which section detail and transcript lines can no longer earn, while a fixed part over the limit still can.
  • Docs: docs/operate/private-repositories.md states the marker rule as a table of the read's answer by mode, and drops the caveat that an apply creates the label on healthy repositories.
  • Under issue: the label is still created on the first healthy run, by the delivery, since that channel always writes its report issue.
  • Library surface: the channel openReportChannel returns has one more method, declaresMarker. Nothing is renamed or removed, and composeReport is as on main.
  • Files: src/report/{composer,delivery,issue-report}.ts, src/flows/deliver.ts, test/report/{composer,delivery,issue-report}.test.ts, test/flows/{deliver,multi}.test.ts, test/e2e/{fuzz.ts,mock/chaos.ts,mock/core-paths.ts}, the two scenarios, the page.
// capture-limit.ts: 300 undeclared labels through the multi-repo flow, against a client double that
// answers an over-long issue body the way GitHub does. Usage: bun capture-limit.ts /repo
const [repo] = process.argv.slice(2);
const { runMulti } = await import(`${repo}/src/flows/multi.ts`);
const { SectionSelection } = await import(`${repo}/src/engine/section-selection.ts`);
const { DEFAULT_DISCOVERY_FILTERS } = await import(`${repo}/src/discovery/discover.ts`);
const { captureIo } = await import(`${repo}/test/io/capture.ts`);
const { MockApi } = await import(`${repo}/test/mock-api.ts`);

const labels = Array.from({ length: 300 }, (_, i) => ({ name: `area/component-${i}`, color: "ededed", description: null }));
const page = (n: number) => `GET /repos/acme/app/labels?per_page=100&page=${n}`;
const api = new MockApi({
  "GET /repos/acme/app": { data: { private: true } },
  "GET /repos/acme/app/contents/.github/settings.yml": { data: "labels: []\n" },
  [page(1)]: { data: labels.slice(0, 100) },
  [page(2)]: { data: labels.slice(100, 200) },
  [page(3)]: { data: labels.slice(200) },
  [page(4)]: { data: [] },
  "POST /repos/acme/app/labels": { data: {} },
  "GET /repos/acme/app/issues?state=all&labels=settings-as-code-report&per_page=100&page=1": { data: [] },
  "GET /repos/acme/app/issues?state=all&sort=created&direction=desc&per_page=100&page=1": { data: [] },
});
const answer = api.tryRequest.bind(api);
api.tryRequest = async (method: string, path: string, payload?: { body?: string }) => {
  if (method !== "POST" || path !== "/repos/acme/app/issues") {
    return answer(method, path, payload);
  }
  const body = payload?.body ?? "";
  console.log(`issue body: ${body.length} characters`);
  for (const line of body.split("\n").filter((text) => text.includes("eft out"))) {
    console.log(line);
  }
  return body.length > 65536
    ? { error: { status: 422, message: "Validation Failed", body: "" } }
    : { data: { number: 1 } };
};
const { io, annotations, logs } = captureIo();
await runMulti(
  api,
  {
    reposDir: "",
    reposInput: "acme/app",
    defaultsFile: "",
    adminOwner: "acme",
    mode: "check",
    onMissingPermission: "fail",
    sections: SectionSelection.ALL,
    discoveryFilters: DEFAULT_DISCOVERY_FILTERS,
    discoveryFiltersSet: [],
    privateRepos: "redact",
    privateReport: "issue",
    reportPublicKey: "",
    selfSlug: "acme/admin",
    runUrl: "",
  },
  io,
);
console.log([...logs, ...annotations].filter((line) => line.includes("report")).join("\n"));

…liver a long report

Two defects in the issue channels of private-report.

A converged check reported drift and wrote. The marker label was declared for every target an issue channel reports on, so a repository that never held the label showed it as missing: a check answered drift and exit 1, and issue-on-failure then created the label and a report issue. The channel now reads the marker label before the target's work and declares it unless the answer is 404. A repository without the label plans nothing, and one that holds it keeps it, in check and in apply. A read that fails any other way declares the label, as every target did before, so an unread label is never deleted. An apply no longer creates the label on a healthy repository, and the issue channels cost one more GET per private target per run.

A report over GitHub's 65536-character issue-body limit earned a 422 on every run. The issue channel now composes its report under that limit: lines that do not fit are left out, section detail kept ahead of the transcript that repeats it, and the body says how many lines were left out and marks each cut. The artifact channel still delivers the whole report.

The e2e mock serves the marker read for a delivery target, and the fuzzer lets a raw target receive it, since the read precedes the parse.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 02:47
@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

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Member shape check

1 finding(s).

File Line Rule
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.

🟡 Changes recommended

A long backtick run in a discarded transcript causes excessive fence-budget reservation, dropping section diagnostics that otherwise fit within the issue-body limit.

1 open issue
What changed in this PR

This PR updates private issue reporting so a missing marker label does not create false settings drift, and long reports can fit GitHub's issue-body limit.

Changes:

  • Read the marker label before target processing and inject it only when present or the read fails.
  • Limit issue reports with counted omissions, prioritizing section detail; artifact reports remain complete.
  • Update regression tests, mock routes, fuzz allowances, and private-report documentation.
File Description
test/​report/​issue-report.test.ts Tests marker-label reads and failure handling.
test/​report/​delivery.test.ts Verifies long artifact reports remain complete.
test/​report/​composer.test.ts Tests report limits, detail allocation, and omission counts.
test/​flows/​multi.test.ts Covers clean targets, long-report delivery, and read-trace redaction.
test/​flows/​deliver.test.ts Tests conditional marker injection and request ordering.
test/​e2e/​scenarios/​multi-report-issue-on-failure-healthy-quiet.yml Checks healthy behavior without a seeded marker label.
test/​e2e/​mock/​core-paths.ts Adds the marker-label read route.
test/​e2e/​mock/​chaos.ts Adds fault injection for marker-label reads.
test/​e2e/​fuzz.ts Allows label reads before settings parsing.
src/​report/​issue-report.ts Adds marker-label probing and the issue-body limit.
src/​report/​delivery.ts Exposes marker decisions and limits issue reports.
src/​report/​composer.ts Adds length budgeting and omission notices.
src/​flows/​deliver.ts Awaits the marker decision before target work.
docs/​operate/​private-repositories.md Documents report truncation and revised marker behavior.

Review effort: Balanced

Comment thread src/report/composer.ts Resolved
… the marker rule's remaining cells

composeReport is again composeReport(input) for a library caller. The cut is composeReportWithin, which the library entry does not export and the issue channel calls with GitHub's limit, so no caller can ask for a limit below the report's fixed part.

The cut's contract is stated as the code keeps it: detail and transcript lines never push the body past the limit, and the fixed part (the heading, the notice, the run table, each section's status, the marks) is never cut, so a limit below that part is not met. The composer test asserts both sides of that part instead of excusing one.

The page states the marker rule as a table of the read's answer by mode, including what a failed read costs on a repository without the label (drift in check, a created label in apply), and no longer says that a healthy run only reads.

The flow table covers all eight cells of that rule, and a new scenario applies and then checks a private target that holds the marker label without declaring it.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 03:35

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 recorded for the marker-label handling and long-report delivery changes.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@Vivswan
Vivswan marked this pull request as ready for review November 12, 2026 04:02
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Nov 12, 2026
@Vivswan
Vivswan merged commit 9f31e5c into main Nov 12, 2026
34 checks passed
@Vivswan
Vivswan deleted the fix/report-check-and-cap branch November 12, 2026 05:51
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