mirror of
https://github.com/actions/checkout.git
synced 2026-03-19 18:40:23 +08:00
Add configurable timeout and retry for git network operations
Add per-attempt timeout (default 300s) and Kubernetes probe-style retry configuration for git fetch, lfs-fetch, and ls-remote. New action inputs: timeout, retry-max-attempts, retry-min-backoff, retry-max-backoff. Fixes https://github.com/actions/checkout/issues/631 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c366fd6a8
commit
5df58a66d1
10 changed files with 342 additions and 81 deletions
22
README.md
22
README.md
|
|
@ -155,6 +155,28 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||
# Default: true
|
||||
set-safe-directory: ''
|
||||
|
||||
# Timeout in seconds for each git network operation attempt (fetch, lfs-fetch,
|
||||
# ls-remote). If a single attempt exceeds this, it is killed and retried. Set to 0
|
||||
# to disable. Default is 300 (5 minutes). Similar to Kubernetes probe
|
||||
# timeoutSeconds.
|
||||
# Default: 300
|
||||
timeout: ''
|
||||
|
||||
# Maximum number of retry attempts for failed git network operations. Similar to
|
||||
# Kubernetes probe failureThreshold.
|
||||
# Default: 3
|
||||
retry-max-attempts: ''
|
||||
|
||||
# Minimum backoff time in seconds between retry attempts. The actual backoff is
|
||||
# randomly chosen between min and max. Similar to Kubernetes probe periodSeconds.
|
||||
# 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
|
||||
# environment defaults to fetch from the same instance that the workflow is
|
||||
# running from unless specified. Example URLs are https://github.com or
|
||||
|
|
|
|||
|
|
@ -1146,7 +1146,9 @@ async function setup(testName: string): Promise<void> {
|
|||
}
|
||||
),
|
||||
tryReset: jest.fn(),
|
||||
version: jest.fn()
|
||||
version: jest.fn(),
|
||||
setTimeout: jest.fn(),
|
||||
setRetryConfig: jest.fn()
|
||||
}
|
||||
|
||||
settings = {
|
||||
|
|
@ -1173,7 +1175,11 @@ async function setup(testName: string): Promise<void> {
|
|||
sshUser: '',
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
githubServerUrl: githubServerUrl
|
||||
githubServerUrl: githubServerUrl,
|
||||
timeout: 300,
|
||||
retryMaxAttempts: 3,
|
||||
retryMinBackoff: 10,
|
||||
retryMaxBackoff: 20
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -506,6 +506,8 @@ async function setup(testName: string): Promise<void> {
|
|||
tryReset: jest.fn(async () => {
|
||||
return true
|
||||
}),
|
||||
version: jest.fn()
|
||||
version: jest.fn(),
|
||||
setTimeout: jest.fn(),
|
||||
setRetryConfig: jest.fn()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
action.yml
23
action.yml
|
|
@ -95,6 +95,29 @@ inputs:
|
|||
set-safe-directory:
|
||||
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
|
||||
default: true
|
||||
timeout:
|
||||
description: >
|
||||
Timeout in seconds for each git network operation attempt (fetch, lfs-fetch, ls-remote).
|
||||
If a single attempt exceeds this, it is killed and retried.
|
||||
Set to 0 to disable. Default is 300 (5 minutes).
|
||||
Similar to Kubernetes probe timeoutSeconds.
|
||||
default: 300
|
||||
retry-max-attempts:
|
||||
description: >
|
||||
Maximum number of retry attempts for failed git network operations.
|
||||
Similar to Kubernetes probe failureThreshold.
|
||||
default: 3
|
||||
retry-min-backoff:
|
||||
description: >
|
||||
Minimum backoff time in seconds between retry attempts.
|
||||
The actual backoff is randomly chosen between min and max.
|
||||
Similar to Kubernetes probe periodSeconds.
|
||||
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:
|
||||
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
|
||||
|
|
|
|||
91
dist/index.js
vendored
91
dist/index.js
vendored
|
|
@ -678,6 +678,8 @@ class GitCommandManager {
|
|||
this.doSparseCheckout = false;
|
||||
this.workingDirectory = '';
|
||||
this.gitVersion = new git_version_1.GitVersion();
|
||||
this.timeoutMs = 0;
|
||||
this.networkRetryHelper = new retryHelper.RetryHelper();
|
||||
}
|
||||
branchDelete(remote, branch) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
|
|
@ -851,15 +853,15 @@ class GitCommandManager {
|
|||
args.push(arg);
|
||||
}
|
||||
const that = this;
|
||||
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
|
||||
yield that.execGit(args);
|
||||
yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
|
||||
yield that.execGit(args, false, false, {}, that.timeoutMs);
|
||||
}));
|
||||
});
|
||||
}
|
||||
getDefaultBranch(repositoryUrl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
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([
|
||||
'ls-remote',
|
||||
'--quiet',
|
||||
|
|
@ -867,7 +869,7 @@ class GitCommandManager {
|
|||
'--symref',
|
||||
repositoryUrl,
|
||||
'HEAD'
|
||||
]);
|
||||
], false, false, {}, this.timeoutMs);
|
||||
}));
|
||||
if (output) {
|
||||
// Satisfy compiler, will always be set
|
||||
|
|
@ -912,8 +914,8 @@ class GitCommandManager {
|
|||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const args = ['lfs', 'fetch', 'origin', ref];
|
||||
const that = this;
|
||||
yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
|
||||
yield that.execGit(args);
|
||||
yield this.networkRetryHelper.execute(() => __awaiter(this, void 0, void 0, function* () {
|
||||
yield that.execGit(args, false, false, {}, that.timeoutMs);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
@ -1107,6 +1109,12 @@ class GitCommandManager {
|
|||
return this.gitVersion;
|
||||
});
|
||||
}
|
||||
setTimeout(timeoutSeconds) {
|
||||
this.timeoutMs = timeoutSeconds * 1000;
|
||||
}
|
||||
setRetryConfig(maxAttempts, minBackoffSeconds, maxBackoffSeconds) {
|
||||
this.networkRetryHelper = new retryHelper.RetryHelper(maxAttempts, minBackoffSeconds, maxBackoffSeconds);
|
||||
}
|
||||
static createCommandManager(workingDirectory, lfs, doSparseCheckout) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const result = new GitCommandManager();
|
||||
|
|
@ -1115,7 +1123,7 @@ class GitCommandManager {
|
|||
});
|
||||
}
|
||||
execGit(args_1) {
|
||||
return __awaiter(this, arguments, void 0, function* (args, allowAllExitCodes = false, silent = false, customListeners = {}) {
|
||||
return __awaiter(this, arguments, void 0, function* (args, allowAllExitCodes = false, silent = false, customListeners = {}, timeoutMs = 0) {
|
||||
fshelper.directoryExistsSync(this.workingDirectory, true);
|
||||
const result = new GitOutput();
|
||||
const env = {};
|
||||
|
|
@ -1139,7 +1147,24 @@ class GitCommandManager {
|
|||
ignoreReturnCode: allowAllExitCodes,
|
||||
listeners: mergedListeners
|
||||
};
|
||||
result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options);
|
||||
const execPromise = exec.exec(`"${this.gitPath}"`, args, options);
|
||||
if (timeoutMs > 0) {
|
||||
let timer;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timer = global.setTimeout(() => {
|
||||
reject(new Error(`Git operation timed out after ${timeoutMs / 1000} seconds: git ${args.slice(0, 3).join(' ')}...`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
try {
|
||||
result.exitCode = yield Promise.race([execPromise, timeoutPromise]);
|
||||
}
|
||||
finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.exitCode = yield execPromise;
|
||||
}
|
||||
result.stdout = stdout.join('');
|
||||
core.debug(result.exitCode.toString());
|
||||
core.debug(result.stdout);
|
||||
|
|
@ -1448,6 +1473,10 @@ function getSource(settings) {
|
|||
core.startGroup('Getting Git version info');
|
||||
const git = yield getGitCommandManager(settings);
|
||||
core.endGroup();
|
||||
if (git) {
|
||||
git.setTimeout(settings.timeout);
|
||||
git.setRetryConfig(settings.retryMaxAttempts, settings.retryMinBackoff, settings.retryMaxBackoff);
|
||||
}
|
||||
let authHelper = null;
|
||||
try {
|
||||
if (git) {
|
||||
|
|
@ -2095,6 +2124,32 @@ function getInputs() {
|
|||
// Determine the GitHub URL that the repository is being hosted from
|
||||
result.githubServerUrl = core.getInput('github-server-url');
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`);
|
||||
// Timeout (per-attempt, like k8s timeoutSeconds)
|
||||
result.timeout = Math.floor(Number(core.getInput('timeout') || '300'));
|
||||
if (isNaN(result.timeout) || result.timeout < 0) {
|
||||
result.timeout = 300;
|
||||
}
|
||||
core.debug(`timeout = ${result.timeout}`);
|
||||
// Retry max attempts (like k8s failureThreshold)
|
||||
result.retryMaxAttempts = Math.floor(Number(core.getInput('retry-max-attempts') || '3'));
|
||||
if (isNaN(result.retryMaxAttempts) || result.retryMaxAttempts < 1) {
|
||||
result.retryMaxAttempts = 3;
|
||||
}
|
||||
core.debug(`retry max attempts = ${result.retryMaxAttempts}`);
|
||||
// Retry backoff (like k8s periodSeconds, but as a min/max range)
|
||||
result.retryMinBackoff = Math.floor(Number(core.getInput('retry-min-backoff') || '10'));
|
||||
if (isNaN(result.retryMinBackoff) || result.retryMinBackoff < 0) {
|
||||
result.retryMinBackoff = 10;
|
||||
}
|
||||
core.debug(`retry min backoff = ${result.retryMinBackoff}`);
|
||||
result.retryMaxBackoff = Math.floor(Number(core.getInput('retry-max-backoff') || '20'));
|
||||
if (isNaN(result.retryMaxBackoff) || result.retryMaxBackoff < 0) {
|
||||
result.retryMaxBackoff = 20;
|
||||
}
|
||||
if (result.retryMaxBackoff < result.retryMinBackoff) {
|
||||
result.retryMaxBackoff = result.retryMinBackoff;
|
||||
}
|
||||
core.debug(`retry max backoff = ${result.retryMaxBackoff}`);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
|
@ -5260,6 +5315,7 @@ class Context {
|
|||
this.action = process.env.GITHUB_ACTION;
|
||||
this.actor = process.env.GITHUB_ACTOR;
|
||||
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.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`;
|
||||
|
|
@ -6136,7 +6192,7 @@ class HttpClient {
|
|||
}
|
||||
const usingSsl = parsedUrl.protocol === 'https:';
|
||||
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;
|
||||
if (usingSsl && this._ignoreSslError) {
|
||||
|
|
@ -6250,11 +6306,11 @@ function getProxyUrl(reqUrl) {
|
|||
})();
|
||||
if (proxyVar) {
|
||||
try {
|
||||
return new URL(proxyVar);
|
||||
return new DecodedURL(proxyVar);
|
||||
}
|
||||
catch (_a) {
|
||||
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
|
||||
return new URL(`http://${proxyVar}`);
|
||||
return new DecodedURL(`http://${proxyVar}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -6313,6 +6369,19 @@ function isLoopbackAddress(host) {
|
|||
hostLower.startsWith('[::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
|
||||
|
||||
/***/ }),
|
||||
|
|
|
|||
123
package-lock.json
generated
123
package-lock.json
generated
|
|
@ -69,20 +69,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz",
|
||||
"integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz",
|
||||
"integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.2.0",
|
||||
"@octokit/core": "^5.0.1",
|
||||
"@octokit/plugin-paginate-rest": "^9.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^10.0.0"
|
||||
"@octokit/plugin-paginate-rest": "^9.2.2",
|
||||
"@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": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz",
|
||||
"integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
|
||||
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tunnel": "^0.0.6",
|
||||
"undici": "^5.25.4"
|
||||
|
|
@ -681,10 +686,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -741,10 +747,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -810,10 +817,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
|
|
@ -1784,10 +1792,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -3136,10 +3145,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -3214,10 +3224,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -3313,10 +3324,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -3536,10 +3548,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
|
|
@ -3590,10 +3603,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||
"dev": true
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
|
|
@ -3779,10 +3793,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -4579,10 +4594,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
@ -5186,10 +5202,11 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
@ -5486,12 +5503,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
|
|
@ -6564,10 +6582,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ export interface IGitCommandManager {
|
|||
): Promise<string[]>
|
||||
tryReset(): Promise<boolean>
|
||||
version(): Promise<GitVersion>
|
||||
setTimeout(timeoutSeconds: number): void
|
||||
setRetryConfig(
|
||||
maxAttempts: number,
|
||||
minBackoffSeconds: number,
|
||||
maxBackoffSeconds: number
|
||||
): void
|
||||
}
|
||||
|
||||
export async function createCommandManager(
|
||||
|
|
@ -104,6 +110,8 @@ class GitCommandManager {
|
|||
private doSparseCheckout = false
|
||||
private workingDirectory = ''
|
||||
private gitVersion: GitVersion = new GitVersion()
|
||||
private timeoutMs = 0
|
||||
private networkRetryHelper = new retryHelper.RetryHelper()
|
||||
|
||||
// Private constructor; use createCommandManager()
|
||||
private constructor() {}
|
||||
|
|
@ -312,22 +320,28 @@ class GitCommandManager {
|
|||
}
|
||||
|
||||
const that = this
|
||||
await retryHelper.execute(async () => {
|
||||
await that.execGit(args)
|
||||
await this.networkRetryHelper.execute(async () => {
|
||||
await that.execGit(args, false, false, {}, that.timeoutMs)
|
||||
})
|
||||
}
|
||||
|
||||
async getDefaultBranch(repositoryUrl: string): Promise<string> {
|
||||
let output: GitOutput | undefined
|
||||
await retryHelper.execute(async () => {
|
||||
output = await this.execGit([
|
||||
await this.networkRetryHelper.execute(async () => {
|
||||
output = await this.execGit(
|
||||
[
|
||||
'ls-remote',
|
||||
'--quiet',
|
||||
'--exit-code',
|
||||
'--symref',
|
||||
repositoryUrl,
|
||||
'HEAD'
|
||||
])
|
||||
],
|
||||
false,
|
||||
false,
|
||||
{},
|
||||
this.timeoutMs
|
||||
)
|
||||
})
|
||||
|
||||
if (output) {
|
||||
|
|
@ -381,8 +395,8 @@ class GitCommandManager {
|
|||
const args = ['lfs', 'fetch', 'origin', ref]
|
||||
|
||||
const that = this
|
||||
await retryHelper.execute(async () => {
|
||||
await that.execGit(args)
|
||||
await this.networkRetryHelper.execute(async () => {
|
||||
await that.execGit(args, false, false, {}, that.timeoutMs)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -595,6 +609,22 @@ class GitCommandManager {
|
|||
return this.gitVersion
|
||||
}
|
||||
|
||||
setTimeout(timeoutSeconds: number): void {
|
||||
this.timeoutMs = timeoutSeconds * 1000
|
||||
}
|
||||
|
||||
setRetryConfig(
|
||||
maxAttempts: number,
|
||||
minBackoffSeconds: number,
|
||||
maxBackoffSeconds: number
|
||||
): void {
|
||||
this.networkRetryHelper = new retryHelper.RetryHelper(
|
||||
maxAttempts,
|
||||
minBackoffSeconds,
|
||||
maxBackoffSeconds
|
||||
)
|
||||
}
|
||||
|
||||
static async createCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean,
|
||||
|
|
@ -613,7 +643,8 @@ class GitCommandManager {
|
|||
args: string[],
|
||||
allowAllExitCodes = false,
|
||||
silent = false,
|
||||
customListeners = {}
|
||||
customListeners = {},
|
||||
timeoutMs = 0
|
||||
): Promise<GitOutput> {
|
||||
fshelper.directoryExistsSync(this.workingDirectory, true)
|
||||
|
||||
|
|
@ -644,7 +675,28 @@ class GitCommandManager {
|
|||
listeners: mergedListeners
|
||||
}
|
||||
|
||||
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
||||
const execPromise = exec.exec(`"${this.gitPath}"`, args, options)
|
||||
|
||||
if (timeoutMs > 0) {
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timer = global.setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`Git operation timed out after ${timeoutMs / 1000} seconds: git ${args.slice(0, 3).join(' ')}...`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
})
|
||||
try {
|
||||
result.exitCode = await Promise.race([execPromise, timeoutPromise])
|
||||
} finally {
|
||||
clearTimeout(timer!)
|
||||
}
|
||||
} else {
|
||||
result.exitCode = await execPromise
|
||||
}
|
||||
|
||||
result.stdout = stdout.join('')
|
||||
|
||||
core.debug(result.exitCode.toString())
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||
const git = await getGitCommandManager(settings)
|
||||
core.endGroup()
|
||||
|
||||
if (git) {
|
||||
git.setTimeout(settings.timeout)
|
||||
git.setRetryConfig(
|
||||
settings.retryMaxAttempts,
|
||||
settings.retryMinBackoff,
|
||||
settings.retryMaxBackoff
|
||||
)
|
||||
}
|
||||
|
||||
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
|
||||
try {
|
||||
if (git) {
|
||||
|
|
|
|||
|
|
@ -118,4 +118,27 @@ export interface IGitSourceSettings {
|
|||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||
*/
|
||||
githubServerUrl: string | undefined
|
||||
|
||||
/**
|
||||
* Timeout in seconds for each network git operation attempt (fetch, lfs-fetch, ls-remote).
|
||||
* 0 means no timeout. Similar to Kubernetes probe timeoutSeconds.
|
||||
*/
|
||||
timeout: number
|
||||
|
||||
/**
|
||||
* Maximum number of retry attempts for failed network git operations.
|
||||
* Similar to Kubernetes probe failureThreshold.
|
||||
*/
|
||||
retryMaxAttempts: number
|
||||
|
||||
/**
|
||||
* Minimum backoff time in seconds between retry attempts.
|
||||
* Similar to Kubernetes probe periodSeconds.
|
||||
*/
|
||||
retryMinBackoff: number
|
||||
|
||||
/**
|
||||
* Maximum backoff time in seconds between retry attempts.
|
||||
*/
|
||||
retryMaxBackoff: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,5 +161,41 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||
result.githubServerUrl = core.getInput('github-server-url')
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||
|
||||
// Timeout (per-attempt, like k8s timeoutSeconds)
|
||||
result.timeout = Math.floor(Number(core.getInput('timeout') || '300'))
|
||||
if (isNaN(result.timeout) || result.timeout < 0) {
|
||||
result.timeout = 300
|
||||
}
|
||||
core.debug(`timeout = ${result.timeout}`)
|
||||
|
||||
// Retry max attempts (like k8s failureThreshold)
|
||||
result.retryMaxAttempts = Math.floor(
|
||||
Number(core.getInput('retry-max-attempts') || '3')
|
||||
)
|
||||
if (isNaN(result.retryMaxAttempts) || result.retryMaxAttempts < 1) {
|
||||
result.retryMaxAttempts = 3
|
||||
}
|
||||
core.debug(`retry max attempts = ${result.retryMaxAttempts}`)
|
||||
|
||||
// Retry backoff (like k8s periodSeconds, but as a min/max range)
|
||||
result.retryMinBackoff = Math.floor(
|
||||
Number(core.getInput('retry-min-backoff') || '10')
|
||||
)
|
||||
if (isNaN(result.retryMinBackoff) || result.retryMinBackoff < 0) {
|
||||
result.retryMinBackoff = 10
|
||||
}
|
||||
core.debug(`retry min backoff = ${result.retryMinBackoff}`)
|
||||
|
||||
result.retryMaxBackoff = Math.floor(
|
||||
Number(core.getInput('retry-max-backoff') || '20')
|
||||
)
|
||||
if (isNaN(result.retryMaxBackoff) || result.retryMaxBackoff < 0) {
|
||||
result.retryMaxBackoff = 20
|
||||
}
|
||||
if (result.retryMaxBackoff < result.retryMinBackoff) {
|
||||
result.retryMaxBackoff = result.retryMinBackoff
|
||||
}
|
||||
core.debug(`retry max backoff = ${result.retryMaxBackoff}`)
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue