Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (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
# environment defaults to fetch from the same instance that the workflow is
# running from unless specified. Example URLs are https://github.com or
Expand Down
10 changes: 8 additions & 2 deletions __test__/git-auth-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
}
}

Expand Down
81 changes: 81 additions & 0 deletions __test__/git-command-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import * as commandManager from '../lib/git-command-manager'
let git: commandManager.IGitCommandManager
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', () => {
beforeAll(async () => {})

Expand Down Expand Up @@ -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)
})
4 changes: 3 additions & 1 deletion __test__/git-directory-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
54 changes: 54 additions & 0 deletions __test__/input-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,58 @@ describe('input-helper tests', () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
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'")
)
})
})
22 changes: 22 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ 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 (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:
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
Expand Down
Loading