diff --git a/.github/actions-scripts/content-changes-table-comment.js b/.github/actions-scripts/content-changes-table-comment.js index 4aecea4709..223dc19491 100755 --- a/.github/actions-scripts/content-changes-table-comment.js +++ b/.github/actions-scripts/content-changes-table-comment.js @@ -1,13 +1,14 @@ #!/usr/bin/env node import * as github from '@actions/github' -import { setOutput } from '@actions/core' +import core from '@actions/core' import { getContents } from '../../script/helpers/git-utils.js' import parse from '../../lib/read-frontmatter.js' import getApplicableVersions from '../../lib/get-applicable-versions.js' import nonEnterpriseDefaultVersion from '../../lib/non-enterprise-default-version.js' import { allVersionShortnames } from '../../lib/all-versions.js' +import { waitUntilUrlIsHealthy } from './lib/wait-until-url-is-healthy.js' const { GITHUB_TOKEN, APP_URL } = process.env const context = github.context @@ -26,135 +27,144 @@ if (!APP_URL) { const MAX_COMMENT_SIZE = 125000 const PROD_URL = 'https://docs.github.com' -const octokit = github.getOctokit(GITHUB_TOKEN) -// get the list of file changes from the PR -const response = await octokit.rest.repos.compareCommitsWithBasehead({ - owner: context.repo.owner, - repo: context.payload.repository.name, - basehead: `${context.payload.pull_request.base.sha}...${context.payload.pull_request.head.sha}`, -}) +run() -const { files } = response.data - -let markdownTable = - '| **Source** | **Preview** | **Production** | **What Changed** |\n|:----------- |:----------- |:----------- |:----------- |\n' - -const pathPrefix = 'content/' -const articleFiles = files.filter( - ({ filename }) => filename.startsWith(pathPrefix) && !filename.endsWith('/index.md') -) - -const lines = await Promise.all( - articleFiles.map(async (file) => { - const sourceUrl = file.blob_url - const fileName = file.filename.slice(pathPrefix.length) - const fileUrl = fileName.slice(0, fileName.lastIndexOf('.')) - - // get the file contents and decode them - // this script is called from the main branch, so we need the API call to get the contents from the branch, instead - const fileContents = await getContents( - context.repo.owner, - context.payload.repository.name, - // Can't get its content if it no longer exists. - // Meaning, you'd get a 404 on the `getContents()` utility function. - // So, to be able to get necessary meta data about what it *was*, - // if it was removed, fall back to the 'base'. - file.status === 'removed' - ? context.payload.pull_request.base.sha - : context.payload.pull_request.head.sha, - file.filename - ) - - // parse the frontmatter - const { data } = parse(fileContents) - - let contentCell = '' - let previewCell = '' - let prodCell = '' - - if (file.status === 'added') contentCell = 'New file: ' - else if (file.status === 'removed') contentCell = 'Removed: ' - contentCell += `[\`${fileName}\`](${sourceUrl})` - - try { - // the try/catch is needed because getApplicableVersions() returns either [] or throws an error when it can't parse the versions frontmatter - // try/catch can be removed if docs-engineering#1821 is resolved - // i.e. for feature based versioning, like ghae: 'issue-6337' - const fileVersions = getApplicableVersions(data.versions) - - for (const plan in allVersionShortnames) { - // plan is the shortName (i.e., fpt) - // allVersionShortNames[plan] is the planName (i.e., free-pro-team) - - // walk by the plan names since we generate links differently for most plans - const versions = fileVersions.filter((fileVersion) => - fileVersion.includes(allVersionShortnames[plan]) - ) - - if (versions.length === 1) { - // for fpt, ghec, and ghae - - if (versions.toString() === nonEnterpriseDefaultVersion) { - // omit version from fpt url - - previewCell += `[${plan}](${APP_URL}/${fileUrl})
` - prodCell += `[${plan}](${PROD_URL}/${fileUrl})
` - } else { - // for non-versioned releases (ghae, ghec) use full url - - previewCell += `[${plan}](${APP_URL}/${versions}/${fileUrl})
` - prodCell += `[${plan}](${PROD_URL}/${versions}/${fileUrl})
` - } - } else if (versions.length) { - // for ghes releases, link each version - - previewCell += `${plan}@ ` - prodCell += `${plan}@ ` - - versions.forEach((version) => { - previewCell += `[${version.split('@')[1]}](${APP_URL}/${version}/${fileUrl}) ` - prodCell += `[${version.split('@')[1]}](${PROD_URL}/${version}/${fileUrl}) ` - }) - previewCell += '
' - prodCell += '
' - } - } - } catch (e) { - console.error( - `Version information for ${file.filename} couldn't be determined from its frontmatter.` - ) - } - let note = '' - if (file.status === 'removed') { - note = 'removed' - // If the file was removed, the `previewCell` no longer makes sense - // since it was based on looking at the base sha. - previewCell = 'n/a' - } - - return `| ${contentCell} | ${previewCell} | ${prodCell} | ${note} |` - }) -) - -// this section limits the size of the comment -const cappedLines = [] -let underMax = true - -lines.reduce((previous, current, index, array) => { - if (underMax) { - if (previous + current.length > MAX_COMMENT_SIZE) { - underMax = false - cappedLines.push('**Note** There are more changes in this PR than we can show.') - return previous - } - - cappedLines.push(array[index]) - return previous + current.length +async function run() { + const isHealthy = await waitUntilUrlIsHealthy(APP_URL) + if (!isHealthy) { + return core.setFailed(`Timeout waiting for preview environment: ${APP_URL}`) } - return previous -}, markdownTable.length) -markdownTable += cappedLines.join('\n') + const octokit = github.getOctokit(GITHUB_TOKEN) + // get the list of file changes from the PR + const response = await octokit.rest.repos.compareCommitsWithBasehead({ + owner: context.repo.owner, + repo: context.payload.repository.name, + basehead: `${context.payload.pull_request.base.sha}...${context.payload.pull_request.head.sha}`, + }) -setOutput('changesTable', markdownTable) + const { files } = response.data + + let markdownTable = + '| **Source** | **Preview** | **Production** | **What Changed** |\n|:----------- |:----------- |:----------- |:----------- |\n' + + const pathPrefix = 'content/' + const articleFiles = files.filter( + ({ filename }) => filename.startsWith(pathPrefix) && !filename.endsWith('/index.md') + ) + + const lines = await Promise.all( + articleFiles.map(async (file) => { + const sourceUrl = file.blob_url + const fileName = file.filename.slice(pathPrefix.length) + const fileUrl = fileName.slice(0, fileName.lastIndexOf('.')) + + // get the file contents and decode them + // this script is called from the main branch, so we need the API call to get the contents from the branch, instead + const fileContents = await getContents( + context.repo.owner, + context.payload.repository.name, + // Can't get its content if it no longer exists. + // Meaning, you'd get a 404 on the `getContents()` utility function. + // So, to be able to get necessary meta data about what it *was*, + // if it was removed, fall back to the 'base'. + file.status === 'removed' + ? context.payload.pull_request.base.sha + : context.payload.pull_request.head.sha, + file.filename + ) + + // parse the frontmatter + const { data } = parse(fileContents) + + let contentCell = '' + let previewCell = '' + let prodCell = '' + + if (file.status === 'added') contentCell = 'New file: ' + else if (file.status === 'removed') contentCell = 'Removed: ' + contentCell += `[\`${fileName}\`](${sourceUrl})` + + try { + // the try/catch is needed because getApplicableVersions() returns either [] or throws an error when it can't parse the versions frontmatter + // try/catch can be removed if docs-engineering#1821 is resolved + // i.e. for feature based versioning, like ghae: 'issue-6337' + const fileVersions = getApplicableVersions(data.versions) + + for (const plan in allVersionShortnames) { + // plan is the shortName (i.e., fpt) + // allVersionShortNames[plan] is the planName (i.e., free-pro-team) + + // walk by the plan names since we generate links differently for most plans + const versions = fileVersions.filter((fileVersion) => + fileVersion.includes(allVersionShortnames[plan]) + ) + + if (versions.length === 1) { + // for fpt, ghec, and ghae + + if (versions.toString() === nonEnterpriseDefaultVersion) { + // omit version from fpt url + + previewCell += `[${plan}](${APP_URL}/${fileUrl})
` + prodCell += `[${plan}](${PROD_URL}/${fileUrl})
` + } else { + // for non-versioned releases (ghae, ghec) use full url + + previewCell += `[${plan}](${APP_URL}/${versions}/${fileUrl})
` + prodCell += `[${plan}](${PROD_URL}/${versions}/${fileUrl})
` + } + } else if (versions.length) { + // for ghes releases, link each version + + previewCell += `${plan}@ ` + prodCell += `${plan}@ ` + + versions.forEach((version) => { + previewCell += `[${version.split('@')[1]}](${APP_URL}/${version}/${fileUrl}) ` + prodCell += `[${version.split('@')[1]}](${PROD_URL}/${version}/${fileUrl}) ` + }) + previewCell += '
' + prodCell += '
' + } + } + } catch (e) { + console.error( + `Version information for ${file.filename} couldn't be determined from its frontmatter.` + ) + } + let note = '' + if (file.status === 'removed') { + note = 'removed' + // If the file was removed, the `previewCell` no longer makes sense + // since it was based on looking at the base sha. + previewCell = 'n/a' + } + + return `| ${contentCell} | ${previewCell} | ${prodCell} | ${note} |` + }) + ) + + // this section limits the size of the comment + const cappedLines = [] + let underMax = true + + lines.reduce((previous, current, index, array) => { + if (underMax) { + if (previous + current.length > MAX_COMMENT_SIZE) { + underMax = false + cappedLines.push('**Note** There are more changes in this PR than we can show.') + return previous + } + + cappedLines.push(array[index]) + return previous + current.length + } + return previous + }, markdownTable.length) + + markdownTable += cappedLines.join('\n') + + core.setOutput('changesTable', markdownTable) +} diff --git a/.github/actions-scripts/lib/wait-until-url-is-healthy.js b/.github/actions-scripts/lib/wait-until-url-is-healthy.js new file mode 100644 index 0000000000..6ed9b10581 --- /dev/null +++ b/.github/actions-scripts/lib/wait-until-url-is-healthy.js @@ -0,0 +1,34 @@ +import got from 'got' + +// Will try for 20 minutes, (15 * 80) seconds / 60 [seconds] +const RETRIES = 80 +const DELAY_SECONDS = 15 + +/* + * Promise resolves once url is healthy or fails if timeout has passed + * @param {string} url - path to server + * @param {string} [healthPath] - endpoint to health check, e.g. "healthz" + */ +export async function waitUntilUrlIsHealthy(url, healthPath = 'healthz') { + let attempt = 1 + while (attempt < RETRIES) { + try { + const res = await got.head(`${url}/${healthPath}`) + if (res.statusCode === 200) { + return true + } + } catch (err) {} + // Delay before next attempt + await sleep(DELAY_SECONDS) + attempt++ + } + return false +} + +/* + * Async-await sleep + * @param {string} seconds - Seconds to sleep + */ +export async function sleep(seconds) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) +} diff --git a/.github/workflows/content-changes-table-comment.yml b/.github/workflows/content-changes-table-comment.yml index 51fa2ce095..b3ee20b4a8 100644 --- a/.github/workflows/content-changes-table-comment.yml +++ b/.github/workflows/content-changes-table-comment.yml @@ -68,6 +68,7 @@ jobs: - name: Get changes table id: changes + timeout-minutes: 20 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APP_URL: ${{ env.APP_URL }}