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
1 change: 1 addition & 0 deletions actions/setup/js/error_recovery.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
5 changes: 5 additions & 0 deletions actions/setup/js/error_recovery.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 32 additions & 2 deletions actions/setup/js/frontmatter_hash_github_api.test.cjs
Original file line number Diff line number Diff line change
@@ -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,
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIVE_API_RETRY_CONFIG.initialDelayMs is likely not the actual delay before the first retry with the current withRetry implementation: withRetry multiplies delay at the end of the first failure, so the first retry sleeps initialDelayMs * backoffMultiplier (default 2). If the intent is a 1s first backoff, either pass backoffMultiplier: 1 / adjust initialDelayMs, or change withRetry to increase the delay after sleeping so initialDelayMs is honored for the first retry.

Suggested change
initialDelayMs: 1000,
initialDelayMs: 1000,
backoffMultiplier: 1,

Copilot uses AI. Check for mistakes.
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.
Expand All @@ -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)),
Comment on lines 34 to +38
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test sets global.core in beforeAll but never restores the previous value. Since Vitest may execute multiple test files in the same worker, this can leak logging mocks into other suites. Please save the prior global.core value and restore it in an afterAll/afterEach (or delete global.core if it was unset).

Copilot uses AI. Check for mistakes.
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 = {
Expand Down Expand Up @@ -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";
Expand Down
Loading