Validate deployment polling intervals

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Yoann Chaudet
2026-09-01 14:13:45 -07:00
co-authored by Copilot App
parent 0143e11abb
commit 7e97763d1f
6 changed files with 70 additions and 18 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
| `token` | `true` | `${{ github.token }}` | The GitHub token used to create an authenticated client - Provided for you by default! |
| `timeout` | `false` | `"600000"` | Time in milliseconds after which to timeout and cancel the deployment (default: 10 minutes) |
| `error_count` | `false` | `"10"` | Maximum number of status report errors before cancelling a deployment (default: 10) |
| `reporting_interval` | `false` | `"5000"` | Initial time in milliseconds between deployment status reports. Subsequent intervals use exponential backoff capped at 30 seconds and ±20% jitter (default: 5 seconds). |
| `reporting_interval` | `false` | `"5000"` | Initial time in milliseconds between deployment status reports. Successful non-terminal polls use exponential backoff up to 30 seconds, or the configured interval when higher, with ±20% jitter. Error backoff is added separately (default: 5 seconds). |
| `artifact_name` | `false` | `"github-pages"` | The name of the artifact to deploy |
| `preview` | `false` | `"false"` | Is this attempting to deploy a pull request as a GitHub Pages preview site? (NOTE: This feature is only in alpha currently and is not available to the public!) |
+1 -1
View File
@@ -18,7 +18,7 @@ inputs:
required: false
default: '10'
reporting_interval:
description: 'Initial time in milliseconds between deployment status reports; subsequent intervals use capped backoff and jitter (default: 5 seconds)'
description: 'Initial time between deployment status reports; successful polls use capped backoff and jitter, with error backoff added separately (default: 5 seconds)'
required: false
default: '5000'
artifact_name:
Generated Vendored
+14 -5
View File
@@ -149920,6 +149920,7 @@ const finalErrorStatus = {
}
const MAX_TIMEOUT = 600000
const DEFAULT_REPORTING_INTERVAL = 5000
const MAX_REPORTING_INTERVAL = 30000
const REPORTING_BACKOFF_MULTIPLIER = 1.5
const REPORTING_JITTER_FACTOR = 0.2
@@ -150042,9 +150043,19 @@ class Deployment {
}
const deploymentId = this.deploymentInfo.id || this.buildVersion
let reportingInterval = Number(core.getInput('reporting_interval'))
const reportingIntervalInput = Number(core.getInput('reporting_interval'))
const initialReportingInterval =
Number.isFinite(reportingIntervalInput) && reportingIntervalInput > 0
? reportingIntervalInput
: DEFAULT_REPORTING_INTERVAL
const maxReportingInterval = Math.max(MAX_REPORTING_INTERVAL, initialReportingInterval)
const maxErrorCount = Number(core.getInput('error_count'))
if (initialReportingInterval !== reportingIntervalInput) {
core.warning(`Invalid reporting_interval value; using the default of ${DEFAULT_REPORTING_INTERVAL} milliseconds.`)
}
let reportingInterval = initialReportingInterval
let errorCount = 0
// Time in milliseconds between two deployment status report when status errored, default 0.
@@ -150083,10 +150094,7 @@ class Deployment {
// reset the error reporting interval once get the proper status back.
errorReportingInterval = 0
reportingInterval = Math.min(
Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER),
MAX_REPORTING_INTERVAL
)
reportingInterval = Math.min(Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER), maxReportingInterval)
} catch (error) {
core.error(error.stack)
@@ -150153,6 +150161,7 @@ class Deployment {
module.exports = {
Deployment,
MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL,
ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+39 -5
View File
@@ -7,6 +7,7 @@ const { MockAgent, setGlobalDispatcher } = require('undici')
const {
Deployment,
MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL,
ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION
@@ -614,6 +615,12 @@ describe('Deployment', () => {
})
describe('#check', () => {
afterEach(() => {
jest.restoreAllMocks()
delete process.env.INPUT_ERROR_COUNT
delete process.env.INPUT_REPORTING_INTERVAL
})
const mockDeploymentStatus = (status, times = 1) => {
mockPool
.intercept({
@@ -1000,7 +1007,7 @@ describe('Deployment', () => {
case 'error_count':
return 10
case 'reporting_interval':
return 0 // The default of 5000 is too long for the test
return 1 // The default of 5000 is too long for the test
case 'timeout':
return 42
default:
@@ -1059,10 +1066,39 @@ describe('Deployment', () => {
timeoutSpy.mockRestore()
randomSpy.mockRestore()
delete process.env.INPUT_ERROR_COUNT
delete process.env.INPUT_REPORTING_INTERVAL
})
it('does not reduce a configured interval above the backoff cap', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
process.env.INPUT_ERROR_COUNT = '10'
process.env.INPUT_REPORTING_INTERVAL = '45000'
mockDeploymentStatus('deployment_in_progress')
mockDeploymentStatus('succeed')
jest.spyOn(Math, 'random').mockReturnValue(0.5)
const timeoutSpy = await runWithoutWaiting(createPendingDeployment())
expect(timeoutSpy.mock.calls.map(([, interval]) => interval)).toEqual([45000, 45000])
})
it.each(['not-a-number', '0', '-1'])(
'uses the default reporting interval for invalid input %s',
async reportingInterval => {
process.env.GITHUB_SHA = 'valid-build-version'
process.env.INPUT_ERROR_COUNT = '10'
process.env.INPUT_REPORTING_INTERVAL = reportingInterval
mockDeploymentStatus('succeed')
jest.spyOn(Math, 'random').mockReturnValue(0.5)
const timeoutSpy = await runWithoutWaiting(createPendingDeployment())
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), DEFAULT_REPORTING_INTERVAL)
expect(core.warning).toHaveBeenCalledWith(
`Invalid reporting_interval value; using the default of ${DEFAULT_REPORTING_INTERVAL} milliseconds.`
)
}
)
it('jitters status check intervals by up to twenty percent', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
process.env.INPUT_ERROR_COUNT = '10'
@@ -1077,7 +1113,6 @@ describe('Deployment', () => {
timeoutSpy.mockRestore()
randomSpy.mockRestore()
delete process.env.INPUT_ERROR_COUNT
})
it('keeps success backoff separate from error backoff', async () => {
@@ -1099,7 +1134,6 @@ describe('Deployment', () => {
timeoutSpy.mockRestore()
randomSpy.mockRestore()
delete process.env.INPUT_ERROR_COUNT
})
})
+14 -5
View File
@@ -24,6 +24,7 @@ const finalErrorStatus = {
}
const MAX_TIMEOUT = 600000
const DEFAULT_REPORTING_INTERVAL = 5000
const MAX_REPORTING_INTERVAL = 30000
const REPORTING_BACKOFF_MULTIPLIER = 1.5
const REPORTING_JITTER_FACTOR = 0.2
@@ -146,9 +147,19 @@ class Deployment {
}
const deploymentId = this.deploymentInfo.id || this.buildVersion
let reportingInterval = Number(core.getInput('reporting_interval'))
const reportingIntervalInput = Number(core.getInput('reporting_interval'))
const initialReportingInterval =
Number.isFinite(reportingIntervalInput) && reportingIntervalInput > 0
? reportingIntervalInput
: DEFAULT_REPORTING_INTERVAL
const maxReportingInterval = Math.max(MAX_REPORTING_INTERVAL, initialReportingInterval)
const maxErrorCount = Number(core.getInput('error_count'))
if (initialReportingInterval !== reportingIntervalInput) {
core.warning(`Invalid reporting_interval value; using the default of ${DEFAULT_REPORTING_INTERVAL} milliseconds.`)
}
let reportingInterval = initialReportingInterval
let errorCount = 0
// Time in milliseconds between two deployment status report when status errored, default 0.
@@ -187,10 +198,7 @@ class Deployment {
// reset the error reporting interval once get the proper status back.
errorReportingInterval = 0
reportingInterval = Math.min(
Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER),
MAX_REPORTING_INTERVAL
)
reportingInterval = Math.min(Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER), maxReportingInterval)
} catch (error) {
core.error(error.stack)
@@ -257,6 +265,7 @@ class Deployment {
module.exports = {
Deployment,
MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL,
ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION