1
0
mirror of synced 2025-12-22 03:16:52 -05:00
Files
docs/script/deploy
James M. Greene f388a3d550 Deploy to staging manually using a script (#19769)
* Add 'script/deploy' to enable manual deploys to Heroku

* Pass API tokens into 'deploy-to-staging' module usage

* Construct Octokit instance to pass in

* Get PR branch name and verify state

* Reorganize

* Rename option to 'octokit'

* Add missing option

* Actually use the convenience methods for convenience

* Simplify top-level script

* Top-level script revisions

* Add parse-pr-url module

* Add create-staging-app-name module

* Remove misplaced comment

* Pass in owner

* Use owner param

* More variables

* Pass owner along more

* Correct prNumber param reference

* Add WIP deploy-to-staging module

* Prevent 'scripts/' and '.github/actions-scripts/' files from being modified in open source repo

* Extract PR author earlier

* Add note about optionally supplying DOCUBOT_REPO_PAT env var

* Override Heroku env var during AppSetup creation instead of later to avoid triggering a second deploy

* Updates to deploy-to-staging module

* Lots of updates

* Add dyno start-up monitoring and warmup requests

* Ignore 'script/deploy' in the repository-references test

* Correct path to Octokit helper

* Temporarily add a 'gha-' prefix to environment names

* Log whole error if terminal. Good for Octokit errors!

* Correct Octokit preview configuration

* Add more logging around Heroku build and release

* Added more timings to log messages

* Monitor dyno states specifically from the dyno list view to avoid 404 oddities when Free dynos are dropped and non-Free dynos are added

* Don't wait for AppSetup status as it includes the Build time

* Updating logging since we don't see DeploymentStatus update messages in the UI =(

* Remove commented out code

* Refactor to extract more properties from the PR object

* Fix reference to pull request number

* Increase Heroku polling intervals from 2.5 seconds to 5 seconds

* Remove unhelpful createDeploymentStatus API calls

* Workaround Heroku's secondary release upon app creation
2021-06-14 22:32:07 +00:00

152 lines
4.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
// [start-readme]
//
// This script is run by a GitHub Actions workflow to trigger deployments
// to Heroku for both staging and production apps.
//
// You can also run it locally if you:
// - Supply a GitHub PAT as the GITHUB_TOKEN environment variable
// - Supply a Heroku API token as the HEROKU_API_TOKEN environment variable
// - Optionally, supply a GitHub PAT as the DOCUBOT_REPO_PAT environment
// variable if you want to support content from the `docs-early-access` repo
//
// Examples:
// - Deploy a PR to Staging:
// 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
// script/deploy --staging https://github.com/github/docs/pull/9876 --rebuild
//
// - Deploy the latest from docs-internal `main` to production
// script/deploy --production
//
// [end-readme]
require('dotenv').config()
const { GITHUB_TOKEN, HEROKU_API_TOKEN } = process.env
// Exit if GitHub Actions PAT is not found
if (!GITHUB_TOKEN) {
throw new Error('You must supply a GITHUB_TOKEN environment variable!')
}
// Exit if Heroku API token is not found
if (!HEROKU_API_TOKEN) {
throw new Error('You must supply a HEROKU_API_TOKEN environment variable!')
}
const program = require('commander')
const { has } = require('lodash')
const getOctokit = require('./helpers/github')
const parsePrUrl = require('./deployment/parse-pr-url')
const deployToStaging = require('./deployment/deploy-to-staging')
const STAGING_FLAG = '--staging'
const PRODUCTION_FLAG = '--production'
const ALLOWED_OWNER = 'github'
const ALLOWED_SOURCE_REPOS = ['docs', 'docs-internal']
const EXPECTED_PR_URL_FORMAT = `https://github.com/${ALLOWED_OWNER}/(${ALLOWED_SOURCE_REPOS.join('|')})/pull/123`
program
.description('Trigger a deployment to Heroku for either staging or production apps')
.option(PRODUCTION_FLAG, 'Deploy the latest internal main branch to Production')
.option(`${STAGING_FLAG} <PR_URL>`, 'Deploy a pull request to Staging')
.option('--rebuild', 'Force a Staging deployment to rebuild the Heroku App from scratch')
.parse(process.argv)
const opts = program.opts()
const isProduction = opts.production === true
const isStaging = has(opts, 'staging')
const prUrl = opts.staging
const forceRebuild = opts.rebuild === true
//
// Verify CLI options
//
if (!isProduction && !isStaging) {
return invalidateAndExit(
'commander.missingArgument',
`error: must specify option '${STAGING_FLAG} <PR_URL>' or '${PRODUCTION_FLAG}'`
)
}
if (isProduction && isStaging) {
return invalidateAndExit(
'commander.conflictingArgument',
`error: must specify option '${STAGING_FLAG} <PR_URL>' or '${PRODUCTION_FLAG}' but not both`
)
}
if (isProduction && forceRebuild) {
return invalidateAndExit(
'commander.conflictingArgument',
`error: cannot specify option '--rebuild' combined with option '${PRODUCTION_FLAG}'`
)
}
// Extract the repository name and pull request number from the URL (if any)
const { owner, repo, pullNumber } = parsePrUrl(prUrl)
if (isStaging) {
if (owner !== ALLOWED_OWNER || !ALLOWED_SOURCE_REPOS.includes(repo) || !pullNumber) {
return invalidateAndExit(
'commander.invalidOptionArgument',
`error: option '${STAGING_FLAG}' argument '${prUrl}' is invalid.
Must match URL format '${EXPECTED_PR_URL_FORMAT}'`
)
}
}
deploy()
//
// Function definitions
//
function invalidateAndExit (errorType, message) {
program._displayError(1, errorType, message)
process.exit(1)
}
async function deploy () {
if (isProduction) {
await deployProduction()
} else if (isStaging) {
await deployStaging({ owner, repo, pullNumber, forceRebuild })
}
}
async function deployProduction () {
// TODO: Request confirmation before deploying to production
return invalidateAndExit(
'commander.invalidOptionArgument',
`error: option '${PRODUCTION_FLAG}' is not yet implemented. SOON!`
)
}
async function deployStaging ({ owner, repo, pullNumber, forceRebuild = false }) {
// This helper uses the `GITHUB_TOKEN` implicitly
const octokit = getOctokit()
const { data: pullRequest } = await octokit.pulls.get({
owner,
repo,
pull_number: pullNumber
})
try {
await deployToStaging({
herokuToken: HEROKU_API_TOKEN,
octokit,
pullRequest,
forceRebuild
})
} catch (error) {
console.error(`Failed to deploy to staging: ${error.message}`)
console.error(error)
process.exit(1)
}
}