This commit is contained in:
Anatoly Rabkin 2026-03-18 23:52:21 +02:00 committed by GitHub
commit 047af58489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 815 additions and 119 deletions

View file

@ -155,6 +155,28 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Default: true # Default: true
set-safe-directory: '' set-safe-directory: ''
# Timeout in seconds for each git network operation attempt (e.g. fetch,
# lfs-fetch, ls-remote). If a single attempt exceeds this, the process is
# terminated. If retries are configured (see retry-max-attempts), the operation
# will be retried. Set to 0 to disable. Default is 300 (5 minutes).
# Default: 300
timeout: ''
# Total number of attempts for each git network operation (including the initial
# attempt). For example, 3 means one initial attempt plus up to 2 retries.
# Default: 3
retry-max-attempts: ''
# Minimum backoff time in seconds between retry attempts. The actual backoff is
# randomly chosen between min and max.
# Default: 10
retry-min-backoff: ''
# Maximum backoff time in seconds between retry attempts. The actual backoff is
# randomly chosen between min and max.
# Default: 20
retry-max-backoff: ''
# The base URL for the GitHub instance that you are trying to clone from, will use # The base URL for the GitHub instance that you are trying to clone from, will use
# environment defaults to fetch from the same instance that the workflow is # environment defaults to fetch from the same instance that the workflow is
# running from unless specified. Example URLs are https://github.com or # running from unless specified. Example URLs are https://github.com or

View file

@ -1146,7 +1146,9 @@ async function setup(testName: string): Promise<void> {
} }
), ),
tryReset: jest.fn(), tryReset: jest.fn(),
version: jest.fn() version: jest.fn(),
setTimeout: jest.fn(),
setRetryConfig: jest.fn()
} }
settings = { settings = {
@ -1173,7 +1175,11 @@ async function setup(testName: string): Promise<void> {
sshUser: '', sshUser: '',
workflowOrganizationId: 123456, workflowOrganizationId: 123456,
setSafeDirectory: true, setSafeDirectory: true,
githubServerUrl: githubServerUrl githubServerUrl: githubServerUrl,
timeout: 300,
retryMaxAttempts: 3,
retryMinBackoff: 10,
retryMaxBackoff: 20
} }
} }

View file

@ -5,6 +5,17 @@ import * as commandManager from '../lib/git-command-manager'
let git: commandManager.IGitCommandManager let git: commandManager.IGitCommandManager
let mockExec = jest.fn() let mockExec = jest.fn()
function createMockGit(): Promise<commandManager.IGitCommandManager> {
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
return commandManager.createCommandManager('test', false, false)
}
describe('git-auth-helper tests', () => { describe('git-auth-helper tests', () => {
beforeAll(async () => {}) beforeAll(async () => {})
@ -494,3 +505,73 @@ describe('git user-agent with orchestration ID', () => {
) )
}) })
}) })
describe('timeout and retry configuration', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
})
it('setTimeout accepts valid values', async () => {
git = await createMockGit()
git.setTimeout(30)
git.setTimeout(0)
})
it('setTimeout rejects negative values', async () => {
git = await createMockGit()
expect(() => git.setTimeout(-1)).toThrow(/non-negative/)
})
it('setRetryConfig accepts valid parameters', async () => {
git = await createMockGit()
git.setRetryConfig(5, 2, 15)
})
it('setRetryConfig rejects min > max backoff', async () => {
git = await createMockGit()
expect(() => git.setRetryConfig(3, 20, 5)).toThrow(
/min seconds should be less than or equal to max seconds/
)
})
it('fetch without timeout uses exec', async () => {
git = await createMockGit()
// timeout defaults to 0 (disabled)
mockExec.mockClear()
await git.fetch(['refs/heads/main'], {})
// exec.exec is used (via retryHelper) when no timeout
const fetchCalls = mockExec.mock.calls.filter(
(call: any[]) => (call[1] as string[]).includes('fetch')
)
expect(fetchCalls).toHaveLength(1)
})
it('fetch with timeout does not use exec', async () => {
git = await createMockGit()
// Short timeout and single attempt so the test completes quickly
git.setTimeout(1)
git.setRetryConfig(1, 0, 0)
mockExec.mockClear()
// fetch will use spawn path (which will fail/timeout since there's
// no real git repo), but we verify exec.exec was NOT called for fetch
try {
await git.fetch(['refs/heads/main'], {})
} catch {
// Expected: spawn will fail/timeout in test environment
}
const fetchCalls = mockExec.mock.calls.filter(
(call: any[]) => (call[1] as string[]).includes('fetch')
)
expect(fetchCalls).toHaveLength(0)
}, 10000)
})

View file

@ -506,6 +506,8 @@ async function setup(testName: string): Promise<void> {
tryReset: jest.fn(async () => { tryReset: jest.fn(async () => {
return true return true
}), }),
version: jest.fn() version: jest.fn(),
setTimeout: jest.fn(),
setRetryConfig: jest.fn()
} }
} }

View file

@ -144,4 +144,58 @@ describe('input-helper tests', () => {
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.workflowOrganizationId).toBe(123456) expect(settings.workflowOrganizationId).toBe(123456)
}) })
it('sets timeout and retry defaults', async () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.timeout).toBe(300)
expect(settings.retryMaxAttempts).toBe(3)
expect(settings.retryMinBackoff).toBe(10)
expect(settings.retryMaxBackoff).toBe(20)
})
it('allows timeout 0 to disable', async () => {
inputs.timeout = '0'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.timeout).toBe(0)
})
it('parses custom timeout and retry values', async () => {
inputs.timeout = '30'
inputs['retry-max-attempts'] = '5'
inputs['retry-min-backoff'] = '2'
inputs['retry-max-backoff'] = '15'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.timeout).toBe(30)
expect(settings.retryMaxAttempts).toBe(5)
expect(settings.retryMinBackoff).toBe(2)
expect(settings.retryMaxBackoff).toBe(15)
})
it('clamps retry-max-backoff to min when less than min and warns', async () => {
inputs['retry-min-backoff'] = '20'
inputs['retry-max-backoff'] = '5'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.retryMaxBackoff).toBe(20)
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining("'retry-max-backoff' (5) is less than 'retry-min-backoff' (20)")
)
})
it('defaults invalid timeout to 300 and warns', async () => {
inputs.timeout = 'garbage'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.timeout).toBe(300)
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining("Invalid value 'garbage' for 'timeout'")
)
})
it('defaults negative retry-max-attempts to 3 and warns', async () => {
inputs['retry-max-attempts'] = '-1'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.retryMaxAttempts).toBe(3)
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining("Invalid value '-1' for 'retry-max-attempts'")
)
})
}) })

View file

@ -95,6 +95,28 @@ inputs:
set-safe-directory: set-safe-directory:
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>` description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
default: true default: true
timeout:
description: >
Timeout in seconds for each git network operation attempt (e.g. fetch, lfs-fetch, ls-remote).
If a single attempt exceeds this, the process is terminated.
If retries are configured (see retry-max-attempts), the operation will be retried.
Set to 0 to disable. Default is 300 (5 minutes).
default: 300
retry-max-attempts:
description: >
Total number of attempts for each git network operation (including the initial attempt).
For example, 3 means one initial attempt plus up to 2 retries.
default: 3
retry-min-backoff:
description: >
Minimum backoff time in seconds between retry attempts.
The actual backoff is randomly chosen between min and max.
default: 10
retry-max-backoff:
description: >
Maximum backoff time in seconds between retry attempts.
The actual backoff is randomly chosen between min and max.
default: 20
github-server-url: github-server-url:
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
required: false required: false

253
dist/index.js vendored
View file

@ -655,6 +655,7 @@ const io = __importStar(__nccwpck_require__(7436));
const path = __importStar(__nccwpck_require__(1017)); const path = __importStar(__nccwpck_require__(1017));
const regexpHelper = __importStar(__nccwpck_require__(3120)); const regexpHelper = __importStar(__nccwpck_require__(3120));
const retryHelper = __importStar(__nccwpck_require__(2155)); const retryHelper = __importStar(__nccwpck_require__(2155));
const child_process_1 = __nccwpck_require__(2081);
const git_version_1 = __nccwpck_require__(3142); const git_version_1 = __nccwpck_require__(3142);
// Auth header not supported before 2.9 // Auth header not supported before 2.9
// Wire protocol v2 not supported before 2.18 // Wire protocol v2 not supported before 2.18
@ -678,6 +679,8 @@ class GitCommandManager {
this.doSparseCheckout = false; this.doSparseCheckout = false;
this.workingDirectory = ''; this.workingDirectory = '';
this.gitVersion = new git_version_1.GitVersion(); this.gitVersion = new git_version_1.GitVersion();
this.timeoutMs = 0;
this.networkRetryHelper = new retryHelper.RetryHelper();
} }
branchDelete(remote, branch) { branchDelete(remote, branch) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
@ -735,7 +738,7 @@ class GitCommandManager {
} }
}; };
// Suppress the output in order to avoid flooding annotations with innocuous errors. // Suppress the output in order to avoid flooding annotations with innocuous errors.
yield this.execGit(args, false, true, listeners); yield this.execGit(args, { silent: true, customListeners: listeners });
core.debug(`stderr callback is: ${stderr}`); core.debug(`stderr callback is: ${stderr}`);
core.debug(`errline callback is: ${errline}`); core.debug(`errline callback is: ${errline}`);
core.debug(`stdout callback is: ${stdout}`); core.debug(`stdout callback is: ${stdout}`);
@ -823,7 +826,7 @@ class GitCommandManager {
'--name-only', '--name-only',
'--get-regexp', '--get-regexp',
pattern pattern
], true); ], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -851,15 +854,15 @@ class GitCommandManager {
args.push(arg); args.push(arg);
} }
const that = this; const that = this;
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
yield that.execGit(args); yield that.execGit(args, { timeoutMs: that.timeoutMs });
})); }));
}); });
} }
getDefaultBranch(repositoryUrl) { getDefaultBranch(repositoryUrl) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let output; let output;
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
output = yield this.execGit([ output = yield this.execGit([
'ls-remote', 'ls-remote',
'--quiet', '--quiet',
@ -867,7 +870,7 @@ class GitCommandManager {
'--symref', '--symref',
repositoryUrl, repositoryUrl,
'HEAD' 'HEAD'
]); ], { timeoutMs: this.timeoutMs });
})); }));
if (output) { if (output) {
// Satisfy compiler, will always be set // Satisfy compiler, will always be set
@ -904,7 +907,7 @@ class GitCommandManager {
isDetached() { isDetached() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22 // Note, "branch --show-current" would be simpler but isn't available until Git 2.22
const output = yield this.execGit(['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'], true); const output = yield this.execGit(['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'], { allowAllExitCodes: true });
return !output.stdout.trim().startsWith('refs/heads/'); return !output.stdout.trim().startsWith('refs/heads/');
}); });
} }
@ -912,8 +915,8 @@ class GitCommandManager {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const args = ['lfs', 'fetch', 'origin', ref]; const args = ['lfs', 'fetch', 'origin', ref];
const that = this; const that = this;
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
yield that.execGit(args); yield that.execGit(args, { timeoutMs: that.timeoutMs });
})); }));
}); });
} }
@ -925,8 +928,8 @@ class GitCommandManager {
log1(format) { log1(format) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const args = format ? ['log', '-1', format] : ['log', '-1']; const args = format ? ['log', '-1', format] : ['log', '-1'];
const silent = format ? false : true; const silent = !format;
const output = yield this.execGit(args, false, silent); const output = yield this.execGit(args, { silent });
return output.stdout; return output.stdout;
}); });
} }
@ -956,7 +959,7 @@ class GitCommandManager {
shaExists(sha) { shaExists(sha) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]; const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`];
const output = yield this.execGit(args, true); const output = yield this.execGit(args, { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -977,7 +980,10 @@ class GitCommandManager {
if (recursive) { if (recursive) {
args.push('--recursive'); args.push('--recursive');
} }
yield this.execGit(args); const that = this;
yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
yield that.execGit(args, { timeoutMs: that.timeoutMs });
}));
}); });
} }
submoduleUpdate(fetchDepth, recursive) { submoduleUpdate(fetchDepth, recursive) {
@ -990,12 +996,15 @@ class GitCommandManager {
if (recursive) { if (recursive) {
args.push('--recursive'); args.push('--recursive');
} }
yield this.execGit(args); const that = this;
yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
yield that.execGit(args, { timeoutMs: that.timeoutMs });
}));
}); });
} }
submoduleStatus() { submoduleStatus() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['submodule', 'status'], true); const output = yield this.execGit(['submodule', 'status'], { allowAllExitCodes: true });
core.debug(output.stdout); core.debug(output.stdout);
return output.exitCode === 0; return output.exitCode === 0;
}); });
@ -1008,7 +1017,7 @@ class GitCommandManager {
} }
tryClean() { tryClean() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['clean', '-ffdx'], true); const output = yield this.execGit(['clean', '-ffdx'], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -1019,7 +1028,7 @@ class GitCommandManager {
globalConfig ? '--global' : '--local', globalConfig ? '--global' : '--local',
'--unset-all', '--unset-all',
configKey configKey
], true); ], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -1033,19 +1042,19 @@ class GitCommandManager {
args.push(globalConfig ? '--global' : '--local'); args.push(globalConfig ? '--global' : '--local');
} }
args.push('--unset', configKey, configValue); args.push('--unset', configKey, configValue);
const output = yield this.execGit(args, true); const output = yield this.execGit(args, { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
tryDisableAutomaticGarbageCollection() { tryDisableAutomaticGarbageCollection() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
tryGetFetchUrl() { tryGetFetchUrl() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', '--get', 'remote.origin.url'], true); const output = yield this.execGit(['config', '--local', '--get', 'remote.origin.url'], { allowAllExitCodes: true });
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
return ''; return '';
} }
@ -1066,7 +1075,7 @@ class GitCommandManager {
args.push(globalConfig ? '--global' : '--local'); args.push(globalConfig ? '--global' : '--local');
} }
args.push('--get-all', configKey); args.push('--get-all', configKey);
const output = yield this.execGit(args, true); const output = yield this.execGit(args, { allowAllExitCodes: true });
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
return []; return [];
} }
@ -1086,7 +1095,7 @@ class GitCommandManager {
args.push(globalConfig ? '--global' : '--local'); args.push(globalConfig ? '--global' : '--local');
} }
args.push('--name-only', '--get-regexp', pattern); args.push('--name-only', '--get-regexp', pattern);
const output = yield this.execGit(args, true); const output = yield this.execGit(args, { allowAllExitCodes: true });
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
return []; return [];
} }
@ -1098,7 +1107,7 @@ class GitCommandManager {
} }
tryReset() { tryReset() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true); const output = yield this.execGit(['reset', '--hard', 'HEAD'], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -1107,6 +1116,25 @@ class GitCommandManager {
return this.gitVersion; return this.gitVersion;
}); });
} }
/**
* Sets the timeout for network git operations.
* @param timeoutSeconds Timeout in seconds. 0 disables the timeout.
*/
setTimeout(timeoutSeconds) {
if (timeoutSeconds < 0) {
throw new Error(`Timeout must be non-negative, got ${timeoutSeconds}`);
}
this.timeoutMs = timeoutSeconds * 1000;
}
/**
* Configures retry behavior for network git operations.
* @param maxAttempts Total attempts including the initial one. Must be >= 1.
* @param minBackoffSeconds Minimum backoff between retries. Must be <= maxBackoffSeconds.
* @param maxBackoffSeconds Maximum backoff between retries.
*/
setRetryConfig(maxAttempts, minBackoffSeconds, maxBackoffSeconds) {
this.networkRetryHelper = new retryHelper.RetryHelper(maxAttempts, minBackoffSeconds, maxBackoffSeconds);
}
static createCommandManager(workingDirectory, lfs, doSparseCheckout) { static createCommandManager(workingDirectory, lfs, doSparseCheckout) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const result = new GitCommandManager(); const result = new GitCommandManager();
@ -1115,8 +1143,19 @@ class GitCommandManager {
}); });
} }
execGit(args_1) { execGit(args_1) {
return __awaiter(this, arguments, void 0, function* (args, allowAllExitCodes = false, silent = false, customListeners = {}) { return __awaiter(this, arguments, void 0, function* (args, options = {}) {
const { allowAllExitCodes = false, silent = false, customListeners = {}, timeoutMs = 0 } = options;
fshelper.directoryExistsSync(this.workingDirectory, true); fshelper.directoryExistsSync(this.workingDirectory, true);
// Use child_process.spawn directly when timeout is set,
// so we can kill the process on timeout and avoid orphaned git processes.
// Note: customListeners are not supported in the timeout path.
if (timeoutMs > 0) {
if (customListeners &&
Object.keys(customListeners).length > 0) {
core.debug('customListeners are not supported with timeoutMs and will be ignored');
}
return yield this.execGitWithTimeout(args, timeoutMs, silent, allowAllExitCodes);
}
const result = new GitOutput(); const result = new GitOutput();
const env = {}; const env = {};
for (const key of Object.keys(process.env)) { for (const key of Object.keys(process.env)) {
@ -1132,20 +1171,123 @@ class GitCommandManager {
}; };
const mergedListeners = Object.assign(Object.assign({}, defaultListener), customListeners); const mergedListeners = Object.assign(Object.assign({}, defaultListener), customListeners);
const stdout = []; const stdout = [];
const options = { const execOptions = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
silent, silent,
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: mergedListeners listeners: mergedListeners
}; };
result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options); result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, execOptions);
result.stdout = stdout.join(''); result.stdout = stdout.join('');
core.debug(result.exitCode.toString()); core.debug(result.exitCode.toString());
core.debug(result.stdout); core.debug(result.stdout);
return result; return result;
}); });
} }
/**
* Executes a git command with a timeout. Uses child_process.spawn directly
* (instead of @actions/exec) so we can kill the process on timeout and
* terminate it cleanly. Does not support customListeners.
*/
execGitWithTimeout(args, timeoutMs, silent, allowAllExitCodes) {
return __awaiter(this, void 0, void 0, function* () {
const result = new GitOutput();
const env = {};
for (const key of Object.keys(process.env)) {
env[key] = process.env[key];
}
for (const key of Object.keys(this.gitEnv)) {
env[key] = this.gitEnv[key];
}
const stdout = [];
const stderr = [];
return new Promise((resolve, reject) => {
var _a;
const child = (0, child_process_1.spawn)(this.gitPath, args, {
cwd: this.workingDirectory,
env,
stdio: ['ignore', 'pipe', 'pipe']
});
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
stdout.push(data.toString());
});
if (child.stderr) {
child.stderr.on('data', (data) => {
stderr.push(data.toString());
if (!silent) {
process.stderr.write(data);
}
});
}
let settled = false;
let timedOut = false;
let forceKillTimer;
const cleanup = () => {
clearTimeout(timer);
if (forceKillTimer) {
clearTimeout(forceKillTimer);
}
};
const timer = global.setTimeout(() => {
timedOut = true;
// SIGTERM first, then force SIGKILL after 5 seconds.
// On Windows, SIGTERM is equivalent to a forced kill, so
// the SIGKILL fallback is effectively a no-op there.
child.kill('SIGTERM');
forceKillTimer = global.setTimeout(() => {
try {
child.kill('SIGKILL');
}
catch (killErr) {
core.debug(`Failed to SIGKILL git process: ${killErr}`);
}
}, 5000);
if (forceKillTimer.unref) {
forceKillTimer.unref();
}
}, timeoutMs);
if (timer.unref) {
timer.unref();
}
child.on('close', (code) => {
if (settled)
return;
settled = true;
cleanup();
if (timedOut) {
reject(new Error(`Git operation timed out after ${timeoutMs / 1000} seconds: git ${args.slice(0, 5).join(' ')}...`));
return;
}
// null code means killed by signal (e.g. OOM killer, external SIGTERM)
if (code === null) {
const stderrText = stderr.join('').trim();
reject(new Error(`The process 'git' was killed by a signal` +
(stderrText ? `\n${stderrText}` : '')));
return;
}
if (code !== 0 && !allowAllExitCodes) {
const stderrText = stderr.join('').trim();
reject(new Error(`The process 'git' failed with exit code ${code}` +
(stderrText ? `\n${stderrText}` : '')));
return;
}
result.exitCode = code;
result.stdout = stdout.join('');
core.debug(result.exitCode.toString());
core.debug(result.stdout);
resolve(result);
});
child.on('error', (err) => {
if (settled)
return;
settled = true;
cleanup();
reject(err);
});
});
});
}
initializeCommandManager(workingDirectory, lfs, doSparseCheckout) { initializeCommandManager(workingDirectory, lfs, doSparseCheckout) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
this.workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
@ -1448,6 +1590,10 @@ function getSource(settings) {
core.startGroup('Getting Git version info'); core.startGroup('Getting Git version info');
const git = yield getGitCommandManager(settings); const git = yield getGitCommandManager(settings);
core.endGroup(); core.endGroup();
if (git) {
git.setTimeout(settings.timeout);
git.setRetryConfig(settings.retryMaxAttempts, settings.retryMinBackoff, settings.retryMaxBackoff);
}
let authHelper = null; let authHelper = null;
try { try {
if (git) { if (git) {
@ -2095,6 +2241,41 @@ function getInputs() {
// Determine the GitHub URL that the repository is being hosted from // Determine the GitHub URL that the repository is being hosted from
result.githubServerUrl = core.getInput('github-server-url'); result.githubServerUrl = core.getInput('github-server-url');
core.debug(`GitHub Host URL = ${result.githubServerUrl}`); core.debug(`GitHub Host URL = ${result.githubServerUrl}`);
// Timeout per network operation attempt
const timeoutInput = core.getInput('timeout');
result.timeout = Math.floor(Number(timeoutInput !== '' ? timeoutInput : '300'));
if (isNaN(result.timeout) || result.timeout < 0) {
core.warning(`Invalid value '${timeoutInput}' for 'timeout' input. Using default: 300 seconds.`);
result.timeout = 300;
}
core.debug(`timeout = ${result.timeout}`);
// Retry max attempts (total attempts including initial)
const retryMaxAttemptsInput = core.getInput('retry-max-attempts');
result.retryMaxAttempts = Math.floor(Number(retryMaxAttemptsInput !== '' ? retryMaxAttemptsInput : '3'));
if (isNaN(result.retryMaxAttempts) || result.retryMaxAttempts < 1) {
core.warning(`Invalid value '${retryMaxAttemptsInput}' for 'retry-max-attempts' input. Using default: 3.`);
result.retryMaxAttempts = 3;
}
core.debug(`retry max attempts = ${result.retryMaxAttempts}`);
// Retry backoff range
const retryMinBackoffInput = core.getInput('retry-min-backoff');
result.retryMinBackoff = Math.floor(Number(retryMinBackoffInput !== '' ? retryMinBackoffInput : '10'));
if (isNaN(result.retryMinBackoff) || result.retryMinBackoff < 0) {
core.warning(`Invalid value '${retryMinBackoffInput}' for 'retry-min-backoff' input. Using default: 10 seconds.`);
result.retryMinBackoff = 10;
}
core.debug(`retry min backoff = ${result.retryMinBackoff}`);
const retryMaxBackoffInput = core.getInput('retry-max-backoff');
result.retryMaxBackoff = Math.floor(Number(retryMaxBackoffInput !== '' ? retryMaxBackoffInput : '20'));
if (isNaN(result.retryMaxBackoff) || result.retryMaxBackoff < 0) {
core.warning(`Invalid value '${retryMaxBackoffInput}' for 'retry-max-backoff' input. Using default: 20 seconds.`);
result.retryMaxBackoff = 20;
}
if (result.retryMaxBackoff < result.retryMinBackoff) {
core.warning(`'retry-max-backoff' (${result.retryMaxBackoff}) is less than 'retry-min-backoff' (${result.retryMinBackoff}). Using retry-min-backoff value for both.`);
result.retryMaxBackoff = result.retryMinBackoff;
}
core.debug(`retry max backoff = ${result.retryMaxBackoff}`);
return result; return result;
}); });
} }
@ -5260,6 +5441,7 @@ class Context {
this.action = process.env.GITHUB_ACTION; this.action = process.env.GITHUB_ACTION;
this.actor = process.env.GITHUB_ACTOR; this.actor = process.env.GITHUB_ACTOR;
this.job = process.env.GITHUB_JOB; this.job = process.env.GITHUB_JOB;
this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT, 10);
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER, 10); this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER, 10);
this.runId = parseInt(process.env.GITHUB_RUN_ID, 10); this.runId = parseInt(process.env.GITHUB_RUN_ID, 10);
this.apiUrl = (_a = process.env.GITHUB_API_URL) !== null && _a !== void 0 ? _a : `https://api.github.com`; this.apiUrl = (_a = process.env.GITHUB_API_URL) !== null && _a !== void 0 ? _a : `https://api.github.com`;
@ -6136,7 +6318,7 @@ class HttpClient {
} }
const usingSsl = parsedUrl.protocol === 'https:'; const usingSsl = parsedUrl.protocol === 'https:';
proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && { proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && {
token: `${proxyUrl.username}:${proxyUrl.password}` token: `Basic ${Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64')}`
}))); })));
this._proxyAgentDispatcher = proxyAgent; this._proxyAgentDispatcher = proxyAgent;
if (usingSsl && this._ignoreSslError) { if (usingSsl && this._ignoreSslError) {
@ -6250,11 +6432,11 @@ function getProxyUrl(reqUrl) {
})(); })();
if (proxyVar) { if (proxyVar) {
try { try {
return new URL(proxyVar); return new DecodedURL(proxyVar);
} }
catch (_a) { catch (_a) {
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
return new URL(`http://${proxyVar}`); return new DecodedURL(`http://${proxyVar}`);
} }
} }
else { else {
@ -6313,6 +6495,19 @@ function isLoopbackAddress(host) {
hostLower.startsWith('[::1]') || hostLower.startsWith('[::1]') ||
hostLower.startsWith('[0:0:0:0:0:0:0:1]')); hostLower.startsWith('[0:0:0:0:0:0:0:1]'));
} }
class DecodedURL extends URL {
constructor(url, base) {
super(url, base);
this._decodedUsername = decodeURIComponent(super.username);
this._decodedPassword = decodeURIComponent(super.password);
}
get username() {
return this._decodedUsername;
}
get password() {
return this._decodedPassword;
}
}
//# sourceMappingURL=proxy.js.map //# sourceMappingURL=proxy.js.map
/***/ }), /***/ }),

123
package-lock.json generated
View file

@ -69,20 +69,25 @@
} }
}, },
"node_modules/@actions/github": { "node_modules/@actions/github": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz",
"integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.2.0", "@actions/http-client": "^2.2.0",
"@octokit/core": "^5.0.1", "@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.0.0", "@octokit/plugin-paginate-rest": "^9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^10.0.0" "@octokit/plugin-rest-endpoint-methods": "^10.4.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"undici": "^5.28.5"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==", "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"undici": "^5.25.4" "undici": "^5.25.4"
@ -681,10 +686,11 @@
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/minimatch": { "node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -741,10 +747,11 @@
} }
}, },
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": { "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -810,10 +817,11 @@
} }
}, },
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -1784,10 +1792,11 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@ -3136,10 +3145,11 @@
} }
}, },
"node_modules/eslint-plugin-import/node_modules/minimatch": { "node_modules/eslint-plugin-import/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -3214,10 +3224,11 @@
} }
}, },
"node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -3313,10 +3324,11 @@
} }
}, },
"node_modules/eslint/node_modules/minimatch": { "node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -3536,10 +3548,11 @@
} }
}, },
"node_modules/filelist/node_modules/minimatch": { "node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6", "version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
@ -3590,10 +3603,11 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.3.1", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
@ -3779,10 +3793,11 @@
} }
}, },
"node_modules/glob/node_modules/minimatch": { "node_modules/glob/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -4579,10 +4594,11 @@
} }
}, },
"node_modules/jake/node_modules/minimatch": { "node_modules/jake/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -5186,10 +5202,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@ -5486,12 +5503,13 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.4", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@ -6564,10 +6582,11 @@
} }
}, },
"node_modules/test-exclude/node_modules/minimatch": { "node_modules/test-exclude/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },

View file

@ -7,6 +7,7 @@ import * as path from 'path'
import * as refHelper from './ref-helper' import * as refHelper from './ref-helper'
import * as regexpHelper from './regexp-helper' import * as regexpHelper from './regexp-helper'
import * as retryHelper from './retry-helper' import * as retryHelper from './retry-helper'
import {spawn} from 'child_process'
import {GitVersion} from './git-version' import {GitVersion} from './git-version'
// Auth header not supported before 2.9 // Auth header not supported before 2.9
@ -80,6 +81,12 @@ export interface IGitCommandManager {
): Promise<string[]> ): Promise<string[]>
tryReset(): Promise<boolean> tryReset(): Promise<boolean>
version(): Promise<GitVersion> version(): Promise<GitVersion>
setTimeout(timeoutSeconds: number): void
setRetryConfig(
maxAttempts: number,
minBackoffSeconds: number,
maxBackoffSeconds: number
): void
} }
export async function createCommandManager( export async function createCommandManager(
@ -104,6 +111,8 @@ class GitCommandManager {
private doSparseCheckout = false private doSparseCheckout = false
private workingDirectory = '' private workingDirectory = ''
private gitVersion: GitVersion = new GitVersion() private gitVersion: GitVersion = new GitVersion()
private timeoutMs = 0
private networkRetryHelper = new retryHelper.RetryHelper()
// Private constructor; use createCommandManager() // Private constructor; use createCommandManager()
private constructor() {} private constructor() {}
@ -168,7 +177,7 @@ class GitCommandManager {
} }
// Suppress the output in order to avoid flooding annotations with innocuous errors. // Suppress the output in order to avoid flooding annotations with innocuous errors.
await this.execGit(args, false, true, listeners) await this.execGit(args, {silent: true, customListeners: listeners})
core.debug(`stderr callback is: ${stderr}`) core.debug(`stderr callback is: ${stderr}`)
core.debug(`errline callback is: ${errline}`) core.debug(`errline callback is: ${errline}`)
@ -269,7 +278,7 @@ class GitCommandManager {
'--get-regexp', '--get-regexp',
pattern pattern
], ],
true {allowAllExitCodes: true}
) )
return output.exitCode === 0 return output.exitCode === 0
} }
@ -312,22 +321,25 @@ class GitCommandManager {
} }
const that = this const that = this
await retryHelper.execute(async () => { await this.networkRetryHelper.execute(async () => {
await that.execGit(args) await that.execGit(args, {timeoutMs: that.timeoutMs})
}) })
} }
async getDefaultBranch(repositoryUrl: string): Promise<string> { async getDefaultBranch(repositoryUrl: string): Promise<string> {
let output: GitOutput | undefined let output: GitOutput | undefined
await retryHelper.execute(async () => { await this.networkRetryHelper.execute(async () => {
output = await this.execGit([ output = await this.execGit(
[
'ls-remote', 'ls-remote',
'--quiet', '--quiet',
'--exit-code', '--exit-code',
'--symref', '--symref',
repositoryUrl, repositoryUrl,
'HEAD' 'HEAD'
]) ],
{timeoutMs: this.timeoutMs}
)
}) })
if (output) { if (output) {
@ -372,7 +384,7 @@ class GitCommandManager {
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22 // Note, "branch --show-current" would be simpler but isn't available until Git 2.22
const output = await this.execGit( const output = await this.execGit(
['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'], ['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
true {allowAllExitCodes: true}
) )
return !output.stdout.trim().startsWith('refs/heads/') return !output.stdout.trim().startsWith('refs/heads/')
} }
@ -381,8 +393,8 @@ class GitCommandManager {
const args = ['lfs', 'fetch', 'origin', ref] const args = ['lfs', 'fetch', 'origin', ref]
const that = this const that = this
await retryHelper.execute(async () => { await this.networkRetryHelper.execute(async () => {
await that.execGit(args) await that.execGit(args, {timeoutMs: that.timeoutMs})
}) })
} }
@ -392,8 +404,8 @@ class GitCommandManager {
async log1(format?: string): Promise<string> { async log1(format?: string): Promise<string> {
const args = format ? ['log', '-1', format] : ['log', '-1'] const args = format ? ['log', '-1', format] : ['log', '-1']
const silent = format ? false : true const silent = !format
const output = await this.execGit(args, false, silent) const output = await this.execGit(args, {silent})
return output.stdout return output.stdout
} }
@ -422,7 +434,7 @@ class GitCommandManager {
async shaExists(sha: string): Promise<boolean> { async shaExists(sha: string): Promise<boolean> {
const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`] const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]
const output = await this.execGit(args, true) const output = await this.execGit(args, {allowAllExitCodes: true})
return output.exitCode === 0 return output.exitCode === 0
} }
@ -443,7 +455,10 @@ class GitCommandManager {
args.push('--recursive') args.push('--recursive')
} }
await this.execGit(args) const that = this
await this.networkRetryHelper.execute(async () => {
await that.execGit(args, {timeoutMs: that.timeoutMs})
})
} }
async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> { async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
@ -457,11 +472,14 @@ class GitCommandManager {
args.push('--recursive') args.push('--recursive')
} }
await this.execGit(args) const that = this
await this.networkRetryHelper.execute(async () => {
await that.execGit(args, {timeoutMs: that.timeoutMs})
})
} }
async submoduleStatus(): Promise<boolean> { async submoduleStatus(): Promise<boolean> {
const output = await this.execGit(['submodule', 'status'], true) const output = await this.execGit(['submodule', 'status'], {allowAllExitCodes: true})
core.debug(output.stdout) core.debug(output.stdout)
return output.exitCode === 0 return output.exitCode === 0
} }
@ -472,7 +490,7 @@ class GitCommandManager {
} }
async tryClean(): Promise<boolean> { async tryClean(): Promise<boolean> {
const output = await this.execGit(['clean', '-ffdx'], true) const output = await this.execGit(['clean', '-ffdx'], {allowAllExitCodes: true})
return output.exitCode === 0 return output.exitCode === 0
} }
@ -487,7 +505,7 @@ class GitCommandManager {
'--unset-all', '--unset-all',
configKey configKey
], ],
true {allowAllExitCodes: true}
) )
return output.exitCode === 0 return output.exitCode === 0
} }
@ -506,14 +524,14 @@ class GitCommandManager {
} }
args.push('--unset', configKey, configValue) args.push('--unset', configKey, configValue)
const output = await this.execGit(args, true) const output = await this.execGit(args, {allowAllExitCodes: true})
return output.exitCode === 0 return output.exitCode === 0
} }
async tryDisableAutomaticGarbageCollection(): Promise<boolean> { async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', 'gc.auto', '0'], ['config', '--local', 'gc.auto', '0'],
true {allowAllExitCodes: true}
) )
return output.exitCode === 0 return output.exitCode === 0
} }
@ -521,7 +539,7 @@ class GitCommandManager {
async tryGetFetchUrl(): Promise<string> { async tryGetFetchUrl(): Promise<string> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--get', 'remote.origin.url'], ['config', '--local', '--get', 'remote.origin.url'],
true {allowAllExitCodes: true}
) )
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
@ -549,7 +567,7 @@ class GitCommandManager {
} }
args.push('--get-all', configKey) args.push('--get-all', configKey)
const output = await this.execGit(args, true) const output = await this.execGit(args, {allowAllExitCodes: true})
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
return [] return []
@ -574,7 +592,7 @@ class GitCommandManager {
} }
args.push('--name-only', '--get-regexp', pattern) args.push('--name-only', '--get-regexp', pattern)
const output = await this.execGit(args, true) const output = await this.execGit(args, {allowAllExitCodes: true})
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
return [] return []
@ -587,7 +605,7 @@ class GitCommandManager {
} }
async tryReset(): Promise<boolean> { async tryReset(): Promise<boolean> {
const output = await this.execGit(['reset', '--hard', 'HEAD'], true) const output = await this.execGit(['reset', '--hard', 'HEAD'], {allowAllExitCodes: true})
return output.exitCode === 0 return output.exitCode === 0
} }
@ -595,6 +613,35 @@ class GitCommandManager {
return this.gitVersion return this.gitVersion
} }
/**
* Sets the timeout for network git operations.
* @param timeoutSeconds Timeout in seconds. 0 disables the timeout.
*/
setTimeout(timeoutSeconds: number): void {
if (timeoutSeconds < 0) {
throw new Error(`Timeout must be non-negative, got ${timeoutSeconds}`)
}
this.timeoutMs = timeoutSeconds * 1000
}
/**
* Configures retry behavior for network git operations.
* @param maxAttempts Total attempts including the initial one. Must be >= 1.
* @param minBackoffSeconds Minimum backoff between retries. Must be <= maxBackoffSeconds.
* @param maxBackoffSeconds Maximum backoff between retries.
*/
setRetryConfig(
maxAttempts: number,
minBackoffSeconds: number,
maxBackoffSeconds: number
): void {
this.networkRetryHelper = new retryHelper.RetryHelper(
maxAttempts,
minBackoffSeconds,
maxBackoffSeconds
)
}
static async createCommandManager( static async createCommandManager(
workingDirectory: string, workingDirectory: string,
lfs: boolean, lfs: boolean,
@ -611,12 +658,42 @@ class GitCommandManager {
private async execGit( private async execGit(
args: string[], args: string[],
options: {
allowAllExitCodes?: boolean
silent?: boolean
customListeners?: {}
timeoutMs?: number
} = {}
): Promise<GitOutput> {
const {
allowAllExitCodes = false, allowAllExitCodes = false,
silent = false, silent = false,
customListeners = {} customListeners = {},
): Promise<GitOutput> { timeoutMs = 0
} = options
fshelper.directoryExistsSync(this.workingDirectory, true) fshelper.directoryExistsSync(this.workingDirectory, true)
// Use child_process.spawn directly when timeout is set,
// so we can kill the process on timeout and avoid orphaned git processes.
// Note: customListeners are not supported in the timeout path.
if (timeoutMs > 0) {
if (
customListeners &&
Object.keys(customListeners).length > 0
) {
core.debug(
'customListeners are not supported with timeoutMs and will be ignored'
)
}
return await this.execGitWithTimeout(
args,
timeoutMs,
silent,
allowAllExitCodes
)
}
const result = new GitOutput() const result = new GitOutput()
const env = {} const env = {}
@ -636,7 +713,7 @@ class GitCommandManager {
const mergedListeners = {...defaultListener, ...customListeners} const mergedListeners = {...defaultListener, ...customListeners}
const stdout: string[] = [] const stdout: string[] = []
const options = { const execOptions = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
silent, silent,
@ -644,7 +721,8 @@ class GitCommandManager {
listeners: mergedListeners listeners: mergedListeners
} }
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options) result.exitCode = await exec.exec(`"${this.gitPath}"`, args, execOptions)
result.stdout = stdout.join('') result.stdout = stdout.join('')
core.debug(result.exitCode.toString()) core.debug(result.exitCode.toString())
@ -653,6 +731,137 @@ class GitCommandManager {
return result return result
} }
/**
* Executes a git command with a timeout. Uses child_process.spawn directly
* (instead of @actions/exec) so we can kill the process on timeout and
* terminate it cleanly. Does not support customListeners.
*/
private async execGitWithTimeout(
args: string[],
timeoutMs: number,
silent: boolean,
allowAllExitCodes: boolean
): Promise<GitOutput> {
const result = new GitOutput()
const env: {[key: string]: string} = {}
for (const key of Object.keys(process.env)) {
env[key] = process.env[key] as string
}
for (const key of Object.keys(this.gitEnv)) {
env[key] = this.gitEnv[key]
}
const stdout: string[] = []
const stderr: string[] = []
return new Promise<GitOutput>((resolve, reject) => {
const child = spawn(this.gitPath, args, {
cwd: this.workingDirectory,
env,
stdio: ['ignore', 'pipe', 'pipe']
})
child.stdout?.on('data', (data: Buffer) => {
stdout.push(data.toString())
})
if (child.stderr) {
child.stderr.on('data', (data: Buffer) => {
stderr.push(data.toString())
if (!silent) {
process.stderr.write(data)
}
})
}
let settled = false
let timedOut = false
let forceKillTimer: ReturnType<typeof setTimeout> | undefined
const cleanup = (): void => {
clearTimeout(timer)
if (forceKillTimer) {
clearTimeout(forceKillTimer)
}
}
const timer = global.setTimeout(() => {
timedOut = true
// SIGTERM first, then force SIGKILL after 5 seconds.
// On Windows, SIGTERM is equivalent to a forced kill, so
// the SIGKILL fallback is effectively a no-op there.
child.kill('SIGTERM')
forceKillTimer = global.setTimeout(() => {
try {
child.kill('SIGKILL')
} catch (killErr) {
core.debug(
`Failed to SIGKILL git process: ${killErr}`
)
}
}, 5000)
if (forceKillTimer.unref) {
forceKillTimer.unref()
}
}, timeoutMs)
if (timer.unref) {
timer.unref()
}
child.on('close', (code: number | null) => {
if (settled) return
settled = true
cleanup()
if (timedOut) {
reject(
new Error(
`Git operation timed out after ${timeoutMs / 1000} seconds: git ${args.slice(0, 5).join(' ')}...`
)
)
return
}
// null code means killed by signal (e.g. OOM killer, external SIGTERM)
if (code === null) {
const stderrText = stderr.join('').trim()
reject(
new Error(
`The process 'git' was killed by a signal` +
(stderrText ? `\n${stderrText}` : '')
)
)
return
}
if (code !== 0 && !allowAllExitCodes) {
const stderrText = stderr.join('').trim()
reject(
new Error(
`The process 'git' failed with exit code ${code}` +
(stderrText ? `\n${stderrText}` : '')
)
)
return
}
result.exitCode = code
result.stdout = stdout.join('')
core.debug(result.exitCode.toString())
core.debug(result.stdout)
resolve(result)
})
child.on('error', (err: Error) => {
if (settled) return
settled = true
cleanup()
reject(err)
})
})
}
private async initializeCommandManager( private async initializeCommandManager(
workingDirectory: string, workingDirectory: string,
lfs: boolean, lfs: boolean,

View file

@ -39,6 +39,15 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
const git = await getGitCommandManager(settings) const git = await getGitCommandManager(settings)
core.endGroup() core.endGroup()
if (git) {
git.setTimeout(settings.timeout)
git.setRetryConfig(
settings.retryMaxAttempts,
settings.retryMinBackoff,
settings.retryMaxBackoff
)
}
let authHelper: gitAuthHelper.IGitAuthHelper | null = null let authHelper: gitAuthHelper.IGitAuthHelper | null = null
try { try {
if (git) { if (git) {

View file

@ -118,4 +118,26 @@ export interface IGitSourceSettings {
* User override on the GitHub Server/Host URL that hosts the repository to be cloned * User override on the GitHub Server/Host URL that hosts the repository to be cloned
*/ */
githubServerUrl: string | undefined githubServerUrl: string | undefined
/**
* Timeout in seconds for each network git operation attempt (e.g. fetch, lfs-fetch, ls-remote).
* 0 means no timeout.
*/
timeout: number
/**
* Total number of attempts for each network git operation (including the initial attempt).
* For example, 3 means one initial attempt plus up to 2 retries.
*/
retryMaxAttempts: number
/**
* Minimum backoff time in seconds between retry attempts.
*/
retryMinBackoff: number
/**
* Maximum backoff time in seconds between retry attempts.
*/
retryMaxBackoff: number
} }

View file

@ -161,5 +161,60 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.githubServerUrl = core.getInput('github-server-url') result.githubServerUrl = core.getInput('github-server-url')
core.debug(`GitHub Host URL = ${result.githubServerUrl}`) core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
// Timeout per network operation attempt
const timeoutInput = core.getInput('timeout')
result.timeout = Math.floor(Number(timeoutInput !== '' ? timeoutInput : '300'))
if (isNaN(result.timeout) || result.timeout < 0) {
core.warning(
`Invalid value '${timeoutInput}' for 'timeout' input. Using default: 300 seconds.`
)
result.timeout = 300
}
core.debug(`timeout = ${result.timeout}`)
// Retry max attempts (total attempts including initial)
const retryMaxAttemptsInput = core.getInput('retry-max-attempts')
result.retryMaxAttempts = Math.floor(
Number(retryMaxAttemptsInput !== '' ? retryMaxAttemptsInput : '3')
)
if (isNaN(result.retryMaxAttempts) || result.retryMaxAttempts < 1) {
core.warning(
`Invalid value '${retryMaxAttemptsInput}' for 'retry-max-attempts' input. Using default: 3.`
)
result.retryMaxAttempts = 3
}
core.debug(`retry max attempts = ${result.retryMaxAttempts}`)
// Retry backoff range
const retryMinBackoffInput = core.getInput('retry-min-backoff')
result.retryMinBackoff = Math.floor(
Number(retryMinBackoffInput !== '' ? retryMinBackoffInput : '10')
)
if (isNaN(result.retryMinBackoff) || result.retryMinBackoff < 0) {
core.warning(
`Invalid value '${retryMinBackoffInput}' for 'retry-min-backoff' input. Using default: 10 seconds.`
)
result.retryMinBackoff = 10
}
core.debug(`retry min backoff = ${result.retryMinBackoff}`)
const retryMaxBackoffInput = core.getInput('retry-max-backoff')
result.retryMaxBackoff = Math.floor(
Number(retryMaxBackoffInput !== '' ? retryMaxBackoffInput : '20')
)
if (isNaN(result.retryMaxBackoff) || result.retryMaxBackoff < 0) {
core.warning(
`Invalid value '${retryMaxBackoffInput}' for 'retry-max-backoff' input. Using default: 20 seconds.`
)
result.retryMaxBackoff = 20
}
if (result.retryMaxBackoff < result.retryMinBackoff) {
core.warning(
`'retry-max-backoff' (${result.retryMaxBackoff}) is less than 'retry-min-backoff' (${result.retryMinBackoff}). Using retry-min-backoff value for both.`
)
result.retryMaxBackoff = result.retryMinBackoff
}
core.debug(`retry max backoff = ${result.retryMaxBackoff}`)
return result return result
} }