1
0
mirror of synced 2025-12-22 11:26:57 -05:00

Undeploy from staging manually using a script (#19875)

* Add skeleton workflow and module for undeploying

* Remove commented out code

* Update undeployment module

* Add '--destroy' flag to 'script/deploy' options

* Add timeout and concurrency key for undeployment

* Remove dangling unneeded function declaration

* Add ant-man preview for inactive deployment state setting

* Fix reference to pull request number

* Refactor to extract more properties from the PR object

* Fix reference to pull request number

* Remove workflow

* Add a README comment about '--destroy' usage

* Fix grammar

* Add missing 'ant-man' preview to createDeploymentStatus calls

* Add missing preview to createDeploymentStatus calls

* Find the latest existing deployment instead of creating a new one

* Remove unused variable

* Deactivate ALL deployments

* Update copy

* Add missing colons
This commit is contained in:
James M. Greene
2021-06-15 10:51:25 -05:00
committed by GitHub
parent 5a50fb0406
commit db75933733
3 changed files with 145 additions and 17 deletions

View File

@@ -15,10 +15,13 @@
// - Deploy a PR to Staging: // - Deploy a PR to Staging:
// script/deploy --staging https://github.com/github/docs-internal/pull/12345 // script/deploy --staging https://github.com/github/docs-internal/pull/12345
// //
// - Deploy a PR to Staging and force the Heroku App to be rebuilt from scratch // - Deploy a PR to Staging and force the Heroku App to be rebuilt from scratch:
// script/deploy --staging https://github.com/github/docs/pull/9876 --rebuild // script/deploy --staging https://github.com/github/docs/pull/9876 --rebuild
// //
// - Deploy the latest from docs-internal `main` to production // - Undeploy a PR from Staging by deleting the Heroku App:
// script/deploy --staging https://github.com/github/docs/pull/9876 --destroy
//
// - Deploy the latest from docs-internal `main` to production:
// script/deploy --production // script/deploy --production
// //
// [end-readme] // [end-readme]
@@ -41,6 +44,7 @@ const { has } = require('lodash')
const getOctokit = require('./helpers/github') const getOctokit = require('./helpers/github')
const parsePrUrl = require('./deployment/parse-pr-url') const parsePrUrl = require('./deployment/parse-pr-url')
const deployToStaging = require('./deployment/deploy-to-staging') const deployToStaging = require('./deployment/deploy-to-staging')
const undeployFromStaging = require('./deployment/undeploy-from-staging')
const STAGING_FLAG = '--staging' const STAGING_FLAG = '--staging'
const PRODUCTION_FLAG = '--production' const PRODUCTION_FLAG = '--production'
@@ -53,6 +57,7 @@ program
.option(PRODUCTION_FLAG, 'Deploy the latest internal main branch to Production') .option(PRODUCTION_FLAG, 'Deploy the latest internal main branch to Production')
.option(`${STAGING_FLAG} <PR_URL>`, 'Deploy a pull request to Staging') .option(`${STAGING_FLAG} <PR_URL>`, 'Deploy a pull request to Staging')
.option('--rebuild', 'Force a Staging deployment to rebuild the Heroku App from scratch') .option('--rebuild', 'Force a Staging deployment to rebuild the Heroku App from scratch')
.option('--destroy', 'Undeploy a Staging deployment by deleting the Heroku App')
.parse(process.argv) .parse(process.argv)
const opts = program.opts() const opts = program.opts()
@@ -60,6 +65,7 @@ const isProduction = opts.production === true
const isStaging = has(opts, 'staging') const isStaging = has(opts, 'staging')
const prUrl = opts.staging const prUrl = opts.staging
const forceRebuild = opts.rebuild === true const forceRebuild = opts.rebuild === true
const destroy = opts.destroy === true
// //
// Verify CLI options // Verify CLI options
@@ -85,6 +91,13 @@ if (isProduction && forceRebuild) {
) )
} }
if (isProduction && destroy) {
return invalidateAndExit(
'commander.conflictingArgument',
`error: cannot specify option '--destroy' combined with option '${PRODUCTION_FLAG}'`
)
}
// Extract the repository name and pull request number from the URL (if any) // Extract the repository name and pull request number from the URL (if any)
const { owner, repo, pullNumber } = parsePrUrl(prUrl) const { owner, repo, pullNumber } = parsePrUrl(prUrl)
@@ -113,7 +126,7 @@ async function deploy () {
if (isProduction) { if (isProduction) {
await deployProduction() await deployProduction()
} else if (isStaging) { } else if (isStaging) {
await deployStaging({ owner, repo, pullNumber, forceRebuild }) await deployStaging({ owner, repo, pullNumber, forceRebuild, destroy })
} }
} }
@@ -126,7 +139,7 @@ async function deployProduction () {
) )
} }
async function deployStaging ({ owner, repo, pullNumber, forceRebuild = false }) { async function deployStaging ({ owner, repo, pullNumber, forceRebuild = false, destroy = false }) {
// This helper uses the `GITHUB_TOKEN` implicitly // This helper uses the `GITHUB_TOKEN` implicitly
const octokit = getOctokit() const octokit = getOctokit()
@@ -137,14 +150,23 @@ async function deployStaging ({ owner, repo, pullNumber, forceRebuild = false })
}) })
try { try {
await deployToStaging({ if (destroy) {
herokuToken: HEROKU_API_TOKEN, await undeployFromStaging({
octokit, herokuToken: HEROKU_API_TOKEN,
pullRequest, octokit,
forceRebuild pullRequest
}) })
} else {
await deployToStaging({
herokuToken: HEROKU_API_TOKEN,
octokit,
pullRequest,
forceRebuild
})
}
} catch (error) { } catch (error) {
console.error(`Failed to deploy to staging: ${error.message}`) const action = destroy ? 'undeploy from' : 'deploy to'
console.error(`Failed to ${action} staging: ${error.message}`)
console.error(error) console.error(error)
process.exit(1) process.exit(1)
} }

View File

@@ -83,9 +83,11 @@ module.exports = async function deployToStaging ({ herokuToken, octokit, pullReq
deployment_id: deploymentId, deployment_id: deploymentId,
state: 'in_progress', state: 'in_progress',
description: 'Deploying the app...', description: 'Deploying the app...',
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued' // The 'ant-man' preview is required for `state` values of 'inactive', as well as
// the use of the `log_url`, `environment_url`, and `auto_inactive` parameters.
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued'.
mediaType: { mediaType: {
previews: ['flash'] previews: ['ant-man', 'flash']
} }
}) })
console.log('🚀 Deployment status: in_progress - Preparing to deploy the app...') console.log('🚀 Deployment status: in_progress - Preparing to deploy the app...')
@@ -409,9 +411,11 @@ module.exports = async function deployToStaging ({ herokuToken, octokit, pullReq
description: successMessage, description: successMessage,
...logUrl && { log_url: logUrl }, ...logUrl && { log_url: logUrl },
environment_url: homepageUrl, environment_url: homepageUrl,
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued' // The 'ant-man' preview is required for `state` values of 'inactive', as well as
// the use of the `log_url`, `environment_url`, and `auto_inactive` parameters.
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued'.
mediaType: { mediaType: {
previews: ['flash'] previews: ['ant-man', 'flash']
} }
}) })
@@ -432,9 +436,11 @@ module.exports = async function deployToStaging ({ herokuToken, octokit, pullReq
description: failureMessage, description: failureMessage,
...logUrl && { log_url: logUrl }, ...logUrl && { log_url: logUrl },
environment_url: homepageUrl, environment_url: homepageUrl,
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued' // The 'ant-man' preview is required for `state` values of 'inactive', as well as
// the use of the `log_url`, `environment_url`, and `auto_inactive` parameters.
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued'.
mediaType: { mediaType: {
previews: ['flash'] previews: ['ant-man', 'flash']
} }
}) })

View File

@@ -0,0 +1,100 @@
const Heroku = require('heroku-client')
const createStagingAppName = require('./create-staging-app-name')
module.exports = async function undeployFromStaging ({
herokuToken,
octokit,
pullRequest
}) {
// Start a timer so we can report how long the deployment takes
const startTime = Date.now()
// Extract some important properties from the PR
const {
number: pullNumber,
base: {
repo: {
name: repo,
owner: { login: owner }
}
},
head: {
ref: branch
}
} = pullRequest
const appName = createStagingAppName({ repo, pullNumber, branch })
try {
const title = `from the 'staging' environment as '${appName}'`
console.log(`About to undeploy ${title}...`)
// Time to talk to Heroku...
const heroku = new Heroku({ token: herokuToken })
// Is there already a Heroku App for this PR?
let appExists = true
try {
await heroku.get(`/apps/${appName}`)
} catch (error) {
appExists = false
}
// If there is an existing app, delete it
if (appExists) {
try {
await heroku.delete(`/apps/${appName}`)
console.log(`Heroku app '${appName}' deleted`)
} catch (error) {
throw new Error(`Failed to delete Heroku app '${appName}'. Error: ${error}`)
}
}
// Get the latest deployment environment to signal its deactivation
const { data: deployments } = await octokit.repos.listDeployments({
owner,
repo,
// In the GitHub API, there can only be one active deployment per environment.
// For our many staging apps, we must use the unique appName as the environment.
environment: appName
})
if (deployments.length === 0) {
console.log('🚀 No deployments to deactivate!')
console.log(`Finished undeploying after ${Math.round((Date.now() - startTime) / 1000)} seconds`)
return
}
console.log(`Found ${deployments.length} GitHub Deployments`, deployments)
// Deactivate ALL of the deployments
for (const deployment of deployments) {
const { data: deploymentStatus } = await octokit.repos.createDeploymentStatus({
owner,
repo,
deployment_id: deployment.id,
state: 'inactive',
description: 'The app was undeployed',
// The 'ant-man' preview is required for `state` values of 'inactive', as well as
// the use of the `log_url`, `environment_url`, and `auto_inactive` parameters.
// The 'flash' preview is required for `state` values of 'in_progress' and 'queued'.
mediaType: {
previews: ['ant-man', 'flash']
}
})
console.log(`🚀 Deployment status (ID: ${deployment.id}): ${deploymentStatus.state} - ${deploymentStatus.description}`)
}
console.log(`Finished undeploying after ${Math.round((Date.now() - startTime) / 1000)} seconds`)
} catch (error) {
// Report failure!
const failureMessage = `Undeployment failed after ${Math.round((Date.now() - startTime) / 1000)} seconds. See logs for more information.`
console.error(failureMessage)
// Re-throw the error to bubble up
throw error
}
}