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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
100
script/deployment/undeploy-from-staging.js
Normal file
100
script/deployment/undeploy-from-staging.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user