Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/scripts/sync-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type CheckRun, REQUIRED_CHECKS } from "./fast-forward.ts";
import { createTestRepo, git, type TestRepo } from "./promotion-test-repo.ts";
import {
buildSuggestions,
failureExcerpt,
leftoverConflictMarkers,
planRepair,
type ReviewThread,
Expand Down Expand Up @@ -340,3 +341,24 @@ describe("leftoverConflictMarkers", () => {
}
});
});

test("failure excerpt keeps an early test failure that the log tail would drop", () => {
const log = [
"2026-10-13T10:48:27.3385710Z supabase:test:integration: \x1b[41m FAIL \x1b[49m src/signing-key.integration.test.ts > flushes telemetry",
"2026-10-13T10:48:27.3386440Z AssertionError: expected 'supabase/.env' to be '.env'",
...Array.from(
{ length: 1000 },
(_, index) => `2026-10-13T10:49:30.0000000Z stack output ${index}`,
),
"2026-10-13T10:49:31.0000000Z ##[error]Process completed with exit code 1.",
].join("\n");

const excerpt = failureExcerpt(log);

expect(excerpt).toContain("AssertionError: expected 'supabase/.env' to be '.env'");
expect(excerpt).toContain("##[error]Process completed with exit code 1.");
expect(excerpt).not.toContain("stack output 100\n");
Comment thread
avallete marked this conversation as resolved.
Resolved
expect(excerpt).toContain("[...]");
expect(excerpt).not.toContain("\x1b");
expect(excerpt).not.toMatch(/^\S+Z /m);
});
49 changes: 46 additions & 3 deletions .github/scripts/sync-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,11 @@ async function fetchOpenFindings(
});
}

async function logTail(token: string, repository: string, jobId: number): Promise<string> {
async function jobFailureExcerpt(
token: string,
repository: string,
jobId: number,
): Promise<string> {
const response = await fetch(
`https://api.github.com/repos/${repository}/actions/jobs/${jobId}/logs`,
{
Expand All @@ -594,7 +598,46 @@ async function logTail(token: string, repository: string, jobId: number): Promis
if (!response.ok) {
return `(log unavailable: ${response.status})`;
}
return (await response.text()).split("\n").slice(-LOG_TAIL_LINES).join("\n");
return failureExcerpt(await response.text());
Comment thread
avallete marked this conversation as resolved.
Resolved
}

const ERROR_CONTEXT_LINES = 40;
const MAX_ERROR_WINDOWS = 10;

/**
* Keeps the lines after each error annotation plus the log tail, because Turbo interleaves packages and the failing
* one's output often ends long before the job does.
*/
export function failureExcerpt(log: string): string {
const lines = log
// oxlint-disable-next-line no-control-regex -- strips ANSI colour codes from job logs
.replace(/\x1b\[[0-9;]*m/g, "")
.split("\n")
.map((line) => line.replace(/^\S+Z /, ""));
const kept = new Set<number>();
let windows = 0;
lines.forEach((line, index) => {
// Turbo prefixes each line with its task name when it streams several packages.
if (windows < MAX_ERROR_WINDOWS && /^(\S+:\s)?\s*(##\[error\]|FAIL\s)/.test(line)) {
windows += 1;
for (let at = index; at < Math.min(index + ERROR_CONTEXT_LINES, lines.length); at += 1) {
Comment thread
avallete marked this conversation as resolved.
Resolved
kept.add(at);
}
}
});
Comment thread
avallete marked this conversation as resolved.
Resolved
for (let at = Math.max(0, lines.length - LOG_TAIL_LINES); at < lines.length; at += 1) {
kept.add(at);
}
let previous = -1;
const excerpt: string[] = [];
for (const index of [...kept].sort((a, b) => a - b)) {
if (index !== previous + 1) {
excerpt.push("[...]");
}
excerpt.push(lines[index] ?? "");
previous = index;
}
return excerpt.join("\n");
}

/** Whether an `AI Review` run for this pull request is queued or running; its run name carries the PR number. */
Expand Down Expand Up @@ -688,7 +731,7 @@ async function collect(outputDir: string): Promise<void> {
const failures = await Promise.all(
plan.failures.map(
async (run) =>
`## ${run.name} (${run.conclusion})\n\n\`\`\`\`text\n${await logTail(token, repository, run.id)}\n\`\`\`\`\n`,
`## ${run.name} (${run.conclusion})\n\n\`\`\`\`text\n${await jobFailureExcerpt(token, repository, run.id)}\n\`\`\`\`\n`,
),
);
writeFileSync(join(contextDir, "ci-failures.md"), failures.join("\n") || "No CI job failed.\n");
Expand Down
2 changes: 1 addition & 1 deletion .github/sync-branches/repair-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ major; `next` collects breaking changes for the following major. The working dir
sync branch: `develop` with `next` merged in and its conflicts already resolved, but some checks
fail. Repository conventions live in `CLAUDE.md`, `AGENTS.md`, and `docs/adr/`. Read these first:

- `ci-failures.md` in the added context directory: the failed checks and the end of their output.
- `ci-failures.md` in the added context directory: the failed checks, the output after each error, and the end of their logs.
- `review-findings.md` in the same directory: the AI review's open findings, numbered.

## Your task
Expand Down
Loading