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/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/creating-templates.md b/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/creating-templates.md new file mode 100644 index 000000000000..70d9b57176b1 --- /dev/null +++ b/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/creating-templates.md @@ -0,0 +1,150 @@ +--- +title: Creating templates +shortTitle: Create templates +intro: '{% data variables.copilot.copilot_chat_short %} can help you create templates to streamline your workflow and ensure consistency across your projects.' +versions: + feature: copilot +category: + - Communicate effectively +complexity: + - Simple +octicon: copilot +topics: + - Copilot +contentType: tutorials +--- + +{% data variables.copilot.copilot_chat %} can help you create templates for various purposes, such as project documentation, code snippets, or issue tracking. By using templates, you can streamline your workflow and ensure consistency across your projects. + +## Example scenario + +You want to create a template for a project README file that includes sections for project description, installation instructions, usage examples, and contribution guidelines. You can ask {% data variables.copilot.copilot_chat_short %} to generate a README template based on best practices and your specific requirements. You can then use this template as a starting point for all your project documentation. + +## Example prompt + +```text copy +Create a README template for a new open-source project that includes sections for project description, installation instructions, usage examples, and contribution guidelines. Make it clear and easy to follow. +``` + +## Example response + +{% data reusables.copilot.example-prompts.response-is-an-example %} + +{% data variables.product.prodname_copilot_short %} will provide you with a structured README template. You can copy the template {% data variables.copilot.copilot_chat_short %} provides and customize it further to fit your project's needs. + +````markdown +# Project Name + +> **Short description:** +> A brief summary of what your project does, its main features, and its purpose. + +--- + +## Table of Contents + +- [Description](#description) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) +- [Contact](#contact) + +--- + +## Description + +Provide a detailed explanation of what your project does, why it exists, and any important background information. +You may want to include: +- Key features +- Supported platforms or languages +- Screenshots or diagrams + +--- + +## Installation + +1. **Clone the repository** + ```sh + git clone https://github.com/your-username/your-repo-name.git + cd your-repo-name + ``` + +2. **Install dependencies** + ```sh + # Example for Node.js projects + npm install + + # Example for Python projects + pip install -r requirements.txt + ``` + +3. **Additional setup** + - List any other required steps (e.g., environment variables, configuration files) + +--- + +## Usage + +Provide clear usage instructions and code examples. + +```sh +# Example command +your-command --option value +``` + +```python +# Example in Python +import your_module + +result = your_module.do_something() +print(result) +``` + +- Explain any CLI options, configuration, or parameters. +- Link to more detailed documentation if available. + +--- + +## Contributing + +Contributions are welcome! Please follow these guidelines: + +1. **Fork the repository** +2. **Create a new branch** + ```sh + git checkout -b feature/your-feature-name + ``` +3. **Make your changes** +4. **Test your changes** +5. **Commit and push** + ```sh + git commit -m "Add feature: your-feature-name" + git push origin feature/your-feature-name + ``` +6. **Open a pull request** + +**Code of Conduct:** +Please be respectful and follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +**Issues & Suggestions:** +Open an issue for bugs, questions, or feature requests. + +--- + +## License + +This project is licensed under the [INSERT LICENSE](link to relevant license information). + +--- + +## Contact + +- **Maintainer:** [Your Name](mailto:your@email.com) +- **Project Link:** https://github.com/your-username/your-repo-name + +--- +```` + +## Further reading + +{% data reusables.copilot.example-prompts.further-reading-items %} diff --git a/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/index.md b/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/index.md index 5b426c7da737..9cd0fa79b2b0 100644 --- a/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/index.md +++ b/content/copilot/tutorials/copilot-chat-cookbook/communicate-effectively/index.md @@ -7,6 +7,7 @@ topics: - Copilot children: - /creating-diagrams + - /creating-templates - /extracting-information - /synthesizing-research contentType: tutorials 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) {