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
47 changes: 47 additions & 0 deletions test/e2e/support/base-image-publication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,53 @@ describe("base-image publication evidence", () => {
).rejects.toThrow(/duplicate id/u);
});

it("restarts collection after a concurrent workflow-run count change", async () => {
const entries = Array.from({ length: 102 }, (_, index) => ({ id: index + 1 }));
const pages = [
{ total_count: 101, workflow_runs: entries.slice(0, 100) },
{ total_count: 102, workflow_runs: entries.slice(100) },
{ total_count: 102, workflow_runs: entries.slice(0, 100) },
{ total_count: 102, workflow_runs: entries.slice(100) },
];
const requests: string[] = [];

await expect(
collectPaginated(
async (requestPath) => {
requests.push(requestPath);
return pages.shift();
},
"/runs?per_page=100",
"workflow_runs",
),
).resolves.toMatchObject({ total_count: 102, workflow_runs: entries });
expect(requests).toEqual([
"/runs?per_page=100&page=1",
"/runs?per_page=100&page=2",
"/runs?per_page=100&page=1",
"/runs?per_page=100&page=2",
]);
});

it("fails closed after three unstable pagination attempts", async () => {
const entries = Array.from({ length: 101 }, (_, index) => ({ id: index + 1 }));
let requests = 0;

await expect(
collectPaginated(
async (requestPath) => {
requests += 1;
return requestPath.endsWith("page=1")
? { total_count: 101, workflow_runs: entries.slice(0, 100) }
: { total_count: 102, workflow_runs: entries.slice(100) };
},
"/runs?per_page=100",
"workflow_runs",
),
).rejects.toThrow(/total_count changed during 3 pagination attempts/u);
expect(requests).toBe(6);
});

it("accepts a batch-push tip that descends from the newest changed input (#7372)", () => {
const selection = selectPublicationRun(
runsPayload([workflowRun({ head_sha: EXPECTED_SHA })]),
Expand Down
37 changes: 29 additions & 8 deletions tools/e2e/base-image-publication.mts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const RUN_URL_ROOT = `https://github.com/${REPOSITORY}/actions/runs`;
const WORKFLOW_URL = `https://github.com/${REPOSITORY}/blob/${MAIN_BRANCH}/${WORKFLOW_PATH}`;
const PAGE_SIZE = 100;
const MAX_API_PAGES = 10;
const PAGINATION_ATTEMPTS = 3;
const REQUEST_ATTEMPTS = 3;
const REQUEST_TIMEOUT_MS = 20_000;
const MAX_RETRY_DELAY_MS = 10_000;
Expand Down Expand Up @@ -516,16 +517,13 @@ export function validateBoundRun(payload: unknown, expected: PublicationRun): vo
}
}

export async function collectPaginated(
async function collectPaginationAttempt(
request: (path: string) => Promise<unknown>,
basePath: string,
collectionKey: "workflow_runs" | "jobs",
maxPages = MAX_API_PAGES,
): Promise<JsonRecord> {
if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
throw new Error("pagination page cap must be a positive integer");
}
const label = collectionKey === "workflow_runs" ? "workflow run" : "publisher job";
maxPages: number,
label: string,
): Promise<JsonRecord | undefined> {
const values: unknown[] = [];
const ids = new Set<number>();
let totalCount: number | undefined;
Expand All @@ -539,7 +537,7 @@ export async function collectPaginated(
}
if (totalCount === undefined) totalCount = pageTotal;
if (pageTotal !== totalCount) {
throw new Error(`${label} total_count changed during pagination`);
return undefined;
}
const pageValues = response[collectionKey];
if (!Array.isArray(pageValues) || pageValues.length > PAGE_SIZE) {
Expand All @@ -563,6 +561,29 @@ export async function collectPaginated(
throw new Error(`${label} pagination exceeded the ${maxPages}-page safety cap`);
}

export async function collectPaginated(
request: (path: string) => Promise<unknown>,
basePath: string,
collectionKey: "workflow_runs" | "jobs",
maxPages = MAX_API_PAGES,
): Promise<JsonRecord> {
if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
throw new Error("pagination page cap must be a positive integer");
}
const label = collectionKey === "workflow_runs" ? "workflow run" : "publisher job";
for (let attempt = 1; attempt <= PAGINATION_ATTEMPTS; attempt += 1) {
const result = await collectPaginationAttempt(
request,
basePath,
collectionKey,
maxPages,
label,
);
if (result) return result;
}
throw new Error(`${label} total_count changed during ${PAGINATION_ATTEMPTS} pagination attempts`);
}

function annotationValue(value: string): string {
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
}
Expand Down
Loading