diff --git a/.github/workflows/ready-for-doc-review.yml b/.github/workflows/ready-for-doc-review.yml index 66a8b0c3699f..c71c0c8fcaaf 100644 --- a/.github/workflows/ready-for-doc-review.yml +++ b/.github/workflows/ready-for-doc-review.yml @@ -1,6 +1,6 @@ name: Ready for docs-content review -# **What it does**: Adds pull requests in the docs-internal repository to the docs-content review board when the "ready-for-doc-review" label is added or when a review by docs-content or docs-reviewers is requested. Adds the "DIY docs" label to the PR if it is connected to a DIY docs issue in the docs-content repo. This workflow is also called as a reusable workflow from other repos including docs-content, docs-strategy, docs-early-access, and github. +# **What it does**: Adds pull requests in the docs-internal repository to the docs-content review board when the "ready-for-doc-review" label is added or when a review by docs-content or docs-reviewers is requested. This workflow is also called as a reusable workflow from other repos including docs-content, docs-strategy, docs-early-access, and github. # **Why we have it**: So that other GitHub teams can easily request reviews from the docs-content team, and so that writers can see when a PR is ready for review # **Who does it impact**: Writers who need to review docs-related PRs diff --git a/assets/images/site/evergreens/balsam.png b/assets/images/site/evergreens/balsam.png new file mode 100644 index 000000000000..88680dede3d7 Binary files /dev/null and b/assets/images/site/evergreens/balsam.png differ diff --git a/assets/images/site/evergreens/hinoki.png b/assets/images/site/evergreens/hinoki.png new file mode 100644 index 000000000000..69ac7584ca58 Binary files /dev/null and b/assets/images/site/evergreens/hinoki.png differ diff --git a/assets/images/site/evergreens/yew.png b/assets/images/site/evergreens/yew.png new file mode 100644 index 000000000000..10e06807efbc Binary files /dev/null and b/assets/images/site/evergreens/yew.png differ diff --git a/config/moda/deployment.yaml b/config/moda/deployment.yaml index c5799ad65f86..7c631ac7da5e 100644 --- a/config/moda/deployment.yaml +++ b/config/moda/deployment.yaml @@ -7,8 +7,20 @@ environments: profile: general region: iad - # 12 staging environments, evergreens only + # 15 staging environments, evergreens only # they should all contain the same configs + - name: staging-balsam + require_pipeline: false + notify_still_locked: true # Notify last person to lock this after an hour + secret_environment: production + required_review_tasks: [] + auto_deploy: true + skip_auto_merge: true + cluster_selector: + profile: general + region: iad + extra_completed_message: ':balsam: Review at https://docs-internal-staging-balsam.githubapp.com/' + - name: staging-boxwood require_pipeline: false notify_still_locked: true # Notify last person to lock this after an hour @@ -69,6 +81,18 @@ environments: region: iad extra_completed_message: ':hemlock: Review at https://docs-internal-staging-hemlock.githubapp.com/' + - name: staging-hinoki + require_pipeline: false + notify_still_locked: true # Notify last person to lock this after an hour + secret_environment: production + required_review_tasks: [] + auto_deploy: true + skip_auto_merge: true + cluster_selector: + profile: general + region: iad + extra_completed_message: ':hinoki: Review at https://docs-internal-staging-hinoki.githubapp.com/' + - name: staging-holly require_pipeline: false notify_still_locked: true # Notify last person to lock this after an hour @@ -153,6 +177,18 @@ environments: region: iad extra_completed_message: ':spruce: Review at https://docs-internal-staging-spruce.githubapp.com/' + - name: staging-yew + require_pipeline: false + notify_still_locked: true # Notify last person to lock this after an hour + secret_environment: production + required_review_tasks: [] + auto_deploy: true + skip_auto_merge: true + cluster_selector: + profile: general + region: iad + extra_completed_message: ':yew: Review at https://docs-internal-staging-yew.githubapp.com/' + required_builds: - docs-internal-moda-config-bundle / docs-internal-moda-config-bundle - docs-internal-docker-image / docs-internal-docker-image diff --git a/src/assets/scripts/find-orphaned-assets.ts b/src/assets/scripts/find-orphaned-assets.ts index 412c9152e264..d6c539766ab2 100755 --- a/src/assets/scripts/find-orphaned-assets.ts +++ b/src/assets/scripts/find-orphaned-assets.ts @@ -28,11 +28,13 @@ const EXCEPTIONS = new Set([ 'assets/images/site/apple-touch-icon-60x60.png', 'assets/images/site/apple-touch-icon-72x72.png', 'assets/images/site/apple-touch-icon-76x76.png', + 'assets/images/site/evergreens/balsam.png', 'assets/images/site/evergreens/boxwood.png', 'assets/images/site/evergreens/cedar.png', 'assets/images/site/evergreens/cypress.png', 'assets/images/site/evergreens/fir.png', 'assets/images/site/evergreens/hemlock.png', + 'assets/images/site/evergreens/hinoki.png', 'assets/images/site/evergreens/holly.png', 'assets/images/site/evergreens/juniper.png', 'assets/images/site/evergreens/laurel.png', @@ -40,6 +42,7 @@ const EXCEPTIONS = new Set([ 'assets/images/site/evergreens/redwood.png', 'assets/images/site/evergreens/sequoia.png', 'assets/images/site/evergreens/spruce.png', + 'assets/images/site/evergreens/yew.png', 'assets/images/social-cards/actions.png', 'assets/images/social-cards/copilot.png', 'assets/images/social-cards/default.png', diff --git a/src/codeql-cli/scripts/convert-markdown-for-docs.js b/src/codeql-cli/scripts/convert-markdown-for-docs.js deleted file mode 100644 index 2347c60c25b1..000000000000 --- a/src/codeql-cli/scripts/convert-markdown-for-docs.js +++ /dev/null @@ -1,267 +0,0 @@ -import { readFile } from 'fs/promises' -import path from 'path' - -import { fromMarkdown } from 'mdast-util-from-markdown' -import { toMarkdown } from 'mdast-util-to-markdown' -import { visitParents } from 'unist-util-visit-parents' -import { visit, SKIP } from 'unist-util-visit' -import { remove } from 'unist-util-remove' - -import { languageKeys } from '@/languages/lib/languages' -import { MARKDOWN_OPTIONS } from '../../content-linter/lib/helpers/unified-formatter-options' - -const { targetDirectory, removeKeywords } = JSON.parse( - await readFile(path.join('src/codeql-cli/lib/config.json'), 'utf-8'), -) -const RELATIVE_LINK_PATH = targetDirectory.replace('content', '') -const LAST_PRIMARY_HEADING = 'Primary options' -const HEADING_BEGIN = '::: {.option}\n' -const END_SECTION = '\n:::' -const PROGRAM_SECTION = '::: {.program}\n' - -// Updates several properties of the Markdown file using the AST -export async function convertContentToDocs( - content, - frontmatterDefaults = {}, - currentFileName = '', -) { - const ast = fromMarkdown(content) - - let depth = 0 - let secondaryOptions = false - const frontmatter = { title: '', ...frontmatterDefaults } - const akaMsLinkMatches = [] - - // Visit all heading nodes - const headingMatcher = (node) => node.type === 'heading' - visit(ast, headingMatcher, (node) => { - // This is the title of the article, so we want to store it to - // the frontmatter - if (node.depth === 1) { - frontmatter.title = node.children[0].value - } - - // There are some headings that include a title followed by - // some markup that looks like - // {#options-to-configure-the-package-manager.} - if (node.children[0].value.includes('{#')) { - node.children[0].value = node.children[0].value.split('{#')[0].trim() - } - - // This is a workaround for the secondary options that are at the - // wrong heading level in the source rst files. Everything after the - // headings "Synopsis", "Description", and "Options" should be - // one level higher in Markdown. - if (secondaryOptions) { - node.depth = node.depth - 1 - } - - // This needs to be assigned after node.depth is modified above - depth = node.depth - if (node.children[0].value === LAST_PRIMARY_HEADING && node.children[0].type === 'text') { - secondaryOptions = true - } - }) - - // Visit heading and paragraph nodes to get intro text - const descriptionMatcher = (node) => node.type === 'heading' || node.type === 'paragraph' - let currentNodeIsDescription = false - visit(ast, descriptionMatcher, (node) => { - // The first paragraph sibling to the heading "Description" is the - // node that contains the first string of the description text. We - // want to use that first string as the intro frontmatter - if (node.children[0].value === 'Description' && node.children[0].type === 'text') { - currentNodeIsDescription = true - } - if (currentNodeIsDescription && node.type === 'paragraph') { - frontmatter.intro = node.children[0].value - currentNodeIsDescription = false - return SKIP - } - }) - - // Modify the text, code, and link nodes - const matchNodeTypes = ['text', 'code', 'link'] - const matcher = (node) => node && matchNodeTypes.includes(node.type) - visitParents(ast, matcher, (node, ancestors) => { - // Add the copy button to the example command - if (node.type === 'code' && node.value.startsWith(`codeql ${frontmatter.title}`)) { - node.lang = 'shell' - node.meta = 'copy' - } - - // This is the beginning of a secondary options section. For example, - // "Output format options." The rst file doesn't have a heading level - // for these, so we want to make it a Markdown heading at one level - // higher than the previous heading (which is a level lower than Options) - if (node.type === 'text' && node.value && node.value.includes(HEADING_BEGIN)) { - node.value = node.value.replace(HEADING_BEGIN, '') - // Ancestors are ordered from the furthest away (root) to the closest. - // Make the text node's parent a heading node. - ancestors[ancestors.length - 1].type = 'heading' - ancestors[ancestors.length - 1].depth = depth + 1 - } - - // There are some keywords like [Plumbing] used by the code comments - // but we don't want to render them in the docs. - if (node.type === 'text' && node.value) { - removeKeywords.forEach((keyword) => { - if (node.value.includes(keyword)) { - node.value = node.value.replace(keyword, '').trim() - } - }) - } - - // The subsections under the main headings (level 2) are commands - // and start with either `-` or `<`. We want to make these inline code - // instead of text. - if ( - node.type === 'text' && - ancestors[ancestors.length - 1].type === 'heading' && - (node.value.startsWith('-') || node.value.startsWith('<')) - ) { - node.type = 'inlineCode' - } - - // Removes the strings that denote the end of an options sections. These - // strings were added during the pandoc conversion. - if (node.type === 'text' && node.value && node.value.includes(END_SECTION)) { - node.value = node.value.replace(END_SECTION, '') - } - - // These are links to other CodeQL CLI docs. We want to convert them to - // Markdown links. Pandoc converts the rst links to a format that - // looks like this: - // `codeql test run`{.interpreted-text role=\"doc\"} - // Link title: codeql test run - // Relative path: test-run - // And the rest can be removed. - // The inline code tag `codeql test run` is one node and the - // string {.interpreted-text role=\"doc\"} is another node. - if (node.type === 'text' && node.value.includes('{.interpreted-text')) { - const paragraph = ancestors[ancestors.length - 1].children - const docRoleTagChild = paragraph.findIndex( - (child) => child.value && child.value.includes('{.interpreted-text'), - ) - const link = paragraph[docRoleTagChild - 1] - // If child node is already a link node, skip it - if (link.type === 'link') { - return - } - // Currently, this applies to the Markdown files generated by Pandoc, - // but it may not always be the case. If we find an exception to this - // rule, we may need to modify this code to handle it. - if (link.type !== 'inlineCode') { - throw new Error( - 'Unexpected node type. The node before a text node with {.interpreted-text role="doc"} should be an inline code or link node.', - ) - } - - // Sometimes there are newline characters in the middle of the title - // or in the link path. We want to remove those. - const linkText = link.value.split('<')[0].replace(/\n/g, ' ').trim() - const linkPath = link.value.split('<')[1].split('>')[0].replace(/'\n/g, '').trim() - - // Remove the string {.interpreted-text role="doc"} from this node - node.value = node.value.replace(/\n/g, ' ').replace('{.interpreted-text role="doc"}', '') - - // Check for circular links - if the link points to the same file we're processing - const currentFileBaseName = currentFileName.replace('.md', '') - if (currentFileBaseName && linkPath === currentFileBaseName) { - // Convert circular link to plain text instead of creating a link - link.type = 'text' - link.value = linkText - } else { - // Make the previous sibling node a link - link.type = 'link' - link.url = `${RELATIVE_LINK_PATH}/${linkPath}` - link.children = [{ type: 'text', value: linkText }] - delete link.value - } - } - - // Save any nodes that contain aka.ms links so we can convert them later - if (node.type === 'link' && node.url.includes('aka.ms')) { - akaMsLinkMatches.push(node) - } - - // There are example links in the format https://containers.GHEHOSTNAME - // that we don't want our link checker to check so we need to make them - // inline code instead of links. Ideally, this should be done in the - // Java program that generates the rst files, but we can do it here for now. - // See https://github.com/syntax-tree/mdast#inlinecode - if (node.type === 'link' && node.url.startsWith('https://containers')) { - // The nodes before and after contain double quotes that we want to remove - const nodeBefore = ancestors[ancestors.length - 1].children[0] - const nodeAfter = ancestors[ancestors.length - 1].children[2] - if (nodeBefore.value.endsWith('"')) { - nodeBefore.value = nodeBefore.value.slice(0, -1) - } - if (nodeAfter.value.startsWith('"')) { - nodeAfter.value = nodeAfter.value.slice(1) - } - // Change the node to an inline code node - node.type = 'inlineCode' - node.value = node.url - node.title = undefined - node.url = undefined - node.children = undefined - } - }) - - // Convert all aka.ms links to the docs.github.com relative path - await Promise.all( - akaMsLinkMatches.map(async (node) => { - const url = await getRedirect(node.url) - // The aka.ms urls are Markdown links in the ast already, - // so we only need to update the url and description - // rewrite the aka.ms link - node.children[0].value = 'AUTOTITLE' - node.url = url - }), - ) - - // remove the program section from the AST - remove(ast, (node) => node.value && node.value.startsWith(PROGRAM_SECTION)) - // remove the first heading from the AST because that becomes frontmatter - remove(ast, (node) => node.type === 'heading' && node.depth === 1) - - return { content: toMarkdown(ast, MARKDOWN_OPTIONS), data: frontmatter } -} - -// performs a get request for a aka.ms url and returns the redirect url -async function getRedirect(url) { - let response = null - try { - response = await fetch(url, { redirect: 'manual' }) - if (!response.ok && response.status !== 301 && response.status !== 302) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - } catch (error) { - console.error(error) - const errorMsg = `Failed to get redirect for ${url} when converting aka.ms links to docs.github.com links.` - throw new Error(errorMsg) - } - - // Get the redirect location from the response header - const redirectLocation = response.headers.get('location') - if (!redirectLocation) { - throw new Error(`No redirect location found for ${url}`) - } - - // Parse the URL to get the pathname - const redirect = new URL(redirectLocation).pathname - - // Some of the aka.ms links have the /en language prefix. - // This removes all language prefixes from the redirect url. - const redirectNoLang = languageKeys.reduce((acc, lang) => { - return acc.replace(`/${lang}`, ``) - }, redirect) - - if (!redirectNoLang) { - const errorMsg = `The aka.ms redirected to an unexpected url: ${url}` - throw new Error(errorMsg) - } - - return redirectNoLang -} diff --git a/src/codeql-cli/scripts/convert-markdown-for-docs.ts b/src/codeql-cli/scripts/convert-markdown-for-docs.ts new file mode 100644 index 000000000000..316116daa10b --- /dev/null +++ b/src/codeql-cli/scripts/convert-markdown-for-docs.ts @@ -0,0 +1,295 @@ +import { readFile } from 'fs/promises' +import path from 'path' + +import { fromMarkdown } from 'mdast-util-from-markdown' +import { toMarkdown } from 'mdast-util-to-markdown' +import { visitParents } from 'unist-util-visit-parents' +import { visit, SKIP } from 'unist-util-visit' +import { remove } from 'unist-util-remove' + +import { languageKeys } from '@/languages/lib/languages' +import { MARKDOWN_OPTIONS } from '../../content-linter/lib/helpers/unified-formatter-options' + +interface Config { + targetDirectory: string + removeKeywords: string[] +} + +interface FrontmatterDefaults { + [key: string]: string +} + +interface Frontmatter { + title: string + intro?: string + [key: string]: string | undefined +} + +interface ConversionResult { + content: string + data: Frontmatter +} + +const config: Config = JSON.parse( + await readFile(path.join('src/codeql-cli/lib/config.json'), 'utf-8'), +) +const { targetDirectory, removeKeywords } = config +const RELATIVE_LINK_PATH = targetDirectory.replace('content', '') +const LAST_PRIMARY_HEADING = 'Primary options' +const HEADING_BEGIN = '::: {.option}\n' +const END_SECTION = '\n:::' +const PROGRAM_SECTION = '::: {.program}\n' + +// Updates several properties of the Markdown file using the AST +export async function convertContentToDocs( + content: string, + frontmatterDefaults: FrontmatterDefaults = {}, + currentFileName = '', +): Promise { + const ast = fromMarkdown(content) + + let depth = 0 + let secondaryOptions = false + const frontmatter: Frontmatter = { title: '', ...frontmatterDefaults } + const akaMsLinkMatches: any[] = [] + + // Visit all heading nodes + visit(ast, 'heading', (node: any) => { + // This is the title of the article, so we want to store it to + // the frontmatter + if (node.depth === 1) { + frontmatter.title = node.children[0].value + } + + // There are some headings that include a title followed by + // some markup that looks like + // {#options-to-configure-the-package-manager.} + if (node.children[0].value.includes('{#')) { + node.children[0].value = node.children[0].value.split('{#')[0].trim() + } + + // This is a workaround for the secondary options that are at the + // wrong heading level in the source rst files. Everything after the + // headings "Synopsis", "Description", and "Options" should be + // one level higher in Markdown. + if (secondaryOptions) { + node.depth = Math.max(1, Math.min(6, node.depth - 1)) + } + + // This needs to be assigned after node.depth is modified above + depth = node.depth + if (node.children[0].value === LAST_PRIMARY_HEADING && node.children[0].type === 'text') { + secondaryOptions = true + } + }) + + // Visit heading and paragraph nodes to get intro text + let currentNodeIsDescription = false + visit(ast, (node: any) => { + if (node.type !== 'heading' && node.type !== 'paragraph') return false + + // The first paragraph sibling to the heading "Description" is the + // node that contains the first string of the description text. We + // want to use that first string as the intro frontmatter + if (node.children[0]?.value === 'Description' && node.children[0]?.type === 'text') { + currentNodeIsDescription = true + } + if (currentNodeIsDescription && node.type === 'paragraph') { + frontmatter.intro = node.children[0]?.value + currentNodeIsDescription = false + return SKIP + } + }) + + // Modify the text, code, and link nodes + const matchNodeTypes = ['text', 'code', 'link'] + visitParents( + ast, + (node: any) => { + return node && matchNodeTypes.includes(node.type) + }, + (node: any, ancestors: any[]) => { + // Add the copy button to the example command + if (node.type === 'code' && node.value.startsWith(`codeql ${frontmatter.title}`)) { + node.lang = 'shell' + node.meta = 'copy' + } + + // This is the beginning of a secondary options section. For example, + // "Output format options." The rst file doesn't have a heading level + // for these, so we want to make it a Markdown heading at one level + // higher than the previous heading (which is a level lower than Options) + if (node.type === 'text' && node.value && node.value.includes(HEADING_BEGIN)) { + node.value = node.value.replace(HEADING_BEGIN, '') + // Ancestors are ordered from the furthest away (root) to the closest. + // Make the text node's parent a heading node. + ancestors[ancestors.length - 1].type = 'heading' + ancestors[ancestors.length - 1].depth = Math.max(1, Math.min(6, depth + 1)) + } + + // There are some keywords like [Plumbing] used by the code comments + // but we don't want to render them in the docs. + if (node.type === 'text' && node.value) { + removeKeywords.forEach((keyword) => { + if (node.value.includes(keyword)) { + node.value = node.value.replace(keyword, '').trim() + } + }) + } + + // The subsections under the main headings (level 2) are commands + // and start with either `-` or `<`. We want to make these inline code + // instead of text. + if ( + node.type === 'text' && + ancestors[ancestors.length - 1].type === 'heading' && + (node.value.startsWith('-') || node.value.startsWith('<')) + ) { + node.type = 'inlineCode' + } + + // Removes the strings that denote the end of an options sections. These + // strings were added during the pandoc conversion. + if (node.type === 'text' && node.value && node.value.includes(END_SECTION)) { + node.value = node.value.replace(END_SECTION, '') + } + + // These are links to other CodeQL CLI docs. We want to convert them to + // Markdown links. Pandoc converts the rst links to a format that + // looks like this: + // `codeql test run`{.interpreted-text role=\"doc\"} + // Link title: codeql test run + // Relative path: test-run + // And the rest can be removed. + // The inline code tag `codeql test run` is one node and the + // string {.interpreted-text role=\"doc\"} is another node. + if (node.type === 'text' && node.value.includes('{.interpreted-text')) { + const paragraph = ancestors[ancestors.length - 1].children + const docRoleTagChild = paragraph.findIndex( + (child: any) => child.value && child.value.includes('{.interpreted-text'), + ) + const link = paragraph[docRoleTagChild - 1] + // If child node is already a link node, skip it + if (link.type === 'link') { + return + } + // Currently, this applies to the Markdown files generated by Pandoc, + // but it may not always be the case. If we find an exception to this + // rule, we may need to modify this code to handle it. + if (link.type !== 'inlineCode') { + throw new Error( + 'Unexpected node type. The node before a text node with {.interpreted-text role="doc"} should be an inline code or link node.', + ) + } + + // Sometimes there are newline characters in the middle of the title + // or in the link path. We want to remove those. + const linkText = link.value.split('<')[0].replace(/\n/g, ' ').trim() + const linkPath = link.value.split('<')[1].split('>')[0].replace(/'\n/g, '').trim() + + // Remove the string {.interpreted-text role="doc"} from this node + node.value = node.value.replace(/\n/g, ' ').replace('{.interpreted-text role="doc"}', '') + + // Check for circular links - if the link points to the same file we're processing + const currentFileBaseName = currentFileName.replace('.md', '') + if (currentFileBaseName && linkPath === currentFileBaseName) { + // Convert circular link to plain text instead of creating a link + link.type = 'text' + link.value = linkText + } else { + // Make the previous sibling node a link + link.type = 'link' + link.url = `${RELATIVE_LINK_PATH}/${linkPath}` + link.children = [{ type: 'text', value: linkText }] + delete link.value + } + } + + // Save any nodes that contain aka.ms links so we can convert them later + if (node.type === 'link' && node.url.includes('aka.ms')) { + akaMsLinkMatches.push(node) + } + + // There are example links in the format https://containers.GHEHOSTNAME + // that we don't want our link checker to check so we need to make them + // inline code instead of links. Ideally, this should be done in the + // Java program that generates the rst files, but we can do it here for now. + // See https://github.com/syntax-tree/mdast#inlinecode + if (node.type === 'link' && node.url.startsWith('https://containers')) { + // The nodes before and after contain double quotes that we want to remove + const nodeBefore = ancestors[ancestors.length - 1].children[0] + const nodeAfter = ancestors[ancestors.length - 1].children[2] + if (nodeBefore.value && nodeBefore.value.endsWith('"')) { + nodeBefore.value = nodeBefore.value.slice(0, -1) + } + if (nodeAfter.value && nodeAfter.value.startsWith('"')) { + nodeAfter.value = nodeAfter.value.slice(1) + } + // Change the node to an inline code node + node.type = 'inlineCode' + node.value = node.url + node.title = undefined + node.url = undefined + node.children = undefined + } + }, + ) + + // Convert all aka.ms links to the docs.github.com relative path + await Promise.all( + akaMsLinkMatches.map(async (node: any) => { + const url = await getRedirect(node.url) + // The aka.ms urls are Markdown links in the ast already, + // so we only need to update the url and description + // rewrite the aka.ms link + if (node.children[0]) { + node.children[0].value = 'AUTOTITLE' + } + node.url = url + }), + ) + + // remove the program section from the AST + remove(ast, (node: any) => node.value && node.value.startsWith(PROGRAM_SECTION)) + // remove the first heading from the AST because that becomes frontmatter + remove(ast, (node: any) => node.type === 'heading' && node.depth === 1) + + return { content: toMarkdown(ast, MARKDOWN_OPTIONS as any), data: frontmatter } +} + +// performs a get request for a aka.ms url and returns the redirect url +async function getRedirect(url: string): Promise { + let response: Response + try { + response = await fetch(url, { redirect: 'manual' }) + if (!response.ok && response.status !== 301 && response.status !== 302) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + } catch (error) { + console.error(error) + const errorMsg = `Failed to get redirect for ${url} when converting aka.ms links to docs.github.com links.` + throw new Error(errorMsg) + } + + // Get the redirect location from the response header + const redirectLocation = response.headers.get('location') + if (!redirectLocation) { + throw new Error(`No redirect location found for ${url}`) + } + + // Parse the URL to get the pathname + const redirect = new URL(redirectLocation).pathname + + // Some of the aka.ms links have the /en language prefix. + // This removes all language prefixes from the redirect url. + const redirectNoLang = languageKeys.reduce((acc, lang) => { + return acc.replace(`/${lang}`, ``) + }, redirect) + + if (!redirectNoLang) { + const errorMsg = `The aka.ms redirected to an unexpected url: ${url}` + throw new Error(errorMsg) + } + + return redirectNoLang +} diff --git a/src/codeql-cli/tests/test-circular-links.js b/src/codeql-cli/tests/test-circular-links.ts similarity index 95% rename from src/codeql-cli/tests/test-circular-links.js rename to src/codeql-cli/tests/test-circular-links.ts index 140764eb9332..d8a2def89dfc 100644 --- a/src/codeql-cli/tests/test-circular-links.js +++ b/src/codeql-cli/tests/test-circular-links.ts @@ -20,7 +20,7 @@ This option has no effect when passed to \`codeql bqrs interpret For more information, see \`codeql database analyze\`{.interpreted-text role="doc"}. ` -async function testCircularLinkFix() { +async function testCircularLinkFix(): Promise { console.log('Testing circular link fix...') try { @@ -65,7 +65,7 @@ async function testCircularLinkFix() { } } -async function testEdgeCases() { +async function testEdgeCases(): Promise { console.log('\nTesting edge cases...') // Test with no filename (should not crash) @@ -96,7 +96,7 @@ async function testEdgeCases() { } // Run all tests -async function runAllTests() { +async function runAllTests(): Promise { const test1 = await testCircularLinkFix() const test2 = await testEdgeCases() diff --git a/src/frame/pages/app.tsx b/src/frame/pages/app.tsx index 4a43f464c94d..a8fbed21a39e 100644 --- a/src/frame/pages/app.tsx +++ b/src/frame/pages/app.tsx @@ -26,11 +26,13 @@ type MyAppProps = AppProps & { } const stagingNames = new Set([ + 'balsam', 'boxwood', 'cedar', 'cypress', 'fir', 'hemlock', + 'hinoki', 'holly', 'juniper', 'laurel', @@ -38,6 +40,7 @@ const stagingNames = new Set([ 'redwood', 'sequoia', 'spruce', + 'yew', ]) function getFaviconHref(stagingName?: string) {