Post PR annotations on failing AUTOTITLE errors (#45129)
Co-authored-by: Rachael Sewell <rachmari@github.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { distance } from 'fastest-levenshtein'
|
||||
import { getPathWithoutLanguage, getVersionStringFromPath } from '#src/frame/lib/path-utils.js'
|
||||
@@ -13,9 +16,65 @@ import findPage from '#src/frame/lib/find-page.js'
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
// This way, if you *set* the `LOG_ERROR_ANNOTATIONS` env var, whatever its
|
||||
// value is, it determines it. But if it's not set, the default is to look
|
||||
// for a truty value in `process.env.CI`.
|
||||
const CI = Boolean(JSON.parse(process.env.CI || 'false'))
|
||||
const LOG_ERROR_ANNOTATIONS =
|
||||
CI || Boolean(JSON.parse(process.env.LOG_ERROR_ANNOTATIONS || 'false'))
|
||||
|
||||
const supportedPlans = new Set(Object.values(allVersions).map((v) => v.plan))
|
||||
const externalRedirects = readJsonFile('./src/redirects/lib/external-sites.json')
|
||||
|
||||
// The reason we "memoize" which lines we've logged is because the same
|
||||
// error might happen more than once in the whole space of one CI run.
|
||||
const _logged = new Set()
|
||||
|
||||
// Printing this to stdout in this format, will automatically be picked up
|
||||
// by Actions to turn that into a PR inline annotation.
|
||||
function logError(file, line, message, title = 'Error') {
|
||||
if (LOG_ERROR_ANNOTATIONS) {
|
||||
const hash = `${file}:${line}:${message}`
|
||||
if (_logged.has(hash)) return
|
||||
_logged.add(hash)
|
||||
message = stripAnsi(
|
||||
// copied from: https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
|
||||
message.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'),
|
||||
)
|
||||
const error = `::error file=${file},line=${line},title=${title}::${message}`
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
function getFrontmatterOffset(filePath) {
|
||||
const rawContent = fs.readFileSync(filePath, 'utf-8')
|
||||
let delimiters = 0
|
||||
let count = 0
|
||||
// The frontmatter is wedged between two `---` lines. But the content
|
||||
// doesn't necessarily start after the second `---`. If the `.md` file looks
|
||||
// like this:
|
||||
//
|
||||
// 1) ---
|
||||
// 2) title: Foo
|
||||
// 3) ---
|
||||
// 4)
|
||||
// 5) # Introduction
|
||||
// 6) Bla bla
|
||||
//
|
||||
// Then line one of the *content* that is processed, starts at line 5.
|
||||
// because after the matter and content is separated, the content portion
|
||||
// is whitespace trimmed.
|
||||
for (const line of rawContent.split(/\n/g)) {
|
||||
count++
|
||||
if (line === '---') {
|
||||
delimiters++
|
||||
} else if (delimiters === 2 && line) {
|
||||
return count
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Meaning it can be 'AUTOTITLE ' or ' AUTOTITLE' or 'AUTOTITLE'
|
||||
const AUTOTITLE = /^\s*AUTOTITLE\s*$/
|
||||
|
||||
@@ -57,12 +116,17 @@ export default function rewriteLocalLinks(context) {
|
||||
// in English, but all AUTOTITLE links in the current language.
|
||||
const newHref = getNewHref(node, autotitleLanguage || currentLanguage, currentVersion)
|
||||
if (newHref) {
|
||||
node.properties._originalHref = node.properties.href
|
||||
node.properties.href = newHref
|
||||
}
|
||||
for (const child of node.children) {
|
||||
if (child.value) {
|
||||
if (AUTOTITLE.test(child.value)) {
|
||||
nodes.push({ href: node.properties.href, child })
|
||||
nodes.push({
|
||||
href: node.properties.href,
|
||||
child,
|
||||
originalHref: node.properties._originalHref,
|
||||
})
|
||||
} else if (
|
||||
// This means CI and local dev
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
@@ -99,18 +163,28 @@ export default function rewriteLocalLinks(context) {
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(nodes.map(({ href, child }) => getNewTitleSetter(child, href, context)))
|
||||
await Promise.all(
|
||||
nodes.map(({ href, child, originalHref }) =>
|
||||
getNewTitleSetter(child, href, context, originalHref),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function getNewTitleSetter(child, href, context) {
|
||||
child.value = await getNewTitle(href, context)
|
||||
async function getNewTitleSetter(child, href, context, originalHref) {
|
||||
child.value = await getNewTitle(href, context, child, originalHref)
|
||||
}
|
||||
|
||||
async function getNewTitle(href, context) {
|
||||
async function getNewTitle(href, context, child, originalHref) {
|
||||
const page = findPage(href, context.pages, context.redirects)
|
||||
if (!page) {
|
||||
throw new TitleFromAutotitleError(`Unable to find Page by href '${href}'`)
|
||||
const line = child.position.start.line + getFrontmatterOffset(context.page.fullPath)
|
||||
const message = `Unable to find Page by '${originalHref || href}'.
|
||||
To fix it, look at ${
|
||||
context.page.fullPath
|
||||
} on line ${line} and see if the link is correct and active.`
|
||||
logError(context.page.fullPath, line, message)
|
||||
throw new TitleFromAutotitleError(message)
|
||||
}
|
||||
return await page.renderProp('title', context, { textOnly: true })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user