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}` // If using GHES, add a special note about compatibility if (new URL(this.githubServerUrl).hostname.toLowerCase() !== 'github.com') { errorMessage += '\nNote: This action version may not yet support GitHub Enterprise Server, please check the compatibility table.' } } 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 }