* Add a Staging build workflow * Remove all commented out code from build workflow It will be handled in https://github.com/github/docs-engineering/issues/726 * Use pinned version of upload-artifact action * Tweaks to build * Minor deployment script refactoring * Update the Staging deployment workflow * Missed refactoring tweak * Add relevant comments * Update Heroku app naming convention for Actions deploy to include 'gha-' prefix * Update Heroku app ConfigVars and SourceBlob for optional prebuilt app * Remove obsolete 'dist/' dir from PR build artifact See https://github.com/github/docs-internal/pull/20405 * Ensure a new enough version of npm is used * Switch to creating a tarball for upload * Remove obsolete 'layouts' dir from file list * Ditch the verbosity for 'tar'... too many files * Add tarball support to deploy * Add esm workaround to deploy script See https://github.com/actions/github-script/issues/168 * Temporarily ignore staging deploy workflow from workflow linter * Update deployment to use a Heroku Build Source instead of a GitHub Actions Artifact * Update undeploy workflow to use ESM workaround See https://github.com/actions/github-script/issues/168 * Add 'esm' package to optionalDependencies to better support workaround See https://github.com/actions/github-script/issues/168 * Add Slack notifications for workflow failures * Wrap AppSetup polling in try-catch * Improve dyno monitoring * Rename 'script/deploy' to have a .js extension #esm * Update script references to include the extension * Use non-deprecated Sources API for Heroku * Use normal quotes * Stub in a step to mark deployment inactive after timing out * Apply suggestions from code review Co-authored-by: Rachael Sewell <rachmari@github.com> Co-authored-by: Rachael Sewell <rachmari@github.com>
181 lines
5.2 KiB
JavaScript
Executable File
181 lines
5.2 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 and force the Heroku App to be rebuilt from scratch (by default):
|
|
// script/deploy.js --staging https://github.com/github/docs/pull/9876
|
|
//
|
|
// - Deploy a PR to Staging and DO NOT rebuild the Heroku App:
|
|
// script/deploy.js --staging https://github.com/github/docs-internal/pull/12345 --no-rebuild
|
|
//
|
|
// - Undeploy a PR from Staging by deleting the Heroku App:
|
|
// script/deploy.js --staging https://github.com/github/docs/pull/9876 --destroy
|
|
//
|
|
// - Deploy the latest from docs-internal `main` to production:
|
|
// script/deploy.js --production
|
|
//
|
|
// [end-readme]
|
|
|
|
import dotenv from 'dotenv'
|
|
import program from 'commander'
|
|
import { has } from 'lodash-es'
|
|
import getOctokit from './helpers/github.js'
|
|
import parsePrUrl from './deployment/parse-pr-url.js'
|
|
import deployToStaging from './deployment/deploy-to-staging.js'
|
|
import undeployFromStaging from './deployment/undeploy-from-staging.js'
|
|
|
|
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 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(
|
|
'--no-rebuild',
|
|
'Do NOT 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)
|
|
|
|
const opts = program.opts()
|
|
const isProduction = opts.production === true
|
|
const isStaging = has(opts, 'staging')
|
|
const prUrl = opts.staging
|
|
const forceRebuild = opts.rebuild !== false
|
|
const destroy = opts.destroy === true
|
|
|
|
//
|
|
// Verify CLI options
|
|
//
|
|
if (!isProduction && !isStaging) {
|
|
invalidateAndExit(
|
|
'commander.missingArgument',
|
|
`error: must specify option '${STAGING_FLAG} <PR_URL>' or '${PRODUCTION_FLAG}'`
|
|
)
|
|
}
|
|
|
|
if (isProduction && isStaging) {
|
|
invalidateAndExit(
|
|
'commander.conflictingArgument',
|
|
`error: must specify option '${STAGING_FLAG} <PR_URL>' or '${PRODUCTION_FLAG}' but not both`
|
|
)
|
|
}
|
|
|
|
if (isProduction && forceRebuild) {
|
|
invalidateAndExit(
|
|
'commander.conflictingArgument',
|
|
`error: cannot specify option '--rebuild' combined with option '${PRODUCTION_FLAG}'`
|
|
)
|
|
}
|
|
|
|
if (isProduction && destroy) {
|
|
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)
|
|
const { owner, repo, pullNumber } = parsePrUrl(prUrl)
|
|
|
|
if (isStaging) {
|
|
if (owner !== ALLOWED_OWNER || !ALLOWED_SOURCE_REPOS.includes(repo) || !pullNumber) {
|
|
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, destroy })
|
|
}
|
|
}
|
|
|
|
async function deployProduction() {
|
|
// TODO: Request confirmation before deploying to production
|
|
|
|
invalidateAndExit(
|
|
'commander.invalidOptionArgument',
|
|
`error: option '${PRODUCTION_FLAG}' is not yet implemented. SOON!`
|
|
)
|
|
}
|
|
|
|
async function deployStaging({ owner, repo, pullNumber, forceRebuild = false, destroy = false }) {
|
|
// This helper uses the `GITHUB_TOKEN` implicitly
|
|
const octokit = getOctokit()
|
|
|
|
const { data: pullRequest } = await octokit.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number: pullNumber,
|
|
})
|
|
|
|
try {
|
|
if (destroy) {
|
|
await undeployFromStaging({
|
|
octokit,
|
|
pullRequest,
|
|
})
|
|
} else {
|
|
await deployToStaging({
|
|
octokit,
|
|
pullRequest,
|
|
forceRebuild,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
const action = destroy ? 'undeploy from' : 'deploy to'
|
|
console.error(`Failed to ${action} staging: ${error.message}`)
|
|
console.error(error)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
export default deploy
|