#!/usr/bin/env node import * as github from '@actions/github' 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 if (!GITHUB_TOKEN) { throw new Error(`GITHUB_TOKEN environment variable not set`) } if (!APP_URL) { throw new Error(`APP_URL environment variable not set`) } // the max size of the comment (in bytes) // the action we use to post the comment caps out at about 144kb // see docs-engineering#1849 for more info const MAX_COMMENT_SIZE = 125000 const PROD_URL = 'https://docs.github.com' // When this file is invoked directly from action as opposed to being imported if (import.meta.url.endsWith(process.argv[1])) { const owner = context.repo.owner const repo = context.payload.repository.name const baseSHA = context.payload.pull_request.base.sha const headSHA = context.payload.pull_request.head.sha const isHealthy = await waitUntilUrlIsHealthy(new URL('/healthz', APP_URL).toString()) if (!isHealthy) { core.setFailed(`Timeout waiting for preview environment: ${APP_URL}`) } else { const markdownTable = await main(owner, repo, baseSHA, headSHA) core.setOutput('changesTable', markdownTable) } } async function main(owner, repo, baseSHA, headSHA) { const octokit = github.getOctokit(GITHUB_TOKEN) // get the list of file changes from the PR const response = await octokit.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${baseSHA}...${headSHA}`, }) const { files } = response.data const markdownTableHead = [ '| **Source** | **Preview** | **Production** | **What Changed** |', '|:----------- |:----------- |:----------- |:----------- |', ] let markdownTable = '' const pathPrefix = 'content/' const articleFiles = files.filter( ({ filename }) => filename.startsWith(pathPrefix) && filename.toLowerCase() !== 'readme.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.replace('/index.md', '').replace(/\.md$/, '') // 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( owner, repo, // 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' ? baseSHA : headSHA, 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) if (cappedLines.length) { cappedLines.unshift(...markdownTableHead) } markdownTable += cappedLines.join('\n') return markdownTable } export default main