Rename 'functions' directory to 'internal'
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
const core = require('@actions/core')
|
||||
const github = require('@actions/github')
|
||||
const hc = require('@actions/http-client')
|
||||
const { RequestError } = require('@octokit/request-error')
|
||||
const HttpStatusMessages = require('http-status-messages')
|
||||
|
||||
// All variables we need from the runtime are loaded here
|
||||
const getContext = require('./context')
|
||||
|
||||
async function processRuntimeResponse(res, requestOptions) {
|
||||
// Parse the response body as JSON
|
||||
let obj = null
|
||||
try {
|
||||
const contents = await res.readBody()
|
||||
if (contents && contents.length > 0) {
|
||||
obj = JSON.parse(contents)
|
||||
}
|
||||
} catch (error) {
|
||||
// Invalid resource (contents not json); leaving resulting obj as null
|
||||
}
|
||||
|
||||
// Specific response shape aligned with Octokit
|
||||
const response = {
|
||||
url: res.message?.url || requestOptions.url,
|
||||
status: res.message?.statusCode || 0,
|
||||
headers: {
|
||||
...res.message?.headers
|
||||
},
|
||||
data: obj
|
||||
}
|
||||
|
||||
// Forcibly throw errors for negative HTTP status codes!
|
||||
// @actions/http-client doesn't do this by default.
|
||||
// Mimic the errors thrown by Octokit for consistency.
|
||||
if (response.status >= 400) {
|
||||
// Try to get an error message from the response body
|
||||
const errorMsg =
|
||||
(typeof response.data === 'string' && response.data) ||
|
||||
response.data?.error ||
|
||||
response.data?.message ||
|
||||
// Try the Node HTTP IncomingMessage's statusMessage property
|
||||
res.message?.statusMessage ||
|
||||
// Fallback to the HTTP status message based on the status code
|
||||
HttpStatusMessages[response.status] ||
|
||||
// Or if the status code is unexpected...
|
||||
`Unknown error (${response.status})`
|
||||
|
||||
throw new RequestError(errorMsg, response.status, {
|
||||
response,
|
||||
request: requestOptions
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async function getSignedArtifactUrl({ runtimeToken, workflowRunId, artifactName }) {
|
||||
const { runTimeUrl: RUNTIME_URL } = getContext()
|
||||
const artifactExchangeUrl = `${RUNTIME_URL}_apis/pipelines/workflows/${workflowRunId}/artifacts?api-version=6.0-preview`
|
||||
|
||||
const httpClient = new hc.HttpClient()
|
||||
let data = null
|
||||
|
||||
try {
|
||||
const requestHeaders = {
|
||||
accept: 'application/json',
|
||||
authorization: `Bearer ${runtimeToken}`
|
||||
}
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
url: artifactExchangeUrl,
|
||||
headers: {
|
||||
...requestHeaders
|
||||
},
|
||||
body: null
|
||||
}
|
||||
|
||||
core.info(`Artifact exchange URL: ${artifactExchangeUrl}`)
|
||||
const res = await httpClient.get(artifactExchangeUrl, requestHeaders)
|
||||
|
||||
// May throw a RequestError (HttpError)
|
||||
const response = await processRuntimeResponse(res, requestOptions)
|
||||
|
||||
data = response.data
|
||||
core.debug(JSON.stringify(data))
|
||||
} catch (error) {
|
||||
core.error('Getting signed artifact URL failed', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const artifactRawUrl = data?.value?.find(artifact => artifact.name === artifactName)?.url
|
||||
if (!artifactRawUrl) {
|
||||
throw new Error(
|
||||
'No uploaded artifact was found! Please check if there are any errors at build step, or uploaded artifact name is correct.'
|
||||
)
|
||||
}
|
||||
|
||||
const signedArtifactUrl = `${artifactRawUrl}&%24expand=SignedContent`
|
||||
return signedArtifactUrl
|
||||
}
|
||||
|
||||
async function createPagesDeployment({ githubToken, artifactUrl, buildVersion, idToken, isPreview = false }) {
|
||||
const octokit = github.getOctokit(githubToken)
|
||||
|
||||
const payload = {
|
||||
artifact_url: artifactUrl,
|
||||
pages_build_version: buildVersion,
|
||||
oidc_token: idToken
|
||||
}
|
||||
if (isPreview === true) {
|
||||
payload.preview = true
|
||||
}
|
||||
core.info(`Creating Pages deployment with payload:\n${JSON.stringify(payload, null, '\t')}`)
|
||||
|
||||
try {
|
||||
const response = await octokit.request('POST /repos/{owner}/{repo}/pages/deployments', {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
...payload
|
||||
})
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
core.error('Creating Pages deployment failed', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getPagesDeploymentStatus({ githubToken, deploymentId }) {
|
||||
const octokit = github.getOctokit(githubToken)
|
||||
|
||||
core.info('Getting Pages deployment status...')
|
||||
try {
|
||||
const response = await octokit.request('GET /repos/{owner}/{repo}/pages/deployments/{deploymentId}', {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
deploymentId
|
||||
})
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
core.error('Getting Pages deployment status failed', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelPagesDeployment({ githubToken, deploymentId }) {
|
||||
const octokit = github.getOctokit(githubToken)
|
||||
|
||||
core.info('Canceling Pages deployment...')
|
||||
try {
|
||||
const response = await octokit.request('POST /repos/{owner}/{repo}/pages/deployments/{deploymentId}/cancel', {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
deploymentId
|
||||
})
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
core.error('Canceling Pages deployment failed', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSignedArtifactUrl,
|
||||
createPagesDeployment,
|
||||
getPagesDeploymentStatus,
|
||||
cancelPagesDeployment
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
const core = require('@actions/core')
|
||||
|
||||
// Load variables from Actions runtime
|
||||
function getRequiredVars() {
|
||||
return {
|
||||
runTimeUrl: process.env.ACTIONS_RUNTIME_URL,
|
||||
workflowRun: process.env.GITHUB_RUN_ID,
|
||||
runTimeToken: process.env.ACTIONS_RUNTIME_TOKEN,
|
||||
repositoryNwo: process.env.GITHUB_REPOSITORY,
|
||||
buildVersion: process.env.GITHUB_SHA,
|
||||
buildActor: process.env.GITHUB_ACTOR,
|
||||
actionsId: process.env.GITHUB_ACTION,
|
||||
githubToken: core.getInput('token'),
|
||||
githubApiUrl: process.env.GITHUB_API_URL ?? 'https://api.github.com',
|
||||
githubServerUrl: process.env.GITHUB_SERVER_URL ?? 'https://github.com',
|
||||
artifactName: core.getInput('artifact_name') || 'github-pages',
|
||||
isPreview: core.getInput('preview') === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function getContext() {
|
||||
const requiredVars = getRequiredVars()
|
||||
for (const variable in requiredVars) {
|
||||
if (requiredVars[variable] === undefined) {
|
||||
throw new Error(`${variable} is undefined. Cannot continue.`)
|
||||
}
|
||||
}
|
||||
core.debug('all variables are set')
|
||||
return requiredVars
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
const core = require('@actions/core')
|
||||
|
||||
// All variables we need from the runtime are loaded here
|
||||
const getContext = require('./context')
|
||||
const {
|
||||
getSignedArtifactUrl,
|
||||
createPagesDeployment,
|
||||
getPagesDeploymentStatus,
|
||||
cancelPagesDeployment
|
||||
} = require('./api-client')
|
||||
|
||||
const temporaryErrorStatus = {
|
||||
unknown_status: 'Unable to get deployment status.',
|
||||
not_found: 'Deployment not found.',
|
||||
deployment_attempt_error: 'Deployment temporarily failed, a retry will be automatically scheduled...'
|
||||
}
|
||||
|
||||
const finalErrorStatus = {
|
||||
deployment_failed: 'Deployment failed, try again later.',
|
||||
deployment_content_failed:
|
||||
'Artifact could not be deployed. Please ensure the content does not contain any hard links, symlinks and total size is less than 10GB.',
|
||||
deployment_cancelled: 'Deployment cancelled.',
|
||||
deployment_lost: 'Deployment failed to report final status.'
|
||||
}
|
||||
|
||||
class Deployment {
|
||||
constructor() {
|
||||
const context = getContext()
|
||||
this.runTimeUrl = context.runTimeUrl
|
||||
this.repositoryNwo = context.repositoryNwo
|
||||
this.runTimeToken = context.runTimeToken
|
||||
this.buildVersion = context.buildVersion
|
||||
this.buildActor = context.buildActor
|
||||
this.actionsId = context.actionsId
|
||||
this.githubToken = context.githubToken
|
||||
this.workflowRun = context.workflowRun
|
||||
this.deploymentInfo = null
|
||||
this.githubApiUrl = context.githubApiUrl
|
||||
this.githubServerUrl = context.githubServerUrl
|
||||
this.artifactName = context.artifactName
|
||||
this.isPreview = context.isPreview === true
|
||||
}
|
||||
|
||||
// Ask the runtime for the unsigned artifact URL and deploy to GitHub Pages
|
||||
// by creating a deployment with that artifact
|
||||
async create(idToken) {
|
||||
try {
|
||||
core.debug(`Actor: ${this.buildActor}`)
|
||||
core.debug(`Action ID: ${this.actionsId}`)
|
||||
core.debug(`Actions Workflow Run ID: ${this.workflowRun}`)
|
||||
|
||||
const artifactUrl = await getSignedArtifactUrl({
|
||||
runtimeToken: this.runTimeToken,
|
||||
workflowRunId: this.workflowRun,
|
||||
artifactName: this.artifactName
|
||||
})
|
||||
|
||||
const deployment = await createPagesDeployment({
|
||||
githubToken: this.githubToken,
|
||||
artifactUrl,
|
||||
buildVersion: this.buildVersion,
|
||||
idToken,
|
||||
isPreview: this.isPreview
|
||||
})
|
||||
|
||||
if (deployment) {
|
||||
this.deploymentInfo = {
|
||||
...deployment,
|
||||
id: deployment.id || deployment.status_url?.split('/')?.pop() || this.buildVersion,
|
||||
pending: true
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Created deployment for ${this.buildVersion}, ID: ${this.deploymentInfo?.id}`)
|
||||
|
||||
core.debug(JSON.stringify(deployment))
|
||||
|
||||
return deployment
|
||||
} catch (error) {
|
||||
core.error(error.stack)
|
||||
|
||||
// output raw error in debug mode.
|
||||
core.debug(JSON.stringify(error))
|
||||
|
||||
// build customized error message based on server response
|
||||
if (error.response) {
|
||||
let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}. `
|
||||
if (error.status === 400) {
|
||||
errorMessage += `Responded with: ${error.message}`
|
||||
} else if (error.status === 403) {
|
||||
errorMessage += 'Ensure GITHUB_TOKEN has permission "pages: write".'
|
||||
} else if (error.status === 404) {
|
||||
const pagesSettingsUrl = `${this.githubServerUrl}/${this.repositoryNwo}/settings/pages`
|
||||
errorMessage += `Ensure GitHub Pages has been enabled: ${pagesSettingsUrl}`
|
||||
} else if (error.status >= 500) {
|
||||
errorMessage +=
|
||||
'Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.'
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
} else {
|
||||
// istanbul ignore next
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll the deployment endpoint for status
|
||||
async check() {
|
||||
// Don't attempt to check status if no deployment was created
|
||||
if (!this.deploymentInfo) {
|
||||
core.setFailed(temporaryErrorStatus.not_found)
|
||||
return
|
||||
}
|
||||
if (this.deploymentInfo.pending !== true) {
|
||||
core.setFailed(temporaryErrorStatus.unknown_status)
|
||||
return
|
||||
}
|
||||
|
||||
const deploymentId = this.deploymentInfo.id || this.buildVersion
|
||||
const timeout = Number(core.getInput('timeout'))
|
||||
const reportingInterval = Number(core.getInput('reporting_interval'))
|
||||
const maxErrorCount = Number(core.getInput('error_count'))
|
||||
|
||||
let startTime = Date.now()
|
||||
let errorCount = 0
|
||||
|
||||
// Time in milliseconds between two deployment status report when status errored, default 0.
|
||||
let errorReportingInterval = 0
|
||||
let deployment = null
|
||||
let errorStatus = 0
|
||||
|
||||
/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
|
||||
while (true) {
|
||||
// Handle reporting interval
|
||||
await new Promise(resolve => setTimeout(resolve, reportingInterval + errorReportingInterval))
|
||||
|
||||
// Check status
|
||||
try {
|
||||
deployment = await getPagesDeploymentStatus({
|
||||
githubToken: this.githubToken,
|
||||
deploymentId
|
||||
})
|
||||
|
||||
if (deployment.status === 'succeed') {
|
||||
core.info('Reported success!')
|
||||
core.setOutput('status', 'succeed')
|
||||
this.deploymentInfo.pending = false
|
||||
break
|
||||
} else if (finalErrorStatus[deployment.status]) {
|
||||
// Fall into permanent error, it may be caused by ongoing incident, malicious deployment content, exhausted automatic retry times, invalid artifact, etc.
|
||||
core.setFailed(finalErrorStatus[deployment.status])
|
||||
this.deploymentInfo.pending = false
|
||||
break
|
||||
} else if (temporaryErrorStatus[deployment.status]) {
|
||||
// A temporary error happened, will query the status again
|
||||
core.warning(temporaryErrorStatus[deployment.status])
|
||||
} else {
|
||||
core.info('Current status: ' + deployment.status)
|
||||
}
|
||||
|
||||
// reset the error reporting interval once get the proper status back.
|
||||
errorReportingInterval = 0
|
||||
} catch (error) {
|
||||
core.error(error.stack)
|
||||
|
||||
// output raw error in debug mode.
|
||||
core.debug(JSON.stringify(error))
|
||||
|
||||
// build customized error message based on server response
|
||||
if (error.response) {
|
||||
errorStatus = error.status || error.response.status
|
||||
|
||||
errorCount++
|
||||
|
||||
// set the maximum error reporting interval greater than 15 sec but below 30 sec.
|
||||
if (errorReportingInterval < 1000 * 15) {
|
||||
errorReportingInterval = (errorReportingInterval << 1) | 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount >= maxErrorCount) {
|
||||
core.error('Too many errors, aborting!')
|
||||
core.setFailed('Failed with status code: ' + errorStatus)
|
||||
|
||||
// Explicitly cancel the deployment
|
||||
await this.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (Date.now() - startTime >= timeout) {
|
||||
core.error('Timeout reached, aborting!')
|
||||
core.setFailed('Timeout reached, aborting!')
|
||||
|
||||
// Explicitly cancel the deployment
|
||||
await this.cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
// Don't attempt to cancel if no deployment was created
|
||||
if (!this.deploymentInfo || this.deploymentInfo.pending !== true) {
|
||||
core.debug('No deployment to cancel')
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel the deployment
|
||||
try {
|
||||
const deploymentId = this.deploymentInfo.id || this.buildVersion
|
||||
await cancelPagesDeployment({
|
||||
githubToken: this.githubToken,
|
||||
deploymentId
|
||||
})
|
||||
core.info(`Canceled deployment with ID ${deploymentId}`)
|
||||
|
||||
this.deploymentInfo.pending = false
|
||||
} catch (error) {
|
||||
core.setFailed(error)
|
||||
if (error.response?.data) {
|
||||
core.error(JSON.stringify(error.response.data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Deployment }
|
||||
Reference in New Issue
Block a user