diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index d842c5923f..8751ed4c40 100644 --- a/actions/setup/js/error_recovery.cjs +++ b/actions/setup/js/error_recovery.cjs @@ -54,6 +54,7 @@ function isTransientError(error) { "secondary rate limit", // GitHub secondary rate limits "abuse detection", // GitHub abuse detection "temporarily unavailable", + "no server is currently available", // GitHub API server unavailability ]; return transientPatterns.some(pattern => errorMsg.includes(pattern)); diff --git a/actions/setup/js/error_recovery.test.cjs b/actions/setup/js/error_recovery.test.cjs index 02d0b0bdb2..45c4bacf26 100644 --- a/actions/setup/js/error_recovery.test.cjs +++ b/actions/setup/js/error_recovery.test.cjs @@ -37,6 +37,11 @@ describe("error_recovery", () => { expect(isTransientError(new Error("Abuse detection triggered"))).toBe(true); }); + it("should identify GitHub server unavailability as transient", () => { + expect(isTransientError(new Error("No server is currently available to service your request"))).toBe(true); + expect(isTransientError(new Error("no server is currently available"))).toBe(true); + }); + it("should not identify validation errors as transient", () => { expect(isTransientError(new Error("Invalid input"))).toBe(false); expect(isTransientError(new Error("Field is required"))).toBe(false); diff --git a/actions/setup/js/frontmatter_hash_github_api.test.cjs b/actions/setup/js/frontmatter_hash_github_api.test.cjs index 63554be1f5..44ef183f09 100644 --- a/actions/setup/js/frontmatter_hash_github_api.test.cjs +++ b/actions/setup/js/frontmatter_hash_github_api.test.cjs @@ -1,8 +1,27 @@ // @ts-check -import { describe, it, expect, beforeAll } from "vitest"; +import { describe, it, expect, beforeAll, vi } from "vitest"; const path = require("path"); const fs = require("fs"); const { computeFrontmatterHash, createGitHubFileReader } = require("./frontmatter_hash_pure.cjs"); +const { withRetry, isTransientError } = require("./error_recovery.cjs"); + +// Retry configuration for live API tests +const LIVE_API_RETRY_CONFIG = { + maxRetries: 3, + initialDelayMs: 1000, + shouldRetry: isTransientError, +}; + +/** + * Wraps a file reader function with retry logic for transient GitHub API errors + * @param {Function} fileReader - The original file reader function + * @returns {Function} File reader with retry logic + */ +function createRetryableFileReader(fileReader) { + return async function (filePath) { + return withRetry(async () => fileReader(filePath), LIVE_API_RETRY_CONFIG, `fetch file ${filePath}`); + }; +} /** * Tests for frontmatter hash computation using GitHub's API to fetch real workflows. @@ -13,6 +32,14 @@ describe("frontmatter_hash with GitHub API", () => { let mockGitHub; beforeAll(() => { + // Mock @actions/core for retry logging in test environment + global.core = { + info: vi.fn((...args) => console.log(...args)), + warning: vi.fn((...args) => console.warn(...args)), + error: vi.fn((...args) => console.error(...args)), + debug: vi.fn((...args) => console.log(...args)), + }; + // Create a mock GitHub API client for testing // In real scenarios, this would be replaced with @actions/github mockGitHub = { @@ -354,7 +381,10 @@ describe("frontmatter_hash with GitHub API", () => { const ref = "main"; // Create file reader with real GitHub API - const fileReader = createGitHubFileReader(octokit, owner, repo, ref); + const baseFileReader = createGitHubFileReader(octokit, owner, repo, ref); + + // Wrap with retry logic to handle transient GitHub API errors + const fileReader = createRetryableFileReader(baseFileReader); // Test with a real public agentic workflow const workflowPath = ".github/workflows/audit-workflows.md";