Validate deployment polling intervals

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Yoann Chaudet
2026-09-01 14:13:45 -07:00
parent 0143e11abb
commit 7e97763d1f
6 changed files with 70 additions and 18 deletions

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! | | `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) | | `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) | | `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 | | `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!) | | `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!) |

View File

@@ -18,7 +18,7 @@ inputs:
required: false required: false
default: '10' default: '10'
reporting_interval: 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 required: false
default: '5000' default: '5000'
artifact_name: artifact_name:

19
dist/index.js generated vendored
View File

@@ -149920,6 +149920,7 @@ const finalErrorStatus = {
} }
const MAX_TIMEOUT = 600000 const MAX_TIMEOUT = 600000
const DEFAULT_REPORTING_INTERVAL = 5000
const MAX_REPORTING_INTERVAL = 30000 const MAX_REPORTING_INTERVAL = 30000
const REPORTING_BACKOFF_MULTIPLIER = 1.5 const REPORTING_BACKOFF_MULTIPLIER = 1.5
const REPORTING_JITTER_FACTOR = 0.2 const REPORTING_JITTER_FACTOR = 0.2
@@ -150042,9 +150043,19 @@ class Deployment {
} }
const deploymentId = this.deploymentInfo.id || this.buildVersion 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')) 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 let errorCount = 0
// Time in milliseconds between two deployment status report when status errored, default 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. // reset the error reporting interval once get the proper status back.
errorReportingInterval = 0 errorReportingInterval = 0
reportingInterval = Math.min( reportingInterval = Math.min(Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER), maxReportingInterval)
Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER),
MAX_REPORTING_INTERVAL
)
} catch (error) { } catch (error) {
core.error(error.stack) core.error(error.stack)
@@ -150153,6 +150161,7 @@ class Deployment {
module.exports = { module.exports = {
Deployment, Deployment,
MAX_TIMEOUT, MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL, MAX_REPORTING_INTERVAL,
ONE_GIGABYTE, ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION SIZE_LIMIT_DESCRIPTION

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ const { MockAgent, setGlobalDispatcher } = require('undici')
const { const {
Deployment, Deployment,
MAX_TIMEOUT, MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL, MAX_REPORTING_INTERVAL,
ONE_GIGABYTE, ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION SIZE_LIMIT_DESCRIPTION
@@ -614,6 +615,12 @@ describe('Deployment', () => {
}) })
describe('#check', () => { describe('#check', () => {
afterEach(() => {
jest.restoreAllMocks()
delete process.env.INPUT_ERROR_COUNT
delete process.env.INPUT_REPORTING_INTERVAL
})
const mockDeploymentStatus = (status, times = 1) => { const mockDeploymentStatus = (status, times = 1) => {
mockPool mockPool
.intercept({ .intercept({
@@ -1000,7 +1007,7 @@ describe('Deployment', () => {
case 'error_count': case 'error_count':
return 10 return 10
case 'reporting_interval': 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': case 'timeout':
return 42 return 42
default: default:
@@ -1059,10 +1066,39 @@ describe('Deployment', () => {
timeoutSpy.mockRestore() timeoutSpy.mockRestore()
randomSpy.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 () => { it('jitters status check intervals by up to twenty percent', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
process.env.INPUT_ERROR_COUNT = '10' process.env.INPUT_ERROR_COUNT = '10'
@@ -1077,7 +1113,6 @@ describe('Deployment', () => {
timeoutSpy.mockRestore() timeoutSpy.mockRestore()
randomSpy.mockRestore() randomSpy.mockRestore()
delete process.env.INPUT_ERROR_COUNT
}) })
it('keeps success backoff separate from error backoff', async () => { it('keeps success backoff separate from error backoff', async () => {
@@ -1099,7 +1134,6 @@ describe('Deployment', () => {
timeoutSpy.mockRestore() timeoutSpy.mockRestore()
randomSpy.mockRestore() randomSpy.mockRestore()
delete process.env.INPUT_ERROR_COUNT
}) })
}) })

View File

@@ -24,6 +24,7 @@ const finalErrorStatus = {
} }
const MAX_TIMEOUT = 600000 const MAX_TIMEOUT = 600000
const DEFAULT_REPORTING_INTERVAL = 5000
const MAX_REPORTING_INTERVAL = 30000 const MAX_REPORTING_INTERVAL = 30000
const REPORTING_BACKOFF_MULTIPLIER = 1.5 const REPORTING_BACKOFF_MULTIPLIER = 1.5
const REPORTING_JITTER_FACTOR = 0.2 const REPORTING_JITTER_FACTOR = 0.2
@@ -146,9 +147,19 @@ class Deployment {
} }
const deploymentId = this.deploymentInfo.id || this.buildVersion 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')) 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 let errorCount = 0
// Time in milliseconds between two deployment status report when status errored, default 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. // reset the error reporting interval once get the proper status back.
errorReportingInterval = 0 errorReportingInterval = 0
reportingInterval = Math.min( reportingInterval = Math.min(Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER), maxReportingInterval)
Math.round(reportingInterval * REPORTING_BACKOFF_MULTIPLIER),
MAX_REPORTING_INTERVAL
)
} catch (error) { } catch (error) {
core.error(error.stack) core.error(error.stack)
@@ -257,6 +265,7 @@ class Deployment {
module.exports = { module.exports = {
Deployment, Deployment,
MAX_TIMEOUT, MAX_TIMEOUT,
DEFAULT_REPORTING_INTERVAL,
MAX_REPORTING_INTERVAL, MAX_REPORTING_INTERVAL,
ONE_GIGABYTE, ONE_GIGABYTE,
SIZE_LIMIT_DESCRIPTION SIZE_LIMIT_DESCRIPTION