diff --git a/.github/workflows/sync-openapi.yml b/.github/workflows/sync-openapi.yml index 0c6e5d838de5..941df7895c28 100644 --- a/.github/workflows/sync-openapi.yml +++ b/.github/workflows/sync-openapi.yml @@ -85,7 +85,7 @@ jobs: # If nothing to commit, exit now. It's fine. No orphans. changes=$(git diff --name-only | wc -l) if [[ $changes -eq 0 ]]; then - echo "There are no changes to commit after running `npm run sync-rest` Exiting..." + echo "There are no changes to commit after running 'npm run sync-rest'. Exiting..." exit 0 fi diff --git a/content/billing/reference/actions-minute-multipliers.md b/content/billing/reference/actions-minute-multipliers.md deleted file mode 100644 index 15c83973ba2e..000000000000 --- a/content/billing/reference/actions-minute-multipliers.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Actions minute multiplier reference -shortTitle: Actions minute multipliers -intro: 'Reference information for calculating the cost of using different {% data variables.product.github %}-hosted runners.' -versions: - fpt: '*' - ghec: '*' - ghes: '*' -topics: - - Billing - - Actions -contentType: reference ---- - -{% data variables.product.github %} rounds the minutes and partial minutes each job uses up to the nearest whole minute. - -## Standard runners - -| Operating system | Per-minute rate (USD) | -|---------------------------------------| ----------------------| -| Linux 1-core | $0.002 | -| Linux 2-core | $0.008 | -| Windows 2-core | $0.016 | -| macOS 3-core or 4-core (M1 or Intel) | $0.08 | - -## x64-powered {% data variables.actions.hosted_runners %} - -| Operating system | Per-minute rate (USD) | -|------------------------| ----------------------| -| Linux Advanced 2-core | $0.008 | -| Linux 4-core | $0.016 | -| Linux 8-core | $0.032 | -| Linux 16-core | $0.064 | -| Linux 32-core | $0.128 | -| Linux 64-core | $0.256 | -| Linux 96-core | $0.384 | -| Windows 4-core | $0.032 | -| Windows 8-core | $0.064 | -| Windows 16-core | $0.128 | -| Windows 32-core | $0.256 | -| Windows 64-core | $0.512 | -| Windows 96-core | $0.768 | -| macOS 12-core | $0.12 | - -## arm64-powered {% data variables.actions.hosted_runners %} - -| Operating system | Per-minute rate (USD) | -|---------------------| -----------| -| Linux 2-core | $0.005 | -| Linux 4-core | $0.01 | -| Linux 8-core | $0.02 | -| Linux 16-core | $0.04 | -| Linux 32-core | $0.08 | -| Linux 64-core | $0.16 | -| Windows 2-core | $0.01 | -| Windows 4-core | $0.02 | -| Windows 8-core | $0.04 | -| Windows 16-core | $0.08 | -| Windows 32-core | $0.16 | -| Windows 64-core | $0.32 | -| macOS 6-core (M1) | $0.16 | - -## GPU-powered {% data variables.actions.hosted_runners %} - -| Operating system | Per-minute rate (USD) | -|---------------------| -----------| -| Linux 4-core | $0.07 | -| Windows 4-core | $0.14 | - -## Points to note about rates for runners - -* The number of jobs you can run concurrently across all repositories in your user or organization account depends on your {% data variables.product.github %} plan. For more information, see [AUTOTITLE](/actions/learn-github-actions/usage-limits-billing-and-administration) for {% data variables.product.github %}-hosted runners and [AUTOTITLE](/actions/hosting-your-own-runners/managing-self-hosted-runners/usage-limits-for-self-hosted-runners) for self-hosted runner usage limits. -* {% data reusables.actions.larger-runner-permissions %} -* {% data reusables.actions.about-larger-runners-billing %} -* For {% data variables.actions.hosted_runner %}s, there is no additional cost for configurations that assign public static IP addresses to a {% data variables.actions.hosted_runner %}. For more information on {% data variables.actions.hosted_runner %}s, see [AUTOTITLE](/actions/using-github-hosted-runners/using-larger-runners/about-larger-runners). -* Included minutes cannot be used for {% data variables.actions.hosted_runner %}s. -* The {% data variables.actions.hosted_runner %}s are not free for public repositories. -* Custom images can only be used with larger runners and are billed at the same per-minute rates as those runners. diff --git a/content/billing/reference/actions-runner-pricing.md b/content/billing/reference/actions-runner-pricing.md new file mode 100644 index 000000000000..f89a247b2ca1 --- /dev/null +++ b/content/billing/reference/actions-runner-pricing.md @@ -0,0 +1,80 @@ +--- +title: Actions runner pricing +shortTitle: Actions runner pricing +intro: Reference information for calculating the cost of using different {% data variables.product.github %}-hosted runners. +versions: + fpt: "*" + ghec: "*" + ghes: "*" +topics: + - Billing + - Actions +contentType: reference +redirect_from: + - /billing/reference/actions-minute-multipliers +--- + +{% data variables.product.github %} rounds the minutes and partial minutes each job uses up to the nearest whole minute. + +## Standard runners + +| Operating system | Per-minute rate (USD) | +| ------------------------------------ | --------------------- | +| Linux 1-core | $0.002 | +| Linux 2-core | $0.008 | +| Windows 2-core | $0.016 | +| macOS 3-core or 4-core (M1 or Intel) | $0.08 | + +## x64-powered {% data variables.actions.hosted_runners %} + +| Operating system | Per-minute rate (USD) | +| --------------------- | --------------------- | +| Linux Advanced 2-core | $0.008 | +| Linux 4-core | $0.016 | +| Linux 8-core | $0.032 | +| Linux 16-core | $0.064 | +| Linux 32-core | $0.128 | +| Linux 64-core | $0.256 | +| Linux 96-core | $0.384 | +| Windows 4-core | $0.032 | +| Windows 8-core | $0.064 | +| Windows 16-core | $0.128 | +| Windows 32-core | $0.256 | +| Windows 64-core | $0.512 | +| Windows 96-core | $0.768 | +| macOS 12-core | $0.12 | + +## arm64-powered {% data variables.actions.hosted_runners %} + +| Operating system | Per-minute rate (USD) | +| ----------------- | --------------------- | +| Linux 2-core | $0.005 | +| Linux 4-core | $0.01 | +| Linux 8-core | $0.02 | +| Linux 16-core | $0.04 | +| Linux 32-core | $0.08 | +| Linux 64-core | $0.16 | +| Windows 2-core | $0.01 | +| Windows 4-core | $0.02 | +| Windows 8-core | $0.04 | +| Windows 16-core | $0.08 | +| Windows 32-core | $0.16 | +| Windows 64-core | $0.32 | +| macOS 6-core (M1) | $0.16 | + +## GPU-powered {% data variables.actions.hosted_runners %} + +| Operating system | Per-minute rate (USD) | +| ---------------- | --------------------- | +| Linux 4-core | $0.07 | +| Windows 4-core | $0.14 | + +## Points to note about rates for runners + +* The number of jobs you can run concurrently across all repositories in your user or organization account depends on your {% data variables.product.github %} plan. For more information, see [AUTOTITLE](/actions/learn-github-actions/usage-limits-billing-and-administration) for {% data variables.product.github %}-hosted runners and [AUTOTITLE](/actions/hosting-your-own-runners/managing-self-hosted-runners/usage-limits-for-self-hosted-runners) for self-hosted runner usage limits. +* {% data reusables.actions.larger-runner-permissions %} +* {% data reusables.actions.about-larger-runners-billing %} +* For {% data variables.actions.hosted_runner %}s, there is no additional cost for configurations that assign public static IP addresses to a {% data variables.actions.hosted_runner %}. For more information on {% data variables.actions.hosted_runner %}s, see [AUTOTITLE](/actions/using-github-hosted-runners/using-larger-runners/about-larger-runners). +* Included minutes cannot be used for {% data variables.actions.hosted_runner %}s. +* The {% data variables.actions.hosted_runner %}s are not free for public repositories. +* Custom images can only be used with larger runners and are billed at the same per-minute rates as those runners. diff --git a/content/billing/reference/index.md b/content/billing/reference/index.md index b10dcc1ea86a..2184863c9582 100644 --- a/content/billing/reference/index.md +++ b/content/billing/reference/index.md @@ -3,14 +3,14 @@ title: Reference for billing shortTitle: Reference intro: Find information to support your use of billing. versions: - fpt: '*' - ghec: '*' - ghes: '*' + fpt: "*" + ghec: "*" + ghes: "*" topics: - Billing children: - /product-usage-included - - /actions-minute-multipliers + - /actions-runner-pricing - /billing-reports - /supported-payment-methods - /azure-billing diff --git a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md index dfadd483dd04..10bd9da4a7b2 100644 --- a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md +++ b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md @@ -153,7 +153,6 @@ Users can include hidden messages in issues assigned to {% data variables.produc ### Limitations in Copilot's compatibility with other features * **{% data variables.product.prodname_copilot_short %} does not sign its commits**. If you have the "Require signed commits" rule or branch protection enabled, you must rewrite the commit history in order to merge {% data variables.product.prodname_copilot_short %}'s pull requests. See [AUTOTITLE](/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-signed-commits). -* **{% data variables.product.prodname_copilot_short %} does not work with self-hosted {% data variables.product.prodname_actions %} runners**. {% data variables.product.prodname_copilot_short %} has access to its own development environment, running in {% data variables.product.prodname_actions %}, and must use {% data variables.product.prodname_dotcom %}-hosted runners. See [AUTOTITLE](/copilot/customizing-copilot/customizing-the-development-environment-for-copilot-coding-agent#upgrading-to-larger-github-hosted-github-actions-runners). * **{% data variables.copilot.copilot_coding_agent %} does not work in personal repositories owned by {% data variables.enterprise.prodname_managed_users %}**. This is because {% data variables.copilot.copilot_coding_agent %} requires {% data variables.product.company_short %}-hosted runners, which are not available to repositories owned by {% data variables.enterprise.prodname_managed_users %}. See [AUTOTITLE](/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners). * **{% data variables.copilot.copilot_coding_agent %} doesn't account for content exclusions**. Content exclusions allow administrators to configure {% data variables.product.prodname_copilot_short %} to ignore certain files. When using {% data variables.copilot.copilot_coding_agent %}, {% data variables.product.prodname_copilot_short %} will not ignore these files, and will be able to see and update them. See [AUTOTITLE](/copilot/managing-copilot/configuring-and-auditing-content-exclusion/excluding-content-from-github-copilot). * **{% data variables.copilot.copilot_coding_agent %} only works with repositories hosted on {% data variables.product.github %}**. If your repository is stored using a different code hosting platform, {% data variables.product.prodname_copilot_short %} won't be able to work on it. diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index 878adcbe4ea1..144e24c7f19a 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -3,14 +3,10 @@ | Rule ID | Rule Name(s) | Description | Severity | Tags | | ------- | ------------ | ----------- | -------- | ---- | | [MD001](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md001.md) | heading-increment | Heading levels should only increment by one level at a time | error | headings | -| [MD004](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md) | ul-style | Unordered list style | error | bullet, ul | -| [MD009](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md) | no-trailing-spaces | Trailing spaces | error | whitespace | | [MD011](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md) | no-reversed-links | Reversed link syntax | error | links | -| [MD012](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md) | no-multiple-blanks | Multiple consecutive blank lines | error | whitespace, blank_lines | | [MD014](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md) | commands-show-output | Dollar signs used before commands without showing output | error | code | | [MD018](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md) | no-missing-space-atx | No space after hash on atx style heading | error | headings, atx, spaces | | [MD019](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md019.md) | no-multiple-space-atx | Multiple spaces after hash on atx style heading | error | headings, atx, spaces | -| [MD022](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md) | blanks-around-headings | Headings should be surrounded by blank lines | error | headings, blank_lines | | [MD023](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md023.md) | heading-start-left | Headings must start at the beginning of the line | error | headings, spaces | | [MD027](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md) | no-multiple-space-blockquote | Multiple spaces after blockquote symbol | error | blockquote, whitespace, indentation | | [MD029](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md) | ol-prefix | Ordered list item prefix | error | ol | @@ -20,8 +16,6 @@ | [MD039](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md) | no-space-in-links | Spaces inside link text | error | whitespace, links | | [MD040](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md) | fenced-code-language | Fenced code blocks should have a language specified | error | code, language | | [MD042](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md) | no-empty-links | No empty links | error | links | -| [MD047](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md) | single-trailing-newline | Files should end with a single newline character | error | blank_lines | -| [MD049](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md) | emphasis-style | Emphasis style | error | emphasis | | [MD050](https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md) | strong-style | Strong style | error | emphasis | | [GH001](https://github.com/github/markdownlint-github/blob/main/docs/rules/GH001-no-default-alt-text.md) | no-default-alt-text | Images should have meaningful alternative text (alt text) | error | accessibility, images | | [GH002](https://github.com/github/markdownlint-github/blob/main/docs/rules/GH002-no-generic-link-text.md) | no-generic-link-text | Avoid using generic link text like `Learn more` or `Click here` | error | accessibility, links | @@ -47,11 +41,9 @@ | GHD020 | liquid-ifversion-tags | Liquid `ifversion` tags should contain valid version names as arguments | error | liquid, versioning | | GHD021 | yaml-scheduled-jobs | YAML snippets that include scheduled workflows must not run on the hour and must be unique | error | feature, actions | | GHD022 | liquid-ifversion-versions | Liquid `ifversion`, `elsif`, and `else` tags should be valid and not contain unsupported versions. | error | liquid, versioning | -| GHD030 | code-fence-line-length | Code fence lines should not exceed a maximum length | warning | code, accessibility | | GHD031 | image-alt-text-exclude-words | Alternate text for images should not begin with words like "image" or "graphic" | error | accessibility, images | | GHD032 | image-alt-text-end-punctuation | Alternate text for images should end with punctuation | error | accessibility, images | | GHD033 | incorrect-alt-text-length | Images alternate text should be between 40-150 characters | warning | accessibility, images | -| GHD034 | list-first-word-capitalization | First word of list item should be capitalized | warning | ul, ol | | GHD035 | rai-reusable-usage | RAI articles and reusables can only reference reusable content in the data/reusables/rai directory | error | feature, rai | | GHD036 | image-no-gif | Image must not be a gif, styleguide reference: contributing/style-guide-and-content-model/style-guide.md#images | error | images | | GHD038 | expired-content | Expired content must be remediated. | warning | expired | @@ -64,13 +56,9 @@ | GHD045 | code-annotation-comment-spacing | Code comments in annotation blocks must have exactly one space after the comment character(s) | warning | code, comments, annotate, spacing | | GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases | | GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting | -| GHD048 | british-english-quotes | Periods and commas should be placed inside quotation marks (American English style) | warning | punctuation, quotes, style, consistency | -| GHD049 | note-warning-formatting | Note and warning tags should be formatted according to style guide | warning | formatting, callouts, notes, warnings, style | -| GHD050 | multiple-emphasis-patterns | Do not use more than one emphasis/strong, italics, or uppercase for a string | warning | formatting, emphasis, style | | GHD051 | frontmatter-versions-whitespace | Versions frontmatter should not contain unnecessary whitespace | warning | frontmatter, versions | | GHD053 | header-content-requirement | Headers must have content between them, such as an introduction | warning | headers, structure, content | | GHD054 | third-party-actions-reusable | Code examples with third-party actions must include disclaimer reusable | warning | actions, reusable, third-party | -| GHD055 | frontmatter-validation | Frontmatter properties must meet character limits and required property requirements | warning | frontmatter, character-limits, required-properties | | GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended | | GHD057 | ctas-schema | CTA URLs must conform to the schema | error | ctas, schema, urls | | GHD058 | journey-tracks-liquid | Journey track properties must use valid Liquid syntax | error | frontmatter, journey-tracks, liquid | diff --git a/eslint.config.ts b/eslint.config.ts index 3668a5fde641..9e40b282ae79 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -25,6 +25,16 @@ export default [ ...globals.node, ...globals.commonjs, ...globals.es2020, + // Fetch API types for TypeScript + RequestInit: 'readonly', + RequestInfo: 'readonly', + HeadersInit: 'readonly', + JSX: 'readonly', + // Node.js types for TypeScript + BufferEncoding: 'readonly', + NodeJS: 'readonly', + // cheerio namespace for TypeScript + cheerio: 'readonly', }, parserOptions: { requireConfigFile: false, @@ -83,15 +93,13 @@ export default [ 'no-redeclare': 'off', // Allow function overloads in TypeScript 'i18n-text/no-en': 'off', // This rule causes eslint to not run at all 'filenames/match-regex': 'off', // This rule causes eslint to not run at all + camelcase: 'off', // Many gh apis use underscores, 600+ uses // Disabled rules to review 'github/no-then': 'off', // 30+ '@typescript-eslint/ban-ts-comment': 'off', // 50+ - 'no-undef': 'off', // 50+ 'no-shadow': 'off', // 150+ - 'prefer-template': 'off', // 150+ 'github/array-foreach': 'off', // 250+ - camelcase: 'off', // 600+ 'no-console': 'off', // 800+ '@typescript-eslint/no-explicit-any': 'off', // 1000+ }, diff --git a/src/app/client-layout.tsx b/src/app/client-layout.tsx index 4cb24a664696..aa657c4b62e2 100644 --- a/src/app/client-layout.tsx +++ b/src/app/client-layout.tsx @@ -1,7 +1,8 @@ 'use client' +import React, { useEffect, useMemo, useState } from 'react' +import type { JSX } from 'react' import { ThemeProvider } from '@primer/react' -import { useEffect, useMemo, useState } from 'react' import { LocaleProvider } from '@/app/lib/locale-context' import { useDetectLocale } from '@/app/lib/use-detect-locale' diff --git a/src/app/components/AppRouterLanguagesContext.tsx b/src/app/components/AppRouterLanguagesContext.tsx index 96aa54097d8d..e78a3e6c1e78 100644 --- a/src/app/components/AppRouterLanguagesContext.tsx +++ b/src/app/components/AppRouterLanguagesContext.tsx @@ -1,6 +1,6 @@ 'use client' -import { createContext, useContext } from 'react' +import React, { createContext, useContext } from 'react' import { languages, type LanguageCode } from '@/languages/lib/languages' export type AppRouterLanguageItem = { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 29fe4be37928..a14c6bd1aaa6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import '@/frame/stylesheets/index.scss' import type { Metadata, Viewport } from 'next' import { ReactNode } from 'react' +import type { JSX } from 'react' export const metadata: Metadata = { title: { diff --git a/src/app/lib/language-utils.ts b/src/app/lib/language-utils.ts index 173f4e9a1f55..c3c88dea6212 100644 --- a/src/app/lib/language-utils.ts +++ b/src/app/lib/language-utils.ts @@ -34,7 +34,7 @@ export function hasLanguagePrefix(path: string): boolean { export function stripLanguagePrefix(path: string): string { if (hasLanguagePrefix(path)) { const pathSegments = path.split('/') - return '/' + pathSegments.slice(2).join('/') + return `/${pathSegments.slice(2).join('/')}` } return path } diff --git a/src/app/lib/locale-context.tsx b/src/app/lib/locale-context.tsx index bad82ed43256..e06327f3c644 100644 --- a/src/app/lib/locale-context.tsx +++ b/src/app/lib/locale-context.tsx @@ -1,6 +1,7 @@ 'use client' import { createContext, useContext, ReactNode, useMemo } from 'react' +import type { JSX } from 'react' import { languages, languageKeys, type LanguageCode } from '@/languages/lib/languages' interface LocaleContextType { diff --git a/src/app/types.ts b/src/app/types.ts index bfa7ce13feac..8b7be385f009 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -2,6 +2,7 @@ * Enhanced type definitions for the app router with strict validation */ +import React from 'react' import type { LanguageCode } from '@/languages/lib/languages' // Core theme types with strict validation diff --git a/src/archives/middleware/archived-enterprise-versions.ts b/src/archives/middleware/archived-enterprise-versions.ts index 848a151f0e03..74dbf9caf2e1 100644 --- a/src/archives/middleware/archived-enterprise-versions.ts +++ b/src/archives/middleware/archived-enterprise-versions.ts @@ -353,14 +353,14 @@ function getProxyPath(reqPath: string, requestedVersion: string) { // Releases 2.18 and higher if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) { - const newReqPath = reqPath.includes('redirects.json') ? `/${reqPath}` : reqPath + '/index.html' + const newReqPath = reqPath.includes('redirects.json') ? `/${reqPath}` : `${reqPath}/index.html` return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath } // Releases 2.13 - 2.17 // redirect.json files don't exist for these versions if (versionSatisfiesRange(requestedVersion, `>=2.13`)) { - return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + reqPath + '/index.html' + return `${ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + reqPath}/index.html` } // Releases 2.12 and lower diff --git a/src/article-api/middleware/article.ts b/src/article-api/middleware/article.ts index eca1e0a97f68..2d73067da152 100644 --- a/src/article-api/middleware/article.ts +++ b/src/article-api/middleware/article.ts @@ -167,7 +167,7 @@ function incrementArticleLookup( const source = req.get('X-Request-Source') || (req.get('Referer') - ? 'external-' + (new URL(req.get('Referer') || '').hostname || 'unknown') + ? `external-${new URL(req.get('Referer') || '').hostname || 'unknown'}` : 'external') const tags = [ diff --git a/src/article-api/scripts/generate-api-docs.ts b/src/article-api/scripts/generate-api-docs.ts index bddac9a9c10e..81b9cad96a1e 100644 --- a/src/article-api/scripts/generate-api-docs.ts +++ b/src/article-api/scripts/generate-api-docs.ts @@ -156,10 +156,10 @@ function updateReadme(readmePath: string, markdown: string): void { // Replace API documentation section, or append to end if (readme.includes(placeholderComment)) { - const pattern = new RegExp(placeholderComment + '[\\s\\S]*', 'g') - readme = readme.replace(pattern, placeholderComment + '\n' + markdown) + const pattern = new RegExp(`${placeholderComment}[\\s\\S]*`, 'g') + readme = readme.replace(pattern, `${placeholderComment}\n${markdown}`) } else { - readme += '\n' + markdown + readme += `\n${markdown}` } writeFileSync(readmePath, readme) diff --git a/src/assets/tests/dynamic-assets.ts b/src/assets/tests/dynamic-assets.ts index b889735b0067..9bfb457a6f67 100644 --- a/src/assets/tests/dynamic-assets.ts +++ b/src/assets/tests/dynamic-assets.ts @@ -103,7 +103,7 @@ describe('dynamic assets', () => { }) test.each(['key', 'key=value'])('any query string (%p) triggers a redirect', async (qs) => { - const res = await get('/assets/images/_fixtures/screenshot.webp?' + qs) + const res = await get(`/assets/images/_fixtures/screenshot.webp?${qs}`) expect(res.statusCode).toBe(302) expect(res.headers.location).toBe('/assets/images/_fixtures/screenshot.webp') expect(res.headers['cache-control']).toContain('public') diff --git a/src/assets/tests/static-assets.ts b/src/assets/tests/static-assets.ts index 479b6af44f96..272aa862017f 100644 --- a/src/assets/tests/static-assets.ts +++ b/src/assets/tests/static-assets.ts @@ -113,7 +113,7 @@ describe('static assets', () => { // This picks the first one found. We just need it to be anything // that actually resolves. const filePath = getNextStaticAsset('css') - const asURL = '/' + filePath.replace('.next', '_next').split(path.sep).join('/') + const asURL = `/${filePath.replace('.next', '_next').split(path.sep).join('/')}` const res = await get(asURL) expect(res.statusCode).toBe(200) checkCachingHeaders(res) diff --git a/src/automated-pipelines/components/AutomatedPage.tsx b/src/automated-pipelines/components/AutomatedPage.tsx index b5635a6bc3fd..be7bcab209ea 100644 --- a/src/automated-pipelines/components/AutomatedPage.tsx +++ b/src/automated-pipelines/components/AutomatedPage.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { DefaultLayout } from '@/frame/components/DefaultLayout' import { ArticleTitle } from '@/frame/components/article/ArticleTitle' import { MarkdownContent } from '@/frame/components/ui/MarkdownContent' diff --git a/src/automated-pipelines/components/AutomatedPageContext.tsx b/src/automated-pipelines/components/AutomatedPageContext.tsx index 39decb834628..50efe899b9f2 100644 --- a/src/automated-pipelines/components/AutomatedPageContext.tsx +++ b/src/automated-pipelines/components/AutomatedPageContext.tsx @@ -1,4 +1,5 @@ import { createContext, useContext } from 'react' +import type { JSX } from 'react' import type { MiniTocItem } from '@/frame/components/context/ArticleContext' export type AutomatedPageContextT = { diff --git a/src/codeql-cli/scripts/sync.ts b/src/codeql-cli/scripts/sync.ts index 407b96e0560f..5f360da6bf72 100755 --- a/src/codeql-cli/scripts/sync.ts +++ b/src/codeql-cli/scripts/sync.ts @@ -41,7 +41,7 @@ async function main() { const matchHeading = '## Options\n' const primaryHeadingSourceContent = sourceContent.replace( matchHeading, - matchHeading + '\n### Primary Options\n', + `${matchHeading}\n### Primary Options\n`, ) const currentFileName = path.basename(file) const { data, content } = await convertContentToDocs( diff --git a/src/codeql-cli/tests/test-circular-links.ts b/src/codeql-cli/tests/test-circular-links.ts index d8a2def89dfc..651d5ebc0dd9 100644 --- a/src/codeql-cli/tests/test-circular-links.ts +++ b/src/codeql-cli/tests/test-circular-links.ts @@ -56,7 +56,7 @@ async function testCircularLinkFix(): Promise { } console.log('\n--- Generated content preview ---') - console.log(result1.content.substring(0, 800) + '...') + console.log(`${result1.content.substring(0, 800)}...`) return !hasCircularLink && hasValidLink } catch (error) { diff --git a/src/content-linter/lib/helpers/rule-utils.ts b/src/content-linter/lib/helpers/rule-utils.ts new file mode 100644 index 000000000000..99d42c902d29 --- /dev/null +++ b/src/content-linter/lib/helpers/rule-utils.ts @@ -0,0 +1,22 @@ +interface LintFlaw { + severity: string + ruleNames: string[] + errorDetail?: string +} + +/** + * Gets all rule names from a flaw, including sub-rules from search-replace errors + */ +export function getAllRuleNames(flaw: LintFlaw): string[] { + const ruleNames = [...flaw.ruleNames] + + // Extract sub-rule name from search-replace error details + if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) { + const match = flaw.errorDetail.match(/^([^:]+):/) + if (match) { + ruleNames.push(match[1]) + } + } + + return ruleNames +} diff --git a/src/content-linter/lib/helpers/should-include-result.ts b/src/content-linter/lib/helpers/should-include-result.ts deleted file mode 100644 index afa77e5afd44..000000000000 --- a/src/content-linter/lib/helpers/should-include-result.ts +++ /dev/null @@ -1,67 +0,0 @@ -import nodePath from 'path' -import { reportingConfig } from '@/content-linter/style/github-docs' - -interface LintFlaw { - severity: string - ruleNames: string[] - errorDetail?: string -} - -/** - * Determines if a lint result should be included based on reporting configuration - * - * @param flaw - The lint flaw object containing rule names, severity, etc. - * @param filePath - The path of the file being linted - * @returns true if the flaw should be included, false if it should be excluded - */ -export function shouldIncludeResult(flaw: LintFlaw, filePath: string): boolean { - if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) { - return true - } - - // Extract all possible rule names including sub-rules from search-replace - const allRuleNames = [...flaw.ruleNames] - - // For search-replace rules, extract the sub-rule name from errorDetail - if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) { - const match = flaw.errorDetail.match(/^([^:]+):/) - if (match) { - allRuleNames.push(match[1]) - } - } - - // Check if any rule name is in the exclude list - const hasExcludedRule = allRuleNames.some((ruleName: string) => - reportingConfig.excludeRules.includes(ruleName), - ) - if (hasExcludedRule) { - return false - } - - // Check if this specific file should be excluded for any of the rules - for (const ruleName of allRuleNames) { - const excludedFiles = - reportingConfig.excludeFilesFromRules?.[ - ruleName as keyof typeof reportingConfig.excludeFilesFromRules - ] - if ( - excludedFiles && - excludedFiles.some((excludedPath: string) => { - // Normalize paths for comparison - const normalizedFilePath = nodePath.normalize(filePath) - const normalizedExcludedPath = nodePath.normalize(excludedPath) - return ( - normalizedFilePath === normalizedExcludedPath || - normalizedFilePath.endsWith(normalizedExcludedPath) - ) - }) - ) { - return false - } - } - - // Default to true - include everything unless explicitly excluded - // This function only handles exclusions; reporting-specific inclusion logic - // (like severity/rule filtering) is handled separately in lint-report.ts - return true -} diff --git a/src/content-linter/lib/linting-rules/british-english-quotes.ts b/src/content-linter/lib/linting-rules/british-english-quotes.ts deleted file mode 100644 index eaa8cf28c7b0..000000000000 --- a/src/content-linter/lib/linting-rules/british-english-quotes.ts +++ /dev/null @@ -1,106 +0,0 @@ -// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations -import { addError } from 'markdownlint-rule-helpers' -import { getRange } from '../helpers/utils' -import frontmatter from '@/frame/lib/read-frontmatter' - -import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' - -export const britishEnglishQuotes = { - names: ['GHD048', 'british-english-quotes'], - description: - 'Periods and commas should be placed inside quotation marks (American English style)', - tags: ['punctuation', 'quotes', 'style', 'consistency'], - severity: 'warning', // Non-blocking as requested in the issue - function: (params: RuleParams, onError: RuleErrorCallback) => { - // Skip autogenerated files - const frontmatterString = params.frontMatterLines.join('\n') - const fm = frontmatter(frontmatterString).data - if (fm && fm.autogenerated) return - - // Check each line for British English quote patterns - for (let i = 0; i < params.lines.length; i++) { - const line = params.lines[i] - const lineNumber = i + 1 - - // Skip code blocks, code spans, and URLs - if (isInCodeContext(line, params.lines, i)) { - continue - } - - // Find British English quote patterns and report them - findAndReportBritishQuotes(line, lineNumber, onError) - } - }, -} - -/** - * Check if the current position is within a code context (code blocks, inline code, URLs) - */ -function isInCodeContext(line: string, allLines: string[], lineIndex: number): boolean { - // Skip if line contains code fences - if (line.includes('```') || line.includes('~~~')) { - return true - } - - // Check if we're inside a code block - let inCodeBlock = false - for (let i = 0; i < lineIndex; i++) { - if (allLines[i].includes('```') || allLines[i].includes('~~~')) { - inCodeBlock = !inCodeBlock - } - } - if (inCodeBlock) { - return true - } - - // Skip if line appears to be mostly code (has multiple backticks) - const backtickCount = (line.match(/`/g) || []).length - if (backtickCount >= 4) { - return true - } - - // Skip URLs and email addresses - if (line.includes('http://') || line.includes('https://') || line.includes('mailto:')) { - return true - } - - return false -} - -/** - * Find and report British English quote patterns in a line - */ -function findAndReportBritishQuotes( - line: string, - lineNumber: number, - onError: RuleErrorCallback, -): void { - // Pattern to find quote followed by punctuation outside - // Matches: "text". or 'text', or "text", etc. - const britishPattern = /(["'])([^"']*?)\1\s*([.,])/g - - let match: RegExpMatchArray | null - while ((match = britishPattern.exec(line)) !== null) { - const quoteChar = match[1] - const quotedText = match[2] - const punctuation = match[3] - const fullMatch = match[0] - const startIndex = match.index ?? 0 - - // Create the corrected version (punctuation inside quotes) - const correctedText = quoteChar + quotedText + punctuation + quoteChar - - const range = getRange(line, fullMatch) - const punctuationName = punctuation === '.' ? 'period' : 'comma' - const errorMessage = `Use American English punctuation: place ${punctuationName} inside the quotation marks` - - // Provide auto-fix - const fixInfo = { - editColumn: startIndex + 1, - deleteCount: fullMatch.length, - insertText: correctedText, - } - - addError(onError, lineNumber, errorMessage, line, range, fixInfo) - } -} diff --git a/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts b/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts index 26ac674bc60a..96c69dcb2d1e 100644 --- a/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts +++ b/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts @@ -81,7 +81,7 @@ export const codeAnnotationCommentSpacing = { // No space after comment character - this is an error const lineNumber: number = token.lineNumber + index + 1 const leadingWhitespace: string = line.match(/^\s*/)![0] - const fixedLine: string = leadingWhitespace + commentChar + ' ' + restOfLine + const fixedLine: string = `${leadingWhitespace + commentChar} ${restOfLine}` addError( onError, diff --git a/src/content-linter/lib/linting-rules/code-fence-line-length.ts b/src/content-linter/lib/linting-rules/code-fence-line-length.ts deleted file mode 100644 index 16b3ef9c2323..000000000000 --- a/src/content-linter/lib/linting-rules/code-fence-line-length.ts +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations -import { addError, filterTokens, newLineRe } from 'markdownlint-rule-helpers' - -import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '@/content-linter/types' - -export const codeFenceLineLength: Rule = { - names: ['GHD030', 'code-fence-line-length'], - description: 'Code fence lines should not exceed a maximum length', - tags: ['code', 'accessibility'], - parser: 'markdownit', - function: (params: RuleParams, onError: RuleErrorCallback) => { - const MAX_LINE_LENGTH: number = params.config?.maxLength || 60 - filterTokens(params, 'fence', (token: MarkdownToken) => { - if (!token.content) return - const lines: string[] = token.content.split(newLineRe) - lines.forEach((line: string, index: number) => { - if (line.length > MAX_LINE_LENGTH) { - // The token line number is the line number of the first line of the - // code fence. We want to report the line number of the content within - // the code fence so we need to add 1 + the index. - const lineNumber: number = token.lineNumber + index + 1 - addError( - onError, - lineNumber, - `Code fence line exceeds ${MAX_LINE_LENGTH} characters.`, - line, - [1, line.length], - null, // No fix possible - ) - } - }) - }) - }, -} diff --git a/src/content-linter/lib/linting-rules/frontmatter-validation.ts b/src/content-linter/lib/linting-rules/frontmatter-validation.ts deleted file mode 100644 index ff91c4b24605..000000000000 --- a/src/content-linter/lib/linting-rules/frontmatter-validation.ts +++ /dev/null @@ -1,214 +0,0 @@ -// @ts-ignore - no types available for markdownlint-rule-helpers -import { addError } from 'markdownlint-rule-helpers' -import { getFrontmatter } from '@/content-linter/lib/helpers/utils' - -import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' - -interface PropertyLimits { - max: number - recommended: number - required?: boolean -} - -interface ContentRules { - title: PropertyLimits - shortTitle: PropertyLimits - intro: PropertyLimits - requiredProperties: string[] -} - -type ContentType = 'category' | 'mapTopic' | 'article' | null - -// Strip liquid tags from text for character counting purposes -function stripLiquidTags(text: unknown): string { - if (typeof text !== 'string') return text as string - // Remove both {% %} and {{ }} liquid tags - return text.replace(/\{%.*?%\}/g, '').replace(/\{\{.*?\}\}/g, '') -} - -export const frontmatterValidation = { - names: ['GHD055', 'frontmatter-validation'], - description: - 'Frontmatter properties must meet character limits and required property requirements', - tags: ['frontmatter', 'character-limits', 'required-properties'], - function: (params: RuleParams, onError: RuleErrorCallback) => { - const fm = getFrontmatter(params.lines as string[]) - if (!fm) return - - // Detect content type based on frontmatter properties and file path - const contentType = detectContentType(fm, params.name) - - // Define character limits and requirements for different content types - const contentRules: Record = { - category: { - title: { max: 70, recommended: 67 }, - shortTitle: { max: 30, recommended: 27 }, - intro: { required: true, recommended: 280, max: 362 }, - requiredProperties: ['intro'], - }, - mapTopic: { - title: { max: 70, recommended: 63 }, - shortTitle: { max: 35, recommended: 30 }, - intro: { required: true, recommended: 280, max: 362 }, - requiredProperties: ['intro'], - }, - article: { - title: { max: 80, recommended: 60 }, - shortTitle: { max: 30, recommended: 25 }, - intro: { required: false, recommended: 251, max: 354 }, - requiredProperties: ['topics'], - }, - } - - const rules = contentType ? contentRules[contentType] : null - if (!rules) return - - // Check required properties - for (const property of rules.requiredProperties) { - if (!fm[property]) { - addError( - onError, - 1, - `Missing required property '${property}' for ${contentType} content type`, - null, - null, - null, - ) - } - } - - // Check title length - if (fm.title) { - validatePropertyLength( - onError, - params.lines as string[], - 'title', - fm.title, - rules.title, - 'Title', - ) - } - - // Check shortTitle length - if (fm.shortTitle) { - validatePropertyLength( - onError, - params.lines as string[], - 'shortTitle', - fm.shortTitle, - rules.shortTitle, - 'ShortTitle', - ) - } - - // Check intro length if it exists - if (fm.intro && rules.intro) { - validatePropertyLength( - onError, - params.lines as string[], - 'intro', - fm.intro, - rules.intro, - 'Intro', - ) - } - - // Cross-property validation: if title is longer than shortTitle limit, shortTitle must exist - const strippedTitle = stripLiquidTags(fm.title) - if (fm.title && (strippedTitle as string).length > rules.shortTitle.max && !fm.shortTitle) { - const titleLine = findPropertyLine(params.lines as string[], 'title') - addError( - onError, - titleLine, - `Title is ${(strippedTitle as string).length} characters, which exceeds the shortTitle limit of ${rules.shortTitle.max} characters. A shortTitle must be provided.`, - fm.title, - null, - null, - ) - } - - // Special validation for articles: should have at least one topic - if (contentType === 'article' && fm.topics) { - if (!Array.isArray(fm.topics)) { - const topicsLine = findPropertyLine(params.lines as string[], 'topics') - addError(onError, topicsLine, 'Topics must be an array', String(fm.topics), null, null) - } else if (fm.topics.length === 0) { - const topicsLine = findPropertyLine(params.lines as string[], 'topics') - addError( - onError, - topicsLine, - 'Articles should have at least one topic', - 'topics: []', - null, - null, - ) - } - } - }, -} - -function validatePropertyLength( - onError: RuleErrorCallback, - lines: string[], - propertyName: string, - propertyValue: string, - limits: PropertyLimits, - displayName: string, -): void { - const strippedValue = stripLiquidTags(propertyValue) - const propertyLength = (strippedValue as string).length - const propertyLine = findPropertyLine(lines, propertyName) - - // Only report the most severe error - maximum takes precedence over recommended - if (propertyLength > limits.max) { - addError( - onError, - propertyLine, - `${displayName} exceeds maximum length of ${limits.max} characters (current: ${propertyLength})`, - propertyValue, - null, - null, - ) - } else if (propertyLength > limits.recommended) { - addError( - onError, - propertyLine, - `${displayName} exceeds recommended length of ${limits.recommended} characters (current: ${propertyLength})`, - propertyValue, - null, - null, - ) - } -} - -// frontmatter object structure varies based on YAML content, using any for flexibility -function detectContentType(frontmatter: any, filePath: string): ContentType { - // Only apply validation to markdown files - if (!filePath || !filePath.endsWith('.md')) { - return null - } - - // Map topics have mapTopic: true - if (frontmatter.mapTopic === true) { - return 'mapTopic' - } - - // Categories are index.md files that contain children but no mapTopic - // Only check files that look like they're in the content directory structure - if ( - filePath.includes('/index.md') && - frontmatter.children && - Array.isArray(frontmatter.children) && - !frontmatter.mapTopic - ) { - return 'category' - } - - // Everything else is an article - return 'article' -} - -function findPropertyLine(lines: string[], property: string): number { - const line = lines.find((line) => line.trim().startsWith(`${property}:`)) - return line ? lines.indexOf(line) + 1 : 1 -} diff --git a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts index c78c29e3501c..d00b8819969c 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts +++ b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts @@ -26,7 +26,7 @@ export const imageAltTextEndPunctuation: Rule = { const range = getRange(token.line, imageAltText) - addFixErrorDetail(onError, token.lineNumber, imageAltText + '.', imageAltText, range, { + addFixErrorDetail(onError, token.lineNumber, `${imageAltText}.`, imageAltText, range, { lineNumber: token.lineNumber, editColumn: isStringQuoted(imageAltText) ? token.line.indexOf(']') diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 9f115734384b..a868ed9c6827 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -3,14 +3,12 @@ import searchReplace from 'markdownlint-rule-search-replace' // @ts-ignore - @github/markdownlint-github doesn't provide TypeScript declarations import markdownlintGitHub from '@github/markdownlint-github' -import { codeFenceLineLength } from '@/content-linter/lib/linting-rules/code-fence-line-length' import { imageAltTextEndPunctuation } from '@/content-linter/lib/linting-rules/image-alt-text-end-punctuation' import { imageFileKebabCase } from '@/content-linter/lib/linting-rules/image-file-kebab-case' import { incorrectAltTextLength } from '@/content-linter/lib/linting-rules/image-alt-text-length' import { internalLinksNoLang } from '@/content-linter/lib/linting-rules/internal-links-no-lang' import { internalLinksSlash } from '@/content-linter/lib/linting-rules/internal-links-slash' import { imageAltTextExcludeStartWords } from '@/content-linter/lib/linting-rules/image-alt-text-exclude-start-words' -import { listFirstWordCapitalization } from '@/content-linter/lib/linting-rules/list-first-word-capitalization' import { linkPunctuation } from '@/content-linter/lib/linting-rules/link-punctuation' import { earlyAccessReferences, @@ -49,11 +47,7 @@ import { linkQuotation } from '@/content-linter/lib/linting-rules/link-quotation import { octiconAriaLabels } from '@/content-linter/lib/linting-rules/octicon-aria-labels' import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions' import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology' -import { britishEnglishQuotes } from '@/content-linter/lib/linting-rules/british-english-quotes' -import { multipleEmphasisPatterns } from '@/content-linter/lib/linting-rules/multiple-emphasis-patterns' -import { noteWarningFormatting } from '@/content-linter/lib/linting-rules/note-warning-formatting' import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace' -import { frontmatterValidation } from '@/content-linter/lib/linting-rules/frontmatter-validation' import { headerContentRequirement } from '@/content-linter/lib/linting-rules/header-content-requirement' import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable' import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended' @@ -103,11 +97,9 @@ export const gitHubDocsMarkdownlint = { liquidIfVersionTags, // GHD020 yamlScheduledJobs, // GHD021 liquidIfversionVersions, // GHD022 - codeFenceLineLength, // GHD030 imageAltTextExcludeStartWords, // GHD031 imageAltTextEndPunctuation, // GHD032 incorrectAltTextLength, // GHD033 - listFirstWordCapitalization, // GHD034 raiReusableUsage, // GHD035 imageNoGif, // GHD036 expiredContent, // GHD038 @@ -120,13 +112,9 @@ export const gitHubDocsMarkdownlint = { codeAnnotationCommentSpacing, // GHD045 outdatedReleasePhaseTerminology, // GHD046 tableColumnIntegrity, // GHD047 - britishEnglishQuotes, // GHD048 - noteWarningFormatting, // GHD049 - multipleEmphasisPatterns, // GHD050 frontmatterVersionsWhitespace, // GHD051 headerContentRequirement, // GHD053 thirdPartyActionsReusable, // GHD054 - frontmatterValidation, // GHD055 frontmatterLandingRecommended, // GHD056 ctasSchema, // GHD057 journeyTracksLiquid, // GHD058 diff --git a/src/content-linter/lib/linting-rules/link-quotation.ts b/src/content-linter/lib/linting-rules/link-quotation.ts index 9d4eb089a170..609896ec5fe3 100644 --- a/src/content-linter/lib/linting-rules/link-quotation.ts +++ b/src/content-linter/lib/linting-rules/link-quotation.ts @@ -26,7 +26,7 @@ export const linkQuotation: Rule = { } else if (inLinkWithPrecedingQuotes && child.type === 'text') { content.push(escapeRegExp((child.content || '').trim())) } else if (inLinkWithPrecedingQuotes && child.type === 'code_inline') { - content.push('`' + escapeRegExp((child.content || '').trim()) + '`') + content.push(`\`${escapeRegExp((child.content || '').trim())}\``) } else if (child.type === 'link_close') { const title = content.join(' ') const regex = new RegExp(`"\\[${title}\\]\\(${linkUrl}\\)({%.*%})?(!|\\.|\\?|,)?"`) @@ -44,7 +44,7 @@ export const linkQuotation: Rule = { newLine = newLine.slice(0, -1) } if (newLine.endsWith('".')) { - newLine = newLine.slice(0, -2) + '.' + newLine = `${newLine.slice(0, -2)}.` } const lineNumber = child.lineNumber addError( diff --git a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts index 97da99f41abe..8748bbca12b3 100644 --- a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts +++ b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts @@ -104,7 +104,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: for (let i = 0; i < condTagItems.length; i++) { const item = condTagItems[i] const tagNameNoCond = item.name === 'endif' || item.name === 'else' - const itemErrorName = tagNameNoCond ? item.name : item.name + ' ' + item.cond + const itemErrorName = tagNameNoCond ? item.name : `${item.name} ${item.cond}` if (item.action.type === 'delete') { // There is no next stack item, the endif tag is alway the @@ -438,7 +438,7 @@ function updateConditionals(condTagItems: any[]) { const newVersions = Object.entries(item.versionsObj).map(([key, value]) => { if (key === 'ghes') { if (value === '*') return key - return key + ' ' + value + return `${key} ${value}` } else return key }) item.action.cond = newVersions.join(' or ') diff --git a/src/content-linter/lib/linting-rules/liquid-syntax.ts b/src/content-linter/lib/linting-rules/liquid-syntax.ts index 1203b7e07367..efab53c928de 100644 --- a/src/content-linter/lib/linting-rules/liquid-syntax.ts +++ b/src/content-linter/lib/linting-rules/liquid-syntax.ts @@ -55,7 +55,7 @@ export const frontmatterLiquidSyntax = { addError( onError, lineNumber, - 'Liquid syntax error: ' + errorDescription, + `Liquid syntax error: ${errorDescription}`, value, range, null, // No fix possible @@ -92,7 +92,7 @@ export const liquidSyntax = { addError( onError, lineNumber, - 'Liquid syntax error: ' + errorDescription, + `Liquid syntax error: ${errorDescription}`, line, range, null, // No fix possible diff --git a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts index 3b06630ac4ad..e0678607accf 100644 --- a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts +++ b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts @@ -46,14 +46,13 @@ export const liquidTagWhitespace: Rule = { const openTag = tag.slice(0, token.contentRange[0] - token.begin) const closeTag = tag.slice(-(token.end - token.contentRange[1])) - const isOpenTagOneSpace = openTag !== openTag.trim() + ' ' - const isCloseTagOneSpace = closeTag !== ' ' + closeTag.trim() + const isOpenTagOneSpace = openTag !== `${openTag.trim()} ` + const isCloseTagOneSpace = closeTag !== ` ${closeTag.trim()}` const moreThanOneSpace = /\s{2,}/ const isArgOneSpace = moreThanOneSpace.test(tag) - const fixedContent = - openTag.trim() + ' ' + token.content.replace(moreThanOneSpace, ' ') + ' ' + closeTag.trim() + const fixedContent = `${openTag.trim()} ${token.content.replace(moreThanOneSpace, ' ')} ${closeTag.trim()}` if (isOpenTagOneSpace || isCloseTagOneSpace || isArgOneSpace) { addFixErrorDetail( diff --git a/src/content-linter/lib/linting-rules/list-first-word-capitalization.ts b/src/content-linter/lib/linting-rules/list-first-word-capitalization.ts deleted file mode 100644 index 128f1e4f77ca..000000000000 --- a/src/content-linter/lib/linting-rules/list-first-word-capitalization.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { addFixErrorDetail, getRange, filterTokensByOrder } from '../helpers/utils' -import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '../../types' - -export const listFirstWordCapitalization: Rule = { - names: ['GHD034', 'list-first-word-capitalization'], - description: 'First word of list item should be capitalized', - tags: ['ul', 'ol'], - function: (params: RuleParams, onError: RuleErrorCallback) => { - // Skip site-policy directory as these are legal documents with specific formatting requirements - if (params.name && params.name.includes('content/site-policy/')) return - - // We're going to look for a sequence of 3 tokens. If the markdown - // is a really small string, it might not even have that many tokens - // in it. Can bail early. - if (!params.tokens || params.tokens.length < 3) return - - const inlineListItems = filterTokensByOrder(params.tokens, [ - 'list_item_open', - 'paragraph_open', - 'inline', - ]).filter((token: MarkdownToken) => token.type === 'inline') - - inlineListItems.forEach((token: MarkdownToken) => { - // Only proceed if all of the token's children start with a text - // node that is not empty. - // This filters out cases where the list item is inline code, or - // a link, or an image, etc. - // This also avoids cases like `- **bold** text` where the first - // child is a text node string but the text node content is empty. - const firstWordTextNode = - token.children && - token.children.length > 0 && - token.children[0].type === 'text' && - token.children[0].content !== '' - if (!firstWordTextNode) return - - const content = (token.content || '').trim() - const firstWord = content.trim().split(' ')[0] - - // If the first character in the first word is not an alphanumeric, - // don't bother. For example `"ubunut-latest"` or `{% data ... %}`. - if (/^[^a-z]/i.test(firstWord)) return - // If the first letter is capitalized, it's not an error - // And any special characters (like @) that can't be capitalized - if (/[A-Z@]/.test(firstWord[0])) return - // There are items that start with a number or words that contain numbers - // e.g., x64 - if (/\d/.test(firstWord)) return - // Catches proper nouns like macOS or openSUSE - if (/[A-Z]/.test(firstWord.slice(1))) return - - const lineNumber = token.lineNumber - const range = getRange(token.line, firstWord) - if (!range) return - addFixErrorDetail( - onError, - lineNumber, - `${firstWord[0].toUpperCase()}${firstWord.slice(1)}`, - firstWord, - range, - { - lineNumber, - editColumn: range[0], - deleteCount: 1, - insertText: firstWord[0].toUpperCase(), - }, - ) - }) - }, -} diff --git a/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts b/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts deleted file mode 100644 index 3075ce51ff8d..000000000000 --- a/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts +++ /dev/null @@ -1,100 +0,0 @@ -// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations -import { addError } from 'markdownlint-rule-helpers' -import { getRange } from '../helpers/utils' -import frontmatter from '@/frame/lib/read-frontmatter' -import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types' - -interface Frontmatter { - autogenerated?: boolean - [key: string]: any -} - -export const multipleEmphasisPatterns: Rule = { - names: ['GHD050', 'multiple-emphasis-patterns'], - description: 'Do not use more than one emphasis/strong, italics, or uppercase for a string', - tags: ['formatting', 'emphasis', 'style'], - severity: 'warning', - function: (params: RuleParams, onError: RuleErrorCallback) => { - // Skip autogenerated files - const frontmatterString = params.frontMatterLines.join('\n') - const fm = frontmatter(frontmatterString).data as Frontmatter - if (fm && fm.autogenerated) return - - const lines = params.lines - let inCodeBlock = false - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const lineNumber = i + 1 - - // Track code block state - if (line.trim().startsWith('```')) { - inCodeBlock = !inCodeBlock - continue - } - - // Skip code blocks and indented code - if (inCodeBlock || line.trim().startsWith(' ')) continue - - // Check for multiple emphasis patterns - checkMultipleEmphasis(line, lineNumber, onError) - } - }, -} - -/** - * Check for multiple emphasis types in a single text segment - */ -function checkMultipleEmphasis(line: string, lineNumber: number, onError: RuleErrorCallback): void { - // Focus on the clearest violations of the style guide - const multipleEmphasisPatterns: Array<{ regex: RegExp; types: string[] }> = [ - // Bold + italic combinations (***text***) - { regex: /\*\*\*([^*]+)\*\*\*/g, types: ['bold', 'italic'] }, - { regex: /___([^_]+)___/g, types: ['bold', 'italic'] }, - - // Bold with code nested inside - { regex: /\*\*([^*]*`[^`]+`[^*]*)\*\*/g, types: ['bold', 'code'] }, - { regex: /__([^_]*`[^`]+`[^_]*)__/g, types: ['bold', 'code'] }, - - // Code with bold nested inside - { regex: /`([^`]*\*\*[^*]+\*\*[^`]*)`/g, types: ['code', 'bold'] }, - { regex: /`([^`]*__[^_]+__[^`]*)`/g, types: ['code', 'bold'] }, - ] - - for (const pattern of multipleEmphasisPatterns) { - let match - while ((match = pattern.regex.exec(line)) !== null) { - // Skip if this is likely intentional or very short - if (shouldSkipMatch(match[0], match[1])) continue - - const range = getRange(line, match[0]) - addError( - onError, - lineNumber, - `Do not use multiple emphasis types in a single string: ${pattern.types.join(' + ')}`, - line, - range, - null, // No auto-fix as this requires editorial judgment - ) - } - } -} - -/** - * Determine if a match should be skipped (likely intentional formatting) - */ -function shouldSkipMatch(fullMatch: string, content: string): boolean { - // Skip common false positives - if (!content) return true - - // Skip very short content (likely intentional single chars) - if (content.trim().length < 2) return true - - // Skip if it's mostly code-like content (constants, variables) - if (/^[A-Z_][A-Z0-9_]*$/.test(content.trim())) return true - - // Skip file extensions or URLs - if (/\.[a-z]{2,4}$/i.test(content.trim()) || /https?:\/\//.test(content)) return true - - return false -} diff --git a/src/content-linter/lib/linting-rules/note-warning-formatting.ts b/src/content-linter/lib/linting-rules/note-warning-formatting.ts deleted file mode 100644 index e55a57da2295..000000000000 --- a/src/content-linter/lib/linting-rules/note-warning-formatting.ts +++ /dev/null @@ -1,236 +0,0 @@ -// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations -import { addError } from 'markdownlint-rule-helpers' -import { getRange } from '../helpers/utils' -import frontmatter from '@/frame/lib/read-frontmatter' -import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' - -interface NoteContentItem { - text: string - lineNumber: number -} - -export const noteWarningFormatting = { - names: ['GHD049', 'note-warning-formatting'], - description: 'Note and warning tags should be formatted according to style guide', - tags: ['formatting', 'callouts', 'notes', 'warnings', 'style'], - severity: 'warning', - function: (params: RuleParams, onError: RuleErrorCallback) => { - // Skip autogenerated files - const frontmatterString = params.frontMatterLines.join('\n') - const fm = frontmatter(frontmatterString).data - if (fm && fm.autogenerated) return - - const lines = params.lines - let inLegacyNote = false - let noteStartLine: number | null = null - let noteContent: NoteContentItem[] = [] - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const lineNumber = i + 1 - - // Check for legacy {% note %} tags - if (line.trim() === '{% note %}') { - inLegacyNote = true - noteStartLine = lineNumber - noteContent = [] - - // Check for missing line break before {% note %} - const prevLine = i > 0 ? lines[i - 1] : '' - if (prevLine.trim() !== '') { - const range = getRange(line, '{% note %}') - addError(onError, lineNumber, 'Add a blank line before {% note %} tag', line, range, { - editColumn: 1, - deleteCount: 0, - insertText: '\n', - }) - } - continue - } - - // Check for end of legacy note - if (line.trim() === '{% endnote %}') { - if (inLegacyNote) { - inLegacyNote = false - - // Check for missing line break after {% endnote %} - const nextLine = i < lines.length - 1 ? lines[i + 1] : '' - if (nextLine.trim() !== '') { - const range = getRange(line, '{% endnote %}') - addError(onError, lineNumber, 'Add a blank line after {% endnote %} tag', line, range, { - editColumn: line.length + 1, - deleteCount: 0, - insertText: '\n', - }) - } - - // Check note content formatting - validateNoteContent(noteContent, noteStartLine, onError) - } - continue - } - - // Collect content inside legacy notes - if (inLegacyNote) { - noteContent.push({ text: line, lineNumber }) - continue - } - - // Check for new-style callouts > [!NOTE], > [!WARNING], > [!DANGER] - const calloutMatch = line.match(/^>\s*\[!(NOTE|WARNING|DANGER)\]\s*$/) - if (calloutMatch) { - const calloutType = calloutMatch[1] - - // Check for missing line break before callout - const prevLine = i > 0 ? lines[i - 1] : '' - if (prevLine.trim() !== '') { - const range = getRange(line, line.trim()) - addError( - onError, - lineNumber, - `Add a blank line before > [!${calloutType}] callout`, - line, - range, - { - editColumn: 1, - deleteCount: 0, - insertText: '\n', - }, - ) - } - - // Find the end of this callout block and validate content - const calloutContent = [] - let j = i + 1 - while (j < lines.length && lines[j].startsWith('>')) { - if (lines[j].trim() !== '>') { - calloutContent.push({ text: lines[j], lineNumber: j + 1 }) - } - j++ - } - - // Check for missing line break after callout - if (j < lines.length && lines[j].trim() !== '') { - const range = getRange(lines[j], lines[j].trim()) - addError( - onError, - j + 1, - `Add a blank line after > [!${calloutType}] callout block`, - lines[j], - range, - { - editColumn: 1, - deleteCount: 0, - insertText: '\n', - }, - ) - } - - validateCalloutContent(calloutContent, calloutType, lineNumber, onError) - i = j - 1 // Skip to end of callout block - continue - } - - // Check for orphaned **Note:**/**Warning:**/**Danger:** outside callouts - const orphanedPrefixMatch = line.match(/\*\*(Note|Warning|Danger):\*\*/) - if (orphanedPrefixMatch && !inLegacyNote && !line.startsWith('>')) { - const range = getRange(line, orphanedPrefixMatch[0]) - addError( - onError, - lineNumber, - `${orphanedPrefixMatch[1]} prefix should be inside a callout block`, - line, - range, - null, // No auto-fix as this requires human decision - ) - } - } - }, -} - -/** - * Validate content inside legacy {% note %} blocks - */ -function validateNoteContent( - noteContent: NoteContentItem[], - noteStartLine: number | null, - onError: RuleErrorCallback, -) { - if (noteContent.length === 0) return - - const contentLines = noteContent.filter((item) => item.text.trim() !== '') - if (contentLines.length === 0) return - - // Count bullet points - const bulletLines = contentLines.filter((item) => item.text.trim().match(/^[*\-+]\s/)) - if (bulletLines.length > 2) { - const range = getRange(bulletLines[2].text, bulletLines[2].text.trim()) - addError( - onError, - bulletLines[2].lineNumber, - 'Do not include more than 2 bullet points inside a callout', - bulletLines[2].text, - range, - null, // No auto-fix as this requires content restructuring - ) - } - - // Check for missing prefix (only if it looks like a traditional note) - const firstContentLine = contentLines[0] - const allContent = contentLines.map((line) => line.text).join(' ') - const hasButtons = - allContent.includes(' item.text.trim() !== '>') - if (contentLines.length === 0) return - - // Count bullet points - const bulletLines = contentLines.filter((item) => item.text.match(/^>\s*[*\-+]\s/)) - if (bulletLines.length > 2) { - const range = getRange(bulletLines[2].text, bulletLines[2].text.trim()) - addError( - onError, - bulletLines[2].lineNumber, - 'Do not include more than 2 bullet points inside a callout', - bulletLines[2].text, - range, - null, // No auto-fix as this requires content restructuring - ) - } - - // For new-style callouts, the prefix is handled by the [!NOTE] syntax itself - // so we don't need to check for manual **Note:** prefixes -} diff --git a/src/content-linter/lib/linting-rules/octicon-aria-labels.ts b/src/content-linter/lib/linting-rules/octicon-aria-labels.ts index a3eb531395ad..376c67185f8e 100644 --- a/src/content-linter/lib/linting-rules/octicon-aria-labels.ts +++ b/src/content-linter/lib/linting-rules/octicon-aria-labels.ts @@ -37,7 +37,7 @@ export const octiconAriaLabels: Rule = { const octiconNameMatch = token.args.match(/["']([^"']+)["']/) const octiconName = octiconNameMatch ? octiconNameMatch[1] : 'icon' const originalContent = token.content - const fixedContent = originalContent + ` aria-label="${octiconName}"` + const fixedContent = `${originalContent} aria-label="${octiconName}"` addFixErrorDetail( onError, diff --git a/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.ts b/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.ts index 28ce1d3e2c8b..d2f457f528b7 100644 --- a/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.ts +++ b/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.ts @@ -26,6 +26,26 @@ const TERMINOLOGY_REPLACEMENTS: [string, string][] = [ ['sunset', 'retired'], ] +// Don't lint filepaths that have legitimate uses of these terms +const EXCLUDED_PATHS: string[] = [ + // Individual files + 'content/actions/reference/runners/github-hosted-runners.md', + 'content/actions/reference/workflows-and-actions/metadata-syntax.md', + 'content/admin/administering-your-instance/administering-your-instance-from-the-command-line/command-line-utilities.md', + 'content/authentication/managing-commit-signature-verification/checking-for-existing-gpg-keys.md', + 'content/codespaces/setting-your-user-preferences/choosing-the-stable-or-beta-host-image.md', + 'content/rest/using-the-rest-api/getting-started-with-the-rest-api.md', + 'data/reusables/actions/jobs/choosing-runner-github-hosted.md', + 'data/reusables/code-scanning/codeql-query-tables/cpp.md', + 'data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md', + 'data/variables/release-phases.yml', + // Directories + 'content/site-policy/', + 'data/features/', + 'data/release-notes/enterprise-server/3-14/', + 'data/release-notes/enterprise-server/3-15/', +] + interface CompiledRegex { regex: RegExp outdatedTerm: string @@ -96,6 +116,13 @@ export const outdatedReleasePhaseTerminology = { tags: ['terminology', 'consistency', 'release-phases'], severity: 'error', function: (params: RuleParams, onError: RuleErrorCallback) => { + // Skip excluded files + for (const filepath of EXCLUDED_PATHS) { + if (params.name.startsWith(filepath)) { + return + } + } + // Skip autogenerated files const frontmatterString = params.frontMatterLines.join('\n') const fm = frontmatter(frontmatterString).data diff --git a/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts b/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts index 6b95c52fa9ab..907495a9a884 100644 --- a/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts +++ b/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts @@ -43,34 +43,46 @@ export const thirdPartyActionsReusable = { /** * Find third-party actions in YAML content - * Third-party actions are identified by the pattern: owner/action@version - * where owner is not 'actions' or 'github' + * Third-party actions are identified by actions that are not GitHub-owned or documentation examples */ function findThirdPartyActions(yamlContent: string): string[] { const thirdPartyActions: string[] = [] - - // Pattern to match 'uses: owner/action@version' where owner is not actions or github const actionPattern = /uses:\s+([^{\s]+\/[^@\s]+@[^\s]+)/g let match while ((match = actionPattern.exec(yamlContent)) !== null) { const actionRef = match[1] - // Extract owner from action reference - const parts = actionRef.split('/') - if (parts.length >= 2) { - const owner = parts[0] - - // Skip GitHub-owned actions (actions/* and github/*) - if (owner !== 'actions' && owner !== 'github') { - thirdPartyActions.push(actionRef) - } + if (!isExampleOrGitHubAction(actionRef)) { + thirdPartyActions.push(actionRef) } } return thirdPartyActions } +/** + * Check if an action should be skipped (GitHub-owned or documentation example) + */ +function isExampleOrGitHubAction(actionRef: string): boolean { + // List of patterns to exclude (GitHub-owned and documentation examples) + const excludePatterns = [ + // GitHub-owned + /^actions\//, + /^github\//, + // Example organizations + /^(octo-org|octocat|different-org|fakeaction|some|OWNER|my-org)\//, + // Example repos (any owner) + /\/example-repo[/@]/, + /\/octo-repo[/@]/, + /\/hello-world-composite-action[/@]/, + /\/monorepo[/@]/, + // Monorepo patterns + ] + + return excludePatterns.some((pattern) => pattern.test(actionRef)) +} + /** * Check if the disclaimer reusable is present before the given line number or inside the code block * Looks backward from the code block and also inside the code block content diff --git a/src/content-linter/scripts/find-unsed-variables.ts b/src/content-linter/scripts/find-unsed-variables.ts index 2e6a5560b8fc..4fbd0ff085ea 100644 --- a/src/content-linter/scripts/find-unsed-variables.ts +++ b/src/content-linter/scripts/find-unsed-variables.ts @@ -104,11 +104,10 @@ async function main(options: Options) { function getVariables(): Map { const variables = new Map() for (const filePath of walkFiles('data/variables', '.yml')) { - const dottedPathBase = - 'variables.' + filePath.replace('data/variables/', '').replace('.yml', '').replace(/\//g, '.') + const dottedPathBase = `variables.${filePath.replace('data/variables/', '').replace('.yml', '').replace(/\//g, '.')}` const data = yaml.load(fs.readFileSync(filePath, 'utf-8')) as Record for (const key of Object.keys(data)) { - const dottedPath = dottedPathBase + '.' + key + const dottedPath = `${dottedPathBase}.${key}` variables.set(dottedPath, filePath) } } diff --git a/src/content-linter/scripts/lint-content.ts b/src/content-linter/scripts/lint-content.ts index aeca69d6ce07..b516d5f5917b 100755 --- a/src/content-linter/scripts/lint-content.ts +++ b/src/content-linter/scripts/lint-content.ts @@ -16,7 +16,14 @@ import { prettyPrintResults } from './pretty-print-results' import { getLintableYml } from '@/content-linter/lib/helpers/get-lintable-yml' import { printAnnotationResults } from '../lib/helpers/print-annotations' import languages from '@/languages/lib/languages-server' -import { shouldIncludeResult } from '../lib/helpers/should-include-result' + +/** + * Config that applies to all rules in all environments (CI, reports, precommit). + */ +export const globalConfig = { + // Do not ever lint these filepaths + excludePaths: ['content/contributing/'], +} program .description('Run GitHub Docs Markdownlint rules.') @@ -197,12 +204,7 @@ async function main() { if (printAnnotations) { printAnnotationResults(formattedResults, { - skippableRules: [ - // As of Feb 2024, this rule is quite noisy. It's present in - // many files and is not always a problem. And besides, when it - // does warn, it's usually a very long one. - 'code-fence-line-length', // a.k.a. GHD030 - ], + skippableRules: [], skippableFlawProperties: [ // As of Feb 2024, we don't support reporting flaws for lines // and columns numbers of YAML files. YAML files consist of one @@ -349,7 +351,14 @@ function getFilesToLint(paths) { (!filePath.endsWith('.md') && !filePath.endsWith('.yml')) ) continue + const relPath = path.relative(root, filePath) + + // Skip files that match any of the excluded paths + if (globalConfig.excludePaths.some((excludePath) => relPath.startsWith(excludePath))) { + continue + } + if (seen.has(relPath)) continue seen.add(relPath) clean.push(relPath) @@ -427,9 +436,7 @@ function getFormattedResults(allResults, isPrecommit) { if (verbose) { output[key] = [...results] } else { - const formattedResults = results - .map((flaw) => formatResult(flaw, isPrecommit)) - .filter((flaw) => shouldIncludeResult(flaw, key)) + const formattedResults = results.map((flaw) => formatResult(flaw, isPrecommit)) // Only add the file to output if there are results after filtering if (formattedResults.length > 0) { @@ -562,9 +569,6 @@ function getMarkdownLintConfig(errorsOnly, runRules) { // Check if the rule should be included based on user-specified rules if (runRules && !shouldIncludeRule(ruleName, runRules)) continue - // Skip british-english-quotes rule in CI/PRs (only run in pre-commit) - if (ruleName === 'british-english-quotes' && !isPrecommit) continue - // There are a subset of rules run on just the frontmatter in files if (githubDocsFrontmatterConfig[ruleName]) { config.frontMatter[ruleName] = ruleConfig diff --git a/src/content-linter/scripts/lint-report.ts b/src/content-linter/scripts/lint-report.ts index 7a5bf0c657f0..098588abec52 100644 --- a/src/content-linter/scripts/lint-report.ts +++ b/src/content-linter/scripts/lint-report.ts @@ -5,12 +5,21 @@ import coreLib from '@actions/core' import github from '@/workflows/github' import { getEnvInputs } from '@/workflows/get-env-inputs' import { createReportIssue, linkReports } from '@/workflows/issue-report' -import { shouldIncludeResult } from '@/content-linter/lib/helpers/should-include-result' -import { reportingConfig } from '@/content-linter/style/github-docs' +import { getAllRuleNames } from '@/content-linter/lib/helpers/rule-utils' // GitHub issue body size limit is ~65k characters, so we'll use 60k as a safe limit const MAX_ISSUE_BODY_SIZE = 60000 +/** + * Config that only applies to automated weekly reports. + */ +export const reportingConfig = { + // Include only rules with these severities in reports + includeSeverities: ['error'], + // Include these rules regardless of severity in reports + includeRules: ['expired-content'], +} + interface LintFlaw { severity: string ruleNames: string[] @@ -19,34 +28,16 @@ interface LintFlaw { /** * Determines if a lint result should be included in the automated report - * Uses shared exclusion logic with additional reporting-specific filtering */ -function shouldIncludeInReport(flaw: LintFlaw, filePath: string): boolean { - if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) { - return false - } - - // First check if it should be excluded (file-specific or rule-specific exclusions) - if (!shouldIncludeResult(flaw, filePath)) { - return false - } - - // Extract all possible rule names including sub-rules from search-replace - const allRuleNames = [...flaw.ruleNames] - if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) { - const match = flaw.errorDetail.match(/^([^:]+):/) - if (match) { - allRuleNames.push(match[1]) - } - } +function shouldIncludeInReport(flaw: LintFlaw): boolean { + const allRuleNames = getAllRuleNames(flaw) - // Apply reporting-specific filtering // Check if severity should be included if (reportingConfig.includeSeverities.includes(flaw.severity)) { return true } - // Check if any rule name is in the include list + // Check if any rule name is in the include list that overrides severity const hasIncludedRule = allRuleNames.some((ruleName: string) => reportingConfig.includeRules.includes(ruleName), ) @@ -101,7 +92,7 @@ async function main() { // Filter results based on reporting configuration const filteredResults: Record = {} for (const [file, flaws] of Object.entries(parsedResults)) { - const filteredFlaws = (flaws as LintFlaw[]).filter((flaw) => shouldIncludeInReport(flaw, file)) + const filteredFlaws = (flaws as LintFlaw[]).filter((flaw) => shouldIncludeInReport(flaw)) // Only include files that have remaining flaws after filtering if (filteredFlaws.length > 0) { diff --git a/src/content-linter/scripts/pretty-print-results.ts b/src/content-linter/scripts/pretty-print-results.ts index 615fb812ef89..d447b1170c01 100644 --- a/src/content-linter/scripts/pretty-print-results.ts +++ b/src/content-linter/scripts/pretty-print-results.ts @@ -170,14 +170,14 @@ function indentWrappedString(str: string, startingIndent: number): string { if ((currentLine + word).length > effectiveWidth) { if (isFirstLine) { - indentedString += currentLine.trim() + '\n' + indentedString += `${currentLine.trim()}\n` isFirstLine = false } else { - indentedString += NEW_LINE_PADDING + currentLine.trim() + '\n' + indentedString += `${NEW_LINE_PADDING + currentLine.trim()}\n` } - currentLine = word + ' ' + currentLine = `${word} ` } else { - currentLine += word + ' ' + currentLine += `${word} ` } } if (isFirstLine) { diff --git a/src/content-linter/style/base.ts b/src/content-linter/style/base.ts index 9faabfeca6f6..600a65017754 100644 --- a/src/content-linter/style/base.ts +++ b/src/content-linter/style/base.ts @@ -33,32 +33,12 @@ export const baseConfig: BaseConfig = { 'partial-markdown-files': false, 'yml-files': false, }, - 'ul-style': { - // MD004 - severity: 'error', - style: 'asterisk', - 'partial-markdown-files': true, - 'yml-files': false, - context: `We use asterisks to format bulleted lists because this gives clearer, more accessible source code.`, - }, - 'no-trailing-spaces': { - // MD009 - severity: 'error', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'no-reversed-links': { // MD011 severity: 'error', 'partial-markdown-files': true, 'yml-files': true, }, - 'no-multiple-blanks': { - // MD012 - severity: 'error', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'commands-show-output': { // MD014 severity: 'error', @@ -77,12 +57,6 @@ export const baseConfig: BaseConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'blanks-around-headings': { - // MD022 - severity: 'error', - 'partial-markdown-files': false, - 'yml-files': false, - }, 'heading-start-left': { // MD023 severity: 'error', @@ -140,19 +114,6 @@ export const baseConfig: BaseConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'single-trailing-newline': { - // MD047 - severity: 'error', - 'partial-markdown-files': true, - 'yml-files': false, - }, - 'emphasis-style': { - // MD049 - severity: 'error', - style: 'underscore', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'strong-style': { // MD050 severity: 'error', diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts index c20a684d9e86..edccc56973e0 100644 --- a/src/content-linter/style/github-docs.ts +++ b/src/content-linter/style/github-docs.ts @@ -1,31 +1,3 @@ -export const reportingConfig = { - // Always include all rules with these severities in automated weekly reports - includeSeverities: ['error'], - - // Specific rules to include regardless of severity - // Add rule names (short or long form) that should always be reported - includeRules: [ - 'GHD038', // expired-content - Content that has passed its expiration date - 'expired-content', - ], - - // Specific rules to exclude from CI and reports (overrides severity-based inclusion) - // Add rule names here if you want to suppress them from reports - excludeRules: [ - // Example: 'GHD030' // Uncomment to exclude code-fence-line-length warnings - 'british-english-quotes', // Exclude from reports but keep for pre-commit - ], - - // Files to exclude from specific rules in CI and reports - // Format: { 'rule-name': ['file/path/pattern1', 'file/path/pattern2'] } - excludeFilesFromRules: { - 'todocs-placeholder': [ - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md', - 'content/contributing/collaborating-on-github-docs/index.md', - ], - }, -} - const githubDocsConfig = { 'link-punctuation': { // GHD001 @@ -129,12 +101,6 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'code-fence-line-length': { - // GHD030 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'image-alt-text-exclude-words': { // GHD031 severity: 'error', @@ -153,12 +119,6 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'list-first-word-capitalization': { - // GHD034 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'rai-reusable-usage': { // GHD035 severity: 'error', @@ -226,25 +186,6 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'british-english-quotes': { - // GHD048 - severity: 'warning', - precommitSeverity: 'warning', // Show warnings locally for writer awareness - 'partial-markdown-files': true, - 'yml-files': true, - }, - 'note-warning-formatting': { - // GHD049 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, - 'multiple-emphasis-patterns': { - // GHD050 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'header-content-requirement': { // GHD053 severity: 'warning', @@ -312,12 +253,6 @@ export const githubDocsFrontmatterConfig = { 'partial-markdown-files': false, 'yml-files': false, }, - 'frontmatter-validation': { - // GHD055 - severity: 'warning', - 'partial-markdown-files': false, - 'yml-files': false, - }, 'frontmatter-landing-recommended': { // GHD056 severity: 'error', diff --git a/src/content-linter/tests/unit/british-english-quotes.ts b/src/content-linter/tests/unit/british-english-quotes.ts deleted file mode 100644 index 6423d7960eda..000000000000 --- a/src/content-linter/tests/unit/british-english-quotes.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { britishEnglishQuotes } from '../../lib/linting-rules/british-english-quotes' - -describe(britishEnglishQuotes.names.join(' - '), () => { - test('Correct American English punctuation passes', async () => { - const markdown = [ - 'She said, "Hello, world."', - 'The guide mentions "Getting started."', - 'See "[AUTOTITLE]."', - 'Zara replied, "That sounds great!"', - 'The section titled "Prerequisites," explains the setup.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('British English quotes with AUTOTITLE are flagged', async () => { - const markdown = [ - 'For more information, see "[AUTOTITLE]".', - 'The article "[AUTOTITLE]", covers this topic.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(1) - if (errors[0].detail) { - expect(errors[0].detail).toContain('place period inside the quotation marks') - } - expect(errors[1].lineNumber).toBe(2) - if (errors[1].detail) { - expect(errors[1].detail).toContain('place comma inside the quotation marks') - } - }) - - test('General British English punctuation patterns are detected', async () => { - const markdown = [ - 'Priya said "Hello".', - 'The tutorial called "Advanced Git", is helpful.', - 'Marcus mentioned "DevOps best practices".', - 'See the guide titled "Getting Started", for details.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(4) - if (errors[0].detail) { - expect(errors[0].detail).toContain('period inside') - } - if (errors[1].detail) { - expect(errors[1].detail).toContain('comma inside') - } - if (errors[2].detail) { - expect(errors[2].detail).toContain('period inside') - } - if (errors[3].detail) { - expect(errors[3].detail).toContain('comma inside') - } - }) - - test('Single quotes are also detected', async () => { - const markdown = [ - "Aisha said 'excellent work'.", - "The term 'API endpoint', refers to a specific URL.", - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(2) - if (errors[0].detail) { - expect(errors[0].detail).toContain('period inside') - } - if (errors[1].detail) { - expect(errors[1].detail).toContain('comma inside') - } - }) - - test('Code blocks and inline code are ignored', async () => { - const markdown = [ - '```javascript', - 'console.log("Hello");', - 'const message = "World";', - '```', - '', - 'In code, use `console.log("Debug");` for logging.', - 'The command `git commit -m "Fix bug";` creates a commit.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(0) - }) - - test('URLs and emails are ignored', async () => { - const markdown = [ - 'Visit https://example.com/api"docs" for more info.', - 'Email support@company.com"help" for assistance.', - 'The webhook URL http://api.service.com"endpoint" should work.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(0) - }) - - test('Auto-fix suggestions work correctly', async () => { - const markdown = [ - 'See "[AUTOTITLE]".', - 'The guide "Setup Instructions", explains everything.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail and fixInfo properties not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(2) - - // Check that fix info is provided - expect(errors[0].fixInfo).toBeDefined() - expect(errors[0].fixInfo.insertText).toContain('."') - expect(errors[1].fixInfo).toBeDefined() - expect(errors[1].fixInfo.insertText).toContain(',"') - }) - - test('Mixed punctuation scenarios', async () => { - const markdown = [ - 'Chen explained, "The process involves three steps". First, prepare the data.', - 'The error message "File not found", appears when the path is incorrect.', - 'As Fatima noted, "Testing is crucial"; quality depends on it.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(1) - expect(errors[1].lineNumber).toBe(2) - }) - - test('Nested quotes are handled appropriately', async () => { - const markdown = [ - 'She said, "The article \'Best Practices\', is recommended".', - 'The message "Error: \'Invalid input\'" appears sometimes.', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(1) - if (errors[0].detail) { - expect(errors[0].detail).toContain('period inside') - } - }) - - test('Edge cases with spacing', async () => { - const markdown = [ - 'The command "npm install" .', - 'See documentation "API Guide" , which covers authentication.', - 'Reference "[AUTOTITLE]" .', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(3) - if (errors[0].detail) { - expect(errors[0].detail).toContain('period inside') - } - if (errors[1].detail) { - expect(errors[1].detail).toContain('comma inside') - } - if (errors[2].detail) { - expect(errors[2].detail).toContain('period inside') - } - }) - - test('Autogenerated files are skipped', async () => { - const frontmatter = ['---', 'title: API Reference', 'autogenerated: rest', '---'].join('\n') - const markdown = ['The endpoint "GET /users", returns user data.', 'See "[AUTOTITLE]".'].join( - '\n', - ) - const result = await runRule(britishEnglishQuotes, { - strings: { - markdown: frontmatter + '\n' + markdown, - }, - }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Complex real-world examples', async () => { - const markdown = [ - '## Configuration Options', - '', - 'To enable the feature, set `enabled: true` in "config.yml".', - 'Aaliyah mentioned that the tutorial "Docker Basics", covers containers.', - 'The error "Permission denied", occurs when access is restricted.', - 'For troubleshooting, see "[AUTOTITLE]".', - '', - '```yaml', - 'name: "production"', - 'debug: false', - '```', - '', - 'Dmitri explained, "The workflow has multiple stages."', - ].join('\n') - const result = await runRule(britishEnglishQuotes, { strings: { markdown } }) - // Markdownlint error objects include detail property not in base LintError type - const errors = result.markdown as any[] - expect(errors.length).toBe(4) - expect(errors[0].lineNumber).toBe(3) // config.yml line - expect(errors[1].lineNumber).toBe(4) // Docker Basics line - expect(errors[2].lineNumber).toBe(5) // Permission denied line - expect(errors[3].lineNumber).toBe(6) // AUTOTITLE line - }) - - test('Warning severity is set correctly', () => { - expect(britishEnglishQuotes.severity).toBe('warning') - }) - - test('Rule has correct metadata', () => { - expect(britishEnglishQuotes.names).toEqual(['GHD048', 'british-english-quotes']) - expect(britishEnglishQuotes.description).toContain('American English style') - expect(britishEnglishQuotes.tags).toContain('punctuation') - expect(britishEnglishQuotes.tags).toContain('quotes') - }) -}) diff --git a/src/content-linter/tests/unit/code-fence-line-length.ts b/src/content-linter/tests/unit/code-fence-line-length.ts deleted file mode 100644 index 26961b8dea81..000000000000 --- a/src/content-linter/tests/unit/code-fence-line-length.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { codeFenceLineLength } from '../../lib/linting-rules/code-fence-line-length' - -describe(codeFenceLineLength.names.join(' - '), () => { - test('line length of max + 1 fails', async () => { - const markdown = [ - '```shell', - '111', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'bbb', - '```', - ].join('\n') - const result = await runRule(codeFenceLineLength, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(3) - expect(errors[0].errorRange).toEqual([1, 61]) - expect(errors[0].fixInfo).toBeNull() - }) - test('line length less than or equal to max length passes', async () => { - const markdown = [ - '```javascript', - '111', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - '```', - ].join('\n') - const result = await runRule(codeFenceLineLength, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - test('multiple lines in code block that exceed max length fail', async () => { - const markdown = [ - '```', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaccc', - '1', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb', - '```', - ].join('\n') - const result = await runRule(codeFenceLineLength, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(2) - expect(errors[1].lineNumber).toBe(4) - expect(errors[0].errorRange).toEqual([1, 61]) - expect(errors[1].errorRange).toEqual([1, 61]) - }) -}) diff --git a/src/content-linter/tests/unit/frontmatter-validation.ts b/src/content-linter/tests/unit/frontmatter-validation.ts deleted file mode 100644 index 30ce723e7a75..000000000000 --- a/src/content-linter/tests/unit/frontmatter-validation.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '@/content-linter/lib/init-test' -import { frontmatterValidation } from '@/content-linter/lib/linting-rules/frontmatter-validation' - -const ruleName = frontmatterValidation.names[1] - -// Configure the test fixture to not split frontmatter and content -const fmOptions = { markdownlintOptions: { frontMatter: null } } - -describe(ruleName, () => { - // Character limit tests - test('category title within limits passes', async () => { - const markdown = `--- -title: 'Short category title' -intro: 'Category introduction' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toEqual([]) - }) - - test('category title exceeds recommended limit shows warning', async () => { - const markdown = `--- -title: 'This category title is exactly 68 characters long for testing purpos' -shortTitle: 'Short title' -intro: 'Category introduction' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(1) - expect(result['content/section/index.md'][0].errorDetail).toContain( - 'exceeds recommended length of 67 characters', - ) - }) - - test('category title exceeds maximum limit shows error', async () => { - const markdown = `--- -title: 'This is exactly 71 characters long to exceed the maximum limit for catx' -shortTitle: 'Short title' -intro: 'Category introduction' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(1) - expect(result['content/section/index.md'][0].errorDetail).toContain( - 'exceeds maximum length of 70 characters', - ) - }) - - test('category shortTitle exceeds limit shows error', async () => { - const markdown = `--- -title: 'Category title' -shortTitle: 'This short title is exactly 31x' -intro: 'Category introduction' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(1) - expect(result['content/section/index.md'][0].errorDetail).toContain('ShortTitle exceeds') - }) - - test('mapTopic title within limits passes', async () => { - const markdown = `--- -title: 'Using workflows' -intro: 'Map topic introduction' -mapTopic: true ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/using-workflows/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/using-workflows/index.md']).toEqual([]) - }) - - test('mapTopic title exceeds recommended limit shows warning', async () => { - const markdown = `--- -title: 'This map topic title is exactly 64 characters long for tests now' -shortTitle: 'Short title' -intro: 'Map topic introduction' -mapTopic: true ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/using-workflows/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/using-workflows/index.md']).toHaveLength(1) - expect(result['content/actions/using-workflows/index.md'][0].errorDetail).toContain( - 'exceeds recommended length of 63 characters', - ) - }) - - test('article title within limits passes', async () => { - const markdown = `--- -title: 'GitHub Actions quickstart' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/quickstart.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/quickstart.md']).toEqual([]) - }) - - test('article title exceeds recommended limit shows warning', async () => { - const markdown = `--- -title: 'This article title is exactly 61 characters long for test now' -shortTitle: 'Short title' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/quickstart.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/quickstart.md']).toHaveLength(1) - expect(result['content/actions/quickstart.md'][0].errorDetail).toContain( - 'exceeds recommended length of 60 characters', - ) - }) - - test('article title exceeds maximum limit shows error', async () => { - const markdown = `--- -title: 'This article title is exactly 81 characters long to exceed the maximum limits now' -shortTitle: 'Short title' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/quickstart.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/quickstart.md']).toHaveLength(1) - expect(result['content/actions/quickstart.md'][0].errorDetail).toContain( - 'exceeds maximum length of 80 characters', - ) - }) - - test('cross-property validation: long title without shortTitle shows error', async () => { - const markdown = `--- -title: 'This article title is exactly 50 characters long' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/quickstart.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/quickstart.md']).toHaveLength(1) - expect(result['content/actions/quickstart.md'][0].errorDetail).toContain( - 'A shortTitle must be provided', - ) - }) - - test('cross-property validation: long title with shortTitle passes', async () => { - const markdown = `--- -title: 'This article title is exactly 50 characters long' -shortTitle: 'Actions quickstart' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/actions/quickstart.md': markdown }, - ...fmOptions, - }) - expect(result['content/actions/quickstart.md']).toEqual([]) - }) - - // Required properties tests - test('category with required intro passes', async () => { - const markdown = `--- -title: 'Category title' -intro: 'This is the category introduction.' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toEqual([]) - }) - - test('category without required intro fails', async () => { - const markdown = `--- -title: 'Category title' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(1) - expect(result['content/section/index.md'][0].errorDetail).toContain( - "Missing required property 'intro' for category content type", - ) - }) - - test('category with intro too long shows warning', async () => { - const longIntro = 'A'.repeat(400) // Exceeds 362 char limit - const markdown = `--- -title: 'Category title' -intro: '${longIntro}' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(1) - expect(result['content/section/index.md'][0].errorDetail).toContain( - 'Intro exceeds maximum length of 362 characters', - ) - }) - - test('mapTopic with required intro passes', async () => { - const markdown = `--- -title: 'Map topic title' -intro: 'This is the map topic introduction.' -mapTopic: true ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/topic.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/topic.md']).toEqual([]) - }) - - test('mapTopic without required intro fails', async () => { - const markdown = `--- -title: 'Map topic title' -mapTopic: true ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/topic.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/topic.md']).toHaveLength(1) - expect(result['content/section/topic.md'][0].errorDetail).toContain( - "Missing required property 'intro' for mapTopic content type", - ) - }) - - test('mapTopic with intro too long shows warning', async () => { - const longIntro = 'A'.repeat(400) // Exceeds 362 char limit - const markdown = `--- -title: 'Map topic title' -intro: '${longIntro}' -mapTopic: true ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/topic.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/topic.md']).toHaveLength(1) - expect(result['content/section/topic.md'][0].errorDetail).toContain( - 'Intro exceeds maximum length of 362 characters', - ) - }) - - test('article with required topics passes', async () => { - const markdown = `--- -title: 'Article title' -topics: - - Actions - - CI/CD ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toEqual([]) - }) - - test('article without required topics fails', async () => { - const markdown = `--- -title: 'Article title' ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain( - "Missing required property 'topics' for article content type", - ) - }) - - test('article with empty topics array fails', async () => { - const markdown = `--- -title: 'Article title' -topics: [] ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain( - 'Articles should have at least one topic', - ) - }) - - test('article with topics as string fails', async () => { - const markdown = `--- -title: 'Article title' -topics: 'Actions' ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain('Topics must be an array') - }) - - test('article with topics as number fails', async () => { - const markdown = `--- -title: 'Article title' -topics: 123 ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain('Topics must be an array') - }) - - test('article with intro too long shows warning', async () => { - const longIntro = 'A'.repeat(400) // Exceeds 354 char limit for articles - const markdown = `--- -title: 'Article title' -intro: '${longIntro}' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain( - 'Intro exceeds maximum length of 354 characters', - ) - }) - - test('article intro exceeds recommended but not maximum shows warning', async () => { - const mediumIntro = 'A'.repeat(300) // Exceeds 251 recommended but under 354 max - const markdown = `--- -title: 'Article title' -intro: '${mediumIntro}' -topics: - - Actions ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain( - 'Intro exceeds recommended length of 251 characters', - ) - }) - - // Combined validation tests - test('multiple violations show multiple errors', async () => { - const longIntro = 'A'.repeat(400) - const markdown = `--- -title: 'This is exactly 71 characters long to exceed the maximum limit for catx' -intro: '${longIntro}' -shortTitle: 'Short title' -children: - - /path/to/child ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': markdown }, - ...fmOptions, - }) - expect(result['content/section/index.md']).toHaveLength(2) - expect(result['content/section/index.md'][0].errorDetail).toContain('Title exceeds') - expect(result['content/section/index.md'][1].errorDetail).toContain('Intro exceeds') - }) - - test('no frontmatter passes', async () => { - const markdown = `# Content without frontmatter` - const result = await runRule(frontmatterValidation, { strings: { markdown }, ...fmOptions }) - expect(result.markdown).toEqual([]) - }) - - test('content type detection works correctly', async () => { - // Test category detection - const categoryMarkdown = `--- -title: 'Category' -intro: 'Category intro' -children: - - /child ---- -# Content -` - const categoryResult = await runRule(frontmatterValidation, { - strings: { 'content/section/index.md': categoryMarkdown }, - ...fmOptions, - }) - expect(categoryResult['content/section/index.md']).toEqual([]) - - // Test mapTopic detection - const mapTopicMarkdown = `--- -title: 'Map Topic' -intro: 'Map topic intro' -mapTopic: true ---- -# Content -` - const mapTopicResult = await runRule(frontmatterValidation, { - strings: { 'content/section/topic.md': mapTopicMarkdown }, - ...fmOptions, - }) - expect(mapTopicResult['content/section/topic.md']).toEqual([]) - - // Test article detection - const articleMarkdown = `--- -title: 'Article' -topics: - - Topic ---- -# Content -` - const articleResult = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': articleMarkdown }, - ...fmOptions, - }) - expect(articleResult['content/section/article.md']).toEqual([]) - }) - - // Liquid variable handling tests - test('title with liquid variables counts characters correctly', async () => { - const markdown = `--- -title: 'Getting started with {% data variables.product.prodname_github %}' -topics: - - GitHub ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - // 'Getting started with ' (21 chars) + liquid tag (0 chars) = 21 chars, should pass - expect(result['content/section/article.md']).toEqual([]) - }) - - test('intro with liquid variables counts characters correctly', async () => { - const markdown = `--- -title: 'Article title' -intro: 'Learn how to use {% data variables.product.prodname_copilot %} for {{ something }}' -topics: - - GitHub ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - // 'Learn how to use for ' (21 chars) should pass - expect(result['content/section/article.md']).toEqual([]) - }) - - test('shortTitle with liquid variables counts characters correctly', async () => { - const markdown = `--- -title: 'This article title is exactly fifty characters!!!!' -shortTitle: '{% data variables.product.prodname_copilot_short %}' -topics: - - GitHub ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - // Liquid tag should count as 0 characters, should pass - expect(result['content/section/article.md']).toEqual([]) - }) - - test('long text with liquid variables still fails when limit exceeded', async () => { - const longText = 'A'.repeat(70) // 70 chars - const markdown = `--- -title: '${longText} {% data variables.product.prodname_github %} extra text' -shortTitle: 'Short title' -topics: - - GitHub ---- -# Content -` - const result = await runRule(frontmatterValidation, { - strings: { 'content/section/article.md': markdown }, - ...fmOptions, - }) - // 70 A's + 1 space + 0 (liquid tag) + 1 space + 10 ('extra text') = 82 chars, should exceed 80 char limit for articles - expect(result['content/section/article.md']).toHaveLength(1) - expect(result['content/section/article.md'][0].errorDetail).toContain( - 'exceeds maximum length of 80 characters', - ) - }) -}) diff --git a/src/content-linter/tests/unit/lint-report-exclusions.ts b/src/content-linter/tests/unit/lint-report-exclusions.ts index a1b5f084f6dd..7879f14a3987 100644 --- a/src/content-linter/tests/unit/lint-report-exclusions.ts +++ b/src/content-linter/tests/unit/lint-report-exclusions.ts @@ -1,6 +1,15 @@ import { describe, expect, test } from 'vitest' -import { shouldIncludeResult } from '../../lib/helpers/should-include-result' -import { reportingConfig } from '../../style/github-docs' +import { getAllRuleNames } from '../../lib/helpers/rule-utils' + +// Use static config objects for testing to avoid Commander.js conflicts +const globalConfig = { + excludePaths: ['content/contributing/'], +} + +const reportingConfig = { + includeSeverities: ['error'], + includeRules: ['expired-content'], +} interface LintFlaw { severity: string @@ -8,159 +17,168 @@ interface LintFlaw { errorDetail?: string } -describe('lint report exclusions', () => { - // Helper function to simulate the reporting logic from lint-report.ts - function shouldIncludeInReport(flaw: LintFlaw, filePath: string): boolean { - if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) { - return false - } - - // First check exclusions using shared function - if (!shouldIncludeResult(flaw, filePath)) { - return false - } +describe('content linter configuration', () => { + describe('global path exclusions (lint-content.ts)', () => { + test('globalConfig.excludePaths is properly configured', () => { + expect(globalConfig.excludePaths).toBeDefined() + expect(Array.isArray(globalConfig.excludePaths)).toBe(true) + expect(globalConfig.excludePaths).toContain('content/contributing/') + }) - // Extract all possible rule names including sub-rules from search-replace - const allRuleNames = [...flaw.ruleNames] - if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) { - const match = flaw.errorDetail.match(/^([^:]+):/) - if (match) { - allRuleNames.push(match[1]) + test('simulates path exclusion logic', () => { + // Simulate the cleanPaths function logic from lint-content.ts + function isPathExcluded(filePath: string): boolean { + return globalConfig.excludePaths.some((excludePath) => filePath.startsWith(excludePath)) } - } - // Apply reporting-specific filtering - // Check if severity should be included - if (reportingConfig.includeSeverities.includes(flaw.severity)) { - return true - } + // Files in contributing directory should be excluded + expect(isPathExcluded('content/contributing/README.md')).toBe(true) + expect(isPathExcluded('content/contributing/how-to-contribute.md')).toBe(true) + expect(isPathExcluded('content/contributing/collaborating-on-github-docs/file.md')).toBe(true) - // Check if any rule name is in the include list - const hasIncludedRule = allRuleNames.some((ruleName) => - reportingConfig.includeRules.includes(ruleName), - ) - if (hasIncludedRule) { - return true - } - - return false - } + // Files outside contributing directory should not be excluded + expect(isPathExcluded('content/actions/README.md')).toBe(false) + expect(isPathExcluded('content/copilot/getting-started.md')).toBe(false) + expect(isPathExcluded('data/variables/example.yml')).toBe(false) - test('TODOCS placeholder errors are excluded for documentation file', () => { - const flaw = { - severity: 'error', - ruleNames: ['search-replace'], - errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.', - } + // Edge case: partial matches should not be excluded + expect(isPathExcluded('content/contributing-guide.md')).toBe(false) + }) + }) - const excludedFilePath = - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md' - const regularFilePath = 'content/some-other-article.md' + describe('report filtering (lint-report.ts)', () => { + // Helper function that matches the actual logic in lint-report.ts + function shouldIncludeInReport(flaw: LintFlaw): boolean { + const allRuleNames = getAllRuleNames(flaw) - // Should be excluded for the specific documentation file - expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(false) + // Check if severity should be included + if (reportingConfig.includeSeverities.includes(flaw.severity)) { + return true + } - // Should still be included for other files - expect(shouldIncludeInReport(flaw, regularFilePath)).toBe(true) - }) + // Check if any rule name is in the include list that overrides severity + const hasIncludedRule = allRuleNames.some((ruleName: string) => + reportingConfig.includeRules.includes(ruleName), + ) + if (hasIncludedRule) { + return true + } - test('TODOCS placeholder errors are excluded with different path formats', () => { - const flaw = { - severity: 'error', - ruleNames: ['search-replace'], - errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.', + return false } - // Test various path formats that should match - const pathVariants = [ - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md', - './content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md', - '/absolute/path/content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md', - ] - - pathVariants.forEach((path) => { - expect(shouldIncludeInReport(flaw, path)).toBe(false) + test('reportingConfig is properly structured', () => { + expect(reportingConfig.includeSeverities).toBeDefined() + expect(Array.isArray(reportingConfig.includeSeverities)).toBe(true) + expect(reportingConfig.includeRules).toBeDefined() + expect(Array.isArray(reportingConfig.includeRules)).toBe(true) }) - }) - - test('other rules are not affected by TODOCS file exclusions', () => { - const flaw = { - severity: 'error', - ruleNames: ['docs-domain'], - } - const excludedFilePath = - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md' + test('includes errors by default (severity-based filtering)', () => { + const errorFlaw = { + severity: 'error', + ruleNames: ['some-rule'], + } - // Should still be included for other rules even in the excluded file - expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(true) - }) + expect(shouldIncludeInReport(errorFlaw)).toBe(true) + }) - test('multiple rule names with mixed exclusions', () => { - const flaw = { - severity: 'error', - ruleNames: ['search-replace', 'docs-domain'], - errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.', - } + test('excludes warnings by default (severity-based filtering)', () => { + const warningFlaw = { + severity: 'warning', + ruleNames: ['some-rule'], + } - const excludedFilePath = - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md' + expect(shouldIncludeInReport(warningFlaw)).toBe(false) + }) - // Should be excluded because one of the rules (todocs-placeholder) is excluded for this file - expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(false) - }) + test('includes specific rules regardless of severity', () => { + const expiredContentWarning = { + severity: 'warning', + ruleNames: ['expired-content'], + } - test('exclusion configuration exists and is properly structured', () => { - expect(reportingConfig.excludeFilesFromRules).toBeDefined() - expect(reportingConfig.excludeFilesFromRules['todocs-placeholder']).toBeDefined() - expect(Array.isArray(reportingConfig.excludeFilesFromRules['todocs-placeholder'])).toBe(true) - expect( - reportingConfig.excludeFilesFromRules['todocs-placeholder'].includes( - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md', - ), - ).toBe(true) - }) + // Should be included because expired-content is in includeRules + expect(shouldIncludeInReport(expiredContentWarning)).toBe(true) + }) - describe('shared shouldIncludeResult function', () => { - test('excludes TODOCS placeholder errors for specific file', () => { - const flaw = { - severity: 'error', + test('handles search-replace sub-rules correctly', () => { + const searchReplaceFlaw = { + severity: 'warning', ruleNames: ['search-replace'], errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.', } - const excludedFilePath = - 'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md' - const regularFilePath = 'content/some-other-article.md' + // Should extract 'todocs-placeholder' as a rule name and check against includeRules + // This will depend on your actual includeRules configuration + const result = shouldIncludeInReport(searchReplaceFlaw) + expect(typeof result).toBe('boolean') + }) - // Should be excluded for the specific documentation file - expect(shouldIncludeResult(flaw, excludedFilePath)).toBe(false) + test('handles missing errorDetail gracefully for search-replace', () => { + const searchReplaceFlawNoDetail = { + severity: 'warning', + ruleNames: ['search-replace'], + // no errorDetail + } - // Should be included for other files - expect(shouldIncludeResult(flaw, regularFilePath)).toBe(true) + // Should not throw an error and return false (warning not in includeSeverities) + expect(shouldIncludeInReport(searchReplaceFlawNoDetail)).toBe(false) }) - test('includes flaws by default when no exclusions apply', () => { - const flaw = { + test('rule extraction logic works correctly', () => { + const regularFlaw = { severity: 'error', - ruleNames: ['some-other-rule'], + ruleNames: ['docs-domain'], } + expect(getAllRuleNames(regularFlaw)).toEqual(['docs-domain']) - const filePath = 'content/some-article.md' - - expect(shouldIncludeResult(flaw, filePath)).toBe(true) - }) - - test('handles missing errorDetail gracefully', () => { - const flaw = { + const searchReplaceFlaw = { severity: 'error', ruleNames: ['search-replace'], - // no errorDetail + errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.', } + expect(getAllRuleNames(searchReplaceFlaw)).toEqual(['search-replace', 'todocs-placeholder']) + + const multipleRulesFlaw = { + severity: 'error', + ruleNames: ['search-replace', 'another-rule'], + errorDetail: 'docs-domain: Some error message.', + } + expect(getAllRuleNames(multipleRulesFlaw)).toEqual([ + 'search-replace', + 'another-rule', + 'docs-domain', + ]) + }) + }) + + describe('integration between systems', () => { + test('path exclusions happen before report filtering', () => { + // This is a conceptual test - in practice, files excluded by globalConfig.excludePaths + // never reach the reporting stage, so they never get filtered by reportingConfig + + // Files in excluded paths should never be linted at all + const isExcluded = (path: string) => + globalConfig.excludePaths.some((excludePath) => path.startsWith(excludePath)) + + expect(isExcluded('content/contributing/some-file.md')).toBe(true) + + // If a file is excluded at the path level, it doesn't matter what the reportingConfig says + // because the file will never be processed for linting in the first place + }) + + test('configurations are independent', () => { + // globalConfig handles what gets linted + expect(globalConfig.excludePaths).toBeDefined() - const filePath = 'content/some-article.md' + // reportingConfig handles what gets reported + expect(reportingConfig.includeSeverities).toBeDefined() + expect(reportingConfig.includeRules).toBeDefined() - expect(shouldIncludeResult(flaw, filePath)).toBe(true) + // They should not overlap or depend on each other + expect(globalConfig).not.toHaveProperty('includeSeverities') + expect(reportingConfig).not.toHaveProperty('excludePaths') }) }) }) diff --git a/src/content-linter/tests/unit/list-first-word-captitalization.ts b/src/content-linter/tests/unit/list-first-word-captitalization.ts deleted file mode 100644 index b3804634ed32..000000000000 --- a/src/content-linter/tests/unit/list-first-word-captitalization.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { listFirstWordCapitalization } from '../../lib/linting-rules/list-first-word-capitalization' - -describe(listFirstWordCapitalization.names.join(' - '), () => { - test('ensure multi-level lists catch incorrect capitalization errors', async () => { - const markdown = [ - '- List item', - ' - `list` item', - ' - list item', - '1. number item', - '1. Number 2 item', - '- `X` item', - '- always start `with code`', - '- remember to go to [foo](/bar)', - ].join('\n') - const result = await runRule(listFirstWordCapitalization, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(4) - expect(errors[0].errorRange).toEqual([7, 4]) - expect(errors[0].lineNumber).toBe(3) - expect(errors[0].fixInfo).toEqual({ - deleteCount: 1, - editColumn: 7, - insertText: 'L', - lineNumber: 3, - }) - expect(errors[1].errorRange).toEqual([4, 6]) - expect(errors[1].lineNumber).toBe(4) - expect(errors[1].fixInfo).toEqual({ - deleteCount: 1, - editColumn: 4, - insertText: 'N', - lineNumber: 4, - }) - }) - - test('list items that start with special characters pass', async () => { - const markdown = [ - '- `X-GitHub-Event`: Name of the event that triggered the delivery.', - '- **October 1, 2018**: GitHub discontinued allowing users to install services. We removed GitHub Services from the GitHub.com user interface.', - '- **boldness** is a cool thing', - '- Always start `with code`', - '- Remember to go to [foo](/bar)', - '- **{% data variables.product.prodname_oauth_apps %}**: Request either the `repo_hook` and/or `org_hook` scope(s) to manage the relevant events on behalf of users.', - '- "[AUTOTITLE](/billing/managing-billing-for-github-marketplace-apps)"', - "- '[AUTOTITLE](/billing/managing-billing-for-github-marketplace-apps)'", - '- [Viewing your sponsors and sponsorships](/sponsors/receiving-sponsorships-through-github-sponsors/viewing-your-sponsors-and-sponsorships)', - '- macOS', - '- [{% data variables.actions.test %}](/apple/test)', - '- {{ foo }} for example', - '- {% data variables.product.prodname_dotcom_the_website %} Services Continuity and Incident Management Plan', - '- {% data variables.product.prodname_dotcom_the_website %} Services Continuity and Incident Management Plan', - '- x64', - '- @mention your friends', - '- @hash tags', - '- 05:00', - '- "keyword" starts with a quotation sign', - ].join('\n') - const result = await runRule(listFirstWordCapitalization, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test("list items that aren't simple lists", async () => { - const markdown = ['- > Blockquote in a list', '- ### Heading in a list'].join('\n') - const result = await runRule(listFirstWordCapitalization, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('works on markdown that has no lists at all, actually', async () => { - const markdown = '- \n' - const result = await runRule(listFirstWordCapitalization, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('skips site-policy directory files', async () => { - const markdown = [ - '- list item should normally be flagged', - '- another uncapitalized item', - '- a. this is alphabetic numbering', - '- b. this is also alphabetic numbering', - ].join('\n') - - // Test normal behavior (should flag errors) - const normalResult = await runRule(listFirstWordCapitalization, { strings: { markdown } }) - expect(normalResult.markdown.length).toBeGreaterThan(0) - - // Test site-policy exclusion (should skip all errors) - const sitePolicyResult = await runRule(listFirstWordCapitalization, { - strings: { - 'content/site-policy/some-policy.md': markdown, - }, - }) - expect(sitePolicyResult['content/site-policy/some-policy.md'].length).toBe(0) - }) -}) diff --git a/src/content-linter/tests/unit/multiple-emphasis-patterns.ts b/src/content-linter/tests/unit/multiple-emphasis-patterns.ts deleted file mode 100644 index b8d642339f08..000000000000 --- a/src/content-linter/tests/unit/multiple-emphasis-patterns.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { multipleEmphasisPatterns } from '../../lib/linting-rules/multiple-emphasis-patterns' - -describe(multipleEmphasisPatterns.names.join(' - '), () => { - test('Single emphasis types pass', async () => { - const markdown = [ - 'This is **bold text** that is fine.', - 'This is *italic text* that is okay.', - 'This is `code text` that is acceptable.', - 'This is a SCREAMING_CASE_WORD that is allowed.', - 'This is __bold with underscores__ that works.', - 'This is _italic with underscores_ that works.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Multiple emphasis types in same string are flagged', async () => { - const markdown = [ - 'This is **bold and `code`** in the same string.', - 'This is ***bold and italic*** combined.', - 'This is `code with **bold**` inside.', - 'This is ___bold and italic___ with underscores.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(4) - expect(errors[0].lineNumber).toBe(1) - expect(errors[1].lineNumber).toBe(2) - expect(errors[2].lineNumber).toBe(3) - expect(errors[3].lineNumber).toBe(4) - }) - - test('Nested emphasis patterns are flagged', async () => { - const markdown = [ - 'This is **bold with `code` inside**.', - 'This is `code with **bold** nested`.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(1) - expect(errors[1].lineNumber).toBe(2) - }) - - test('Separate emphasis patterns on same line pass', async () => { - const markdown = [ - 'This is **bold** and this is *italic* but separate.', - 'Here is `code` and here is UPPERCASE but apart.', - 'First **bold**, then some text, then *italic*.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Code blocks are ignored', async () => { - const markdown = [ - '```javascript', - 'const text = "**bold** and `code` mixed";', - 'const more = "***triple emphasis***";', - '```', - '', - ' // Indented code block', - ' const example = "**bold** with `code`";', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Inline code prevents other emphasis detection', async () => { - const markdown = [ - 'Use `**bold**` to make text bold.', - 'The `*italic*` syntax creates italic text.', - 'Type `__bold__` for bold formatting.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(2) // Code with bold inside is detected - }) - - test('Complex mixed emphasis patterns', async () => { - const markdown = [ - 'This is **bold and `code`** mixed.', - 'Here is ***bold italic*** combined.', - 'Text with __bold and `code`__ together.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(3) - expect(errors[0].lineNumber).toBe(1) - expect(errors[1].lineNumber).toBe(2) - expect(errors[2].lineNumber).toBe(3) - }) - - test('Edge case: adjacent emphasis without overlap passes', async () => { - const markdown = [ - 'This is **bold**_italic_ adjacent but not overlapping.', - 'Here is `code`**bold** touching but separate.', - 'Text with UPPERCASE**bold** next to each other.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Triple asterisk bold+italic is flagged', async () => { - const markdown = [ - 'This is ***bold and italic*** combined.', - 'Here is ___bold and italic___ with underscores.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(1) - expect(errors[1].lineNumber).toBe(2) - }) - - test('Mixed adjacent emphasis types are allowed', async () => { - const markdown = [ - 'This has **bold** and normal text.', - 'This has **bold** and other text.', - 'The API key and **configuration** work.', - 'The API key and **setup** process.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Autogenerated files are skipped', async () => { - const frontmatter = ['---', 'title: API Reference', 'autogenerated: rest', '---'].join('\n') - const markdown = [ - 'This is **bold and `code`** mixed.', - 'This is ***bold italic*** combined.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { - strings: { - markdown: frontmatter + '\n' + markdown, - }, - }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Links with emphasis are handled correctly', async () => { - const markdown = [ - 'See [**bold link**](http://example.com) for details.', - 'Check [*italic link*](http://example.com) here.', - 'Visit [`code link`](http://example.com) for info.', - 'Go to [**bold and `code`**](http://example.com) - should be flagged.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(4) - }) - - test('Headers with emphasis are checked', async () => { - const markdown = [ - '# This is **bold** header', - '## This is *italic* header', - '### This is **bold and `code`** header', - '#### This is normal header', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(3) - }) - - test('List items with emphasis are checked', async () => { - const markdown = [ - '- This is **bold** item', - '- This is *italic* item', - '- This is **bold and `code`** item', - '1. This is numbered **bold** item', - '2. This is numbered ***bold italic*** item', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(3) - expect(errors[1].lineNumber).toBe(5) - }) - - test('Escaped emphasis characters are ignored', async () => { - const markdown = [ - 'This has \\*\\*escaped\\*\\* asterisks.', - 'This has \\`escaped\\` backticks.', - 'This has \\_escaped\\_ underscores.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Rule has correct metadata', () => { - expect(multipleEmphasisPatterns.names).toEqual(['GHD050', 'multiple-emphasis-patterns']) - expect(multipleEmphasisPatterns.description).toContain('emphasis') - expect(multipleEmphasisPatterns.tags).toContain('formatting') - expect(multipleEmphasisPatterns.tags).toContain('emphasis') - expect(multipleEmphasisPatterns.tags).toContain('style') - expect(multipleEmphasisPatterns.severity).toBe('warning') - }) - - test('Empty content does not cause errors', async () => { - const markdown = ['', ' ', '\t'].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Single character emphasis is handled', async () => { - const markdown = [ - 'This is **a** single letter.', - 'This is *b* single letter.', - 'This is `c` single letter.', - 'This is **a** and *b* separate.', - 'This is **`x`** nested single chars.', - ].join('\n') - const result = await runRule(multipleEmphasisPatterns, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) // Nested single chars still flagged - expect(errors[0].lineNumber).toBe(5) - }) -}) diff --git a/src/content-linter/tests/unit/note-warning-formatting.ts b/src/content-linter/tests/unit/note-warning-formatting.ts deleted file mode 100644 index c2451553e0ca..000000000000 --- a/src/content-linter/tests/unit/note-warning-formatting.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { noteWarningFormatting } from '../../lib/linting-rules/note-warning-formatting' - -describe(noteWarningFormatting.names.join(' - '), () => { - test('Correctly formatted legacy notes pass', async () => { - const markdown = [ - 'This is a paragraph.', - '', - '{% note %}', - '', - '**Note:** This is a properly formatted note.', - '', - '{% endnote %}', - '', - 'Another paragraph follows.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Correctly formatted new-style callouts pass', async () => { - const markdown = [ - 'This is a paragraph.', - '', - '> [!NOTE]', - '> This is a properly formatted callout note.', - '', - 'Another paragraph follows.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Missing line break before legacy note is flagged', async () => { - const markdown = [ - 'This is a paragraph.', - '{% note %}', - '**Note:** This note needs a line break before it.', - '{% endnote %}', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(2) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Add a blank line before {% note %}') - } - }) - - test('Missing line break after legacy note is flagged', async () => { - const markdown = [ - '', - '{% note %}', - '**Note:** This note needs a line break after it.', - '{% endnote %}', - 'This paragraph is too close.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(4) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Add a blank line after {% endnote %}') - } - }) - - test('Missing line break before new-style callout is flagged', async () => { - const markdown = [ - 'This is a paragraph.', - '> [!WARNING]', - '> This warning needs a line break before it.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(2) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Add a blank line before > [!WARNING]') - } - }) - - test('Missing line break after new-style callout is flagged', async () => { - const markdown = [ - '', - '> [!DANGER]', - '> This danger callout needs a line break after it.', - 'This paragraph is too close.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(4) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Add a blank line after > [!DANGER]') - } - }) - - test('Too many bullet points in legacy note is flagged', async () => { - const markdown = [ - '', - '{% note %}', - '', - '**Note:** This note has too many bullets:', - '', - '* First bullet point', - '* Second bullet point', - '* Third bullet point (this should be flagged)', - '', - '{% endnote %}', - '', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(8) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Do not include more than 2 bullet points') - } - }) - - test('Too many bullet points in new-style callout is flagged', async () => { - const markdown = [ - '', - '> [!NOTE]', - '> This callout has too many bullets:', - '>', - '> * First bullet point', - '> * Second bullet point', - '> * Third bullet point (this should be flagged)', - '', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(7) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Do not include more than 2 bullet points') - } - }) - - test('Missing prefix in legacy note is flagged and fixable', async () => { - const markdown = [ - '', - '{% note %}', - '', - 'This note is missing the proper prefix.', - '', - '{% endnote %}', - '', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(4) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('should start with **Note:**') - } - expect(errors[0].fixInfo).toBeDefined() - if (errors[0].fixInfo) { - expect(errors[0].fixInfo?.insertText).toBe('**Note:** ') - } - }) - - test('Orphaned note prefix outside callout is flagged', async () => { - const markdown = [ - 'This is a regular paragraph.', - '', - '**Note:** This note prefix should be inside a callout block.', - '', - 'Another paragraph.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(3) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('should be inside a callout block') - } - }) - - test('Orphaned warning prefix outside callout is flagged', async () => { - const markdown = [ - 'Regular content here.', - '', - '**Warning:** This warning should be in a proper callout.', - '', - 'More content.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(3) - if (errors[0].errorDetail) { - expect(errors[0].errorDetail).toContain('Warning prefix should be inside a callout block') - } - }) - - test('Feedback forms in legacy notes are not flagged for missing prefix', async () => { - const markdown = [ - '', - '{% note %}', - '', - 'Did you successfully complete this task?', - '', - 'Yes', - '', - '{% endnote %}', - '', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - // Should only flag missing line breaks, not missing prefix for feedback forms - expect(errors.length).toBe(0) - }) - - test('Multiple formatting issues are all caught', async () => { - const markdown = [ - 'Paragraph without break.', - '{% note %}', - 'Missing prefix and has bullets:', - '* First bullet', - '* Second bullet', - '* Third bullet (too many)', - '{% endnote %}', - 'No break after note.', - '', - '**Danger:** Orphaned danger prefix.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(5) - - // Check we get all expected error types by line numbers and error count - const errorLines = errors.map((e) => e.lineNumber).sort((a, b) => a - b) - expect(errorLines).toEqual([2, 3, 6, 7, 10]) - - // Verify we have the expected number of different types of errors: - // 1. Missing line break before note (line 2) - // 2. Missing prefix in note content (line 3) - // 3. Too many bullet points (line 6) - // 4. Missing line break after note (line 7) - // 5. Orphaned danger prefix (line 10) - expect(errors.length).toBe(5) - }) - - test('Mixed legacy and new-style callouts work correctly', async () => { - const markdown = [ - 'Some content.', - '', - '{% note %}', - '**Note:** This is a legacy note.', - '{% endnote %}', - '', - 'More content.', - '', - '> [!WARNING]', - '> This is a new-style warning.', - '', - 'Final content.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Different callout types are handled correctly', async () => { - const markdown = [ - '', - '> [!NOTE]', - '> This is a note callout.', - '', - '> [!WARNING]', - '> This is a warning callout.', - '', - '> [!DANGER]', - '> This is a danger callout.', - '', - ].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Autogenerated files are skipped', async () => { - const frontmatter = ['---', 'title: API Reference', 'autogenerated: rest', '---'].join('\n') - const markdown = [ - 'Content.', - '{% note %}', - 'Badly formatted note.', - '{% endnote %}', - 'More content.', - ].join('\n') - const result = await runRule(noteWarningFormatting, { - strings: { - markdown: frontmatter + '\n' + markdown, - }, - }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Empty notes and callouts do not cause errors', async () => { - const markdown = ['', '{% note %}', '', '{% endnote %}', '', '> [!NOTE]', '>', ''].join('\n') - const result = await runRule(noteWarningFormatting, { strings: { markdown } }) - const errors = result.markdown - expect(errors.length).toBe(0) - }) - - test('Warning severity is set correctly', () => { - expect(noteWarningFormatting.severity).toBe('warning') - }) - - test('Rule has correct metadata', () => { - expect(noteWarningFormatting.names).toEqual(['GHD049', 'note-warning-formatting']) - expect(noteWarningFormatting.description).toContain('style guide') - expect(noteWarningFormatting.tags).toContain('callouts') - expect(noteWarningFormatting.tags).toContain('notes') - expect(noteWarningFormatting.tags).toContain('warnings') - }) -}) diff --git a/src/content-linter/tests/unit/outdated-release-phase-terminology.ts b/src/content-linter/tests/unit/outdated-release-phase-terminology.ts index 166a0126140c..d986c769b885 100644 --- a/src/content-linter/tests/unit/outdated-release-phase-terminology.ts +++ b/src/content-linter/tests/unit/outdated-release-phase-terminology.ts @@ -145,7 +145,7 @@ describe(outdatedReleasePhaseTerminology.names.join(' - '), () => { const markdown = ['This feature is in beta.'].join('\n') const result = await runRule(outdatedReleasePhaseTerminology, { strings: { - markdown: frontmatter + '\n' + markdown, + markdown: `${frontmatter}\n${markdown}`, }, }) const errors = result.markdown diff --git a/src/content-linter/tests/unit/rule-filtering.ts b/src/content-linter/tests/unit/rule-filtering.ts index 606ee95e7d19..22cebe280f97 100644 --- a/src/content-linter/tests/unit/rule-filtering.ts +++ b/src/content-linter/tests/unit/rule-filtering.ts @@ -19,8 +19,8 @@ vi.mock('../../lib/helpers/get-rules', () => ({ description: 'Headers must have content below them', }, { - names: ['GHD030', 'code-fence-line-length'], - description: 'Code fence content should not exceed line length limit', + names: ['GHD001', 'link-punctuation'], + description: 'Internal link titles must not contain punctuation', }, ], allConfig: {}, @@ -41,12 +41,12 @@ describe('shouldIncludeRule', () => { test('includes custom rule by short code', () => { expect(shouldIncludeRule('header-content-requirement', ['GHD053'])).toBe(true) - expect(shouldIncludeRule('code-fence-line-length', ['GHD030'])).toBe(true) + expect(shouldIncludeRule('link-punctuation', ['GHD001'])).toBe(true) }) test('excludes rule not in list', () => { expect(shouldIncludeRule('heading-increment', ['MD002'])).toBe(false) - expect(shouldIncludeRule('header-content-requirement', ['GHD030'])).toBe(false) + expect(shouldIncludeRule('header-content-requirement', ['GHD001'])).toBe(false) }) test('handles multiple rules', () => { diff --git a/src/content-render/liquid/octicon.ts b/src/content-render/liquid/octicon.ts index 3a9e1c72c5eb..88bab333cf8d 100644 --- a/src/content-render/liquid/octicon.ts +++ b/src/content-render/liquid/octicon.ts @@ -18,7 +18,7 @@ interface OcticonsMatch { } const OptionsSyntax = /([a-zA-Z-]+)="([\w\s-]+)"*/g -const Syntax = new RegExp('"(?[a-zA-Z-]+)"(?(?:\\s' + OptionsSyntax.source + ')*)') +const Syntax = new RegExp(`"(?[a-zA-Z-]+)"(?(?:\\s${OptionsSyntax.source})*)`) const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "" ' /** diff --git a/src/content-render/scripts/move-content.ts b/src/content-render/scripts/move-content.ts index 018b98b57dcf..2a699d7c00e1 100755 --- a/src/content-render/scripts/move-content.ts +++ b/src/content-render/scripts/move-content.ts @@ -268,7 +268,7 @@ function makeHref(root, filePath) { } else { nameSplit.push(nameSplit.pop().replace(/\.md$/, '')) } - return '/' + nameSplit.join('/') + return `/${nameSplit.join('/')}` } function moveFolder(oldPath, newPath, files, opts) { diff --git a/src/content-render/scripts/render-content-markdown.ts b/src/content-render/scripts/render-content-markdown.ts index a7d3d8cb296e..8e976014d324 100755 --- a/src/content-render/scripts/render-content-markdown.ts +++ b/src/content-render/scripts/render-content-markdown.ts @@ -1,6 +1,5 @@ import fs from 'fs' import path from 'path' -import { execSync } from 'child_process' import { renderLiquid } from '@/content-render/liquid/index' import shortVersionsMiddleware from '@/versions/middleware/short-versions' @@ -83,7 +82,4 @@ for (const page of pages) { console.log(err) } } -console.log('---\nWriting files done. Now linting content...\n') -// Content linter to remove any blank lines -execSync('npm run lint-content -- --paths content-copilot --rules no-multiple-blanks --fix') console.log(`Finished - content is available in: ${contentCopilotDir}`) diff --git a/src/content-render/scripts/reusables-cli/find/unused.ts b/src/content-render/scripts/reusables-cli/find/unused.ts index e3d0e893d706..82feb590ab87 100644 --- a/src/content-render/scripts/reusables-cli/find/unused.ts +++ b/src/content-render/scripts/reusables-cli/find/unused.ts @@ -28,7 +28,7 @@ export function findUnused({ absolute }: { absolute: boolean }) { (name === 'data' || name === 'indented_data_reference') && args.startsWith('reusables.') ) { - const reusableName = path.join('data', ...args.split(' ')[0].split('.')) + '.md' + const reusableName = `${path.join('data', ...args.split(' ')[0].split('.'))}.md` // Special cases where we don't want them to count as reusables. It's an example in a how-to doc if ( reusableName.includes('foo/bar.md') || diff --git a/src/content-render/scripts/reusables-cli/find/used.ts b/src/content-render/scripts/reusables-cli/find/used.ts index c6815cf501d3..24e1851a1a68 100644 --- a/src/content-render/scripts/reusables-cli/find/used.ts +++ b/src/content-render/scripts/reusables-cli/find/used.ts @@ -65,7 +65,7 @@ export function findTopUsed(numberOfMostUsedToFind: number, { absolute }: { abso console.log(`\nTop ${numberOfMostUsedToFind} most used reusables:`) let i = 0 for (const [reusable, count] of sortedCounts.slice(0, numberOfMostUsedToFind)) { - let printReusablePath = path.join('data', ...reusable.split('.')) + '.md' + let printReusablePath = `${path.join('data', ...reusable.split('.'))}.md` if (absolute) { printReusablePath = path.resolve(printReusablePath) } diff --git a/src/content-render/scripts/test-moved-content.ts b/src/content-render/scripts/test-moved-content.ts index 3916b9158de4..7b5dcac10151 100644 --- a/src/content-render/scripts/test-moved-content.ts +++ b/src/content-render/scripts/test-moved-content.ts @@ -31,7 +31,7 @@ async function main(nameTuple: [string, string]) { const parentIndexMd = path.join(path.dirname(after), 'index.md') const fileContent = fs.readFileSync(parentIndexMd, 'utf-8') const { data } = readFrontmatter(fileContent) - const afterShortname = '/' + after.split('/').slice(-1)[0].replace(/\.md$/, '') + const afterShortname = `/${after.split('/').slice(-1)[0].replace(/\.md$/, '')}` if (data) assert(data.children.includes(afterShortname), `Child ${afterShortname} not found`) } } else { @@ -43,7 +43,7 @@ async function main(nameTuple: [string, string]) { const parentIndexMd = path.join(path.dirname(after), 'index.md') const fileContent = fs.readFileSync(parentIndexMd, 'utf-8') const { data } = readFrontmatter(fileContent) - const afterShortname = '/' + after.split('/').slice(-1) + const afterShortname = `/${after.split('/').slice(-1)}` if (data) assert(data.children.includes(afterShortname), `Child ${afterShortname} not found`) } } @@ -57,5 +57,5 @@ function makeHref(root: string, filePath: string) { const last = nameSplit.pop() if (last) nameSplit.push(last.replace(/\.md$/, '')) } - return '/' + nameSplit.join('/') + return `/${nameSplit.join('/')}` } diff --git a/src/content-render/unified/alerts.ts b/src/content-render/unified/alerts.ts index dd9e3ecc7898..5fe2436a7dad 100644 --- a/src/content-render/unified/alerts.ts +++ b/src/content-render/unified/alerts.ts @@ -43,7 +43,7 @@ export default function alerts({ alertTitles = {} }: { alertTitles?: Record { // Playwright will cache this redirect, so we need to add something // to "cache bust" the URL const cb = `?cb=${Math.random()}` - await page.goto('/get-started/start-your-journey/hello-world' + cb) - await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world' + cb) + await page.goto(`/get-started/start-your-journey/hello-world${cb}`) + await expect(page).toHaveURL(`/ja/get-started/start-your-journey/hello-world${cb}`) // If you go, with the Japanese cookie, to the English page directly, // it will offer a link to the Japanese URL in a banner. diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index cf96acfe6490..a23a377995c7 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -1,3 +1,4 @@ +import React from 'react' import Head from 'next/head' import { useRouter } from 'next/router' @@ -63,7 +64,7 @@ export const DefaultLayout = (props: Props) => { const metaDescription = page.introPlainText ? page.introPlainText : t('default_description') const SOCIAL_CATEGORIES = new Set(['code-security', 'actions', 'issues', 'copilot']) - const SOCIAL_CARD_IMG_BASE_URL = `${xHost ? 'https://' + xHost : ''}/assets/cb-345/images/social-cards` + const SOCIAL_CARD_IMG_BASE_URL = `${xHost ? `https://${xHost}` : ''}/assets/cb-345/images/social-cards` function getCategoryImageUrl(category: string): string { return `${SOCIAL_CARD_IMG_BASE_URL}/${category}.png` diff --git a/src/frame/components/article/ArticleTitle.tsx b/src/frame/components/article/ArticleTitle.tsx index a8c3a0185c27..e9ebc3ef6864 100644 --- a/src/frame/components/article/ArticleTitle.tsx +++ b/src/frame/components/article/ArticleTitle.tsx @@ -1,3 +1,5 @@ +import React from 'react' + type Props = { children: React.ReactNode } diff --git a/src/frame/components/article/HeadingLink.tsx b/src/frame/components/article/HeadingLink.tsx index 7e4c4e56e0c3..1b7280159e40 100644 --- a/src/frame/components/article/HeadingLink.tsx +++ b/src/frame/components/article/HeadingLink.tsx @@ -1,4 +1,5 @@ import GithubSlugger from 'github-slugger' +import type { JSX } from 'react' const slugger = new GithubSlugger() diff --git a/src/frame/components/context/ArticleContext.tsx b/src/frame/components/context/ArticleContext.tsx index 3f35eebd5430..222fcebd0bd2 100644 --- a/src/frame/components/context/ArticleContext.tsx +++ b/src/frame/components/context/ArticleContext.tsx @@ -1,5 +1,6 @@ import { SupportPortalVaIframeProps } from '@/frame/components/article/SupportPortalVaIframe' import { createContext, useContext } from 'react' +import type { JSX } from 'react' import type { JourneyContext } from '@/journeys/lib/journey-path-resolver' export type LearningTrack = { diff --git a/src/frame/components/context/MainContext.tsx b/src/frame/components/context/MainContext.tsx index 714dadb3a256..f90691cd4d8a 100644 --- a/src/frame/components/context/MainContext.tsx +++ b/src/frame/components/context/MainContext.tsx @@ -266,7 +266,7 @@ export const getMainContext = async (req: any, res: any): Promise enterpriseServerVersions: req.context.enterpriseServerVersions, error: req.context.error ? req.context.error.toString() : '', featureFlags: {}, - fullUrl: req.protocol + '://' + req.hostname + req.originalUrl, // does not include port for localhost + fullUrl: `${req.protocol}://${req.hostname}${req.originalUrl}`, // does not include port for localhost isHomepageVersion: req.context.page?.documentType === 'homepage', nonEnterpriseDefaultVersion: req.context.nonEnterpriseDefaultVersion, page: pageInfo, diff --git a/src/frame/components/page-header/Header.tsx b/src/frame/components/page-header/Header.tsx index 07e8409fcbb3..49508d32b802 100644 --- a/src/frame/components/page-header/Header.tsx +++ b/src/frame/components/page-header/Header.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import type { JSX } from 'react' import cx from 'classnames' import { useRouter } from 'next/router' import { Dialog, IconButton } from '@primer/react' diff --git a/src/frame/components/page-header/HeaderSearchAndWidgets.tsx b/src/frame/components/page-header/HeaderSearchAndWidgets.tsx index c400fc6a3e9c..1826e92f2e23 100644 --- a/src/frame/components/page-header/HeaderSearchAndWidgets.tsx +++ b/src/frame/components/page-header/HeaderSearchAndWidgets.tsx @@ -1,4 +1,5 @@ import cx from 'classnames' +import type { JSX } from 'react' import { KebabHorizontalIcon, LinkExternalIcon } from '@primer/octicons-react' import { IconButton, ActionMenu, ActionList } from '@primer/react' diff --git a/src/frame/components/ui/Lead/Lead.tsx b/src/frame/components/ui/Lead/Lead.tsx index 443e1866da0d..96b735db3da1 100644 --- a/src/frame/components/ui/Lead/Lead.tsx +++ b/src/frame/components/ui/Lead/Lead.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react' +import type { JSX } from 'react' import cx from 'classnames' import styles from './Lead.module.scss' diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx index 93b67dda731e..3f3790287234 100644 --- a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx +++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react' +import type { JSX } from 'react' import cx from 'classnames' import styles from './MarkdownContent.module.scss' diff --git a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx index 98f02c43c773..c22dcfdabc52 100644 --- a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx +++ b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactMarkdown from 'react-markdown' import type { Components } from 'react-markdown' +import type { JSX } from 'react' import remarkGfm from 'remark-gfm' import cx from 'classnames' import { IconButton } from '@primer/react' diff --git a/src/frame/lib/fetch-utils.ts b/src/frame/lib/fetch-utils.ts index 5b3c57fd9a91..12dc2723822c 100644 --- a/src/frame/lib/fetch-utils.ts +++ b/src/frame/lib/fetch-utils.ts @@ -2,6 +2,7 @@ * Utility functions for fetch with retry and timeout functionality * to replace got library functionality */ + export interface FetchWithRetryOptions { retries?: number retryDelay?: number diff --git a/src/frame/lib/get-mini-toc-items.ts b/src/frame/lib/get-mini-toc-items.ts index e2d0c8cf8fd9..a701e6bb9e98 100644 --- a/src/frame/lib/get-mini-toc-items.ts +++ b/src/frame/lib/get-mini-toc-items.ts @@ -174,7 +174,7 @@ export async function getAutomatedPageMiniTocItems( for (let i = 0; i < depth; i++) { title += '#' } - return title + ` ${item}\n` + return `${title} ${item}\n` }) .join('') diff --git a/src/frame/lib/read-json-file.ts b/src/frame/lib/read-json-file.ts index 99824f21bedf..44b6758fd2ea 100644 --- a/src/frame/lib/read-json-file.ts +++ b/src/frame/lib/read-json-file.ts @@ -57,7 +57,7 @@ export function readCompressedJsonFileFallbackLazily(xpath: string): () => any { // err is any because fs errors can have various shapes with code property if (err.code === 'ENOENT') { try { - fs.accessSync(xpath + '.br') + fs.accessSync(`${xpath}.br`) } catch (err: any) { // err is any because fs errors can have various shapes with code property if (err.code === 'ENOENT') { diff --git a/src/frame/middleware/find-page.ts b/src/frame/middleware/find-page.ts index 00b0013da895..617b162d901d 100644 --- a/src/frame/middleware/find-page.ts +++ b/src/frame/middleware/find-page.ts @@ -110,7 +110,7 @@ async function rereadByPath( // but perhaps one day we can always and only do these kinds of lookups // at runtime. const possible = path.join(contentRoot, withoutVersion) - const filePath = existsSync(possible) ? path.join(possible, 'index.md') : possible + '.md' + const filePath = existsSync(possible) ? path.join(possible, 'index.md') : `${possible}.md` const relativePath = path.relative(contentRoot, filePath) const basePath = contentRoot diff --git a/src/frame/middleware/handle-next-data-path.ts b/src/frame/middleware/handle-next-data-path.ts index 8aff87bbdabf..882720748ce0 100644 --- a/src/frame/middleware/handle-next-data-path.ts +++ b/src/frame/middleware/handle-next-data-path.ts @@ -30,7 +30,7 @@ export default function handleNextDataPath( if (parts[1] === 'free-pro-team@latest') { parts.splice(1, 1) } - req.pagePath = '/' + parts.join('/').replace(/.json+$/, '') + req.pagePath = `/${parts.join('/').replace(/.json+$/, '')}` } else { req.pagePath = req.path } diff --git a/src/frame/middleware/render-page.ts b/src/frame/middleware/render-page.ts index f647718ef790..eeb2eb0bf929 100644 --- a/src/frame/middleware/render-page.ts +++ b/src/frame/middleware/render-page.ts @@ -106,7 +106,7 @@ export default async function renderPage(req: ExtendedRequest, res: Response) { req.context.currentVersion === 'free-pro-team@latest' || !allVersions[req.context.currentVersion!] ) { - page.fullTitle += ' - ' + context.site!.data.ui.header.github_docs + page.fullTitle += ` - ${context.site!.data.ui.header.github_docs}` } else { const { versionTitle } = allVersions[req.context.currentVersion!] page.fullTitle += ' - ' @@ -116,7 +116,7 @@ export default async function renderPage(req: ExtendedRequest, res: Response) { if (!versionTitle.includes('GitHub')) { page.fullTitle += 'GitHub ' } - page.fullTitle += versionTitle + ' Docs' + page.fullTitle += `${versionTitle} Docs` } } diff --git a/src/frame/tests/pages.ts b/src/frame/tests/pages.ts index af8c4efc412e..99d000e884e5 100644 --- a/src/frame/tests/pages.ts +++ b/src/frame/tests/pages.ts @@ -78,16 +78,14 @@ describe('pages module', () => { .map(([path]) => path) // Build a detailed message with sources for each duplicate - const message = - `Found ${duplicates.length} duplicate redirect_from path${duplicates.length === 1 ? '' : 's'}. + const message = `Found ${duplicates.length} duplicate redirect_from path${duplicates.length === 1 ? '' : 's'}. Ensure that you don't define the same path more than once in the redirect_from property in a single file and across all English files. - You may also receive this error if you have defined the same children property more than once.\n` + - duplicates + You may also receive this error if you have defined the same children property more than once.\n${duplicates .map((dup) => { const files = Array.from(redirectToFiles.get(dup) || []) return `${dup}\n Defined in:\n ${files.join('\n ')}` }) - .join('\n\n') + .join('\n\n')}` expect(duplicates.length, message).toBe(0) }) @@ -136,10 +134,12 @@ describe('pages module', () => { .flatten() .value() - const failureMessage = - JSON.stringify(frontmatterErrors, null, 2) + - '\n\n' + - chain(frontmatterErrors).map('filepath').join('\n').value() + const failureMessage = `${JSON.stringify(frontmatterErrors, null, 2)}\n\n${chain( + frontmatterErrors, + ) + .map('filepath') + .join('\n') + .value()}` expect(frontmatterErrors.length, failureMessage).toBe(0) }) diff --git a/src/frame/tests/toc-links.ts b/src/frame/tests/toc-links.ts index 3288fb21799b..16dc42c754c4 100644 --- a/src/frame/tests/toc-links.ts +++ b/src/frame/tests/toc-links.ts @@ -47,7 +47,7 @@ describe('toc links', () => { } } - const message = 'broken link in a TOC: ' + JSON.stringify(issues, null, 2) + const message = `broken link in a TOC: ${JSON.stringify(issues, null, 2)}` expect(issues.length, message).toBe(0) }) }) diff --git a/src/ghes-releases/scripts/create-enterprise-issue.ts b/src/ghes-releases/scripts/create-enterprise-issue.ts index e7fe92ee61f1..73eda3325789 100644 --- a/src/ghes-releases/scripts/create-enterprise-issue.ts +++ b/src/ghes-releases/scripts/create-enterprise-issue.ts @@ -359,10 +359,10 @@ async function isExistingIssue( let query = encodeURIComponent(`is:issue repo:${repo} `) if (searchQuery) { - query += '+' + searchQuery + query += `+${searchQuery}` } if (labelQuery) { - query += '+' + labelQuery + query += `+${labelQuery}` } const issues = await octokit.request(`GET /search/issues?q=${query}`) diff --git a/src/ghes-releases/scripts/deprecate/update-content.ts b/src/ghes-releases/scripts/deprecate/update-content.ts index 86cf54309fee..36d0e56ef801 100644 --- a/src/ghes-releases/scripts/deprecate/update-content.ts +++ b/src/ghes-releases/scripts/deprecate/update-content.ts @@ -28,7 +28,7 @@ export function updateContentFiles() { let featureData = undefined if (data.versions.feature) { - const featureFilePath = 'data/features/' + data.versions.feature + '.yml' + const featureFilePath = `data/features/${data.versions.feature}.yml` const featureContent = fs.readFileSync(featureFilePath, 'utf8') featureData = yaml.load(featureContent) as featureDataType if (!featureData || !featureData.versions) @@ -117,8 +117,8 @@ function removeFileUpdateParent(filePath: string) { if (!data) return // Children paths are relative to the index.md file's directory const childPath = filePath.endsWith('index.md') - ? '/' + path.basename(path.dirname(filePath)) - : '/' + path.basename(filePath, '.md') + ? `/${path.basename(path.dirname(filePath))}` + : `/${path.basename(filePath, '.md')}` // Remove the childPath from the parent index.md file's children frontmatter data.children = data.children.filter((child) => child !== childPath) diff --git a/src/github-apps/scripts/sync.ts b/src/github-apps/scripts/sync.ts index 000b510e0062..01a6cfa91443 100755 --- a/src/github-apps/scripts/sync.ts +++ b/src/github-apps/scripts/sync.ts @@ -448,11 +448,11 @@ function getDisplayTitle( const displayTitle = isRest ? !resourceGroup - ? sentenceCase(title) + ' permissions' - : `"${sentenceCase(title)}" ` + resourceGroup + ' permissions' + ? `${sentenceCase(title)} permissions` + : `"${sentenceCase(title)}" ${resourceGroup} permissions` : !resourceGroup - ? sentenceCase(title) + ' permissions' - : sentenceCase(resourceGroup) + ` permissions for "${title}"` + ? `${sentenceCase(title)} permissions` + : `${sentenceCase(resourceGroup)} permissions for "${title}"` return { title, displayTitle } } diff --git a/src/github-apps/tests/rendering.ts b/src/github-apps/tests/rendering.ts index 2a352c666dea..19ebb9ca7e78 100644 --- a/src/github-apps/tests/rendering.ts +++ b/src/github-apps/tests/rendering.ts @@ -76,7 +76,7 @@ describe('REST references docs', () => { ...value.map( (item: EnabledItem) => `/en/rest/${key}${ - categoriesWithoutSubcategories.includes(key) ? '' : '/' + item.subcategory + categoriesWithoutSubcategories.includes(key) ? '' : `/${item.subcategory}` }#${item.slug}`, ), ) @@ -110,7 +110,7 @@ describe('REST references docs', () => { ...value.permissions.map( (item: PermissionItem) => `/en/rest/${item.category}${ - categoriesWithoutSubcategories.includes(item.category) ? '' : '/' + item.subcategory + categoriesWithoutSubcategories.includes(item.category) ? '' : `/${item.subcategory}` }#${item.slug}`, ), ) diff --git a/src/graphql/components/GraphqlItem.tsx b/src/graphql/components/GraphqlItem.tsx index a37841bbba5e..4a8916111d76 100644 --- a/src/graphql/components/GraphqlItem.tsx +++ b/src/graphql/components/GraphqlItem.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { HeadingLink } from '@/frame/components/article/HeadingLink' import type { GraphqlT } from './types' import { Notice } from './Notice' diff --git a/src/graphql/components/GraphqlPage.tsx b/src/graphql/components/GraphqlPage.tsx index 0557f1f5741d..c22f772630b1 100644 --- a/src/graphql/components/GraphqlPage.tsx +++ b/src/graphql/components/GraphqlPage.tsx @@ -1,4 +1,5 @@ import React from 'react' +import type { JSX } from 'react' import cx from 'classnames' import { Enum } from './Enum' diff --git a/src/graphql/scripts/build-changelog.ts b/src/graphql/scripts/build-changelog.ts index 1a5b248c3585..f02b51026146 100644 --- a/src/graphql/scripts/build-changelog.ts +++ b/src/graphql/scripts/build-changelog.ts @@ -175,12 +175,9 @@ export async function createChangelogEntry( }), ) const cleanTitle = cleanPreviewTitle(previewTitle) - const entryTitle = - 'The [' + - cleanTitle + - '](/graphql/overview/schema-previews#' + - previewAnchor(cleanTitle) + - ') includes these changes:' + const entryTitle = `The [${cleanTitle}](/graphql/overview/schema-previews#${previewAnchor( + cleanTitle, + )}) includes these changes:` changelogEntry.previewChanges.push({ title: entryTitle, changes: renderedPreviewChanges, @@ -220,7 +217,7 @@ export function cleanPreviewTitle(title: string): string { } else if (title === 'MergeInfoPreview') { title = 'Merge info preview' } else if (!title.endsWith('preview')) { - title = title + ' preview' + title = `${title} preview` } return title } diff --git a/src/graphql/scripts/utils/schema-helpers.ts b/src/graphql/scripts/utils/schema-helpers.ts index 1d1968276068..0bd516f1e257 100644 --- a/src/graphql/scripts/utils/schema-helpers.ts +++ b/src/graphql/scripts/utils/schema-helpers.ts @@ -59,7 +59,7 @@ const graphqlTypes: GraphQLTypeInfo[] = JSON.parse( const singleQuotesInsteadOfBackticks = / '(\S+?)' / function addPeriod(string: string): string { - return string.endsWith('.') ? string : string + '.' + return string.endsWith('.') ? string : `${string}.` } async function getArguments( diff --git a/src/landings/components/LandingHero.tsx b/src/landings/components/LandingHero.tsx index abee30152333..658899337f00 100644 --- a/src/landings/components/LandingHero.tsx +++ b/src/landings/components/LandingHero.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import cx from 'classnames' import { useRouter } from 'next/router' import { LinkExternalIcon, NoteIcon } from '@primer/octicons-react' diff --git a/src/landings/components/LandingSection.tsx b/src/landings/components/LandingSection.tsx index d318ce9a40f0..35902d4bda7b 100644 --- a/src/landings/components/LandingSection.tsx +++ b/src/landings/components/LandingSection.tsx @@ -1,3 +1,4 @@ +import React from 'react' import cx from 'classnames' import { HeadingLink } from '@/frame/components/article/HeadingLink' diff --git a/src/landings/components/ProductLandingContext.tsx b/src/landings/components/ProductLandingContext.tsx index f732228e93da..edd65a6daa0c 100644 --- a/src/landings/components/ProductLandingContext.tsx +++ b/src/landings/components/ProductLandingContext.tsx @@ -146,7 +146,7 @@ export const getProductLandingContextFromRequest = async ( key, label: key === 'popular' || key === 'videos' - ? req.context.page.featuredLinks[key + 'Heading'] || req.context.site.data.ui.toc[key] + ? req.context.page.featuredLinks[`${key}Heading`] || req.context.site.data.ui.toc[key] : req.context.site.data.ui.toc[key], viewAllHref: key === 'startHere' && !req.context.currentCategory && hasGuidesPage diff --git a/src/landings/components/SidebarProduct.tsx b/src/landings/components/SidebarProduct.tsx index 89494c3b1e6f..c899fff64a78 100644 --- a/src/landings/components/SidebarProduct.tsx +++ b/src/landings/components/SidebarProduct.tsx @@ -135,10 +135,10 @@ function RestNavListItem({ category }: { category: ProductTreeNode }) { (entries) => { entries.forEach((entry) => { if (entry.target.id) { - const anchor = '#' + entry.target.id.split('--')[0] + const anchor = `#${entry.target.id.split('--')[0]}` if (entry.isIntersecting === true) setVisibleAnchor(anchor) } else if (asPath.includes('#')) { - setVisibleAnchor('#' + asPath.split('#')[1]) + setVisibleAnchor(`#${asPath.split('#')[1]}`) } else { setVisibleAnchor('') } diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx index 90ab78167ffb..f719b4cf428e 100644 --- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx +++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useMemo } from 'react' +import React, { useState, useRef, useEffect, useMemo } from 'react' import { TextInput, ActionMenu, ActionList, Token, Pagination } from '@primer/react' import { SearchIcon } from '@primer/octicons-react' import cx from 'classnames' diff --git a/src/languages/components/LanguagePicker.tsx b/src/languages/components/LanguagePicker.tsx index b2ed1b50b632..05c46694d8a9 100644 --- a/src/languages/components/LanguagePicker.tsx +++ b/src/languages/components/LanguagePicker.tsx @@ -84,7 +84,7 @@ export const LanguagePicker = ({ xs, mediumOrLower }: Props) => { className={`color-fg-default width-full ${styles.menuButton}`} aria-label={`Select language: current language is ${selectedLang.name}`} > - {t('language_picker_label') + '\n'} + {`${t('language_picker_label')}\n`} {selectedLang.name} diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index fda0b9af99ae..8916afaf9351 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -253,7 +253,7 @@ export function correctTranslatedContentStrings( return match } - const withLinebreak = match.slice(0, -1) + '\n' + const withLinebreak = `${match.slice(0, -1)}\n` if (englishContent.includes(withLinebreak) && !englishContent.includes(match)) { return withLinebreak } @@ -332,7 +332,7 @@ export function correctTranslatedContentStrings( const keyString = '5DE3 E050 9C47 EA3C F04A 42D3 4AEE 18F8 3AFD EB23' const translatedSentences = [ // ru - 'Полный отпечаток ключа\u00A0\u2014 `' + keyString + '`.', + `Полный отпечаток ключа\u00A0\u2014 \`${keyString}\`.`, // ko `키의 전체 지문은 \`${keyString}\`입니다.`, // es diff --git a/src/languages/lib/render-with-fallback.ts b/src/languages/lib/render-with-fallback.ts index 3a76c90da54c..30c3686e5439 100644 --- a/src/languages/lib/render-with-fallback.ts +++ b/src/languages/lib/render-with-fallback.ts @@ -70,7 +70,7 @@ export function createTranslationFallbackComment(error: Error, property: string) // Limit message length to keep comment manageable if (cleanMessage.length > 200) { - cleanMessage = cleanMessage.substring(0, 200) + '...' + cleanMessage = `${cleanMessage.substring(0, 200)}...` } errorDetails.push(`msg="${cleanMessage.replace(/"/g, "'")}"`) @@ -141,7 +141,7 @@ export async function renderContentWithFallback( // Skip for textOnly rendering to avoid breaking plain text output if (context.currentLanguage !== 'en' && !options?.textOnly) { const errorComment = createTranslationFallbackComment(error as Error, property) - return errorComment + '\n' + fallbackContent + return `${errorComment}\n${fallbackContent}` } return fallbackContent @@ -181,7 +181,7 @@ export async function executeWithFallback( // Only for HTML content (detected by presence of HTML tags) if (typeof fallbackContent === 'string' && /<[^>]+>/.test(fallbackContent)) { const errorComment = createTranslationFallbackComment(error as Error, 'content') - return (errorComment + '\n' + fallbackContent) as T + return `${errorComment}\n${fallbackContent}` as T } return fallbackContent diff --git a/src/languages/lib/translation-utils.ts b/src/languages/lib/translation-utils.ts index 40acaa3b7369..e190e94b87d1 100644 --- a/src/languages/lib/translation-utils.ts +++ b/src/languages/lib/translation-utils.ts @@ -19,8 +19,7 @@ export function createTranslationFunctions(uiData: UIStrings, namespaces: string if (missingNamespaces.length > 0) { console.warn( `Missing namespaces [${missingNamespaces.join(', ')}] in UI data. ` + - 'Available namespaces: ' + - Object.keys(uiData).sort().join(', '), + `Available namespaces: ${Object.keys(uiData).sort().join(', ')}`, ) // For 404 pages, we can't afford to throw errors; create defensive fallbacks diff --git a/src/languages/tests/api-search.ts b/src/languages/tests/api-search.ts index ed5adb37e15d..1cd9f87c1dc8 100644 --- a/src/languages/tests/api-search.ts +++ b/src/languages/tests/api-search.ts @@ -14,7 +14,7 @@ describeIfElasticsearchURL('search v1 middleware in non-English', () => { // which clearly has a record with the title "Foo" sp.set('query', 'foo') sp.set('language', 'ja') - const res = await get('/api/search/v1?' + sp) + const res = await get(`/api/search/v1?${sp}`) expect(res.statusCode).toBe(200) const results = JSON.parse(res.body) diff --git a/src/links/scripts/rendered-content-link-checker.ts b/src/links/scripts/rendered-content-link-checker.ts index 3218a712d14a..fbc81c0eddb6 100755 --- a/src/links/scripts/rendered-content-link-checker.ts +++ b/src/links/scripts/rendered-content-link-checker.ts @@ -611,7 +611,7 @@ function flawIssueDisplay(flaws: LinkFlaw[], opts: Options, mentionExternalExclu // limit is 65536 if (output.length > 60000) { - output = output.slice(0, 60000) + '\n\n---\n\nOUTPUT TRUNCATED' + output = `${output.slice(0, 60000)}\n\n---\n\nOUTPUT TRUNCATED` } return output @@ -950,7 +950,7 @@ async function checkHrefLink( // 6. 'https://example.com' (external link) const [pathFragment, hashFragment] = href.split('#') - const hash = '#' + hashFragment // the hash is the part that starts with `#` + const hash = `#${hashFragment}` // the hash is the part that starts with `#` // this conditional handles cases in which the link is to the current article (cases 1-3 above) if (checkAnchors && (!pathFragment || pathFragment === permalink.href)) { diff --git a/src/links/scripts/validate-github-github-docs-urls/generate-new-json.ts b/src/links/scripts/validate-github-github-docs-urls/generate-new-json.ts index 9c026d1ff234..20ad6e3ab5bb 100644 --- a/src/links/scripts/validate-github-github-docs-urls/generate-new-json.ts +++ b/src/links/scripts/validate-github-github-docs-urls/generate-new-json.ts @@ -38,7 +38,7 @@ export function generateNewJSON( const writeTo = options.output || destinationFilePath // It's important that this serializes exactly like the Ruby code // that is the CLI script `script/add-docs-url` in github/github. - const serialized = JSON.stringify(destination, null, 2) + '\n' + const serialized = `${JSON.stringify(destination, null, 2)}\n` fs.writeFileSync(writeTo, serialized, 'utf-8') console.log(`Wrote ${countChanges} change${countChanges === 1 ? '' : 's'} to ${writeTo}`) if (writeTo !== destinationFilePath) { diff --git a/src/metrics/scripts/docsaudit.ts b/src/metrics/scripts/docsaudit.ts index a49d6ccce9a4..75ef1d552ffe 100644 --- a/src/metrics/scripts/docsaudit.ts +++ b/src/metrics/scripts/docsaudit.ts @@ -77,7 +77,7 @@ async function main(): Promise { console.log(csvEntry) results.push(csvEntry) } - csvString += results.join('\n') + '\n' + csvString += `${results.join('\n')}\n` fs.writeFileSync(outputFile, csvString.trim(), 'utf8') console.log(`Done! Wrote ${outputFile}`) diff --git a/src/observability/logger/lib/to-logfmt.ts b/src/observability/logger/lib/to-logfmt.ts index fad0475869e4..0f59c7ee850c 100644 --- a/src/observability/logger/lib/to-logfmt.ts +++ b/src/observability/logger/lib/to-logfmt.ts @@ -40,13 +40,13 @@ function stringify(data: Record): string { stringValue = stringValue.replace(/["\\]/g, '\\$&') } if (needs_quoting || needs_escaping) { - stringValue = '"' + stringValue + '"' + stringValue = `"${stringValue}"` } if (stringValue === '' && !is_null) { stringValue = '""' } - line += key + '=' + stringValue + ' ' + line += `${key}=${stringValue} ` } // trim trailing space diff --git a/src/observability/logger/middleware/get-automatic-request-logger.ts b/src/observability/logger/middleware/get-automatic-request-logger.ts index 7e9d01508303..0ea1992178bb 100644 --- a/src/observability/logger/middleware/get-automatic-request-logger.ts +++ b/src/observability/logger/middleware/get-automatic-request-logger.ts @@ -47,7 +47,7 @@ export function getAutomaticRequestLogger() { toLogfmt({ ...loggerContext, status, - responseTime: responseTime + ' ms', + responseTime: `${responseTime} ms`, contentLength: String(contentLength), method, url, @@ -71,7 +71,7 @@ export function getAutomaticRequestLogger() { chalk.reset(method), chalk.reset(url), chalk[color](status), - chalk.reset(responseTime + ' ms'), + chalk.reset(`${responseTime} ms`), chalk.reset('-'), chalk.reset(String(contentLength)), ].join(' ') diff --git a/src/redirects/lib/get-redirect.ts b/src/redirects/lib/get-redirect.ts index 8cc5e1d0439d..99b1d7556453 100644 --- a/src/redirects/lib/get-redirect.ts +++ b/src/redirects/lib/get-redirect.ts @@ -73,11 +73,10 @@ export default function getRedirect(uri: string, context: Context): string | und if (withoutLanguage.startsWith(nonEnterpriseDefaultVersionPrefix)) { // E.g. '/free-pro-team@latest/foo/bar' or '/free-pro-team@latest' - basicCorrection = - `/${language}` + withoutLanguage.replace(nonEnterpriseDefaultVersionPrefix, '') + basicCorrection = `/${language}${withoutLanguage.replace(nonEnterpriseDefaultVersionPrefix, '')}` } else if (withoutLanguage.replace('/', '') in allVersions && !languagePrefixRegex.test(uri)) { // E.g. just '/github-ae@latest' or '/enterprise-cloud@latest' - basicCorrection = `/${language}` + withoutLanguage + basicCorrection = `/${language}${withoutLanguage}` return basicCorrection } @@ -86,18 +85,20 @@ export default function getRedirect(uri: string, context: Context): string | und withoutLanguage.startsWith('/enterprise-server/') ) { // E.g. '/enterprise-server' or '/enterprise-server/3.0/foo' - basicCorrection = - `/${language}` + - withoutLanguage.replace('/enterprise-server', `/enterprise-server@${latestStable}`) + basicCorrection = `/${language}${withoutLanguage.replace( + '/enterprise-server', + `/enterprise-server@${latestStable}`, + )}` // If it's now just the version, without anything after, exit here if (withoutLanguage === '/enterprise-server') { return basicCorrection } } else if (withoutLanguage.startsWith('/enterprise-server@latest')) { // E.g. '/enterprise-server@latest' or '/enterprise-server@latest/3.3/foo' - basicCorrection = - `/${language}` + - withoutLanguage.replace('/enterprise-server@latest', `/enterprise-server@${latestStable}`) + basicCorrection = `/${language}${withoutLanguage.replace( + '/enterprise-server@latest', + `/enterprise-server@${latestStable}`, + )}` // If it was *just* '/enterprise-server@latest' all that's needed is // the language but with 'latest' replaced with the value of `latest` if (withoutLanguage === '/enterprise-server@latest') { @@ -115,14 +116,16 @@ export default function getRedirect(uri: string, context: Context): string | und const version = withoutLanguage.split('/')[2] if (withoutLanguage === `/enterprise/${version}`) { // E.g. `/enterprise/3.0` - basicCorrection = - `/${language}` + - withoutLanguage.replace(`/enterprise/${version}`, `/enterprise-server@${version}`) + basicCorrection = `/${language}${withoutLanguage.replace( + `/enterprise/${version}`, + `/enterprise-server@${version}`, + )}` return basicCorrection } else { - basicCorrection = - `/${language}` + - withoutLanguage.replace(`/enterprise/${version}/`, `/enterprise-server@${version}/`) + basicCorrection = `/${language}${withoutLanguage.replace( + `/enterprise/${version}/`, + `/enterprise-server@${version}/`, + )}` } } else if (withoutLanguage === '/enterprise') { // E.g. `/enterprise` exactly @@ -136,11 +139,9 @@ export default function getRedirect(uri: string, context: Context): string | und // If the URL is without a language, and no redirect is necessary, // but it has as version prefix, the language has to be there // otherwise it will never be found in `req.context.pages` - basicCorrection = - `/${language}` + - withoutLanguage - .replace(`/enterprise/`, `/enterprise-server@${latest}/`) - .replace('/user/', '/') + basicCorrection = `/${language}${withoutLanguage + .replace(`/enterprise/`, `/enterprise-server@${latest}/`) + .replace('/user/', '/')}` } else if (withoutLanguage.startsWith('/insights')) { // E.g. '/insights/foo' basicCorrection = uri.replace('/insights', `${language}/enterprise-server@${latest}/insights`) @@ -171,7 +172,7 @@ export default function getRedirect(uri: string, context: Context): string | und if (supported.includes(version) || version === 'latest') { prefix = `/${majorVersion}@${version}` - suffix = '/' + split.slice(2).join('/') + suffix = `/${split.slice(2).join('/')}` if ( suffix.includes('/user') || @@ -183,7 +184,7 @@ export default function getRedirect(uri: string, context: Context): string | und } else { // If version is not supported, we still need to set these values prefix = `/${majorVersion}@${version}` - suffix = '/' + split.slice(2).join('/') + suffix = `/${split.slice(2).join('/')}` } const newURL = prefix + suffix @@ -319,7 +320,7 @@ function tryReplacements(prefix: string, suffix: string, context: Context): stri return false } const candidateAsRedirect = prefix + suffix - const candidateAsURL = '/en' + candidateAsRedirect + const candidateAsURL = `/en${candidateAsRedirect}` return candidateAsRedirect in redirects || candidateAsURL in pages } diff --git a/src/redirects/middleware/handle-redirects.ts b/src/redirects/middleware/handle-redirects.ts index 82d88882dece..23d806c53a59 100644 --- a/src/redirects/middleware/handle-redirects.ts +++ b/src/redirects/middleware/handle-redirects.ts @@ -76,7 +76,7 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex // have to do this now because searchPath replacement changes the path as well as the query params if (queryParams) { - queryParams = '?' + queryParams + queryParams = `?${queryParams}` } // remove query params temporarily so we can find the path in the redirects object diff --git a/src/rest/components/RestAuth.tsx b/src/rest/components/RestAuth.tsx index aaed841f339a..f808dfec8ce0 100644 --- a/src/rest/components/RestAuth.tsx +++ b/src/rest/components/RestAuth.tsx @@ -106,8 +106,8 @@ function FineGrainedAccess({ progAccess }: FineGrainedProps) { numPermissionSets === 0 ? t('no_permission_sets') : numPermissionSets > 1 - ? t('permission_sets') + ':' - : t('permission_set') + ':' + ? `${t('permission_sets')}:` + : `${t('permission_set')}:` const publicAccessMsg = numPermissionSets === 0 ? t('allows_public_read_access_no_permissions') diff --git a/src/rest/components/RestCodeSamples.tsx b/src/rest/components/RestCodeSamples.tsx index b03338fbbd7f..f3568e6f1003 100644 --- a/src/rest/components/RestCodeSamples.tsx +++ b/src/rest/components/RestCodeSamples.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, FormEvent } from 'react' +import React, { useState, useEffect, useRef, FormEvent } from 'react' import { FormControl, IconButton, Select, SegmentedControl } from '@primer/react' import { CheckIcon, CopyIcon, InfoIcon } from '@primer/octicons-react' import { announce } from '@primer/live-region-element' diff --git a/src/rest/components/RestRedirect.tsx b/src/rest/components/RestRedirect.tsx index cbb16ee87fcb..cb6829bd5507 100644 --- a/src/rest/components/RestRedirect.tsx +++ b/src/rest/components/RestRedirect.tsx @@ -33,7 +33,7 @@ export function RestRedirect() { const params = new URLSearchParams(asPathQuery) params.set('apiVersion', date) - const url = `/${router.locale}${asPathRoot}?${params}${hash ? '#' + hash : ''}` + const url = `/${router.locale}${asPathRoot}?${params}${hash ? `#${hash}` : ''}` router.replace(url) } }, [router.asPath, currentVersion]) diff --git a/src/rest/docs.ts b/src/rest/docs.ts index 86cf35d0d7be..756ca4ab6a52 100755 --- a/src/rest/docs.ts +++ b/src/rest/docs.ts @@ -29,48 +29,46 @@ log(chalk.white.bold(' npm run dev\n')) log(chalk.green.bold.underline('REST docs script examples\n')) log(chalk.green.bold(' Examples of ways you can build the REST docs locally:\n')) log( - chalk.cyan.bold(' - REST All versions:') + - ' ' + - chalk.magenta('npm run sync-rest && npm run dev'), + `${chalk.cyan.bold(' - REST All versions:')} ${chalk.magenta( + 'npm run sync-rest && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - REST Dotcom only:') + - ' ' + - chalk.magenta('npm run sync-rest -- --versions api.github.com && npm run dev'), + `${chalk.cyan.bold(' - REST Dotcom only:')} ${chalk.magenta( + 'npm run sync-rest -- --versions api.github.com && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - REST Two versions:') + - ' ' + - chalk.magenta('npm run sync-rest -- --versions ghes-3.7 ghes-3.8 && npm run dev'), + `${chalk.cyan.bold(' - REST Two versions:')} ${chalk.magenta( + 'npm run sync-rest -- --versions ghes-3.7 ghes-3.8 && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - REST Dotcom and next calendar date version:') + - ' ' + - chalk.magenta('npm run sync-rest -- --next --versions api.github.com && npm run dev'), + `${chalk.cyan.bold(' - REST Dotcom and next calendar date version:')} ${chalk.magenta( + 'npm run sync-rest -- --next --versions api.github.com && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - REST Dotcom only, including unpublished operations:') + - ' ' + - chalk.magenta( - 'npm run sync-rest -- --versions api.github.com --include-unpublished && npm run dev', - ), + `${chalk.cyan.bold(' - REST Dotcom only, including unpublished operations:')} ${chalk.magenta( + 'npm run sync-rest -- --versions api.github.com --include-unpublished && npm run dev', + )}`, ) log(chalk.green.bold.underline('\nWebhook docs script examples\n')) log(chalk.green.bold(' Examples of ways you can build the Webhook docs locally:\n')) log( - chalk.cyan.bold(' - Webhooks All versions:') + - ' ' + - chalk.magenta('npm run sync-webhooks && npm run dev'), + `${chalk.cyan.bold(' - Webhooks All versions:')} ${chalk.magenta( + 'npm run sync-webhooks && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - Webhooks Dotcom only:') + - ' ' + - chalk.magenta('npm run sync-webhooks -- --versions api.github.com && npm run dev'), + `${chalk.cyan.bold(' - Webhooks Dotcom only:')} ${chalk.magenta( + 'npm run sync-webhooks -- --versions api.github.com && npm run dev', + )}`, ) log( - chalk.cyan.bold(' - Webhooks Two versions:') + - ' ' + - chalk.magenta('npm run sync-webhooks -- --versions ghes-3.7 ghes-3.8 && npm run dev'), + `${chalk.cyan.bold(' - Webhooks Two versions:')} ${chalk.magenta( + 'npm run sync-webhooks -- --versions ghes-3.7 ghes-3.8 && npm run dev', + )}`, ) log(chalk.green.bold('\nFor more info and additional options, run:\n')) log(chalk.white.bold(' npm run sync-rest -- --help')) diff --git a/src/rest/scripts/utils/create-rest-examples.ts b/src/rest/scripts/utils/create-rest-examples.ts index f1fdf3a73358..e6d71b715316 100644 --- a/src/rest/scripts/utils/create-rest-examples.ts +++ b/src/rest/scripts/utils/create-rest-examples.ts @@ -72,11 +72,7 @@ export default async function getCodeSamples(operation: Operation): Promise 1 - ? example.request.description + - ' ' + - (i + 1) + - ': Status Code ' + - example.response!.statusCode + ? `${example.request.description} ${i + 1}: Status Code ${example.response!.statusCode}` : example.request.description, }, })) diff --git a/src/rest/tests/api.ts b/src/rest/tests/api.ts index 837301d0f0e3..2341262d0241 100644 --- a/src/rest/tests/api.ts +++ b/src/rest/tests/api.ts @@ -20,7 +20,7 @@ describe('anchor-redirect api', () => { const sp = new URLSearchParams() sp.set('path', path) sp.set('hash', hash) - const res = await get('/api/anchor-redirect?' + sp) + const res = await get(`/api/anchor-redirect?${sp}`) expect(res.statusCode).toBe(200) const { to } = JSON.parse(res.body) expect(to).toBe(value) @@ -31,7 +31,7 @@ describe('anchor-redirect api', () => { const hash = key.split('#')[1] const sp = new URLSearchParams() sp.set('hash', hash) - const res = await get('/api/anchor-redirect?' + sp) + const res = await get(`/api/anchor-redirect?${sp}`) expect(res.statusCode).toBe(400) }) test('errors when path is not passed', async () => { @@ -40,14 +40,14 @@ describe('anchor-redirect api', () => { const path = key.split('#')[0] const sp = new URLSearchParams() sp.set('path', path) - const res = await get('/api/anchor-redirect?' + sp) + const res = await get(`/api/anchor-redirect?${sp}`) expect(res.statusCode).toBe(400) }) test('unfound redirect returns undefined', async () => { const sp = new URLSearchParams() sp.set('path', 'foo') sp.set('hash', 'bar') - const res = await get('/api/anchor-redirect?' + sp) + const res = await get(`/api/anchor-redirect?${sp}`) const { to } = JSON.parse(res.body) expect(to).toBe(undefined) }) @@ -55,7 +55,7 @@ describe('anchor-redirect api', () => { const sp = new URLSearchParams() sp.set('path', 'foo') sp.set('hash', 'bar') - const res = await get('/api/anchor-redirect?' + sp) + const res = await get(`/api/anchor-redirect?${sp}`) expect(res.headers['cache-control']).toContain('public') expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/) expect(res.headers['surrogate-control']).toContain('public') diff --git a/src/rest/tests/create-rest-examples.ts b/src/rest/tests/create-rest-examples.ts index 09b021fe0307..239f97e6de29 100644 --- a/src/rest/tests/create-rest-examples.ts +++ b/src/rest/tests/create-rest-examples.ts @@ -55,7 +55,7 @@ describe('rest example requests and responses', () => { // example is any because getCodeSamples returns objects from untyped JavaScript module mergedExamples.forEach((example: any, index: number) => { expect(example.request.description).toBe( - 'Example ' + (index + 1) + ': Status Code ' + example.response.statusCode, + `Example ${index + 1}: Status Code ${example.response.statusCode}`, ) }) }) diff --git a/src/rest/tests/rendering.ts b/src/rest/tests/rendering.ts index 56c9e85006ef..645401bf0072 100644 --- a/src/rest/tests/rendering.ts +++ b/src/rest/tests/rendering.ts @@ -109,7 +109,7 @@ describe('REST references docs', () => { .text() .trim() if (apiVersion === allVersions[version].latestApiVersion) { - expect(versionName).toBe(apiVersion + ' (latest)') + expect(versionName).toBe(`${apiVersion} (latest)`) } else { expect(versionName).toBe(apiVersion) } @@ -148,12 +148,11 @@ describe('REST references docs', () => { function formatErrors(differences: Record): string { let errorMessage = 'There are differences in Categories/Subcategories in:\n' for (const schema in differences) { - errorMessage += 'Version: ' + schema + '\n' + errorMessage += `Version: ${schema}\n` for (const category in differences[schema]) { - errorMessage += 'Category: ' + category + '\nSubcategories: \n' - errorMessage += - ' - content/rest directory: ' + differences[schema][category].contentDir + '\n' - errorMessage += ' - OpenAPI Schema: ' + differences[schema][category].openAPI + '\n' + errorMessage += `Category: ${category}\nSubcategories: \n` + errorMessage += ` - content/rest directory: ${differences[schema][category].contentDir}\n` + errorMessage += ` - OpenAPI Schema: ${differences[schema][category].openAPI}\n` errorMessage += '---\n' } } diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx index 5fce5019c0d6..dbaffc6355d7 100644 --- a/src/search/components/input/AskAIResults.tsx +++ b/src/search/components/input/AskAIResults.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { uniqBy } from 'lodash-es' import { executeAISearch } from '../helpers/execute-search-actions' import { useRouter } from 'next/router' diff --git a/src/search/components/input/SearchBarButton.tsx b/src/search/components/input/SearchBarButton.tsx index 039b4851a139..0560fbfb8c6a 100644 --- a/src/search/components/input/SearchBarButton.tsx +++ b/src/search/components/input/SearchBarButton.tsx @@ -1,3 +1,4 @@ +import React from 'react' import cx from 'classnames' import { IconButton } from '@primer/react' import { CopilotIcon, SearchIcon } from '@primer/octicons-react' diff --git a/src/search/components/input/SearchOverlay.tsx b/src/search/components/input/SearchOverlay.tsx index af97f3a04992..bf9d40ea8ebd 100644 --- a/src/search/components/input/SearchOverlay.tsx +++ b/src/search/components/input/SearchOverlay.tsx @@ -299,7 +299,7 @@ export function SearchOverlay({ // When loading, capture the last height of the suggestions list so we can use it for the loading div const previousSuggestionsListHeight = useMemo(() => { if (generalSearchResults.length || aiAutocompleteOptions.length) { - return 7 * (generalSearchResults.length + aiAutocompleteOptions.length) + '' + return `${7 * (generalSearchResults.length + aiAutocompleteOptions.length)}` } else { return '150' // Default height for just 2 suggestions } diff --git a/src/search/components/input/SearchOverlayContainer.tsx b/src/search/components/input/SearchOverlayContainer.tsx index 103ee815d8fb..cd504ca62b6f 100644 --- a/src/search/components/input/SearchOverlayContainer.tsx +++ b/src/search/components/input/SearchOverlayContainer.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { SearchOverlay } from './SearchOverlay' import { QueryParams } from '../hooks/useMultiQueryParams' diff --git a/src/search/components/results/SearchResults.tsx b/src/search/components/results/SearchResults.tsx index 9cac096f17be..27c3a0d5b260 100644 --- a/src/search/components/results/SearchResults.tsx +++ b/src/search/components/results/SearchResults.tsx @@ -1,7 +1,7 @@ import { Box, Pagination, Text } from '@primer/react' import { SearchIcon } from '@primer/octicons-react' import { useRouter } from 'next/router' -import { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import cx from 'classnames' import { useTranslation } from '@/languages/components/useTranslation' diff --git a/src/search/lib/ai-search-proxy.ts b/src/search/lib/ai-search-proxy.ts index 75a4a42b4cbf..dd84458da35f 100644 --- a/src/search/lib/ai-search-proxy.ts +++ b/src/search/lib/ai-search-proxy.ts @@ -132,8 +132,7 @@ export const aiSearchProxy = async (req: ExtendedRequest, res: Response) => { res.status(500).json({ errors: [{ message: 'Internal server error' }] }) } else { // Send error message via the stream - const errorMessage = - JSON.stringify({ errors: [{ message: 'Internal server error' }] }) + '\n' + const errorMessage = `${JSON.stringify({ errors: [{ message: 'Internal server error' }] })}\n` res.write(errorMessage) res.end() } diff --git a/src/search/lib/helpers/strings.ts b/src/search/lib/helpers/strings.ts index d8ca26383cc4..f7e546377e9e 100644 --- a/src/search/lib/helpers/strings.ts +++ b/src/search/lib/helpers/strings.ts @@ -4,7 +4,7 @@ export function safeUrlDisplay(url: string): string { parsed.password = '***' } if (parsed.username) { - parsed.username = parsed.username.slice(0, 4) + '***' + parsed.username = `${parsed.username.slice(0, 4)}***` } return parsed.toString() } diff --git a/src/search/lib/helpers/time.ts b/src/search/lib/helpers/time.ts index 36579358d4ef..2353d513c1ea 100644 --- a/src/search/lib/helpers/time.ts +++ b/src/search/lib/helpers/time.ts @@ -29,7 +29,7 @@ export function utcTimestamp() { d.getUTCSeconds(), ] // If it's a number make it a zero-padding 2 character string - .map((x) => (typeof x === 'number' ? ('0' + x).slice(-2) : x)) + .map((x) => (typeof x === 'number' ? `0${x}`.slice(-2) : x)) .join('') ) } diff --git a/src/search/scripts/scrape/lib/build-records.ts b/src/search/scripts/scrape/lib/build-records.ts index b50ca772406e..7f40baac8bd3 100644 --- a/src/search/scripts/scrape/lib/build-records.ts +++ b/src/search/scripts/scrape/lib/build-records.ts @@ -178,36 +178,35 @@ export default async function buildRecords( // Report failed pages if any if (failedPages.length > 0) { - console.log( - '\n' + - boxen( - chalk.bold.red(`${failedPages.length} page(s) failed to scrape\n\n`) + - failedPages - .slice(0, 10) // Show first 10 failures - .map((failure, idx) => { - return ( - chalk.gray(`${idx + 1}. `) + - chalk.yellow(failure.errorType) + - '\n' + - (failure.relativePath - ? chalk.cyan(` Path: `) + failure.relativePath + '\n' - : '') + - (failure.url ? chalk.cyan(` URL: `) + failure.url + '\n' : '') + - chalk.gray(` Error: ${failure.error}`) - ) - }) - .join('\n\n') + - (failedPages.length > 10 - ? `\n\n${chalk.gray(`... and ${failedPages.length - 10} more`)}` - : ''), - { - title: chalk.red('⚠ Failed Pages'), - padding: 1, - borderColor: 'yellow', - }, - ) + - '\n', - ) + const failureCount = failedPages.length + const header = chalk.bold.red(`${failureCount} page(s) failed to scrape\n\n`) + + const failureList = failedPages + .slice(0, 10) // Show first 10 failures + .map((failure, idx) => { + const number = chalk.gray(`${idx + 1}. `) + const errorType = chalk.yellow(failure.errorType) + const pathLine = failure.relativePath + ? `\n${chalk.cyan(' Path: ')}${failure.relativePath}` + : '' + const urlLine = failure.url ? `\n${chalk.cyan(' URL: ')}${failure.url}` : '' + const errorLine = `\n${chalk.gray(` Error: ${failure.error}`)}` + + return `${number}${errorType}${pathLine}${urlLine}${errorLine}` + }) + .join('\n\n') + + const remaining = + failureCount > 10 ? `\n\n${chalk.gray(`... and ${failureCount - 10} more`)}` : '' + + const boxContent = header + failureList + remaining + const box = boxen(boxContent, { + title: chalk.red('⚠ Failed Pages'), + padding: 1, + borderColor: 'yellow', + }) + + console.log(`\n${box}\n`) // Log suggestion console.log( diff --git a/src/search/tests/api-search.ts b/src/search/tests/api-search.ts index a77abcdb9144..b20227b40b24 100644 --- a/src/search/tests/api-search.ts +++ b/src/search/tests/api-search.ts @@ -33,7 +33,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { // see src/search/tests/fixtures/search-indexes/github-docs-dotcom-en-records.json // which clearly has a record with the title "Foo" sp.set('query', 'foo') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) @@ -75,7 +75,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('debug', '1') // Note! - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) // safe because we know exactly the fixtures @@ -90,7 +90,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { { const sp = new URLSearchParams() sp.set('query', 'sill') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) // Fixtures contains no word called 'sill'. It does contain the term @@ -105,7 +105,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'sill') sp.set('autocomplete', 'true') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) expect(results.meta.found.value).toBeGreaterThanOrEqual(1) @@ -119,7 +119,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { test('find nothing', async () => { const sp = new URLSearchParams() sp.set('query', 'xojixjoiwejhfoiuwehjfioweufhj') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) expect(results.hits.length).toBe(0) @@ -130,7 +130,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'introduction heading') sp.append('highlights', 'content') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) expect(results.meta.found.value).toBeGreaterThanOrEqual(1) @@ -145,7 +145,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { // This will match because it's in the 'content' but not in 'headings' sp.set('query', 'Fact of life') sp.set('highlights', 'title') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) expect(results.meta.found.value).toBeGreaterThanOrEqual(1) @@ -159,12 +159,12 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('version', 'dotcom') - const res1 = await get('/api/search/v1?' + sp.toString()) + const res1 = await get(`/api/search/v1?${sp.toString()}`) expect(res1.statusCode).toBe(200) const results1: GeneralSearchResponse = JSON.parse(res1.body) sp.set('version', 'free-pro-team@latest') - const res2 = await get('/api/search/v1?' + sp.toString()) + const res2 = await get(`/api/search/v1?${sp.toString()}`) expect(res2.statusCode).toBe(200) const results2: GeneralSearchResponse = JSON.parse(res2.body) expect(results1.hits[0].id).toBe(results2.hits[0].id) @@ -185,7 +185,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { { const sp = new URLSearchParams() sp.set('query', ' ') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -198,7 +198,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('language', 'xxx') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -211,7 +211,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('page', '9999') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -224,7 +224,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('version', 'xxxxx') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -238,7 +238,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('size', 'not a number') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -251,7 +251,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('sort', 'neverheardof') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -264,7 +264,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.set('query', 'test') sp.set('highlights', 'neverheardof') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -277,7 +277,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { const sp = new URLSearchParams() sp.append('query', 'test1') sp.append('query', 'test2') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const errorResponse = JSON.parse(res.body) as { error: string @@ -290,7 +290,7 @@ describeIfElasticsearchURL('search v1 middleware', () => { test('breadcrumbless records should always return a string', async () => { const sp = new URLSearchParams() sp.set('query', 'breadcrumbs') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) // safe because we know exactly the fixtures @@ -305,7 +305,7 @@ describeIfElasticsearchURL("additional fields with 'include'", () => { test("'intro' and 'headings' are omitted by default", async () => { const sp = new URLSearchParams() sp.set('query', 'foo') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) const firstKeys = Object.keys(results.hits[0]) @@ -317,7 +317,7 @@ describeIfElasticsearchURL("additional fields with 'include'", () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('include', 'intro') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) const firstKeys = Object.keys(results.hits[0]) @@ -330,7 +330,7 @@ describeIfElasticsearchURL("additional fields with 'include'", () => { sp.set('query', 'foo') sp.append('include', 'intro') sp.append('include', 'headings') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) const firstKeys = Object.keys(results.hits[0]) @@ -342,7 +342,7 @@ describeIfElasticsearchURL("additional fields with 'include'", () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('include', 'xxxxx') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const results = JSON.parse(res.body) as { error: string @@ -359,7 +359,7 @@ describeIfElasticsearchURL('filter by toplevel', () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('include', 'toplevel') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) // In the fixtures, there are two distinct `toplevel` that @@ -373,7 +373,7 @@ describeIfElasticsearchURL('filter by toplevel', () => { sp.set('query', 'foo') sp.set('include', 'toplevel') sp.set('toplevel', 'Baring') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) const toplevels = new Set(results.hits.map((hit) => hit.toplevel)) @@ -386,7 +386,7 @@ describeIfElasticsearchURL('filter by toplevel', () => { sp.set('include', 'toplevel') sp.append('toplevel', 'Baring') sp.append('toplevel', 'Fooing') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) const toplevels = new Set(results.hits.map((hit) => hit.toplevel)) @@ -398,7 +398,7 @@ describeIfElasticsearchURL('filter by toplevel', () => { sp.set('query', 'foo') sp.set('include', 'toplevel') sp.set('toplevel', 'Never heard of') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse = JSON.parse(res.body) expect(results.meta.found.value).toBe(0) @@ -412,7 +412,7 @@ describeIfElasticsearchURL('aggregate', () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('aggregate', 'toplevel') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(200) const results: GeneralSearchResponse & { aggregations?: SearchResultAggregations } = JSON.parse( res.body, @@ -428,7 +428,7 @@ describeIfElasticsearchURL('aggregate', () => { const sp = new URLSearchParams() sp.set('query', 'foo') sp.set('aggregate', 'unrecognizedxxx') - const res = await get('/api/search/v1?' + sp.toString()) + const res = await get(`/api/search/v1?${sp.toString()}`) expect(res.statusCode).toBe(400) const results = JSON.parse(res.body) as { error: string diff --git a/src/tools/components/Fields.tsx b/src/tools/components/Fields.tsx index e6ae59f4e563..73c3dae6c54e 100644 --- a/src/tools/components/Fields.tsx +++ b/src/tools/components/Fields.tsx @@ -1,5 +1,5 @@ import { ActionList } from '@primer/react' -import { ReactNode } from 'react' +import React, { ReactNode } from 'react' import cx from 'classnames' import { PickerItem } from './Picker' diff --git a/src/webhooks/tests/api.ts b/src/webhooks/tests/api.ts index 102738e413ef..a9e136f794c8 100644 --- a/src/webhooks/tests/api.ts +++ b/src/webhooks/tests/api.ts @@ -14,7 +14,7 @@ describe('webhooks v1 middleware', () => { // field which all webhook types should have. sp.set('category', 'branch_protection_rule') sp.set('version', 'free-pro-team@latest') - const res = await get('/api/webhooks/v1?' + sp) + const res = await get(`/api/webhooks/v1?${sp}`) expect(res.statusCode).toBe(200) const results = JSON.parse(res.body) const actionTypes = Object.keys(results) @@ -36,7 +36,7 @@ describe('webhooks v1 middleware', () => { const sp = new URLSearchParams() sp.set('category', 'branch_protection_rule') sp.set('version', 'enterprise-cloud@latest') - const res = await get('/api/webhooks/v1?' + sp) + const res = await get(`/api/webhooks/v1?${sp}`) expect(res.statusCode).toBe(200) const results = JSON.parse(res.body) const actionTypes = Object.keys(results) @@ -50,7 +50,7 @@ describe('webhooks v1 middleware', () => { const sp = new URLSearchParams() sp.set('category', 'no-such-category') sp.set('version', 'free-pro-team@latest') - const res = await get('/api/webhooks/v1?' + sp) + const res = await get(`/api/webhooks/v1?${sp}`) expect(res.statusCode).toBe(404) expect(JSON.parse(res.body).error).toBeTruthy() @@ -60,7 +60,7 @@ describe('webhooks v1 middleware', () => { const sp = new URLSearchParams() sp.set('category', 'branch_protection_rule') sp.set('version', 'no-such-version') - const res = await get('/api/webhooks/v1?' + sp) + const res = await get(`/api/webhooks/v1?${sp}`) expect(res.statusCode).toBe(404) expect(JSON.parse(res.body).error).toBeTruthy() diff --git a/src/workflows/content-changes-table-comment.ts b/src/workflows/content-changes-table-comment.ts index 26750f4658d3..7b49a7e9aa82 100755 --- a/src/workflows/content-changes-table-comment.ts +++ b/src/workflows/content-changes-table-comment.ts @@ -167,13 +167,13 @@ async function main(owner: string, repo: string, baseSHA: string, headSHA: strin `| ${headings.map((heading) => `**${heading}**`).join(' | ')} |`, `| ${headings.map(() => ':---').join(' | ')} |`, ] - let markdownTable = markdownTableHead.join('\n') + '\n' + let markdownTable = `${markdownTableHead.join('\n')}\n` for (const filteredLine of filteredLines) { if ((markdownTable + filteredLine).length > MAX_COMMENT_SIZE) { markdownTable += '\n**Note** There are more changes in this PR than we can show.' break } - markdownTable += filteredLine + '\n' + markdownTable += `${filteredLine}\n` } return markdownTable diff --git a/src/workflows/enable-automerge.ts b/src/workflows/enable-automerge.ts index 54217e763583..740a201ddab6 100644 --- a/src/workflows/enable-automerge.ts +++ b/src/workflows/enable-automerge.ts @@ -37,12 +37,13 @@ async function main() { } const graph: Record = await github.graphql(mutation, variables) - console.log('GraphQL mutation result:\n' + JSON.stringify(graph)) + console.log(`GraphQL mutation result:\n${JSON.stringify(graph)}`) if (graph.errors && graph.errors.length > 0) { console.error( - 'ERROR! Failed to enable auto-merge:\n - ' + - graph.errors.map((error: any) => error.message).join('\n - '), + `ERROR! Failed to enable auto-merge:\n - ${graph.errors + .map((error: any) => error.message) + .join('\n - ')}`, ) } else { console.log('Auto-merge enabled!') diff --git a/src/workflows/experimental/readability-report.ts b/src/workflows/experimental/readability-report.ts index 0ca6d7232415..c31ffe55d427 100644 --- a/src/workflows/experimental/readability-report.ts +++ b/src/workflows/experimental/readability-report.ts @@ -118,7 +118,7 @@ Note: Requires a local server running on localhost:4000 (npm start) const report = generateReport(results) // Always output to console for local development - console.log('\n' + report) + console.log(`\n${report}`) // If running in CI, also save report for commenting on PR if (process.env.GITHUB_ACTIONS) { @@ -198,12 +198,10 @@ async function waitForServer(): Promise { async function analyzeFile(filePath: string): Promise { // Convert file path to URL path // content/get-started/foo.md -> /get-started/foo - const urlPath = - '/' + - filePath - .replace(/^content\//, '') - .replace(/\.md$/, '') - .replace(/\/index$/, '') + const urlPath = `/${filePath + .replace(/^content\//, '') + .replace(/\.md$/, '') + .replace(/\/index$/, '')}` try { // Fetch the rendered page