diff --git a/.github/workflows/check-broken-links-github-github.yml b/.github/workflows/check-broken-links-github-github.yml index 06f3bcc895d8..fc7ea6ff06bd 100644 --- a/.github/workflows/check-broken-links-github-github.yml +++ b/.github/workflows/check-broken-links-github-github.yml @@ -64,7 +64,7 @@ jobs: - name: Create issue from file if: ${{ hashFiles('broken_github_github_links.md') != '' }} id: github-github-broken-link-report - uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 + uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 with: token: ${{ env.GITHUB_TOKEN }} title: ${{ steps.check.outputs.title }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e6240e2d050..b761b7d6d659 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,10 +36,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + - uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 with: languages: javascript # comma separated list of values from {go, python, javascript, java, cpp, csharp, ruby} - - uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + - uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 continue-on-error: true - uses: ./.github/actions/slack-alert diff --git a/.github/workflows/comment-release-note-info.yml b/.github/workflows/comment-release-note-info.yml index af000c7bb5dd..8aabdb277200 100644 --- a/.github/workflows/comment-release-note-info.yml +++ b/.github/workflows/comment-release-note-info.yml @@ -21,7 +21,7 @@ jobs: if: github.event.pull_request.user.login != 'release-controller[bot]' && github.repository == 'github/docs-internal' runs-on: ubuntu-latest steps: - - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.pull_request.number }} body: | diff --git a/.github/workflows/dont-delete-assets.yml b/.github/workflows/dont-delete-assets.yml index c509ad60c6db..e2ed22fe2d01 100644 --- a/.github/workflows/dont-delete-assets.yml +++ b/.github/workflows/dont-delete-assets.yml @@ -42,7 +42,7 @@ jobs: - name: Find possible previous comment if: ${{ steps.comment.outputs.markdown != '' }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad id: findComment with: issue-number: ${{ github.event.number }} @@ -51,7 +51,7 @@ jobs: - name: Update comment if: ${{ steps.comment.outputs.markdown != '' }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.findComment.outputs.comment-id }} issue-number: ${{ github.event.number }} diff --git a/.github/workflows/dont-delete-features.yml b/.github/workflows/dont-delete-features.yml index 665714ac28e5..950c3377a3ce 100644 --- a/.github/workflows/dont-delete-features.yml +++ b/.github/workflows/dont-delete-features.yml @@ -42,7 +42,7 @@ jobs: - name: Find possible previous comment if: ${{ steps.comment.outputs.markdown != '' }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad id: findComment with: issue-number: ${{ github.event.number }} @@ -51,7 +51,7 @@ jobs: - name: Update comment if: ${{ steps.comment.outputs.markdown != '' }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.findComment.outputs.comment-id }} issue-number: ${{ github.event.number }} diff --git a/.github/workflows/needs-sme-workflow.yml b/.github/workflows/needs-sme-workflow.yml index 171a4d8093f3..9e930545c1f6 100644 --- a/.github/workflows/needs-sme-workflow.yml +++ b/.github/workflows/needs-sme-workflow.yml @@ -24,7 +24,7 @@ jobs: - name: Check out repo uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.issue.number }} body: | @@ -43,7 +43,7 @@ jobs: - name: Check out repo uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.pull_request.number }} body: | diff --git a/.github/workflows/notify-about-deployment.yml b/.github/workflows/notify-about-deployment.yml index 331dc5e740b1..c16fecc57bb5 100644 --- a/.github/workflows/notify-about-deployment.yml +++ b/.github/workflows/notify-about-deployment.yml @@ -47,7 +47,7 @@ jobs: - name: Find content directory changes comment if: ${{ steps.get-number.outputs.number != '' }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad id: findComment with: issue-number: ${{ steps.get-number.outputs.number }} @@ -56,7 +56,7 @@ jobs: - name: Update comment if: ${{ steps.get-number.outputs.number != '' }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.findComment.outputs.comment-id }} issue-number: ${{ steps.get-number.outputs.number }} diff --git a/.github/workflows/readability.yml b/.github/workflows/readability.yml index 32ffec056a33..ef68d9c7982c 100644 --- a/.github/workflows/readability.yml +++ b/.github/workflows/readability.yml @@ -69,7 +69,7 @@ jobs: - name: Find existing readability comment if: ${{ steps.changed_files.outputs.filtered_changed_files }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad id: findComment with: issue-number: ${{ github.event_name == 'workflow_dispatch' && inputs.pull_request_number || github.event.number }} @@ -90,7 +90,7 @@ jobs: - name: Create or update readability comment if: ${{ steps.changed_files.outputs.filtered_changed_files && steps.read_report.outputs.report }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.findComment.outputs.comment-id }} issue-number: ${{ github.event_name == 'workflow_dispatch' && inputs.pull_request_number || github.event.number }} diff --git a/.github/workflows/review-comment.yml b/.github/workflows/review-comment.yml index d5c11214515b..78aff73f539d 100644 --- a/.github/workflows/review-comment.yml +++ b/.github/workflows/review-comment.yml @@ -49,7 +49,7 @@ jobs: echo "APP_URL=https://adjective-noun-hash-4000.app.github.dev" >> $GITHUB_ENV fi - name: Find code changes comment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad id: findComment with: issue-number: ${{ github.event.pull_request.number }} @@ -65,7 +65,7 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: npm run content-changes-table-comment - name: Update comment - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.findComment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/site-policy-reminder.yml b/.github/workflows/site-policy-reminder.yml index 2a3a50cb67fe..4bedf71d8f17 100644 --- a/.github/workflows/site-policy-reminder.yml +++ b/.github/workflows/site-policy-reminder.yml @@ -19,7 +19,7 @@ jobs: github.repository == 'github/docs-internal' runs-on: ubuntu-latest steps: - - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 env: GITHUB_TOKEN: ${{ secrets.API_TOKEN_SITEPOLICY }} with: diff --git a/content/copilot/concepts/agents/about-copilot-cli.md b/content/copilot/concepts/agents/about-copilot-cli.md index ab05afb4f899..0993b3f450a4 100644 --- a/content/copilot/concepts/agents/about-copilot-cli.md +++ b/content/copilot/concepts/agents/about-copilot-cli.md @@ -266,17 +266,7 @@ You can mitigate the risks associated with using the automatic approval options The default model used by {% data variables.copilot.copilot_cli %} is {% data variables.copilot.cca_current_model %}. {% data variables.product.github %} reserves the right to change this model. -You can change the model by setting the `COPILOT_MODEL` environment variable to one of the supported values. For example, change the model to {% data variables.copilot.copilot_gpt_5 %} by setting `COPILOT_MODEL` to `gpt-5`. - -{% rowheaders %} - -| Model name | `COPILOT_MODEL` value | -|------------------------------------------------------------------|-----------------------| -| {% data variables.copilot.cca_current_model %} | `claude-sonnet-4` | -| {% data variables.copilot.copilot_claude_sonnet_45 %} | `claude-sonnet-4.5` | -| {% data variables.copilot.copilot_gpt_5 %} | `gpt-5` | - -{% endrowheaders %} +You can change the model used by {% data variables.copilot.copilot_cli %} by using the `/model` slash command. Enter this command and select a model from the list. Each time you submit a prompt to {% data variables.product.prodname_copilot_short %} in {% data variables.copilot.copilot_cli_short %}'s interactive mode, and each time you use {% data variables.copilot.copilot_cli_short %} in programmatic mode, your monthly quota of {% data variables.product.prodname_copilot_short %} premium requests is reduced by one. For information about premium requests, see [AUTOTITLE](/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests). diff --git a/content/copilot/how-tos/use-copilot-agents/use-copilot-cli.md b/content/copilot/how-tos/use-copilot-agents/use-copilot-cli.md index 5c2091c69420..eaeebf70223d 100644 --- a/content/copilot/how-tos/use-copilot-agents/use-copilot-cli.md +++ b/content/copilot/how-tos/use-copilot-agents/use-copilot-cli.md @@ -102,6 +102,14 @@ If all of the files you want to work with are in a different location, you can s /cwd /path/to/directory ``` +### Run shell commands + +You can prepend your input with `!` to directly run shell commands, without making a call to the model. + +```shell +!git clone https://github.com/github/copilot-cli +``` + ### Resume an interactive session You can return to a previous interactive session, and continue your conversation with {% data variables.product.prodname_copilot_short %}, by using the `--resume` command line option, then choosing the session you want to resume from the list that's displayed. @@ -135,6 +143,10 @@ To extend the functionality available to you in {% data variables.copilot.copilo Details of your configured MCP servers are stored in the `mcp-config.json` file, which is located, by default, in the `~/.config` directory. This location can be changed by setting the `XDG_CONFIG_HOME` environment variable. For information about the JSON structure of a server definition, see [AUTOTITLE](/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp#writing-a-json-configuration-for-mcp-servers). +### View context and usage statistics for the current session + +You can use the `/usage` slash command to view how many premium requests you've used in the current session, the duration of the session, how many lines of code have been edited, and the breakdown of token usage per model. When you have less than 20% of a model's token limit remaining, {% data variables.copilot.copilot_cli_short %} will display a warning that the context will be truncated when the token limit is reached. + ## Find out more For a complete list of the command line options and slash commands that you can use with {% data variables.copilot.copilot_cli_short %}, do one of the following: diff --git a/src/audit-logs/components/GroupedEvents.module.scss b/src/audit-logs/components/GroupedEvents.module.scss new file mode 100644 index 000000000000..b0c6f70f3e4c --- /dev/null +++ b/src/audit-logs/components/GroupedEvents.module.scss @@ -0,0 +1,16 @@ +.eventItem { + margin-bottom: 3rem; +} + +.eventAction { + font-style: normal; +} + +.eventDetail { + margin-left: 1rem; + font-style: normal; +} + +.eventDescription { + margin-left: 1rem; +} diff --git a/src/audit-logs/components/GroupedEvents.tsx b/src/audit-logs/components/GroupedEvents.tsx index d5c161b03f76..1240f328bcdc 100644 --- a/src/audit-logs/components/GroupedEvents.tsx +++ b/src/audit-logs/components/GroupedEvents.tsx @@ -4,6 +4,8 @@ import { HeadingLink } from '@/frame/components/article/HeadingLink' import { useTranslation } from '@/languages/components/useTranslation' import type { AuditLogEventT } from '../types' +import styles from './GroupedEvents.module.scss' + type Props = { auditLogEvents: AuditLogEventT[] category: string @@ -47,15 +49,15 @@ export default function GroupedEvents({ auditLogEvents, category, categoryNote } )}
{auditLogEvents.map((event) => ( -
+
-
+
{event.action}
{event.description}
-
{t('fields')}
-
+
{t('fields')}
+
{event.fields ? event.fields.map((field, index) => ( @@ -68,8 +70,8 @@ export default function GroupedEvents({ auditLogEvents, category, categoryNote } {event.docs_reference_links && event.docs_reference_links !== 'N/A' && ( <> -
{t('reference')}
-
{renderReferenceLinks(event)}
+
{t('reference')}
+
{renderReferenceLinks(event)}
)}
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 520a09e51e86..c78c29e3501c 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 @@ -5,16 +5,16 @@ import { isStringQuoted, isStringPunctuated, } from '../helpers/utils' -import type { RuleParams, RuleErrorCallback } from '../../types' +import type { RuleParams, RuleErrorCallback, Rule, MarkdownToken } from '../../types' -export const imageAltTextEndPunctuation = { +export const imageAltTextEndPunctuation: Rule = { names: ['GHD032', 'image-alt-text-end-punctuation'], description: 'Alternate text for images should end with punctuation', tags: ['accessibility', 'images'], parser: 'markdownit', function: (params: RuleParams, onError: RuleErrorCallback) => { - forEachInlineChild(params, 'image', function forToken(token: any) { - const imageAltText = token.content.trim() + forEachInlineChild(params, 'image', function forToken(token: MarkdownToken) { + const imageAltText = token.content?.trim() // If the alt text is empty, there is nothing to check and you can't // produce a valid range. diff --git a/src/content-linter/lib/linting-rules/internal-links-old-version.js b/src/content-linter/lib/linting-rules/internal-links-old-version.ts similarity index 83% rename from src/content-linter/lib/linting-rules/internal-links-old-version.js rename to src/content-linter/lib/linting-rules/internal-links-old-version.ts index f9e90f8c0c9c..b66781ca2f93 100644 --- a/src/content-linter/lib/linting-rules/internal-links-old-version.js +++ b/src/content-linter/lib/linting-rules/internal-links-old-version.ts @@ -1,21 +1,24 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' import { getRange } from '../helpers/utils' +import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '../../types' -export const internalLinksOldVersion = { +export const internalLinksOldVersion: Rule = { names: ['GHD006', 'internal-links-old-version'], description: 'Internal links must not have a hardcoded version using old versioning syntax', tags: ['links', 'url', 'versioning'], parser: 'markdownit', - function: (params, onError) => { - filterTokens(params, 'inline', (token) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'inline', (token: MarkdownToken) => { if ( params.name.endsWith('migrating-from-github-enterprise-1110x-to-2123.md') || params.name.endsWith('all-releases.md') ) return - for (const child of token.children) { + for (const child of token.children || []) { if (child.type !== 'link_open') continue + if (!child.attrs) continue // Things matched by this RegExp: // - /enterprise/2.19/admin/blah // - https://docs.github.com/enterprise/11.10.340/admin/blah diff --git a/src/content-linter/lib/linting-rules/link-quotation.js b/src/content-linter/lib/linting-rules/link-quotation.ts similarity index 70% rename from src/content-linter/lib/linting-rules/link-quotation.js rename to src/content-linter/lib/linting-rules/link-quotation.ts index 4fd2ce623d37..9d4eb089a170 100644 --- a/src/content-linter/lib/linting-rules/link-quotation.js +++ b/src/content-linter/lib/linting-rules/link-quotation.ts @@ -1,36 +1,41 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' import { getRange, quotePrecedesLinkOpen } from '../helpers/utils' import { escapeRegExp } from 'lodash-es' +import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '../../types' -export const linkQuotation = { +export const linkQuotation: Rule = { names: ['GHD043', 'link-quotation'], description: 'Internal link titles must not be surrounded by quotations', tags: ['links', 'url'], parser: 'markdownit', - function: (params, onError) => { - filterTokens(params, 'inline', (token) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'inline', (token: MarkdownToken) => { const { children } = token - let previous_child = children[0] + if (!children) return + let previous_child: MarkdownToken = children[0] let inLinkWithPrecedingQuotes = false let linkUrl = '' - let content = [] - let line = '' + let content: string[] = [] for (let i = 1; i < children.length; i++) { const child = children[i] - if (child.type === 'link_open' && quotePrecedesLinkOpen(previous_child.content)) { + if (child.type === 'link_open' && quotePrecedesLinkOpen(previous_child.content || '')) { + if (!child.attrs) continue inLinkWithPrecedingQuotes = true linkUrl = escapeRegExp(child.attrs[0][1]) - line = child.line } else if (inLinkWithPrecedingQuotes && child.type === 'text') { - content.push(escapeRegExp(child.content.trim())) + 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}\\)({%.*%})?(!|\\.|\\?|,)?"`) if (regex.test(child.line)) { - const match = child.line.match(regex)[0] + const matchResult = child.line.match(regex) + if (!matchResult) continue + const match = matchResult[0] const range = getRange(child.line, match) + if (!range) continue let newLine = match if (newLine.startsWith('"')) { newLine = newLine.slice(1) @@ -58,7 +63,6 @@ export const linkQuotation = { } inLinkWithPrecedingQuotes = false content = [] - line = '' linkUrl = '' } previous_child = child diff --git a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.js b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts similarity index 82% rename from src/content-linter/lib/linting-rules/liquid-tag-whitespace.js rename to src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts index 62b5d90df622..3b06630ac4ad 100644 --- a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.js +++ b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts @@ -2,6 +2,15 @@ import { TokenKind } from 'liquidjs' import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils' import { addFixErrorDetail } from '../helpers/utils' +import type { RuleParams, RuleErrorCallback, Rule } from '../../types' + +interface LiquidToken { + kind: number + content: string + contentRange: [number, number] + begin: number + end: number +} /* Liquid tags should start and end with one whitespace. For example: @@ -16,14 +25,16 @@ Liquid tags should start and end with one whitespace. For example: {%data arg1 arg2 %} */ -export const liquidTagWhitespace = { +export const liquidTagWhitespace: Rule = { names: ['GHD042', 'liquid-tag-whitespace'], description: 'Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace.', tags: ['liquid', 'format'], - function: (params, onError) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { const content = params.lines.join('\n') - const tokens = getLiquidTokens(content).filter((token) => token.kind === TokenKind.Tag) + const tokens = (getLiquidTokens(content) as LiquidToken[]).filter( + (token: LiquidToken) => token.kind === TokenKind.Tag, + ) for (const token of tokens) { const { lineNumber, column, length } = getPositionData(token, params.lines) diff --git a/src/content-linter/lib/linting-rules/list-first-word-capitalization.js b/src/content-linter/lib/linting-rules/list-first-word-capitalization.ts similarity index 82% rename from src/content-linter/lib/linting-rules/list-first-word-capitalization.js rename to src/content-linter/lib/linting-rules/list-first-word-capitalization.ts index 8d79bd12c9bd..128f1e4f77ca 100644 --- a/src/content-linter/lib/linting-rules/list-first-word-capitalization.js +++ b/src/content-linter/lib/linting-rules/list-first-word-capitalization.ts @@ -1,25 +1,26 @@ import { addFixErrorDetail, getRange, filterTokensByOrder } from '../helpers/utils' +import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '../../types' -export const listFirstWordCapitalization = { +export const listFirstWordCapitalization: Rule = { names: ['GHD034', 'list-first-word-capitalization'], description: 'First word of list item should be capitalized', tags: ['ul', 'ol'], - function: (params, onError) => { + 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.length < 3) return + if (!params.tokens || params.tokens.length < 3) return const inlineListItems = filterTokensByOrder(params.tokens, [ 'list_item_open', 'paragraph_open', 'inline', - ]).filter((token) => token.type === 'inline') + ]).filter((token: MarkdownToken) => token.type === 'inline') - inlineListItems.forEach((token) => { + 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 @@ -27,12 +28,13 @@ export const listFirstWordCapitalization = { // 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 content = (token.content || '').trim() const firstWord = content.trim().split(' ')[0] // If the first character in the first word is not an alphanumeric, @@ -49,6 +51,7 @@ export const listFirstWordCapitalization = { const lineNumber = token.lineNumber const range = getRange(token.line, firstWord) + if (!range) return addFixErrorDetail( onError, lineNumber, diff --git a/src/content-linter/lib/linting-rules/rai-reusable-usage.js b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts similarity index 65% rename from src/content-linter/lib/linting-rules/rai-reusable-usage.js rename to src/content-linter/lib/linting-rules/rai-reusable-usage.ts index 0eb6156bd9c6..135c1e77a0b5 100644 --- a/src/content-linter/lib/linting-rules/rai-reusable-usage.js +++ b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts @@ -1,24 +1,43 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError } from 'markdownlint-rule-helpers' import { TokenKind } from 'liquidjs' import path from 'path' import { getFrontmatter } from '../helpers/utils' import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils' +import type { RuleParams, RuleErrorCallback, Rule } from '../../types' -export const raiReusableUsage = { +interface Frontmatter { + type?: string + // Allow any additional frontmatter properties since we only care about 'type' + [key: string]: any +} + +interface LiquidToken { + kind: number + name?: string + args: string + content: string + begin: number + end: number +} + +export const raiReusableUsage: Rule = { names: ['GHD035', 'rai-reusable-usage'], description: 'RAI articles and reusables can only reference reusable content in the data/reusables/rai directory', tags: ['feature', 'rai'], - function: (params, onError) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { if (!isFileRai(params)) return const content = params.lines.join('\n') - const tokens = getLiquidTokens(content) - .filter((token) => token.kind === TokenKind.Tag) - .filter((token) => token.name === 'data' || token.name === 'indented_data_reference') + const tokens = (getLiquidTokens(content) as LiquidToken[]) + .filter((token: LiquidToken) => token.kind === TokenKind.Tag) + .filter( + (token: LiquidToken) => token.name === 'data' || token.name === 'indented_data_reference', + ) // It's ok to reference variables from rai content - .filter((token) => !token.args.startsWith('variables')) + .filter((token: LiquidToken) => !token.args.startsWith('variables')) for (const token of tokens) { // if token is 'data foo.bar` or `indented_data_reference foo.bar depth=3` @@ -42,7 +61,7 @@ export const raiReusableUsage = { // Rai file content can be in either the data/reusables/rai directory // or anywhere in the content directory -function isFileRai(params) { +function isFileRai(params: RuleParams): boolean { // ROOT is set in the test environment to src/fixtures/fixtures otherwise // it is set to the root of the project. const ROOT = process.env.ROOT || '.' @@ -53,6 +72,6 @@ function isFileRai(params) { return params.name.startsWith(dataRai) } - const fm = getFrontmatter(params.frontMatterLines) || {} + const fm: Frontmatter = (getFrontmatter(params.frontMatterLines) as Frontmatter) || {} return fm.type === 'rai' } diff --git a/src/content-linter/lib/linting-rules/yaml-scheduled-jobs.js b/src/content-linter/lib/linting-rules/yaml-scheduled-jobs.ts similarity index 61% rename from src/content-linter/lib/linting-rules/yaml-scheduled-jobs.js rename to src/content-linter/lib/linting-rules/yaml-scheduled-jobs.ts index 3013d25e4b9c..d3c684d68377 100644 --- a/src/content-linter/lib/linting-rules/yaml-scheduled-jobs.js +++ b/src/content-linter/lib/linting-rules/yaml-scheduled-jobs.ts @@ -1,21 +1,35 @@ import yaml from 'js-yaml' +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' import { liquid } from '@/content-render/index' import { allVersions } from '@/versions/lib/all-versions' +import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '../../types' -const scheduledYamlJobs = [] +interface YamlSchedule { + cron: string +} + +interface YamlWorkflow { + on?: { + schedule?: YamlSchedule[] + } +} + +const scheduledYamlJobs: string[] = [] -export const yamlScheduledJobs = { +export const yamlScheduledJobs: Rule = { names: ['GHD021', 'yaml-scheduled-jobs'], description: 'YAML snippets that include scheduled workflows must not run on the hour and must be unique', tags: ['feature', 'actions'], parser: 'markdownit', asynchronous: true, - function: (params, onError) => { - filterTokens(params, 'fence', async (token) => { - const lang = token.info.trim().split(/\s+/u).shift().toLowerCase() + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'fence', async (token: MarkdownToken) => { + if (!token.info) return + if (!token.content) return + const lang = token.info.trim().split(/\s+/u).shift()?.toLowerCase() if (lang !== 'yaml' && lang !== 'yml') return if (!token.content.includes('schedule:')) return if (!token.content.includes('- cron:')) return @@ -26,15 +40,15 @@ export const yamlScheduledJobs = { } // If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags const renderedYaml = await liquid.parseAndRender(token.content, context) - const yamlObj = yaml.load(renderedYaml) + const yamlObj = yaml.load(renderedYaml) as YamlWorkflow if (!yamlObj.on) return if (!yamlObj.on.schedule) return - yamlObj.on.schedule.forEach((schedule) => { + yamlObj.on.schedule.forEach((schedule: YamlSchedule) => { if (schedule.cron.split(' ')[0] === '0') { addError( onError, - getLineNumber(token.content, schedule.cron) + token.lineNumber, + getLineNumber(token.content!, schedule.cron) + token.lineNumber, `YAML scheduled workflow must not run on the hour`, schedule.cron, ) @@ -43,7 +57,7 @@ export const yamlScheduledJobs = { if (scheduledYamlJobs.includes(schedule.cron)) { addError( onError, - getLineNumber(token.content, schedule.cron) + token.lineNumber, + getLineNumber(token.content!, schedule.cron) + token.lineNumber, `YAML scheduled workflow must be unique`, schedule.cron, ) @@ -55,7 +69,7 @@ export const yamlScheduledJobs = { }, } -function getLineNumber(tokenContent, schedule) { +function getLineNumber(tokenContent: string, schedule: string): number { const contentLines = tokenContent.split('\n') return contentLines.findIndex((line) => line.includes(schedule)) + 1 } diff --git a/src/content-linter/tests/site-data-references.js b/src/content-linter/tests/site-data-references.ts similarity index 75% rename from src/content-linter/tests/site-data-references.js rename to src/content-linter/tests/site-data-references.ts index 4cca081f5179..ced39b018c5f 100644 --- a/src/content-linter/tests/site-data-references.js +++ b/src/content-linter/tests/site-data-references.ts @@ -17,7 +17,7 @@ const getDataPathRegex = const rawLiquidPattern = /{%\s*raw\s*%}.*?{%\s*endraw\s*%}/gs -const getDataReferences = (content) => { +const getDataReferences = (content: string): string[] => { // When looking for things like `{% data reusables.foo %}` in the // content, we first have to exclude any Liquid that isn't real. // E.g. @@ -26,14 +26,15 @@ const getDataReferences = (content) => { // {% endraw %} const withoutRawLiquidBlocks = content.replace(rawLiquidPattern, '') const refs = withoutRawLiquidBlocks.match(patterns.dataReference) || [] - return refs.map((ref) => ref.replace(getDataPathRegex, '$1')) + return refs.map((ref: string) => ref.replace(getDataPathRegex, '$1')) } describe('data references', () => { vi.setConfig({ testTimeout: 60 * 1000 }) test('every data reference found in English variable files is defined and has a value', async () => { - let errors = [] + // value can be any type returned by getDataByLanguage - we check if it's a string + let errors: Array<{ key: string; value: unknown; variableFile: string }> = [] const allVariables = getDeepDataByLanguage('variables', 'en') const variables = Object.values(allVariables) expect(variables.length).toBeGreaterThan(0) @@ -42,13 +43,11 @@ describe('data references', () => { variables.map(async (variablesPerFile) => { const variableRefs = getDataReferences(JSON.stringify(variablesPerFile)) - variableRefs.forEach((key) => { + variableRefs.forEach((key: string) => { const value = getDataByLanguage(key, 'en') if (typeof value !== 'string') { - const variableFile = path.join( - 'data/variables', - getFilenameByValue(allVariables, variablesPerFile), - ) + const filename = getFilenameByValue(allVariables, variablesPerFile) + const variableFile = path.join('data/variables', filename || '') errors.push({ key, value, variableFile }) } }) @@ -60,6 +59,7 @@ describe('data references', () => { }) }) -function getFilenameByValue(object, value) { +// object is the allVariables object with dynamic keys, value is the nested object we're searching for +function getFilenameByValue(object: Record, value: unknown): string | undefined { return Object.keys(object).find((key) => object[key] === value) } diff --git a/src/content-linter/tests/unit/image-alt-text-end-punctuation.js b/src/content-linter/tests/unit/image-alt-text-end-punctuation.ts similarity index 100% rename from src/content-linter/tests/unit/image-alt-text-end-punctuation.js rename to src/content-linter/tests/unit/image-alt-text-end-punctuation.ts diff --git a/src/content-render/liquid/tool.js b/src/content-render/liquid/tool.ts similarity index 72% rename from src/content-render/liquid/tool.js rename to src/content-render/liquid/tool.ts index c7731299b1bd..42152db538a5 100644 --- a/src/content-render/liquid/tool.js +++ b/src/content-render/liquid/tool.ts @@ -1,7 +1,7 @@ import { allTools } from '@/tools/lib/all-tools' import { allPlatforms } from '@/tools/lib/all-platforms' -export const tags = Object.keys(allTools).concat(allPlatforms).concat(['rowheaders']) +export const tags: string[] = Object.keys(allTools).concat(allPlatforms).concat(['rowheaders']) // The trailing newline is important. Without it, the line immediately after // the `
` will be considered part of the previous block, which means the Markdown following the `
` will not be rendered to HTML correctly. For example: @@ -44,23 +44,29 @@ export const tags = Object.keys(allTools).concat(allPlatforms).concat(['rowheade const template = '
{{ output }}
\n' export const Tool = { - type: 'block', + type: 'block' as const, + tagName: '', + // Liquid template objects don't have TypeScript definitions + templates: [] as any[], - parse(tagToken, remainTokens) { + // tagToken and remainTokens are Liquid internal types without TypeScript definitions + parse(tagToken: any, remainTokens: any) { this.tagName = tagToken.name this.templates = [] const stream = this.liquid.parser.parseStream(remainTokens) stream .on(`tag:end${this.tagName}`, () => stream.stop()) - .on('template', (tpl) => this.templates.push(tpl)) + // tpl is a Liquid template object without TypeScript definitions + .on('template', (tpl: any) => this.templates.push(tpl)) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) stream.start() }, - render: function* (scope) { + // scope is a Liquid scope object, Generator yields/returns Liquid template values - no TypeScript definitions available + render: function* (scope: any): Generator { const output = yield this.liquid.renderer.renderTemplates(this.templates, scope) return yield this.liquid.parseAndRender(template, { tagName: this.tagName, diff --git a/src/content-render/unified/copilot-prompt.js b/src/content-render/unified/copilot-prompt.ts similarity index 59% rename from src/content-render/unified/copilot-prompt.js rename to src/content-render/unified/copilot-prompt.ts index 1d2ca48d67c1..1874b4aec91e 100644 --- a/src/content-render/unified/copilot-prompt.js +++ b/src/content-render/unified/copilot-prompt.ts @@ -4,12 +4,15 @@ import { find } from 'unist-util-find' import { h } from 'hastscript' +// @ts-ignore - @primer/octicons doesn't have TypeScript declarations import octicons from '@primer/octicons' import { parse } from 'parse5' import { fromParse5 } from 'hast-util-from-parse5' import { getPreMeta } from './code-header' -export function getPrompt(node, tree, code) { +// node and tree are hast/unist AST nodes without proper TypeScript definitions +// Returns a hast element node for the prompt button +export function getPrompt(node: any, tree: any, code: string): any { const hasPrompt = Boolean(getPreMeta(node).prompt) if (!hasPrompt) return null @@ -28,7 +31,12 @@ export function getPrompt(node, tree, code) { ) } -function buildPromptData(node, tree, code) { +// node and tree are hast/unist AST nodes without proper TypeScript definitions +function buildPromptData( + node: any, + tree: any, + code: string, +): { promptContent: string; ariaLabel: string } { // Find a ref meta in the format 'ref=' const ref = getPreMeta(node).ref @@ -43,31 +51,38 @@ function buildPromptData(node, tree, code) { console.warn(`Can't find referenced code block with id=${ref}`) return promptOnly(code) } - const matchingCode = matchingCodeEl?.children[0].children[0].value || null + // Cast needed to access children property on untyped AST node + const matchingCode = (matchingCodeEl as any)?.children[0].children[0].value || null return promptAndContext(code, matchingCode) } -function promptOnly(code) { +function promptOnly(code: string): { promptContent: string; ariaLabel: string } { return { promptContent: code, ariaLabel: 'Run this prompt in Copilot Chat', } } -function promptAndContext(code, matchingCode) { +function promptAndContext( + code: string, + matchingCode: string, +): { promptContent: string; ariaLabel: string } { return { promptContent: `${matchingCode}\n${code}`, ariaLabel: 'Run this prompt with context in Copilot Chat', } } -function findMatchingCode(ref, tree) { - return find(tree, (node) => { - return node.type === 'element' && node.tagName === 'pre' && getPreMeta(node).id === ref +// tree and node are hast/unist AST nodes without proper TypeScript definitions +function findMatchingCode(ref: string, tree: any): any { + return find(tree, (node: any) => { + // Cast needed to access tagName property on untyped element node + return node.type === 'element' && (node as any).tagName === 'pre' && getPreMeta(node).id === ref }) } -function copilotIcon() { +// Returns a hast element node for the Copilot icon +function copilotIcon(): any { const copilotIconHtml = octicons.copilot.toSVG() const copilotIconAst = parse(String(copilotIconHtml), { sourceCodeLocationInfo: true }) const copilotIcon = fromParse5(copilotIconAst, { file: copilotIconHtml }) diff --git a/src/content-render/unified/rewrite-empty-table-rows.js b/src/content-render/unified/rewrite-empty-table-rows.ts similarity index 67% rename from src/content-render/unified/rewrite-empty-table-rows.js rename to src/content-render/unified/rewrite-empty-table-rows.ts index ff3a740815f4..d10e95e875c5 100644 --- a/src/content-render/unified/rewrite-empty-table-rows.js +++ b/src/content-render/unified/rewrite-empty-table-rows.ts @@ -1,4 +1,4 @@ -import { visit } from 'unist-util-visit' +import { visit, SKIP } from 'unist-util-visit' /** * Where it can mutate the AST to swap from: @@ -49,22 +49,31 @@ import { visit } from 'unist-util-visit' * isn't the same all the way down. But Unified will still parse it. * */ -function matcher(node) { +// node is a hast element node without proper TypeScript definitions +function matcher(node: any): boolean { return node.type === 'element' && node.tagName === 'tr' } -function visitor(node, index, parent) { +// node, parent, and grandChild are hast element nodes without proper TypeScript definitions +function visitor( + node: any, + index: number | undefined, + parent: any, +): [typeof SKIP, number] | undefined { if ( node.children.every( - (grandChild) => + (grandChild: any) => grandChild.type === 'element' && grandChild.tagName === 'td' && !grandChild.children.length, ) ) { - parent.children.splice(index, 1) - return [visit.SKIP, index] + if (index !== undefined) { + parent.children.splice(index, 1) + return [SKIP, index] + } } } +// tree is a hast root node without proper TypeScript definitions export default function rewriteEmptyTableRows() { - return (tree) => visit(tree, matcher, visitor) + return (tree: any) => visit(tree, matcher, visitor) } diff --git a/src/content-render/unified/rewrite-for-rowheaders.js b/src/content-render/unified/rewrite-for-rowheaders.ts similarity index 57% rename from src/content-render/unified/rewrite-for-rowheaders.js rename to src/content-render/unified/rewrite-for-rowheaders.ts index dedcccbfae43..f86e1d7162dc 100644 --- a/src/content-render/unified/rewrite-for-rowheaders.js +++ b/src/content-render/unified/rewrite-for-rowheaders.ts @@ -1,5 +1,21 @@ import { visitParents } from 'unist-util-visit-parents' +interface ElementNode { + type: 'element' + tagName: string + properties: { + // Properties can have any value type (strings, booleans, arrays, etc.) + [key: string]: any + } + _scoped?: boolean +} + +interface AncestorNode { + properties?: { + className?: string[] + } +} + /** * Where it can mutate the AST to swap from: * @@ -33,22 +49,23 @@ import { visitParents } from 'unist-util-visit-parents' * * */ -function matcher(node) { +function matcher(node: any): node is ElementNode { return node.type === 'element' && node.tagName === 'td' && !('scope' in node.properties) } -function insideRowheaders(ancestors) { +function insideRowheaders(ancestors: AncestorNode[]): boolean { return ancestors.some( - (node) => + (node: AncestorNode) => node.properties && node.properties.className && node.properties.className.includes('rowheaders'), ) } -function visitor(node, ancestors) { +// ancestors is an array of hast nodes without proper TypeScript definitions +function visitor(node: ElementNode, ancestors: any[]): void { if (insideRowheaders(ancestors)) { - const tr = ancestors.at(-1) + const tr = ancestors.at(-1) as ElementNode if (!tr._scoped) { tr._scoped = true node.properties.scope = 'row' @@ -57,6 +74,7 @@ function visitor(node, ancestors) { } } +// tree is a hast root node without proper TypeScript definitions export default function rewriteForRowheaders() { - return (tree) => visitParents(tree, matcher, visitor) + return (tree: any) => visitParents(tree, matcher, visitor) } diff --git a/src/frame/components/DefaultLayout.module.scss b/src/frame/components/DefaultLayout.module.scss new file mode 100644 index 000000000000..3612da8c340b --- /dev/null +++ b/src/frame/components/DefaultLayout.module.scss @@ -0,0 +1,3 @@ +.mainContent { + scroll-margin-top: 5rem; +} diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index 69f8db09638e..5c3d5bf2990c 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -15,6 +15,8 @@ import { useLanguages } from '@/languages/components/LanguagesContext' import { ClientSideLanguageRedirect } from './ClientSideLanguageRedirect' import { SearchOverlayContextProvider } from '@/search/components/context/SearchOverlayContext' +import styles from './DefaultLayout.module.scss' + const MINIMAL_RENDER = Boolean(JSON.parse(process.env.MINIMAL_RENDER || 'false')) type Props = { children?: React.ReactNode } @@ -51,7 +53,7 @@ export const DefaultLayout = (props: Props) => {
-
+
{props.children}
@@ -150,7 +152,7 @@ export const DefaultLayout = (props: Props) => { {/* Need to set an explicit height for sticky elements since we also set overflow to auto */}
-
+
diff --git a/src/frame/components/GenericError.module.scss b/src/frame/components/GenericError.module.scss new file mode 100644 index 000000000000..247697d1cbf3 --- /dev/null +++ b/src/frame/components/GenericError.module.scss @@ -0,0 +1,3 @@ +.logoContainer { + z-index: 3; +} diff --git a/src/frame/components/GenericError.tsx b/src/frame/components/GenericError.tsx index 3e5f0b36c7eb..d2b59d47deef 100644 --- a/src/frame/components/GenericError.tsx +++ b/src/frame/components/GenericError.tsx @@ -5,6 +5,8 @@ import { useRouter } from 'next/router' import { MarkGithubIcon, CommentDiscussionIcon } from '@primer/octicons-react' import { Lead } from '@/frame/components/ui/Lead' +import styles from './GenericError.module.scss' + export function GenericError() { return (
@@ -44,7 +46,7 @@ export const SimpleHeader = () => { role="banner" aria-label="Main" > -
+
diff --git a/src/frame/components/page-header/Header.module.scss b/src/frame/components/page-header/Header.module.scss index 27b117d45644..3a5e1285e4d0 100644 --- a/src/frame/components/page-header/Header.module.scss +++ b/src/frame/components/page-header/Header.module.scss @@ -57,3 +57,7 @@ padding: 0 !important; } } + +.headerContainer { + row-gap: 1rem; +} diff --git a/src/frame/components/page-header/Header.tsx b/src/frame/components/page-header/Header.tsx index ed4154567027..07e8409fcbb3 100644 --- a/src/frame/components/page-header/Header.tsx +++ b/src/frame/components/page-header/Header.tsx @@ -156,11 +156,10 @@ export const Header = () => { aria-label="Main" >