text: #!/usr/bin/env node
  import sleep from 'await-sleep'
  import got from 'got'
  import Heroku from 'heroku-client'
  import { setOutput } from '@actions/core'
  
  const SLEEP_INTERVAL = 5000
  const HEROKU_LOG_LINES_TO_SHOW = 25
  
  // Allow for a few 404 (Not Found) or 429 (Too Many Requests) responses from the
  // semi-unreliable Heroku API when we're polling for status updates
  const ALLOWED_MISSING_RESPONSE_COUNT = 5
  
  export default async function deployToProduction({
    octokit,
    // These parameters will only be set by Actions
    sourceBlobUrl = null,
    runId = null,
  }) {
    // Start a timer so we can report how long the deployment takes
    const startTime = Date.now()
    const [owner, repo, branch] = ['github', 'docs-internal', 'main']
  
    let sha
    try {
      const {
        data: { sha: latestSha },
      } = await octokit.repos.getCommit({
        owner,
        repo,
        ref: branch,
      })
      sha = latestSha
  
      if (!sha) {
        throw new Error('Latest commit SHA could not be found')
      }
    } catch (error) {
      console.error(`Error: ${error}`)
      console.log(`🛑 There was an error getting latest commit.`)
      process.exit(1)
    }
  
    // Put together application configuration variables
    const isPrebuilt = !!sourceBlobUrl
    const { DOCUBOT_REPO_PAT } = process.env
    const appConfigVars = {
      // Track the git branch
      GIT_BRANCH: branch,
      // If prebuilt: prevent the Heroku Node.js buildpack from installing devDependencies
      NPM_CONFIG_PRODUCTION: isPrebuilt.toString(),
      // If prebuilt: prevent the Heroku Node.js buildpack from using `npm ci` as it would
      // delete all of the vendored "node_modules/" directory.
      USE_NPM_INSTALL: isPrebuilt.toString(),
      ...(!isPrebuilt && DOCUBOT_REPO_PAT && { DOCUBOT_REPO_PAT }),
    }
  
    const workflowRunLog = runId ? `https://github.com/${owner}/${repo}/actions/runs/${runId}` : null
    let deploymentId = null
    let logUrl = workflowRunLog
  
    const appName = 'help-docs-prod-gha'
    const homepageUrl = `https://${appName}.herokuapp.com/`
  
    try {
      const title = `branch '${branch}' at commit '${sha}' in the 'production' environment as '${appName}'`
  
      console.log(`About to deploy ${title}...`)
  
      // Kick off a pending GitHub Deployment right away, so the PR author
      // will have instant feedback that their work is being deployed.
      const { data: deployment } = await octokit.repos.createDeployment({
        owner,
        repo,
        description: `Deploying ${title}`,
        ref: sha,
  
        // In the GitHub API, there can only be one active deployment per environment.
        environment: appName,
  
        // The status contexts to verify against commit status checks. If you omit
        // this parameter, GitHub verifies all unique contexts before creating a
        // deployment. To bypass checking entirely, pass an empty array. Defaults
        // to all unique contexts.
        required_contexts: [],
  
        // Do not try to merge the base branch into the feature branch
        auto_merge: false,
      })
      console.log('GitHub Deployment created', deployment)
  
      // Store this ID for later updating
      deploymentId = deployment.id
  
      // Set some output variables for workflow steps that run after this script
      if (process.env.GITHUB_ACTIONS) {
        setOutput('deploymentId', deploymentId)
        setOutput('logUrl', logUrl)
      }
  
      await octokit.repos.createDeploymentStatus({
        owner,
        repo,
        deployment_id: deploymentId,
        state: 'in_progress',
        description: 'Deploying the app...',
        // 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: in_progress - Preparing to deploy the app...')
  
      // Time to talk to Heroku...
      const heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN })
      let build = null
  
      if (!sourceBlobUrl) {
        try {
          sourceBlobUrl = await getTarballUrl({
            octokit,
            owner,
            repo,
            sha,
          })
        } catch (error) {
          throw new Error(`Failed to generate source blob URL. Error: ${error}`)
        }
      }
  
      console.log('Updating Heroku app configuration variables...')
  
      // Reconfigure environment variables
      // https://devcenter.heroku.com/articles/platform-api-reference#config-vars-update
      try {
        await heroku.patch(`/apps/${appName}/config-vars`, {
          body: appConfigVars,
        })
      } catch (error) {
        throw new Error(`Failed to update Heroku app configuration variables. Error: ${error}`)
      }
  
      console.log('Reconfigured')
      console.log('Building Heroku app...')
  
      try {
        build = await heroku.post(`/apps/${appName}/builds`, {
          body: {
            source_blob: {
              url: sourceBlobUrl,
            },
          },
        })
      } catch (error) {
        throw new Error(`Failed to create Heroku build. Error: ${error}`)
      }
  
      console.log('Heroku build created', build)
  
      const buildStartTime = Date.now() // Close enough...
      const buildId = build.id
      logUrl = build.output_stream_url
  
      console.log('🚀 Deployment status: in_progress - Building a new Heroku slug...')
  
      // Poll until the Build's status changes from "pending" to "succeeded" or "failed".
      let buildAcceptableErrorCount = 0
      while (!build || build.status === 'pending' || !build.release || !build.release.id) {
        await sleep(SLEEP_INTERVAL)
        try {
          build = await heroku.get(`/apps/${appName}/builds/${buildId}`)
        } catch (error) {
          // Allow for a few bad responses from the Heroku API
          if (error.statusCode === 404 || error.statusCode === 429) {
            buildAcceptableErrorCount += 1
            if (buildAcceptableErrorCount <= ALLOWED_MISSING_RESPONSE_COUNT) {
              continue
            }
          }
          throw new Error(`Failed to get build status. Error: ${error}`)
        }
  
        if (build && build.status === 'failed') {
          throw new Error(
            `Failed to build after ${Math.round(
              (Date.now() - buildStartTime) / 1000
            )} seconds. See Heroku logs for more information:\n${logUrl}`
          )
        }
  
        console.log(
          `Heroku build status: ${(build || {}).status} (after ${Math.round(
            (Date.now() - buildStartTime) / 1000
          )} seconds)`
        )
      }
  
      console.log(
        `Finished Heroku build after ${Math.round((Date.now() - buildStartTime) / 1000)} seconds.`,
        build
      )
  
      const releaseStartTime = Date.now() // Close enough...
      const releaseId = build.release.id
      let release = null
  
      // Poll until the associated Release's status changes from "pending" to "succeeded" or "failed".
      let releaseAcceptableErrorCount = 0
      while (!release || release.status === 'pending') {
        await sleep(SLEEP_INTERVAL)
        try {
          const result = await heroku.get(`/apps/${appName}/releases/${releaseId}`)
  
          // Update the deployment status but only on the first retrieval
          if (!release) {
            logUrl = result.output_stream_url
  
            console.log('Heroku Release created', result)
  
            console.log('🚀 Deployment status: in_progress - Releasing the built Heroku slug...')
          }
  
          release = result
        } catch (error) {
          // Allow for a few bad responses from the Heroku API
          if (error.statusCode === 404 || error.statusCode === 429) {
            releaseAcceptableErrorCount += 1
            if (releaseAcceptableErrorCount <= ALLOWED_MISSING_RESPONSE_COUNT) {
              continue
            }
          }
          throw new Error(`Failed to get release status. Error: ${error}`)
        }
  
        if (release && release.status === 'failed') {
          throw new Error(
            `Failed to release after ${Math.round(
              (Date.now() - releaseStartTime) / 1000
            )} seconds. See Heroku logs for more information:\n${logUrl}`
          )
        }
  
        console.log(
          `Release status: ${(release || {}).status} (after ${Math.round(
            (Date.now() - releaseStartTime) / 1000
          )} seconds)`
        )
      }
  
      console.log(
        `Finished Heroku release after ${Math.round(
          (Date.now() - releaseStartTime) / 1000
        )} seconds.`,
        release
      )
  
      // Monitor dyno state for this release to ensure it reaches "up" rather than crashing.
      // This will help us catch issues with faulty startup code and/or the package manifest.
      const dynoBootStartTime = Date.now()
      console.log('Checking Heroku dynos...')
      logUrl = workflowRunLog
  
      console.log('🚀 Deployment status: in_progress - Monitoring the Heroku dyno start-up...')
  
      // Keep checking while there are still dynos in non-terminal states
      let newDynos = []
      let dynoAcceptableErrorCount = 0
      while (newDynos.length === 0 || newDynos.some((dyno) => dyno.state === 'starting')) {
        await sleep(SLEEP_INTERVAL)
        try {
          const dynoList = await heroku.get(`/apps/${appName}/dynos`)
          const dynosForThisRelease = dynoList.filter((dyno) => dyno.release.id === releaseId)
  
          // To track them afterward
          newDynos = dynosForThisRelease
  
          console.log(
            `Dyno states: ${JSON.stringify(newDynos.map((dyno) => dyno.state))} (after ${Math.round(
              (Date.now() - dynoBootStartTime) / 1000
            )} seconds)`
          )
        } catch (error) {
          // Allow for a few bad responses from the Heroku API
          if (error.statusCode === 404 || error.statusCode === 429) {
            dynoAcceptableErrorCount += 1
            if (dynoAcceptableErrorCount <= ALLOWED_MISSING_RESPONSE_COUNT) {
              continue
            }
          }
          throw new Error(`Failed to find dynos for this release. Error: ${error}`)
        }
      }
  
      const crashedDynos = newDynos.filter((dyno) => ['crashed', 'restarting'].includes(dyno.state))
      const runningDynos = newDynos.filter((dyno) => dyno.state === 'up')
  
      // If any dynos crashed on start-up, fail the deployment
      if (crashedDynos.length > 0) {
        const errorMessage = `At least ${crashedDynos.length} Heroku dyno(s) crashed on start-up!`
  
        console.error(errorMessage)
  
        // Attempt to dump some of the Heroku log here for debugging
        try {
          const logSession = await heroku.post(`/apps/${appName}/log-sessions`, {
            body: {
              dyno: crashedDynos[0].name,
              lines: HEROKU_LOG_LINES_TO_SHOW,
              tail: false,
            },
          })
  
          logUrl = logSession.logplex_url
  
          const logText = await got(logUrl).text()
          console.error(
            `Here are the last ${HEROKU_LOG_LINES_TO_SHOW} lines of the Heroku log:\n\n${logText}`
          )
        } catch (error) {
          // Don't fail because of this error
          console.error(`Failed to retrieve the Heroku logs for the crashed dynos. Error: ${error}`)
        }
  
        throw new Error(errorMessage)
      }
  
      console.log(
        `At least ${runningDynos.length} Heroku dyno(s) are ready after ${Math.round(
          (Date.now() - dynoBootStartTime) / 1000
        )} seconds.`
      )
  
      //
      // TODO:
      // Should we consider adding an explicit 2-minute pause here to allow for
      // Heroku Preboot to actually swap in the new dynos?
      //
  
      // Report success!
      const successMessage = `Deployment succeeded after ${Math.round(
        (Date.now() - startTime) / 1000
      )} seconds.`
      console.log(successMessage)
  
      await octokit.repos.createDeploymentStatus({
        owner,
        repo,
        deployment_id: deploymentId,
        state: 'success',
        description: successMessage,
        ...(logUrl && { log_url: logUrl }),
        environment_url: homepageUrl,
        // 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: success - ${successMessage}`)
      console.log(`Visit the newly deployed app at: ${homepageUrl}`)
    } catch (error) {
      // Report failure!
      const failureMessage = `Deployment failed after ${Math.round(
        (Date.now() - startTime) / 1000
      )} seconds. See logs for more information.`
      console.error(failureMessage)
  
      try {
        if (deploymentId) {
          await octokit.repos.createDeploymentStatus({
            owner,
            repo,
            deployment_id: deploymentId,
            state: 'error',
            description: failureMessage,
            ...(logUrl && { log_url: logUrl }),
            environment_url: homepageUrl,
            // 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: error - ${failureMessage}` + (logUrl ? ` Logs: ${logUrl}` : '')
          )
        }
      } catch (error) {
        console.error(`Failed to finalize GitHub Deployment Status as a failure. Error: ${error}`)
      }
  
      // Re-throw the error to bubble up
      throw error
    }
  }
  
  async function getTarballUrl({ octokit, owner, repo, sha }) {
    // Get a URL for the tarballed source code bundle
    const {
      headers: { location: tarballUrl },
    } = await octokit.repos.downloadTarballArchive({
      owner,
      repo,
      ref: sha,
      // Override the underlying `node-fetch` module's `redirect` option
      // configuration to prevent automatically following redirects.
      request: {
        redirect: 'manual',
      },
    })
    return tarballUrl
  }
  