1
0
mirror of synced 2025-12-20 18:36:31 -05:00
Files
docs/script/remove-stale-staging-apps.js
James M. Greene 08db1c970c Refactor Staging deployment workflow to support open source PRs (#20459)
* 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>
2021-08-25 20:45:51 +00:00

146 lines
3.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
// [start-readme]
//
// This script removes all stale Heroku staging apps that outlasted the closure
// of their corresponding pull requests, or correspond to spammy pull requests.
//
// [end-readme]
import dotenv from 'dotenv'
import { chain } from 'lodash-es'
import chalk from 'chalk'
import Heroku from 'heroku-client'
import getOctokit from './helpers/github.js'
dotenv.config()
// Check for required Heroku API token
if (!process.env.HEROKU_API_TOKEN) {
console.error(
'Error! You must have a HEROKU_API_TOKEN environment variable for deployer-level access.'
)
process.exit(1)
}
// Check for required GitHub PAT
if (!process.env.GITHUB_TOKEN) {
console.error('Error! You must have a GITHUB_TOKEN environment variable for repo access.')
process.exit(1)
}
const heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN })
// This helper uses the `GITHUB_TOKEN` implicitly
const octokit = getOctokit()
const protectedAppNames = ['help-docs', 'help-docs-deployer']
main()
async function main() {
const apps = chain(await heroku.get('/apps'))
.orderBy('name')
.value()
const prInfoMatch = /^(?:gha-)?(?<repo>docs(?:-internal)?)-(?<pullNumber>\d+)--.*$/
const appsPlusPullIds = apps.map((app) => {
const match = prInfoMatch.exec(app.name)
const { repo, pullNumber } = (match || {}).groups || {}
return {
app,
repo,
pullNumber: parseInt(pullNumber, 10) || null,
}
})
const appsWithPullIds = appsPlusPullIds.filter((appi) => appi.repo && appi.pullNumber > 0)
const nonMatchingAppNames = appsPlusPullIds
.filter((appi) => !(appi.repo && appi.pullNumber > 0))
.map((appi) => appi.app.name)
.filter((name) => !protectedAppNames.includes(name))
let staleCount = 0
let spammyCount = 0
for (const awpi of appsWithPullIds) {
const { isStale, isSpammy } = await assessPullRequest(awpi.repo, awpi.pullNumber)
if (isSpammy) spammyCount++
if (isStale) staleCount++
if (isSpammy || isStale) {
await deleteHerokuApp(awpi.app.name)
}
}
const matchingCount = appsWithPullIds.length
const counts = {
total: matchingCount,
alive: matchingCount - staleCount,
stale: {
total: staleCount,
spammy: spammyCount,
closed: staleCount - spammyCount,
},
}
console.log(`🧮 COUNTS!\n${JSON.stringify(counts, null, 2)}`)
const nonMatchingCount = nonMatchingAppNames.length
if (nonMatchingCount > 0) {
console.log(
'⚠️ 👀',
chalk.yellow(
`Non-matching app names (${nonMatchingCount}):\n - ${nonMatchingAppNames.join('\n - ')}`
)
)
}
}
function displayParams(params) {
const { owner, repo, pull_number: pullNumber } = params
return `${owner}/${repo}#${pullNumber}`
}
async function assessPullRequest(repo, pullNumber) {
const params = {
owner: 'github',
repo: repo,
pull_number: pullNumber,
}
let isStale = false
let isSpammy = false
try {
const { data: pullRequest } = await octokit.pulls.get(params)
if (pullRequest && pullRequest.state === 'closed') {
isStale = true
console.debug(chalk.green(`STALE: ${displayParams(params)} is closed`))
}
} catch (error) {
// Using a standard GitHub PAT, PRs from spammy users will respond as 404
if (error.status === 404) {
isStale = true
isSpammy = true
console.debug(chalk.yellow(`STALE: ${displayParams(params)} is spammy or deleted`))
} else {
console.debug(chalk.red(`ERROR: ${displayParams(params)} - ${error.message}`))
}
}
return { isStale, isSpammy }
}
async function deleteHerokuApp(appName) {
try {
await heroku.delete(`/apps/${appName}`)
console.log('✅', chalk.green(`Removed stale app "${appName}"`))
} catch (error) {
console.log(
'❌',
chalk.red(`ERROR: Failed to remove stale app "${appName}" - ${error.message}`)
)
}
}