mirror of
https://code.forgejo.org/actions/checkout.git
synced 2026-03-19 23:03:13 +00: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
|
|
@ -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([
|
||||
'ls-remote',
|
||||
'--quiet',
|
||||
'--exit-code',
|
||||
'--symref',
|
||||
repositoryUrl,
|
||||
'HEAD'
|
||||
])
|
||||
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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue