mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-16 04:56:47 -07:00
style: fix prettier formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+57
-7
@@ -8737,6 +8737,16 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|||||||
exports.RunnerAvailabilityService = void 0;
|
exports.RunnerAvailabilityService = void 0;
|
||||||
const core_1 = __nccwpck_require__(76762);
|
const core_1 = __nccwpck_require__(76762);
|
||||||
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
||||||
|
/**
|
||||||
|
* Maximum number of pages to fetch when paginating through GitHub API results.
|
||||||
|
* 100 pages * 100 per page = 10,000 runners maximum.
|
||||||
|
*/
|
||||||
|
const MAX_PAGINATION_PAGES = 100;
|
||||||
|
/**
|
||||||
|
* Total timeout in milliseconds for the pagination loop.
|
||||||
|
* Prevents indefinite API calls if GitHub is slow or pagination is unexpectedly deep.
|
||||||
|
*/
|
||||||
|
const PAGINATION_TIMEOUT_MS = 30000;
|
||||||
/**
|
/**
|
||||||
* Checks GitHub Actions runner availability to support automatic provider fallback.
|
* Checks GitHub Actions runner availability to support automatic provider fallback.
|
||||||
*
|
*
|
||||||
@@ -8807,24 +8817,64 @@ class RunnerAvailabilityService {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Fetch all runners for a repository, handling pagination.
|
* Fetch all runners for a repository, handling pagination.
|
||||||
|
*
|
||||||
|
* Includes defensive limits:
|
||||||
|
* - Maximum page count (MAX_PAGINATION_PAGES) to prevent infinite loops
|
||||||
|
* - Total timeout (PAGINATION_TIMEOUT_MS) to prevent indefinite API calls
|
||||||
|
* - Rate-limit detection (HTTP 403/429 with X-RateLimit-Remaining header)
|
||||||
*/
|
*/
|
||||||
static async fetchRunners(octokit, owner, repo) {
|
static async fetchRunners(octokit, owner, repo) {
|
||||||
const allRunners = [];
|
const allRunners = [];
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const perPage = 100;
|
const perPage = 100;
|
||||||
while (true) {
|
const startTime = Date.now();
|
||||||
const response = await octokit.request('GET /repos/{owner}/{repo}/actions/runners', {
|
while (page <= MAX_PAGINATION_PAGES) {
|
||||||
owner,
|
// Check total timeout
|
||||||
repo,
|
if (Date.now() - startTime > PAGINATION_TIMEOUT_MS) {
|
||||||
per_page: perPage,
|
orchestrator_logger_1.default.logWarning(`[RunnerAvailability] Pagination timeout reached after ${page - 1} pages and ${Date.now() - startTime}ms. ` +
|
||||||
page,
|
`Using ${allRunners.length} runners found so far.`);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await octokit.request('GET /repos/{owner}/{repo}/actions/runners', {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: perPage,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (requestError) {
|
||||||
|
// Octokit throws for non-2xx responses. Check if this is a rate limit error.
|
||||||
|
const status = requestError.status ?? requestError.response?.status;
|
||||||
|
if (status === 403 || status === 429) {
|
||||||
|
const resetTime = requestError.response?.headers?.['x-ratelimit-reset'] ?? requestError.headers?.['x-ratelimit-reset'];
|
||||||
|
const resetMessage = resetTime
|
||||||
|
? ` Resets at ${new Date(Number.parseInt(String(resetTime), 10) * 1000).toISOString()}`
|
||||||
|
: '';
|
||||||
|
orchestrator_logger_1.default.logWarning(`[RunnerAvailability] GitHub API rate limit reached (HTTP ${status}).${resetMessage} ` +
|
||||||
|
`Using ${allRunners.length} runners found so far.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Re-throw non-rate-limit errors to be handled by the outer catch
|
||||||
|
throw requestError;
|
||||||
|
}
|
||||||
const runners = (response.data.runners || []);
|
const runners = (response.data.runners || []);
|
||||||
allRunners.push(...runners);
|
allRunners.push(...runners);
|
||||||
if (runners.length < perPage)
|
if (runners.length < perPage)
|
||||||
break;
|
break;
|
||||||
page++;
|
page++;
|
||||||
}
|
}
|
||||||
|
if (page > MAX_PAGINATION_PAGES) {
|
||||||
|
orchestrator_logger_1.default.logWarning(`[RunnerAvailability] Maximum pagination limit reached (${MAX_PAGINATION_PAGES} pages). ` +
|
||||||
|
`Using ${allRunners.length} runners found so far.`);
|
||||||
|
}
|
||||||
|
if (allRunners.length === 0) {
|
||||||
|
orchestrator_logger_1.default.log('[RunnerAvailability] No runners found. Possible causes: ' +
|
||||||
|
'wrong token permissions (needs repo or actions scope), ' +
|
||||||
|
'no self-hosted runners registered, ' +
|
||||||
|
'or runners are registered at the organization level instead of the repository.');
|
||||||
|
}
|
||||||
return allRunners;
|
return allRunners;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -84,7 +84,9 @@ class Orchestrator {
|
|||||||
const token = Orchestrator.buildParameters.gitPrivateToken || process.env.GITHUB_TOKEN || '';
|
const token = Orchestrator.buildParameters.gitPrivateToken || process.env.GITHUB_TOKEN || '';
|
||||||
|
|
||||||
OrchestratorLogger.log(
|
OrchestratorLogger.log(
|
||||||
`Checking runner availability (labels: [${Orchestrator.buildParameters.runnerCheckLabels.join(', ')}], min: ${Orchestrator.buildParameters.runnerCheckMinAvailable})`,
|
`Checking runner availability (labels: [${Orchestrator.buildParameters.runnerCheckLabels.join(', ')}], min: ${
|
||||||
|
Orchestrator.buildParameters.runnerCheckMinAvailable
|
||||||
|
})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability(
|
const result = await RunnerAvailabilityService.checkAvailability(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
it('should fallback when no runners are registered', async () => {
|
it('should fallback when no runners are registered', async () => {
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners: [] } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners: [] } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(true);
|
expect(result.shouldFallback).toBe(true);
|
||||||
@@ -58,7 +58,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'runner-2', status: 'online', busy: false, labels: ['self-hosted', 'linux'] },
|
{ name: 'runner-2', status: 'online', busy: false, labels: ['self-hosted', 'linux'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(false);
|
expect(result.shouldFallback).toBe(false);
|
||||||
@@ -72,7 +72,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'runner-2', status: 'online', busy: true, labels: ['self-hosted'] },
|
{ name: 'runner-2', status: 'online', busy: true, labels: ['self-hosted'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(true);
|
expect(result.shouldFallback).toBe(true);
|
||||||
@@ -85,7 +85,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'runner-1', status: 'offline', busy: false, labels: ['self-hosted'] },
|
{ name: 'runner-1', status: 'offline', busy: false, labels: ['self-hosted'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(true);
|
expect(result.shouldFallback).toBe(true);
|
||||||
@@ -98,7 +98,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'windows-runner', status: 'online', busy: false, labels: ['self-hosted', 'windows'] },
|
{ name: 'windows-runner', status: 'online', busy: false, labels: ['self-hosted', 'windows'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability(
|
const result = await RunnerAvailabilityService.checkAvailability(
|
||||||
'owner',
|
'owner',
|
||||||
@@ -119,7 +119,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'windows-runner', status: 'online', busy: false, labels: ['self-hosted', 'windows'] },
|
{ name: 'windows-runner', status: 'online', busy: false, labels: ['self-hosted', 'windows'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability(
|
const result = await RunnerAvailabilityService.checkAvailability(
|
||||||
'owner',
|
'owner',
|
||||||
@@ -135,11 +135,9 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should respect minAvailable threshold', async () => {
|
it('should respect minAvailable threshold', async () => {
|
||||||
const runners = createMockRunners([
|
const runners = createMockRunners([{ name: 'runner-1', status: 'online', busy: false, labels: ['self-hosted'] }]);
|
||||||
{ name: 'runner-1', status: 'online', busy: false, labels: ['self-hosted'] },
|
|
||||||
]);
|
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
// Need 2, have 1 — should fallback
|
// Need 2, have 1 — should fallback
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 2);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 2);
|
||||||
@@ -152,7 +150,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'runner-1', status: 'online', busy: false, labels: ['Self-Hosted', 'Linux'] },
|
{ name: 'runner-1', status: 'online', busy: false, labels: ['Self-Hosted', 'Linux'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability(
|
const result = await RunnerAvailabilityService.checkAvailability(
|
||||||
'owner',
|
'owner',
|
||||||
@@ -167,7 +165,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
it('should not fallback on API error (fail-open)', async () => {
|
it('should not fallback on API error (fail-open)', async () => {
|
||||||
const mockRequest = jest.fn().mockRejectedValue(new Error('403 Forbidden'));
|
const mockRequest = jest.fn().mockRejectedValue(new Error('403 Forbidden'));
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(false);
|
expect(result.shouldFallback).toBe(false);
|
||||||
@@ -181,7 +179,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
{ name: 'offline', status: 'offline', busy: false, labels: ['self-hosted'] },
|
{ name: 'offline', status: 'offline', busy: false, labels: ['self-hosted'] },
|
||||||
]);
|
]);
|
||||||
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
const mockRequest = jest.fn().mockResolvedValue({ status: 200, data: { runners } });
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
expect(result.shouldFallback).toBe(false);
|
expect(result.shouldFallback).toBe(false);
|
||||||
@@ -208,7 +206,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
return Promise.resolve({ status: 200, data: { runners } });
|
return Promise.resolve({ status: 200, data: { runners } });
|
||||||
});
|
});
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
|
|
||||||
@@ -245,7 +243,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
return Promise.resolve({ status: 200, data: { runners } });
|
return Promise.resolve({ status: 200, data: { runners } });
|
||||||
});
|
});
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
|
|
||||||
@@ -271,7 +269,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
return Promise.resolve({ status: 200, data: { runners: [] } });
|
return Promise.resolve({ status: 200, data: { runners: [] } });
|
||||||
});
|
});
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
|
|
||||||
@@ -304,7 +302,7 @@ describe('RunnerAvailabilityService', () => {
|
|||||||
|
|
||||||
return Promise.resolve({ status: 200, data: { runners } });
|
return Promise.resolve({ status: 200, data: { runners } });
|
||||||
});
|
});
|
||||||
MockedOctokit.mockImplementation(() => ({ request: mockRequest }) as any);
|
MockedOctokit.mockImplementation(() => ({ request: mockRequest } as any));
|
||||||
|
|
||||||
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
const result = await RunnerAvailabilityService.checkAvailability('owner', 'repo', 'token', [], 1);
|
||||||
|
|
||||||
|
|||||||
@@ -149,8 +149,7 @@ export class RunnerAvailabilityService {
|
|||||||
const status = requestError.status ?? requestError.response?.status;
|
const status = requestError.status ?? requestError.response?.status;
|
||||||
if (status === 403 || status === 429) {
|
if (status === 403 || status === 429) {
|
||||||
const resetTime =
|
const resetTime =
|
||||||
requestError.response?.headers?.['x-ratelimit-reset'] ??
|
requestError.response?.headers?.['x-ratelimit-reset'] ?? requestError.headers?.['x-ratelimit-reset'];
|
||||||
requestError.headers?.['x-ratelimit-reset'];
|
|
||||||
const resetMessage = resetTime
|
const resetMessage = resetTime
|
||||||
? ` Resets at ${new Date(Number.parseInt(String(resetTime), 10) * 1000).toISOString()}`
|
? ` Resets at ${new Date(Number.parseInt(String(resetTime), 10) * 1000).toISOString()}`
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
Reference in New Issue
Block a user