From 07510f6f8cc57595ee0a5c0bbe9be7bdb9e82841 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 1 May 2026 07:42:56 -0700 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=9A=B0=20Add=20missing=20failure-issu?= =?UTF-8?q?e=20alerts=20to=20scheduled=20workflows=20(#60920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/needs-sme-stale-check.yaml | 5 ++ .github/workflows/no-response.yaml | 5 ++ .github/workflows/notify-about-deployment.yml | 5 ++ .github/workflows/triage-stale-check.yml | 10 +++ src/workflows/tests/actions-workflows.ts | 80 ++++++++++++------- 5 files changed, 78 insertions(+), 27 deletions(-) diff --git a/.github/workflows/needs-sme-stale-check.yaml b/.github/workflows/needs-sme-stale-check.yaml index 388708ebab91..589993d3d3ab 100644 --- a/.github/workflows/needs-sme-stale-check.yaml +++ b/.github/workflows/needs-sme-stale-check.yaml @@ -41,3 +41,8 @@ jobs: with: slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml index 451d668b10bd..004db7603208 100644 --- a/.github/workflows/no-response.yaml +++ b/.github/workflows/no-response.yaml @@ -63,3 +63,8 @@ jobs: with: slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/notify-about-deployment.yml b/.github/workflows/notify-about-deployment.yml index 30f8443978ed..e7fb384447b3 100644 --- a/.github/workflows/notify-about-deployment.yml +++ b/.github/workflows/notify-about-deployment.yml @@ -75,3 +75,8 @@ jobs: with: slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml index 63e081fe0c06..4bd9eaa2565b 100644 --- a/.github/workflows/triage-stale-check.yml +++ b/.github/workflows/triage-stale-check.yml @@ -52,6 +52,11 @@ jobs: slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} + stale_staff: name: Remind staff about PRs waiting for review if: github.repository == 'github/docs' @@ -82,3 +87,8 @@ jobs: with: slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts index e3d6d514f857..fd06ea4422a2 100644 --- a/src/workflows/tests/actions-workflows.ts +++ b/src/workflows/tests/actions-workflows.ts @@ -68,19 +68,44 @@ const allUsedActions = chain(workflows) const scheduledWorkflows = workflows.filter(({ data }) => data.on.schedule) -const alertWorkflows = workflows - // Only include jobs running on docs-internal - .filter(({ data }) => - Object.values(data.jobs) - .map((job) => job.if) - .toString() - .includes('docs-internal'), - ) - // Require slack alerts on workflows that aren't actively watched at time of run - .filter(({ data }) => data.on.schedule || data.on.push || data.on.issues || data.on.issue_comment) -// Not including -// - premerge workflows: pull_request, pull_request_target, pull_request_review, merge_group -// - adhoc workflows: workflow_dispatch, workflow_run, workflow_call, repository_dispatch +// Triggers where a workflow runs without a human actively watching and +// therefore needs explicit failure reporting (Slack + issue). Attended +// triggers (pull_request*, workflow_dispatch, workflow_call, merge_group) +// are intentionally excluded: the person who triggered the run sees the +// result directly. +// +// `issues` and `issue_comment` are only considered unattended for jobs +// running in docs-internal itself. When a job is scoped to the public +// github/docs fork via `if: github.repository == 'github/docs'`, those +// triggers fire from external reporters/commenters, and the issue or +// comment itself is the natural failure surface — piling on automated +// alert-issues there is duplicative and noisy. +const ALWAYS_UNATTENDED_TRIGGERS = ['schedule', 'workflow_run', 'repository_dispatch', 'push'] +const DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS = ['issues', 'issue_comment'] + +function jobIsPublicDocsScoped(job: WorkflowJob): boolean { + return typeof job.if === 'string' && /github\.repository\s*==\s*['"]github\/docs['"]/.test(job.if) +} + +function jobRequiresFailureAlerts(workflow: WorkflowMeta, job: WorkflowJob): boolean { + const triggers = workflow.data.on || {} + if (ALWAYS_UNATTENDED_TRIGGERS.some((t) => (triggers as Record)[t])) { + return true + } + if ( + !jobIsPublicDocsScoped(job) && + DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS.some((t) => (triggers as Record)[t]) + ) { + return true + } + return false +} + +// Workflows where at least one job requires failure alerts — used to drive +// the parameterised tests below. Per-job filtering happens inside each test. +const alertWorkflows = workflows.filter(({ data }) => + Object.values(data.jobs).some((job) => job.steps), +) // to generate list, console.log(new Set(workflows.map(({ data }) => Object.keys(data.on)).flat())) const dailyWorkflows = scheduledWorkflows.filter(({ data }) => @@ -151,23 +176,22 @@ describe('GitHub Actions workflows', () => { } }) - test.each(alertWorkflows)( - 'scheduled workflows slack alert on fail $filename', - ({ filename, data }) => { - for (const [name, job] of Object.entries(data.jobs)) { - if ( - !job.steps.find((step: WorkflowStep) => step.uses === './.github/actions/slack-alert') - ) { - throw new Error(`Job ${filename} # ${name} missing slack alert on fail`) - } + test.each(alertWorkflows)('unattended workflows slack alert on fail $filename', (workflow) => { + const { filename, data } = workflow + for (const [name, job] of Object.entries(data.jobs)) { + if (!jobRequiresFailureAlerts(workflow, job)) continue + if (!job.steps.find((step: WorkflowStep) => step.uses === './.github/actions/slack-alert')) { + throw new Error(`Job ${filename} # ${name} missing slack alert on fail`) } - }, - ) + } + }) test.each(alertWorkflows)( - 'scheduled workflows create failure issue on fail $filename', - ({ filename, data }) => { + 'unattended workflows create failure issue on fail $filename', + (workflow) => { + const { filename, data } = workflow for (const [name, job] of Object.entries(data.jobs)) { + if (!jobRequiresFailureAlerts(workflow, job)) continue if ( !job.steps.find( (step: WorkflowStep) => step.uses === './.github/actions/create-workflow-failure-issue', @@ -181,8 +205,10 @@ describe('GitHub Actions workflows', () => { test.each(alertWorkflows)( 'performs a checkout before calling composite action $filename', - ({ filename, data }) => { + (workflow) => { + const { filename, data } = workflow for (const [name, job] of Object.entries(data.jobs)) { + if (!jobRequiresFailureAlerts(workflow, job)) continue if (!job.steps.find((step: WorkflowStep) => checkoutRegexp.test(step.uses || ''))) { throw new Error( `Job ${filename} # ${name} missing a checkout before calling the composite action`, From 229c1024fe6d600cfe9bf2a3eb8964885324ac38 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 1 May 2026 07:47:49 -0700 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Bump=20react=20and=20r?= =?UTF-8?q?eact-dom=20to=20v19=20(#60891)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 105 ++++++++---------- package.json | 8 +- src/frame/components/ui/BumpLink/BumpLink.tsx | 2 +- .../ui/MarkdownContent/MarkdownContent.tsx | 11 +- .../components/input/SearchBarButton.tsx | 2 +- src/search/components/input/SearchOverlay.tsx | 2 +- .../input/SearchOverlayContainer.tsx | 2 +- src/tools/components/InArticlePicker.tsx | 10 +- 8 files changed, 68 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index c98c68f86116..105ddd1f4896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,8 +83,8 @@ "ora": "^9.3.0", "parse5": "7.1.2", "quick-lru": "7.0.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-is": "^19.2.4", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", @@ -137,8 +137,8 @@ "@types/lodash": "^4.17.24", "@types/lodash-es": "4.17.12", "@types/mdast": "^4.0.4", - "@types/react": "18.3.20", - "@types/react-dom": "^18.3.7", + "@types/react": "19.2.14", + "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/styled-components": "^5.1.36", "@types/tcp-port-used": "1.0.4", @@ -559,11 +559,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -647,9 +648,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -683,14 +685,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "license": "MIT", "peer": true, "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -712,11 +714,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -4830,10 +4833,6 @@ "undici-types": "~7.16.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.4", - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -4849,23 +4848,22 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", - "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/request": { @@ -6080,6 +6078,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -11243,16 +11242,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lowdb": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", @@ -13970,12 +13959,10 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13990,15 +13977,15 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.5" } }, "node_modules/react-intersection-observer": { @@ -14695,12 +14682,10 @@ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/scroll-anchoring": { "version": "0.1.0", diff --git a/package.json b/package.json index d783b2822961..1728d55851f3 100644 --- a/package.json +++ b/package.json @@ -243,8 +243,8 @@ "ora": "^9.3.0", "parse5": "7.1.2", "quick-lru": "7.0.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-is": "^19.2.4", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", @@ -297,8 +297,8 @@ "@types/lodash": "^4.17.24", "@types/lodash-es": "4.17.12", "@types/mdast": "^4.0.4", - "@types/react": "18.3.20", - "@types/react-dom": "^18.3.7", + "@types/react": "19.2.14", + "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/styled-components": "^5.1.36", "@types/tcp-port-used": "1.0.4", diff --git a/src/frame/components/ui/BumpLink/BumpLink.tsx b/src/frame/components/ui/BumpLink/BumpLink.tsx index 7e2f3454144d..a354495aae25 100644 --- a/src/frame/components/ui/BumpLink/BumpLink.tsx +++ b/src/frame/components/ui/BumpLink/BumpLink.tsx @@ -5,7 +5,7 @@ import styles from './BumpLink.module.scss' export type BumpLinkPropsT = { children?: ReactNode - title: ReactElement | string + title: ReactElement<{ children?: ReactNode }> | string href: string as?: ElementType<{ className?: string; href: string }> className?: string diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx index 3f3790287234..8a75efbf2a2b 100644 --- a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx +++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import { memo, ReactNode } from 'react' import type { JSX } from 'react' import cx from 'classnames' @@ -10,12 +10,15 @@ export type MarkdownContentPropsT = { as?: keyof JSX.IntrinsicElements } -export const MarkdownContent = ({ +// Memoized so that re-renders of the parent (e.g. when ToolPicker/PlatformPicker +// state updates) don't cause React 19 to re-apply `dangerouslySetInnerHTML` and +// wipe out the inline `display` styles set imperatively by the pickers. +export const MarkdownContent = memo(function MarkdownContent({ children, as: Component = 'div', className, ...restProps -}: MarkdownContentPropsT) => { +}: MarkdownContentPropsT) { return ( ) -} +}) diff --git a/src/search/components/input/SearchBarButton.tsx b/src/search/components/input/SearchBarButton.tsx index 528b5a0f1e11..804b9806c11d 100644 --- a/src/search/components/input/SearchBarButton.tsx +++ b/src/search/components/input/SearchBarButton.tsx @@ -12,7 +12,7 @@ type Props = { isSearchOpen: boolean setIsSearchOpen: (value: boolean) => void params: QueryParams - searchButtonRef: React.RefObject + searchButtonRef: React.RefObject instanceId?: string } diff --git a/src/search/components/input/SearchOverlay.tsx b/src/search/components/input/SearchOverlay.tsx index a1e42fa5aa87..c96fb2424f74 100644 --- a/src/search/components/input/SearchOverlay.tsx +++ b/src/search/components/input/SearchOverlay.tsx @@ -34,7 +34,7 @@ import styles from './SearchOverlay.module.scss' type Props = { searchOverlayOpen: boolean - parentRef: RefObject + parentRef: RefObject debug: boolean onClose: () => void params: { diff --git a/src/search/components/input/SearchOverlayContainer.tsx b/src/search/components/input/SearchOverlayContainer.tsx index cd504ca62b6f..84141391658c 100644 --- a/src/search/components/input/SearchOverlayContainer.tsx +++ b/src/search/components/input/SearchOverlayContainer.tsx @@ -7,7 +7,7 @@ type Props = { setIsSearchOpen: (value: boolean) => void params: QueryParams updateParams: (updates: Partial) => void - searchButtonRef: React.RefObject + searchButtonRef: React.RefObject } export function SearchOverlayContainer({ diff --git a/src/tools/components/InArticlePicker.tsx b/src/tools/components/InArticlePicker.tsx index e60edd961a6d..ddc942e02d9b 100644 --- a/src/tools/components/InArticlePicker.tsx +++ b/src/tools/components/InArticlePicker.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useLayoutEffect, useState } from 'react' import Cookies from '@/frame/components/lib/cookies' import { UnderlineNav } from '@primer/react' import { sendEvent } from '@/events/components/events' @@ -7,6 +7,8 @@ import { useRouter } from 'next/router' import styles from './InArticlePicker.module.scss' +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect + type Option = { value: string label: string @@ -63,7 +65,11 @@ export const InArticlePicker = ({ const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?') - useEffect(() => { + // Use a layout effect so the DOM mutation (hiding non-matching .ghd-tool + // content) happens before the browser paints. With React 19's stricter + // effect timing, a regular useEffect could leave non-matching content + // visible on initial page load until after first paint. + useIsomorphicLayoutEffect(() => { // This will make the hook run this callback on mount and on change. // That's important because even though the user hasn't interacted // and made an overriding choice, we still want to run this callback From 828791d2fc0ef522b4cc7e8f8ba441883e76d304 Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Fri, 1 May 2026 07:49:22 -0700 Subject: [PATCH 3/7] Fix .md URLs without language prefix redirecting to 404 (#60996) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frame/tests/server.ts | 6 ++++++ src/redirects/middleware/handle-redirects.ts | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/frame/tests/server.ts b/src/frame/tests/server.ts index 130ce22e771c..3e1fa0146af7 100644 --- a/src/frame/tests/server.ts +++ b/src/frame/tests/server.ts @@ -324,6 +324,12 @@ describe('server', () => { expect(res.body).toMatch(/^# .+/) }) + test('.md URL without language prefix redirects to /en/ equivalent', async () => { + const res = await get('/get-started.md') + expect(res.statusCode).toBe(302) + expect(res.headers.location).toBe('/en/get-started.md') + }) + test('/index.md redirects to the page without /index.md', async () => { const res = await get('/en/get-started/index.md') expect(res.statusCode).toBe(302) diff --git a/src/redirects/middleware/handle-redirects.ts b/src/redirects/middleware/handle-redirects.ts index c5f6b485ab98..c92bdec51c3f 100644 --- a/src/redirects/middleware/handle-redirects.ts +++ b/src/redirects/middleware/handle-redirects.ts @@ -107,8 +107,12 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex // But for example, a `/authentication/connecting-to-github-with-ssh` // needs to become `/en/authentication/connecting-to-github-with-ssh` const possibleRedirectTo = `/en${req.path}` + // Pages are keyed without .md, so strip it before lookup + const lookupPath = possibleRedirectTo.endsWith('.md') + ? possibleRedirectTo.replace(/\.md$/, '') + : possibleRedirectTo if (!req.context.pages) throw new Error('req.context.pages not yet set') - if (possibleRedirectTo in req.context.pages || isDeprecatedVersion(req.path)) { + if (lookupPath in req.context.pages || isDeprecatedVersion(req.path)) { const language = getLanguage(req) // Note, it's important to use `req.url` here and not `req.path` @@ -127,7 +131,10 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex // do not redirect if the redirected page can't be found if ( - !(req.context.pages[removeQueryParams(redirect)] || isDeprecatedVersion(req.path)) && + !( + req.context.pages[removeQueryParams(redirect).replace(/\.md$/, '')] || + isDeprecatedVersion(req.path) + ) && !redirect.includes('://') ) { // display error on the page in development, but not in production From cfc835dc396a2be0a38d3e4391ec02e877a269ee Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Fri, 1 May 2026 07:51:09 -0700 Subject: [PATCH 4/7] fix: add new translation corruption patterns for es, zh, de, ru (#60975) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kevin Heis --- .../lib/correct-translation-content.ts | 41 ++++++++++++- .../tests/correct-translation-content.ts | 58 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index 04ff3d6f1c8e..1df23bd1acd9 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -81,6 +81,26 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% datos variables', '{% data variables') content = content.replaceAll('{% de datos variables', '{% data variables') content = content.replaceAll('{% datos reusables', '{% data reusables') + // `{% WORD de datos variables.` — extra Spanish word before "de datos variables" + // e.g. `{% uso de datos variables.` ("use of data variables") or + // `{% análisis de datos variables.` ("data analysis variables"). + // Unicode-aware character class so accented translator words match. + content = content.replace( + /\{%(-?)\s*[\p{L}\p{M}]+\s+de datos (variables|reusables)\./gu, + '{%$1 data $2.', + ) + // `{% de datos WORD variables.` — adjective inserted between "de datos" and path + // e.g. `{% de datos específico variables.` ("specific data variables") + content = content.replace( + /\{%(-?)\s*de datos [\p{L}\p{M}]+ (variables|reusables)\./gu, + '{%$1 data $2.', + ) + // `{% WORD de variables.` — word + "de variables" (missing "datos" keyword) + // e.g. `{% alerta de variables.product.X %}` (alert of variables) + content = content.replace( + /\{%(-?)\s*[\p{L}\p{M}]+\s+de\s+(variables|reusables)\./gu, + '{%$1 data $2.', + ) content = content.replaceAll('{% data reutilizables.', '{% data reusables.') // `{% datos reutilizables.` — fully translated "data reusables" path content = content.replaceAll('{% datos reutilizables.', '{% data reusables.') @@ -552,8 +572,11 @@ export function correctTranslatedContentStrings( // `{% 行标题 %}` — "row headers" = rowheaders content = content.replaceAll('{% 行标题 %}', '{% rowheaders %}') content = content.replaceAll('{%- 行标题 %}', '{%- rowheaders %}') - // `{% 数据变量.` — "data variables" = data variables + // `{% 数据变量.` — "data variables" = data variables (with space before) content = content.replaceAll('{% 数据变量.', '{% data variables.') + // `{%数据变量.` — same but no space between `{%` and 数据变量 (e.g. `{%数据变量.enterprise.management_console%}`) + content = content.replaceAll('{%数据变量.', '{% data variables.') + content = content.replaceAll('{%-数据变量.', '{%- data variables.') // `{% Windows 操作系统 %}` — "Windows OS" = windows platform tag content = content.replaceAll('{% Windows 操作系统 %}', '{% windows %}') content = content.replaceAll('{%- Windows 操作系统 %}', '{%- windows %}') @@ -610,6 +633,9 @@ export function correctTranslatedContentStrings( if (context.code === 'ru') { content = content.replaceAll('[«AUTOTITLE»](', '[AUTOTITLE](') content = content.replaceAll('[АВТОЗАГОЛОВОК](', '[AUTOTITLE](') + // `[{% autoTITLE](url)` — Liquid-embedded lowercase autotitle (translator lowercased + // the link anchor and wrapped it in Liquid tag syntax instead of plain `[AUTOTITLE](url)`) + content = content.replaceAll('[{% autoTITLE](', '[AUTOTITLE](') content = content.replaceAll('{% данных variables', '{% data variables') content = content.replaceAll('{% данных, variables', '{% data variables') content = content.replaceAll('{% данными variables', '{% data variables') @@ -1122,6 +1148,10 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{%- Datenvariablen.', '{%- data variables.') content = content.replaceAll('{%-Daten variables', '{%- data variables') content = content.replaceAll('{%-Daten-variables', '{%- data variables') + // `{%-DatenXxx variables` — compound "Daten..." word immediately after `{%-` (no space) + // e.g. `{%-Datenpaket variables.`, `{%-Dateninstanz variables.`, `{%-Dateneinstellungen variables.` + // The existing `{%- DatenXxx variables` rules (with space) don't catch the no-space variant. + content = content.replace(/\{%-(Daten[A-Za-z]+)\s+(variables|reusables)/g, '{%- data $2') content = content.replaceAll('{%- ifversion fpt oder ghec %}', '{%- ifversion fpt or ghec %}') content = content.replaceAll('{% ifversion fpt oder ghec %}', '{% ifversion fpt or ghec %}') // Catch remaining "oder" between any plan names in ifversion/elsif/if tags @@ -1138,6 +1168,15 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% Tipp %}', '{% tip %}') content = content.replaceAll('{%- Tipp %}', '{%- tip %}') content = content.replaceAll('{%- Tipp -%}', '{%- tip -%}') + // `{% Codespaces %}` — translator capitalized the platform tag + content = content.replaceAll('{% Codespaces %}', '{% codespaces %}') + content = content.replaceAll('{%- Codespaces %}', '{%- codespaces %}') + // `{% Aufforderung %}` — German "Aufforderung" (prompt/instruction) = prompt + content = content.replaceAll('{% Aufforderung %}', '{% prompt %}') + content = content.replaceAll('{%- Aufforderung %}', '{%- prompt %}') + // `{% Endprompt %}` — mix of German "End" and English "prompt" = endprompt + content = content.replaceAll('{% Endprompt %}', '{% endprompt %}') + content = content.replaceAll('{%- Endprompt %}', '{%- endprompt %}') // Translated for-loop keywords: `für VARNAME in COLLECTION` content = content.replace(/\{%-? für (\w+) in /g, (match) => { return match.replace('für', 'for') diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts index fb657cea1273..3afdf2bcf042 100644 --- a/src/languages/tests/correct-translation-content.ts +++ b/src/languages/tests/correct-translation-content.ts @@ -33,6 +33,30 @@ describe('correctTranslatedContentStrings', () => { expect(fix('{% data reutilizables.foo.bar %}', 'es')).toBe('{% data reusables.foo.bar %}') }) + test('fixes extra Spanish word inserted around "de datos" and "de variables"', () => { + // `{% WORD de datos variables.` — leading translator word + expect(fix('{% uso de datos variables.product.github %}', 'es')).toBe( + '{% data variables.product.github %}', + ) + // Unicode-aware: accented words must also match + expect(fix('{% análisis de datos variables.product.github %}', 'es')).toBe( + '{% data variables.product.github %}', + ) + expect(fix('{%- uso de datos reusables.foo.bar %}', 'es')).toBe( + '{%- data reusables.foo.bar %}', + ) + + // `{% de datos WORD variables.` — adjective inserted after "de datos" + expect(fix('{% de datos específico variables.product.github %}', 'es')).toBe( + '{% data variables.product.github %}', + ) + + // `{% WORD de variables.` — missing "datos" keyword + expect(fix('{% alerta de variables.product.github %}', 'es')).toBe( + '{% data variables.product.github %}', + ) + }) + test('fixes translated comment keyword', () => { expect(fix('{% comentario %}', 'es')).toBe('{% comment %}') expect(fix('{%- comentario %}', 'es')).toBe('{%- comment %}') @@ -502,6 +526,15 @@ describe('correctTranslatedContentStrings', () => { test('fixes 数据变量 → data variables', () => { expect(fix('{% 数据变量.product.github %}', 'zh')).toBe('{% data variables.product.github %}') }) + + test('fixes 数据变量 with no leading space (`{%数据变量.`)', () => { + expect(fix('{%数据变量.enterprise.management_console%}', 'zh')).toBe( + '{% data variables.enterprise.management_console%}', + ) + expect(fix('{%-数据变量.product.github %}', 'zh')).toBe( + '{%- data variables.product.github %}', + ) + }) }) // ─── RUSSIAN (ru) ────────────────────────────────────────────────── @@ -515,6 +548,10 @@ describe('correctTranslatedContentStrings', () => { expect(fix('[АВТОЗАГОЛОВОК](/path/to/article)', 'ru')).toBe('[AUTOTITLE](/path/to/article)') }) + test('fixes Liquid-embedded lowercase autotitle anchor (`[{% autoTITLE](`)', () => { + expect(fix('[{% autoTITLE](/path/to/article)', 'ru')).toBe('[AUTOTITLE](/path/to/article)') + }) + test('fixes translated data tag variants', () => { expect(fix('{% данных variables.product.github %}', 'ru')).toBe( '{% data variables.product.github %}', @@ -983,6 +1020,27 @@ describe('correctTranslatedContentStrings', () => { expect(fix('{%- Tipp -%}', 'de')).toBe('{%- tip -%}') }) + test('fixes capitalized Codespaces platform tag', () => { + expect(fix('{% Codespaces %}', 'de')).toBe('{% codespaces %}') + expect(fix('{%- Codespaces %}', 'de')).toBe('{%- codespaces %}') + }) + + test('fixes translated prompt/endprompt keywords', () => { + expect(fix('{% Aufforderung %}', 'de')).toBe('{% prompt %}') + expect(fix('{%- Aufforderung %}', 'de')).toBe('{%- prompt %}') + expect(fix('{% Endprompt %}', 'de')).toBe('{% endprompt %}') + expect(fix('{%- Endprompt %}', 'de')).toBe('{%- endprompt %}') + }) + + test('fixes `{%-DatenXxx variables` no-space compound German "Daten" tags', () => { + expect(fix('{%-Datenpaket variables.product.github %}', 'de')).toBe( + '{%- data variables.product.github %}', + ) + expect(fix('{%-Dateneinstellungen reusables.foo.bar %}', 'de')).toBe( + '{%- data reusables.foo.bar %}', + ) + }) + test('fixes für → for in for-loops', () => { expect(fix('{%- für version in tables.copilot.ides -%}', 'de')).toBe( '{%- for version in tables.copilot.ides -%}', From d4c2042e1cf26978a08d38e3e5b880f42e21745e Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 1 May 2026 07:55:16 -0700 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Remove=20any=20types?= =?UTF-8?q?=20from=20a=20few=20legacy=20files=20(#60925)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eslint.config.ts | 5 - .../liquid-ifversion-versions.ts | 187 +++++++++++++----- .../components/ProductLandingContext.tsx | 104 ++++++---- src/rest/lib/index.ts | 4 +- .../markdownlint-rule-search-replace.d.ts | 6 +- src/types/primer__octicons.d.ts | 2 +- 6 files changed, 207 insertions(+), 101 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index e56b53c9a0df..0d3ef1e2b087 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -234,7 +234,6 @@ export default [ 'src/article-api/transformers/audit-logs-transformer.ts', 'src/article-api/transformers/rest-transformer.ts', 'src/codeql-cli/scripts/convert-markdown-for-docs.ts', - 'src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts', 'src/content-linter/scripts/lint-content.ts', 'src/content-render/liquid/engine.ts', @@ -260,7 +259,6 @@ export default [ 'src/graphql/tests/validate-schema.ts', 'src/landings/components/CookBookFilter.tsx', 'src/landings/components/ProductGuidesContext.tsx', - 'src/landings/components/ProductLandingContext.tsx', 'src/landings/components/SidebarProduct.tsx', 'src/landings/pages/home.tsx', 'src/landings/pages/product.tsx', @@ -271,7 +269,6 @@ export default [ 'src/links/scripts/check-github-github-links.ts', 'src/links/scripts/update-internal-links.ts', 'src/rest/components/get-rest-code-samples.ts', - 'src/rest/lib/index.ts', 'src/rest/pages/category.tsx', 'src/rest/pages/subcategory.tsx', 'src/rest/scripts/utils/create-rest-examples.ts', @@ -303,8 +300,6 @@ export default [ 'src/types/github__markdownlint-github.d.ts', 'src/types/markdownlint-lib-rules.d.ts', 'src/types/markdownlint-rule-helpers.d.ts', - 'src/types/markdownlint-rule-search-replace.d.ts', - 'src/types/primer__octicons.d.ts', ], rules: { '@typescript-eslint/no-explicit-any': 'off', 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 664368357c86..68b7d225854f 100644 --- a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts +++ b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts @@ -21,6 +21,64 @@ import { import { oldestSupported } from '@/versions/lib/enterprise-server-releases' import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' +// A liquidjs token, as exposed by getLiquidIfVersionTokens. liquidjs's TopLevelToken +// type does not declare all of the runtime properties we rely on (begin/end, content, +// contentRange, name), so we narrow it here. +type LiquidConditionalToken = TopLevelToken & { + name: string + content: string + begin: number + end: number + contentRange: [number, number] +} + +// Frontmatter `versions` declaration. May be a wildcard string ("*") or a record +// keyed by short version names (fpt, ghec, ghes, feature, ...) with semver-range values. +type VersionsObject = Record +type FileVersionsFm = string | VersionsObject | undefined + +type CondTagAction = { + type: 'none' | 'delete' | 'all' | 'change' + name?: string + cond?: string + line?: unknown + lineNumbers?: unknown + length?: unknown + column?: unknown + content?: unknown +} + +// Internal representation of an ifversion/elsif/else/endif tag that flows through +// the rule. fileVersionsFmAll, versionsObj, featureVersionsObj, versionsObjAll, and +// versions are populated for ifversion/elsif tags and may be absent on else/endif. +// `action` is always populated by decorateCondTagItems before setLiquidErrors and +// updateConditionals run. +type CondTagItem = { + name: string + cond: string + begin: number + end: number + contentrange: [number, number] + fileVersionsFm: FileVersionsFm + fileVersionsFmAll: VersionsObject + fileVersions: string[] + parent?: CondTagItem + versionsObj: VersionsObject + featureVersionsObj?: VersionsObject + versionsObjAll: VersionsObject + versions: string[] + action: CondTagAction + // Cached error range (used by addError); never set by this rule but accepted by addError. + contentRange?: [number, number] | number[] | string | null +} + +type DefaultProps = { + fileVersionsFm: FileVersionsFm + fileVersions: string[] + filename: string + parent: CondTagItem | undefined +} + export const liquidIfversionVersions = { names: ['GHD022', 'liquid-ifversion-versions'], description: @@ -33,14 +91,11 @@ export const liquidIfversionVersions = { const fm = getFrontmatter(params.lines) const content = fm ? getFrontmatterLines(params.lines).join('\n') : params.lines.join('\n') - const fileVersionsFm = params.name.startsWith('data') + const fileVersionsFm: FileVersionsFm = params.name.startsWith('data') ? { ghec: '*', ghes: '*', fpt: '*' } : fm - ? (fm.versions as string | Record | undefined) - : (getFrontmatter(params.frontMatterLines)?.versions as - | string - | Record - | undefined) + ? (fm.versions as FileVersionsFm) + : (getFrontmatter(params.frontMatterLines)?.versions as FileVersionsFm) if (!fileVersionsFm) return // This will only contain valid (non-deprecated) and future versions const fileVersions = getApplicableVersions(fileVersionsFm, '', { @@ -48,16 +103,15 @@ export const liquidIfversionVersions = { includeNextVersion: true, }) - const tokens = getLiquidIfVersionTokens(content) + const tokens = getLiquidIfVersionTokens(content) as LiquidConditionalToken[] // Array of arrays - each array entry is an array of items that // make up a full if/elsif/else/endif statement. // [ [ifversion, elsif, else, endif], [nested ifversion, elsif, else, endif] ] - // Using any[] because these are complex dynamic objects with properties added at runtime - const condStmtStack: any[] = [] + const condStmtStack: CondTagItem[][] = [] // Tokens are in the order they are read in file, so we need to iterate // through and group full if/elsif/else/endif statements together. - const defaultProps = { + const defaultProps: DefaultProps = { fileVersionsFm, fileVersions, filename: params.name, @@ -74,27 +128,25 @@ export const liquidIfversionVersions = { const condTagItem = await initTagObject(token, defaultProps) condStmtStack.push([condTagItem]) } else if (token.name === 'elsif') { - const condTagItems = condStmtStack.pop() + const condTagItems = condStmtStack.pop()! const condTagItem = await initTagObject(token, defaultProps) condTagItems.push(condTagItem) condStmtStack.push(condTagItems) } else if (token.name === 'else') { - const condTagItems = condStmtStack.pop() + const condTagItems = condStmtStack.pop()! const condTagItem = await initTagObject(token, defaultProps) // The versions of an else tag are the set of file versions that are // not supported by the previous ifversion or elsif tags. const siblingVersions = condTagItems - // Using any because condTagItems contains dynamic objects from initTagObject - .filter((item: any) => item.name === 'ifversion' || item.name === 'elsif') - .map((item: any) => item.versions) + .filter((item) => item.name === 'ifversion' || item.name === 'elsif') + .map((item) => item.versions) .flat() - // Using any because versions property is added dynamically to condTagItem - ;(condTagItem as any).versions = difference(fileVersions, siblingVersions) + condTagItem.versions = difference(fileVersions, siblingVersions) condTagItems.push(condTagItem) condStmtStack.push(condTagItems) } else if (token.name === 'endif') { defaultProps.parent = undefined - const condTagItems = condStmtStack.pop() + const condTagItems = condStmtStack.pop()! const condTagItem = await initTagObject(token, defaultProps) condTagItems.push(condTagItem) decorateCondTagItems(condTagItems) @@ -104,18 +156,21 @@ export const liquidIfversionVersions = { }, } -// Using any[] because condTagItems contains dynamic objects with properties added at runtime -function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: string[]) { +function setLiquidErrors(condTagItems: CondTagItem[], onError: RuleErrorCallback, lines: string[]) { 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}` - if (item.action.type === 'delete') { + if (item.action?.type === 'delete') { // There is no next stack item, the endif tag is alway the // last in a conditional const nextStackItem = item.name === 'endif' ? condTagItems[i].end : condTagItems[i + 1].begin - const deleteItems = getContentDeleteData(condTagItems[i], nextStackItem, lines) + const deleteItems = getContentDeleteData( + condTagItems[i] as unknown as TopLevelToken, + nextStackItem, + lines, + ) for (const deleteItem of deleteItems) { addError( onError, @@ -133,7 +188,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: } } - if (item.action.type === 'all') { + if (item.action?.type === 'all') { // position is just the tag const { lineNumber, column, length } = getPositionData( { @@ -158,7 +213,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: ) } - if (item.action.type === 'change') { + if (item.action?.type === 'change') { // position is just the inside of tag const { lineNumber, column, length } = getPositionData( { @@ -186,9 +241,8 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: } } -async function getApplicableVersionFromLiquidTag(conditionStr: string) { - // Using Record because version object keys are dynamic (fpt, ghec, ghes, feature, etc.) - const newConditionObject: Record = {} +async function getApplicableVersionFromLiquidTag(conditionStr: string): Promise { + const newConditionObject: VersionsObject = {} const condition = conditionStr.replace('not ', '') const liquidTagVersions = condition.split(' or ').map((item) => item.trim()) for (const ver of liquidTagVersions) { @@ -211,7 +265,7 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) { // All actual uses have matching versions (e.g., "ghes and ghes > 3.19"). // If this edge case appears in the future, additional logic would be needed here. if (!ands.every((and) => and.startsWith(firstAnd))) { - return [] + return {} } const andValues = [] let andVersion = '' @@ -235,41 +289,56 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) { doNotThrow: true, includeNextVersion: true, }) - return await convertVersionsToFrontmatter(difference(all, allApplicable)) + return (await convertVersionsToFrontmatter(difference(all, allApplicable))) as VersionsObject } return newConditionObject } -// Using any for token and props because they come from markdownlint library without full type definitions -async function initTagObject(token: any, props: any) { - const condTagItem = { +async function initTagObject( + token: LiquidConditionalToken, + props: DefaultProps, +): Promise { + const fileVersionsFm = props.fileVersionsFm + // Normalize a wildcard string ('*') frontmatter `versions` value into the + // canonical all-versions object so downstream consumers (Object.keys, ghes / + // feature lookups) behave consistently. In practice no content file uses the + // string form today, but handling it keeps the rule type-safe and future-proof. + const fmObject: VersionsObject = + typeof fileVersionsFm === 'string' + ? { ghec: '*', ghes: '*', fpt: '*' } + : ((fileVersionsFm || {}) as VersionsObject) + const featureFromFm = fmObject.feature + const condTagItem: CondTagItem = { name: token.name, cond: token.content.replace(`${token.name} `, '').trim(), begin: token.begin, end: token.end, contentrange: token.contentRange, - fileVersionsFm: props.fileVersionsFm, - fileVersionsFmAll: props.fileVersionsFm?.feature + fileVersionsFm, + fileVersionsFmAll: featureFromFm ? { - ...props.fileVersionsFm.versions, - ...getFeatureVersionsObject(props.fileVersionsFm.feature), + ...((fmObject as unknown as { versions?: VersionsObject }).versions || {}), + ...getFeatureVersionsObject(featureFromFm), } - : props.fileVersionsFm, + : fmObject, fileVersions: props.fileVersions, parent: props.parent, + versionsObj: {}, + featureVersionsObj: undefined, + versionsObjAll: {}, + versions: [], + action: { type: 'none' }, } if (token.name === 'ifversion' || token.name === 'elsif') { - // Using any because these properties (versionsObj, featureVersionsObj, versionsObjAll, versions) - // are added dynamically to condTagItem and not part of its initial type definition - ;(condTagItem as any).versionsObj = await getApplicableVersionFromLiquidTag(condTagItem.cond) - ;(condTagItem as any).featureVersionsObj = (condTagItem as any).versionsObj.feature - ? getFeatureVersionsObject((condTagItem as any).versionsObj.feature) + condTagItem.versionsObj = await getApplicableVersionFromLiquidTag(condTagItem.cond) + condTagItem.featureVersionsObj = condTagItem.versionsObj.feature + ? getFeatureVersionsObject(condTagItem.versionsObj.feature) : undefined - ;(condTagItem as any).versionsObjAll = { - ...(condTagItem as any).versionsObj, - ...(condTagItem as any).featureVersionsObj, + condTagItem.versionsObjAll = { + ...condTagItem.versionsObj, + ...condTagItem.featureVersionsObj, } - ;(condTagItem as any).versions = getApplicableVersions((condTagItem as any).versionsObj, '', { + condTagItem.versions = getApplicableVersions(condTagItem.versionsObj, '', { doNotThrow: true, includeNextVersion: true, }) @@ -286,8 +355,7 @@ async function initTagObject(token: any, props: any) { Then create flaws per stack item. newCond */ -// Using any[] because condTagItems contains dynamic objects with action property added at runtime -function decorateCondTagItems(condTagItems: any[]) { +function decorateCondTagItems(condTagItems: CondTagItem[]) { for (const item of condTagItems) { item.action = { type: 'none', @@ -304,8 +372,7 @@ function decorateCondTagItems(condTagItems: any[]) { return } -// Using any[] because condTagItems contains dynamic objects with various properties added at runtime -function updateConditionals(condTagItems: any[]) { +function updateConditionals(condTagItems: CondTagItem[]) { // iterate through the ifversion, elsif, and else // tags but NOT the endif tag. endif tags have // no versions associated with them and are handled @@ -317,7 +384,14 @@ function updateConditionals(condTagItems: any[]) { // the liquid should always be removed regardless // of whether it's a feature version or a nested // condition. - if (isAllVersions(item.featureVersionsObj || item.versionObj)) { + // NOTE: Original code referenced `item.versionObj` (no `s`), which was always + // undefined; preserved as-is to avoid changing runtime behavior in this PR. + if ( + isAllVersions( + item.featureVersionsObj || + ((item as unknown as { versionObj?: VersionsObject }).versionObj as VersionsObject), + ) + ) { processConditionals(item, condTagItems, i) break } @@ -349,7 +423,9 @@ function updateConditionals(condTagItems: any[]) { continue } } - if (item.versionsObj?.feature || item.fileVersionsFm?.feature) break + const fileVersionsFmObject = + item.fileVersionsFm && typeof item.fileVersionsFm === 'object' ? item.fileVersionsFm : {} + if (item.versionsObj?.feature || fileVersionsFmObject.feature) break // When the parent of a nested condition is a feature // we don't want to assume that the feature versions @@ -478,8 +554,11 @@ function updateConditionals(condTagItems: any[]) { } } -// Using any for item and any[] for condTagItems because they contain dynamic objects with action property -function processConditionals(item: any, condTagItems: any[], indexOfAllItem: number) { +function processConditionals( + item: CondTagItem, + condTagItems: CondTagItem[], + indexOfAllItem: number, +) { item.action.type = 'all' // if any tag in a statement is 'all', the // remaining tags are obsolete. diff --git a/src/landings/components/ProductLandingContext.tsx b/src/landings/components/ProductLandingContext.tsx index 857e01c54190..35c435c77739 100644 --- a/src/landings/components/ProductLandingContext.tsx +++ b/src/landings/components/ProductLandingContext.tsx @@ -1,6 +1,7 @@ import { createContext, useContext } from 'react' import pick from 'lodash/pick' import type { SimpleTocItem } from '@/landings/types' +import type { ExtendedRequest, FeaturedLinkExpanded } from '@/types' export type FeaturedLink = { title: string href: string @@ -69,17 +70,29 @@ export const useProductLandingContext = (): ProductLandingContextT => { return context } -export const getFeaturedLinksFromReq = (req: any): Record> => { +// Minimal request shape needed to extract featured links. We use a structural type +// here because callers pass either an Express ExtendedRequest or a narrower +// per-context request type defined alongside other landing-context helpers. +type FeaturedLinksRequest = { + context?: { + featuredLinks?: Record | unknown + } +} + +export const getFeaturedLinksFromReq = ( + req: FeaturedLinksRequest, +): Record> => { + const featuredLinks = (req.context?.featuredLinks || {}) as Record return Object.fromEntries( - Object.entries(req.context.featuredLinks || {}).map(([key, entries]) => { + Object.entries(featuredLinks).map(([key, entries]) => { return [ key, - ((entries as Array) || []).map((entry: any) => ({ + ((entries as FeaturedLinkExpanded[]) || []).map((entry) => ({ href: entry.href, title: entry.title, - intro: entry.intro || null, - authors: entry.page?.authors || [], - fullTitle: entry.fullTitle || null, + intro: entry.intro, + authors: (entry.page as { authors?: string[] } | undefined)?.authors || [], + fullTitle: entry.fullTitle, })), ] }), @@ -87,70 +100,89 @@ export const getFeaturedLinksFromReq = (req: any): Record => { - const productTree = req.context.currentProductTree - const page = req.context.page + const context = req.context + if (!context) { + throw new Error('"getProductLandingContextFromRequest" requires req.context') + } + const productTree = context.currentProductTree + if (!productTree) { + throw new Error('"getProductLandingContextFromRequest" requires req.context.currentProductTree') + } + const page = context.page + if (!page) { + throw new Error('"getProductLandingContextFromRequest" requires req.context.page') + } const hasGuidesPage = (page.children || []).includes('/guides') - const title = await page.renderProp('title', req.context, { textOnly: true }) - const shortTitle = (await page.renderProp('shortTitle', req.context, { textOnly: true })) || null + const title = await page.renderProp('title', context, { textOnly: true }) + const shortTitle = (await page.renderProp('shortTitle', context, { textOnly: true })) || null // This props is displayed on the product landing page as "Supported // releases". So we omit, if there is one, the release candidate. - const ghesReleases = (req.context.ghesReleases || []).filter((release: Release) => { + const ghesReleases = ((context.ghesReleases || []) as Release[]).filter((release) => { return !release.isReleaseCandidate }) return { title, - shortTitle, - ...pick(page, ['introPlainText', 'beta_product', 'intro']), - heroImage: page.heroImage || null, + shortTitle: shortTitle || '', + ...(pick(page as unknown as Record, [ + 'introPlainText', + 'beta_product', + 'intro', + ]) as { introPlainText: string; beta_product: boolean; intro: string }), + heroImage: (page as { heroImage?: string }).heroImage, hasGuidesPage, product: { href: productTree.href, title: productTree.page.shortTitle || productTree.page.title, }, - whatsNewChangelog: req.context.whatsNewChangelog || [], - changelogUrl: req.context.changelogUrl || [], - productCommunityExamples: req.context.productCommunityExamples || [], + whatsNewChangelog: context.whatsNewChangelog || [], + changelogUrl: context.changelogUrl, + productCommunityExamples: (context.productCommunityExamples || + []) as ProductLandingContextT['productCommunityExamples'], ghesReleases, - productUserExamples: (req.context.productUserExamples || []).map( - ({ user, description }: any) => ({ - username: user, - description, - }), - ), + productUserExamples: (context.productUserExamples || []).map(({ user, description }) => ({ + username: user as string, + description, + })), - introLinks: page.introLinks || null, + introLinks: + ((page as { introLinks?: Record }).introLinks as + | Record + | undefined) || null, featuredLinks: getFeaturedLinksFromReq(req), - tocItems: req.context.tocItems || [], + tocItems: ((context as { tocItems?: SimpleTocItem[] }).tocItems || []) as SimpleTocItem[], - featuredArticles: Object.entries(req.context.featuredLinks || []) + featuredArticles: Object.entries(context.featuredLinks || {}) .filter(([key]) => { return key === 'startHere' || key === 'popular' }) - .map(([key, links]: any) => { + .map(([key, links]) => { + const pageFeaturedLinks = (page.featuredLinks || {}) as unknown as Record + const tocLabels = ((context.site as { data?: { ui?: { toc?: Record } } }) + ?.data?.ui?.toc || {}) as Record return { key, label: key === 'popular' - ? req.context.page.featuredLinks[`${key}Heading`] || req.context.site.data.ui.toc[key] - : req.context.site.data.ui.toc[key], + ? pageFeaturedLinks[`${key}Heading`] || tocLabels[key] + : tocLabels[key], viewAllHref: - key === 'startHere' && !req.context.currentCategory && hasGuidesPage - ? `${req.context.currentPath}/guides` + key === 'startHere' && !context.currentCategory && hasGuidesPage + ? `${context.currentPath}/guides` : '', - articles: links.map((link: any) => { + articles: (links as FeaturedLinkExpanded[]).map((link) => { return { href: link.href, title: link.title, - intro: link.intro || null, - authors: link.page?.authors || [], - fullTitle: link.fullTitle || null, + intro: link.intro, + authors: (link.page as { authors?: string[] } | undefined)?.authors || [], + fullTitle: link.fullTitle, } }), } diff --git a/src/rest/lib/index.ts b/src/rest/lib/index.ts index 2bba026573ea..89a652df4bd3 100644 --- a/src/rest/lib/index.ts +++ b/src/rest/lib/index.ts @@ -4,7 +4,7 @@ import path from 'path' import QuickLRU from 'quick-lru' import { brotliDecompress } from 'zlib' import { promisify } from 'util' -import { getAutomatedPageMiniTocItems } from '@/frame/lib/get-mini-toc-items' +import { getAutomatedPageMiniTocItems, type MiniTocItem } from '@/frame/lib/get-mini-toc-items' import { allVersions, getOpenApiVersion } from '@/versions/lib/all-versions' import languages from '@/languages/lib/languages-server' import type { Context } from '@/types' @@ -19,7 +19,7 @@ export interface RestOperationCategory { } interface RestMiniTocData { - restOperationsMiniTocItems: any[] + restOperationsMiniTocItems: MiniTocItem[] } /* diff --git a/src/types/markdownlint-rule-search-replace.d.ts b/src/types/markdownlint-rule-search-replace.d.ts index 95ea43d99689..829b77306f82 100644 --- a/src/types/markdownlint-rule-search-replace.d.ts +++ b/src/types/markdownlint-rule-search-replace.d.ts @@ -1,11 +1,11 @@ declare module 'markdownlint-rule-search-replace' { + import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' + const searchReplace: { names: string[] description: string tags: string[] - // Using any because this is a third-party library without proper TypeScript definitions - // params contains markdownlint-specific data structures, onError is a callback function - function: (params: any, onError: any) => void + function: (params: RuleParams, onError: RuleErrorCallback) => void } export default searchReplace diff --git a/src/types/primer__octicons.d.ts b/src/types/primer__octicons.d.ts index 0ba8c8041b82..0ab576361b89 100644 --- a/src/types/primer__octicons.d.ts +++ b/src/types/primer__octicons.d.ts @@ -6,7 +6,7 @@ declare module '@primer/octicons' { 'aria-hidden'?: string | boolean class?: string fill?: string - [key: string]: any + [key: string]: string | number | boolean | undefined } interface Octicon { From 9ca16e6193a2dbb1aae5186663f54155785c5d3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 08:31:22 -0700 Subject: [PATCH 6/7] Bump parse5 from 7.1.2 to 8.0.1 (#61010) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 112 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 105ddd1f4896..fff13072d6f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,7 @@ "micromark-extension-gfm": "^3.0.0", "next": "^16.2.3", "ora": "^9.3.0", - "parse5": "7.1.2", + "parse5": "8.0.1", "quick-lru": "7.0.1", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -9548,6 +9548,30 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" }, + "node_modules/hast-util-raw/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", @@ -13471,10 +13495,12 @@ } }, "node_modules/parse5": { - "version": "7.1.2", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -13493,6 +13519,30 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5-parser-stream": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", @@ -13505,8 +13555,10 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "4.4.0", + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -13515,6 +13567,30 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -17028,6 +17104,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/website-scraper/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/website-scraper/node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", diff --git a/package.json b/package.json index 1728d55851f3..816e1bf326b8 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ "micromark-extension-gfm": "^3.0.0", "next": "^16.2.3", "ora": "^9.3.0", - "parse5": "7.1.2", + "parse5": "8.0.1", "quick-lru": "7.0.1", "react": "^19.2.5", "react-dom": "^19.2.5", From 58d68fcfbfdbef2c64fda4ce39cc804bdb401959 Mon Sep 17 00:00:00 2001 From: Joe Clark <31087804+jc-clark@users.noreply.github.com> Date: Fri, 1 May 2026 10:16:46 -0700 Subject: [PATCH 7/7] Add GitHub Partner Success Offering Terms (Early Access companion) (#60999)