fix(ci): retry fresh artifact metadata reads (#121876)

This commit is contained in:
Vincent Koc
2026-08-11 14:01:31 +08:00
committed by GitHub
parent 432356a684
commit 65d13cb80b
2 changed files with 102 additions and 6 deletions
+76 -2
View File
@@ -235,6 +235,11 @@ case "$path" in
count_file="$FAKE_GH_STATE/attempt"
count="$(( $(cat "$count_file" 2>/dev/null || printf 0) + 1 ))"
printf '%s\\n' "$count" > "$count_file"
if [[ "$count" -le "\${FAKE_GH_ATTEMPT_FAILURES:-0}" ]]; then
printf 'partial attempt response\\n'
printf '%s\\n' "\${FAKE_GH_ATTEMPT_ERROR:-gh: request failed}" >&2
exit 1
fi
printf '{"id":%s,"run_attempt":%s}\\n' \
"$FAKE_ATTEMPT_RUN_ID" "$FAKE_ARTIFACT_RUN_ATTEMPT"
;;
@@ -363,6 +368,69 @@ describe("shared Docker image artifacts", () => {
}
});
it("retries a fresh artifact metadata 404 and then succeeds", () => {
const fixture = createFixture();
try {
const verified = verifyUploadedArtifact(fixture, {
env: {
FAKE_GH_ARTIFACT_ERROR: "gh: Not Found (HTTP 404)",
FAKE_GH_ARTIFACT_FAILURES: "1",
},
});
expect(verified.status, `${verified.stdout}\n${verified.stderr}`).toBe(0);
expect(verified.stderr).toContain(
"artifact metadata GitHub API GET failed transiently on attempt 1/3; retrying in 2s",
);
expect(readFileSync(fixture.sleepLog, "utf8")).toBe("2\n");
const calls = readFileSync(fixture.ghLog, "utf8");
expect(calls.match(/actions\/artifacts/g)).toHaveLength(2);
expect(calls.match(/actions\/runs/g)).toHaveLength(1);
} finally {
rmSync(fixture.root, { force: true, recursive: true });
}
});
it("fails after three fresh artifact metadata 404 responses", () => {
const fixture = createFixture();
try {
const failed = verifyUploadedArtifact(fixture, {
env: {
FAKE_GH_ARTIFACT_ERROR: "gh: Not Found (HTTP 404)",
FAKE_GH_ARTIFACT_FAILURES: "3",
},
});
expect(failed.status).not.toBe(0);
expect(failed.stderr).toContain("GitHub API GET failed after 3 attempt(s)");
expect(readFileSync(fixture.sleepLog, "utf8")).toBe("2\n4\n");
const calls = readFileSync(fixture.ghLog, "utf8");
expect(calls.match(/actions\/artifacts/g)).toHaveLength(3);
expect(calls).not.toContain("actions/runs");
} finally {
rmSync(fixture.root, { force: true, recursive: true });
}
});
it("fails immediately when producer run-attempt metadata returns 404", () => {
const fixture = createFixture();
try {
const failed = verifyUploadedArtifact(fixture, {
env: {
FAKE_GH_ATTEMPT_ERROR: "gh: Not Found (HTTP 404)",
FAKE_GH_ATTEMPT_FAILURES: "3",
},
});
expect(failed.status).not.toBe(0);
expect(failed.stderr).toContain("GitHub API GET failed after 1 attempt(s)");
expect(failed.stderr).not.toContain("retrying");
expect(readFileSync(fixture.sleepLog, "utf8")).toBe("");
const calls = readFileSync(fixture.ghLog, "utf8");
expect(calls.match(/actions\/artifacts/g)).toHaveLength(1);
expect(calls.match(/actions\/runs/g)).toHaveLength(1);
} finally {
rmSync(fixture.root, { force: true, recursive: true });
}
});
it("recovers on the third attempt and fails cleanly when all attempts are exhausted", () => {
const recoveredFixture = createFixture();
try {
@@ -402,7 +470,7 @@ describe("shared Docker image artifacts", () => {
}
});
it("retries explicit rate limiting but not permanent HTTP or credential failures", () => {
it("retries explicit rate limiting but fails fast on permanent HTTP and credential errors", () => {
const rateLimitFixture = createFixture();
try {
const rateLimited = verifyUploadedArtifact(rateLimitFixture, {
@@ -417,7 +485,13 @@ describe("shared Docker image artifacts", () => {
rmSync(rateLimitFixture.root, { force: true, recursive: true });
}
for (const error of ["gh: Not Found (HTTP 404)", "gh: Bad credentials (HTTP 500)"]) {
for (const error of [
"gh: Unauthorized (HTTP 401)",
"gh: Forbidden (HTTP 403)",
"gh: Unprocessable Entity (HTTP 422)",
"gh: Bad credentials (HTTP 500)",
"gh: authentication failed (HTTP 500)",
]) {
const fixture = createFixture();
try {
const failed = verifyUploadedArtifact(fixture, {