diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a665c2b40f3..840c65510fca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Docs changelog
+**2 April 2026**
+
+We've expanded the documentation for custom agents in Copilot CLI, adding information about the built-in agents.
+
+[About custom agents](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-custom-agents#built-in-agents)
+
+
+
**27 March 2026**
We've introduced a new discovery landing page design for all the top-level doc sets on docs.github.com. The landing pages highlight recommended articles and give users the ability to filter articles by category with a drop down menu. Every article across the site now includes category metadata, making it easier to browse doc sets without relying solely on search. This replaces the previous product-landing layout across 35 doc sets.
diff --git a/content/actions/how-tos/reuse-automations/create-workflow-templates.md b/content/actions/how-tos/reuse-automations/create-workflow-templates.md
index e49980094cb1..a9e95193379e 100644
--- a/content/actions/how-tos/reuse-automations/create-workflow-templates.md
+++ b/content/actions/how-tos/reuse-automations/create-workflow-templates.md
@@ -24,7 +24,7 @@ category:
This procedure demonstrates how to create a workflow template and metadata file. The metadata file describes how the workflow templates will be presented to users when they are creating a new workflow.
-1. If it doesn't already exist, create a new repository named `.github` in your organization.
+1. If it doesn't already exist, create a new {% ifversion actions-nga %} {% else %}public {% endif %}repository named `.github` in your organization.
1. Create a directory named `workflow-templates`.
1. Create your new workflow file inside the `workflow-templates` directory.
diff --git a/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md b/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md
index b8b58de3e11b..c1a247229fc3 100644
--- a/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md
+++ b/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md
@@ -115,6 +115,7 @@ When a reusable workflow is triggered by a caller workflow, the `github` context
Reference information to use when creating workflow templates for your organization.
+{% ifversion actions-nga %}
### Workflow template availability
You can use templates in repositories that match or have more restricted visibility than the template repository.
@@ -132,6 +133,7 @@ Because public workflow templates require a public `.github` repository, they ar
### Granting access for private/internal repositories
If you're using a private or internal `.github` repository, you need to grant Read access to users or teams who should be able to use the templates.
+{% endif %}
### The `$default-branch` placeholder
diff --git a/content/admin/data-residency/feature-overview-for-github-enterprise-cloud-with-data-residency.md b/content/admin/data-residency/feature-overview-for-github-enterprise-cloud-with-data-residency.md
index 50cacc26934e..c9cb90d4bfc5 100644
--- a/content/admin/data-residency/feature-overview-for-github-enterprise-cloud-with-data-residency.md
+++ b/content/admin/data-residency/feature-overview-for-github-enterprise-cloud-with-data-residency.md
@@ -109,6 +109,6 @@ Some features on {% data variables.enterprise.data_residency_site %} are current
### {% data variables.product.prodname_github_codespaces %}
-{% data variables.product.prodname_github_codespaces %} on {% data variables.enterprise.data_residency_site %} are in {% data variables.release-phases.public_preview %} and are available in all {% data variables.enterprise.data_residency %} regions: EU, Australia, US, and Japan.
+{% data variables.product.prodname_github_codespaces %} on {% data variables.enterprise.data_residency_site %} are in {% data variables.release-phases.public_preview %} and are available in all {% data variables.enterprise.data_residency %} regions.
To use {% data variables.product.prodname_github_codespaces %} from {% data variables.product.prodname_vscode_shortname %} desktop with an enterprise on {% data variables.enterprise.data_residency_site %}, you must configure the `Github-enterprise: Uri` and `Github > Codespaces: Auth Provider` settings. For more information, see [AUTOTITLE](/codespaces/developing-in-a-codespace/using-github-codespaces-in-visual-studio-code#connecting-to-an-enterprise-on-ghecom).
diff --git a/content/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr.md b/content/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr.md
index 3ac709b5568d..de0a317b47e7 100644
--- a/content/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr.md
+++ b/content/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr.md
@@ -430,7 +430,7 @@ gh api \
{% data reusables.copilot.optional-select-custom-agent-generic %}
{% data reusables.copilot.optional-select-copilot-coding-agent-model %}
1. Optionally, provide additional instructions. These will be passed to {% data variables.product.prodname_copilot_short %} alongside your issue contents.
-1. Press Command+Enter to assign the issue.
+1. Press Command+Enter (macOS) or Ctrl+Enter (Windows) to assign the issue.
{% data variables.product.prodname_copilot_short %} will start a new session. {% data variables.product.prodname_copilot_short %} will work on the task and push changes to its pull request, then add you as a reviewer when it has finished, triggering a notification.
@@ -613,7 +613,7 @@ To see all of the available options, run `gh agent-task create --help`.
1. Optionally, select a base branch for {% data variables.product.prodname_copilot_short %}'s pull request. {% data variables.product.prodname_copilot_short %} will create a new branch based on this branch, then push the changes to a pull request targeting that branch.
{% data reusables.copilot.optional-select-custom-agent-generic %}
{% data reusables.copilot.optional-select-copilot-coding-agent-model %}
-1. Press Command+Enter to start the task.
+1. Press Command+Enter (macOS) or Ctrl+Enter (Windows) to start the task.
{% data variables.product.prodname_copilot_short %} will start a new session. {% data variables.product.prodname_copilot_short %} will work on the task and push changes to its pull request, then add you as a reviewer when it has finished, triggering a notification.
diff --git a/content/copilot/how-tos/use-copilot-agents/coding-agent/track-copilot-sessions.md b/content/copilot/how-tos/use-copilot-agents/coding-agent/track-copilot-sessions.md
index 9e2f8bb89502..8fe6de681752 100644
--- a/content/copilot/how-tos/use-copilot-agents/coding-agent/track-copilot-sessions.md
+++ b/content/copilot/how-tos/use-copilot-agents/coding-agent/track-copilot-sessions.md
@@ -57,7 +57,10 @@ To see all of the available options, run `gh agent-task list --help` or `gh agen
{% data reusables.copilot.coding-agent.raycast-setup %}
1. Open Raycast, search for "{% data variables.product.prodname_copilot_short %}," find the **View Tasks** command, then press Enter.
1. Click **Sign in with {% data variables.product.github %}**, then complete the authentication flow. Raycast will re-open.
-1. You'll see a list of your tasks. To navigate to the linked pull request, press Enter. To view the session logs, press Command+L.
+1. You'll see a list of your tasks. Select a task, then use the following keyboard shortcuts:
+ * To watch the session logs live, press Enter. The logs update in real time, so you can monitor {% data variables.product.prodname_copilot_short %}'s progress without leaving Raycast.
+ * To open the session logs in the browser, press Command+Enter (macOS) or Ctrl+Enter (Windows).
+ * To open the linked pull request, press Command+P (macOS) or Ctrl+P (Windows).
> [!NOTE]
> If you are unable to see some tasks in Raycast, the organization that owns the repository may have enabled {% data variables.product.prodname_oauth_app %} access restrictions. To learn how to request approval for the "{% data variables.product.prodname_copilot %} for Raycast" {% data variables.product.prodname_oauth_app %}, see [AUTOTITLE](/account-and-profile/how-tos/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/requesting-organization-approval-for-oauth-apps).
@@ -131,7 +134,7 @@ Commits from {% data variables.copilot.copilot_coding_agent %} have the followin
## Using the session logs to understand {% data variables.product.prodname_copilot_short %}'s approach
-You can dive into {% data variables.product.prodname_copilot_short %}'s session logs in {% data variables.product.github %} or {% data variables.product.prodname_vscode %} to understand how it approached your task.
+You can dive into {% data variables.product.prodname_copilot_short %}'s session logs in {% data variables.product.github %}, {% data variables.product.prodname_vscode %}, or Raycast to understand how it approached your task.
In the session logs, you can see {% data variables.product.prodname_copilot_short %}'s internal monologue and the tools it used to understand your repository, make changes and validate its work.
diff --git a/data/features/actions-nga.yml b/data/features/actions-nga.yml
new file mode 100644
index 000000000000..aba7652c8a4e
--- /dev/null
+++ b/data/features/actions-nga.yml
@@ -0,0 +1,5 @@
+# New feature: actions-nga
+# Versioning for Actions NGA (Next Generation Architecture) features
+versions:
+ fpt: '*'
+ ghec: '*'
diff --git a/data/reusables/copilot/coding-agent/raycast-intro.md b/data/reusables/copilot/coding-agent/raycast-intro.md
index f40d12899f12..7c7bfa345150 100644
--- a/data/reusables/copilot/coding-agent/raycast-intro.md
+++ b/data/reusables/copilot/coding-agent/raycast-intro.md
@@ -1 +1 @@
-[Raycast](https://www.raycast.com/) is an extensible launcher for Windows and macOS. With the {% data variables.product.prodname_copilot %} extension for Raycast, you can start and track {% data variables.copilot.copilot_coding_agent %} tasks wherever you are on your computer.
+[Raycast](https://www.raycast.com/) is an extensible launcher for Windows and macOS. With the {% data variables.product.prodname_copilot %} extension for Raycast, you can start and track {% data variables.copilot.copilot_coding_agent %} tasks and watch session logs live wherever you are on your computer.
diff --git a/data/reusables/data-residency/when-you-adopt-data-residency.md b/data/reusables/data-residency/when-you-adopt-data-residency.md
index 3f34c59e575b..640f8144ed85 100644
--- a/data/reusables/data-residency/when-you-adopt-data-residency.md
+++ b/data/reusables/data-residency/when-you-adopt-data-residency.md
@@ -4,7 +4,7 @@ Data residency makes it easy to separate open source and enterprise work, and he
The available regions are:
-* EU
+* EU (includes EFTA countries, Norway and Switzerland, as of May 1, 2026)
* Australia
* US
* Japan
diff --git a/eslint.config.ts b/eslint.config.ts
index 97380c8f8ab0..a5135704d5c3 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -204,13 +204,9 @@ 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/helpers/get-lintable-yml.ts',
- 'src/content-linter/lib/helpers/print-annotations.ts',
- 'src/content-linter/lib/helpers/utils.ts',
'src/content-linter/lib/init-test.ts',
'src/content-linter/lib/linting-rules/code-annotations.ts',
'src/content-linter/lib/linting-rules/index.ts',
- 'src/content-linter/lib/linting-rules/internal-links-no-lang.ts',
'src/content-linter/lib/linting-rules/journey-tracks-liquid.ts',
'src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts',
'src/content-linter/lib/linting-rules/liquid-versioning.ts',
@@ -253,8 +249,6 @@ export default [
'src/frame/lib/page-data.ts',
'src/frame/lib/page.ts',
'src/frame/lib/read-frontmatter.ts',
- 'src/frame/middleware/find-page.ts',
- 'src/frame/middleware/resolve-carousels.ts',
'src/frame/tests/page.ts',
'src/frame/tests/read-frontmatter.ts',
'src/frame/tests/server.ts',
@@ -278,7 +272,6 @@ export default [
'src/links/lib/update-internal-links.ts',
'src/links/scripts/check-github-github-links.ts',
'src/links/scripts/update-internal-links.ts',
- 'src/pages/_error.tsx',
'src/redirects/middleware/handle-redirects.ts',
'src/rest/components/get-rest-code-samples.ts',
'src/rest/lib/index.ts',
@@ -302,8 +295,6 @@ export default [
'src/search/lib/get-elasticsearch-results/general-search.ts',
'src/search/lib/routes/combined-search-route.ts',
'src/search/lib/search-request-params/get-search-from-request-params.ts',
- 'src/search/lib/search-request-params/search-params-objects.ts',
- 'src/search/lib/search-request-params/types.ts',
'src/search/middleware/search-routes.ts',
'src/search/scripts/index/index-cli.ts',
'src/search/scripts/index/utils/indexing-elasticsearch-utils.ts',
@@ -318,9 +309,7 @@ export default [
'src/types/markdownlint-rule-search-replace.d.ts',
'src/types/primer__octicons.d.ts',
'src/versions/scripts/use-short-versions.ts',
- 'src/webhooks/lib/index.ts',
'src/workflows/projects.ts',
- 'src/workflows/tests/actions-workflows.ts',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
diff --git a/src/article-api/README.md b/src/article-api/README.md
index 93188ad44d0a..b1741e09a82f 100644
--- a/src/article-api/README.md
+++ b/src/article-api/README.md
@@ -24,7 +24,11 @@ The `/api/article` endpoints return information about a page by `pathname`.
`api/article/meta` is highly cached, in JSON format.
-### Autogenerated Content Transformers
+### Redirects and the pagelist
+
+The pagelist (`/api/pagelist/:lang/:version`) returns only **canonical permalinks**. The article API (`/api/article`, `/api/article/body`, `/api/article/meta`) transparently follows redirects—so URLs that don't appear in the pagelist (such as `redirect_from` aliases or old paths) may still return content.
+
+When the article API resolves a redirect through the redirect table, the JSON response includes a `redirectedFrom` field containing the normalized pathname that was looked up (after trailing-slash removal and other standard normalization, not the raw originally-requested pathname). This field is only set for redirect-table lookups; it is not set for the bare `/` to `/` language rewrite. This lets consumers detect that the URL they requested is not canonical. The `/api/article/body` endpoint returns plain text, so `redirectedFrom` is not included in its response.
For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture.
diff --git a/src/article-api/middleware/article-pageinfo.ts b/src/article-api/middleware/article-pageinfo.ts
index c3670cd782bc..db5c7a3a9424 100644
--- a/src/article-api/middleware/article-pageinfo.ts
+++ b/src/article-api/middleware/article-pageinfo.ts
@@ -134,7 +134,7 @@ export async function getMetadata(req: ExtendedRequestWithPageInfo) {
// /articles or '/en/enterprise-server@latest/foo/bar)
// So by the time we get here, the pathname should be one of the
// page's valid permalinks.
- const { page, pathname, archived } = req.pageinfo
+ const { page, pathname, archived, redirectedFrom } = req.pageinfo
const documentType = page?.documentType ?? null
if (archived && archived.isArchived) {
@@ -157,5 +157,8 @@ export async function getMetadata(req: ExtendedRequestWithPageInfo) {
const fromCache = await getPageInfoFromCache(page, pathname)
const { cacheInfo, ...meta } = fromCache
- return { meta: { ...meta, documentType }, cacheInfo }
+ return {
+ meta: { ...meta, documentType, ...(redirectedFrom && { redirectedFrom }) },
+ cacheInfo,
+ }
}
diff --git a/src/article-api/middleware/validation.ts b/src/article-api/middleware/validation.ts
index 6ef27385ccc9..031b60704e49 100644
--- a/src/article-api/middleware/validation.ts
+++ b/src/article-api/middleware/validation.ts
@@ -109,6 +109,7 @@ export const pageValidationMiddleware = (
if (!req.pageinfo.archived.isArchived) {
const redirect = getRedirect(pathname, redirectsContext)
if (redirect) {
+ req.pageinfo.redirectedFrom = pathname
pathname = redirect
}
}
diff --git a/src/article-api/tests/pageinfo.ts b/src/article-api/tests/pageinfo.ts
index ed70757237b2..0e0815b9201f 100644
--- a/src/article-api/tests/pageinfo.ts
+++ b/src/article-api/tests/pageinfo.ts
@@ -12,6 +12,7 @@ interface PageMetadata {
title: string
intro: string
documentType: string | null
+ redirectedFrom?: string
}
interface ErrorResponse {
@@ -46,6 +47,8 @@ describe('pageinfo api', () => {
'Get started using HubGit to manage Git repositories and collaborate with others.',
)
expect(meta.documentType).toBe('category')
+ // Canonical URLs should not have redirectedFrom
+ expect(meta.redirectedFrom).toBeUndefined()
// Check that it can be cached at the CDN
expect(res.headers['set-cookie']).toBeUndefined()
expect(res.headers['cache-control']).toContain('public')
@@ -90,6 +93,7 @@ describe('pageinfo api', () => {
expect(res.statusCode).toBe(200)
const meta = JSON.parse(res.body) as PageMetadata
expect(meta.title).toBe('HubGit.com Fixture Documentation')
+ expect(meta.redirectedFrom).toBe('/en/olden-days')
}
// Trailing slashes are always removed
{
@@ -97,6 +101,7 @@ describe('pageinfo api', () => {
expect(res.statusCode).toBe(200)
const meta = JSON.parse(res.body) as PageMetadata
expect(meta.title).toBe('HubGit.com Fixture Documentation')
+ expect(meta.redirectedFrom).toBe('/en/olden-days')
}
// Short code for latest version
{
diff --git a/src/article-api/types.ts b/src/article-api/types.ts
index 2f5e34f1a5b2..c4c45e30e7b2 100644
--- a/src/article-api/types.ts
+++ b/src/article-api/types.ts
@@ -10,5 +10,6 @@ export type ExtendedRequestWithPageInfo = ExtendedRequest & {
pathname: string
page: Page
archived?: ArchivedVersion
+ redirectedFrom?: string
}
}
diff --git a/src/content-linter/lib/helpers/get-lintable-yml.ts b/src/content-linter/lib/helpers/get-lintable-yml.ts
index e6cb50790d1b..7bc333db7ac6 100755
--- a/src/content-linter/lib/helpers/get-lintable-yml.ts
+++ b/src/content-linter/lib/helpers/get-lintable-yml.ts
@@ -37,9 +37,13 @@ ajv.addKeyword({
type: 'string',
// For docs on defining validate see
// https://ajv.js.org/keywords.html#define-keyword-with-validate-function
- // Using any for validate function params because AJV's type definitions for custom keywords are complex
- validate: (compiled: any, data: any, schema: any, parentInfo: any): boolean => {
- mdDict.set(parentInfo.instancePath, data)
+ validate: (
+ _compiled: boolean,
+ data: string,
+ _schema: unknown,
+ parentInfo?: { instancePath: string },
+ ): boolean => {
+ if (parentInfo) mdDict.set(parentInfo.instancePath, data)
return true
},
errors: false,
diff --git a/src/content-linter/lib/helpers/print-annotations.ts b/src/content-linter/lib/helpers/print-annotations.ts
index 080d551acc0c..c252f8c253d4 100644
--- a/src/content-linter/lib/helpers/print-annotations.ts
+++ b/src/content-linter/lib/helpers/print-annotations.ts
@@ -5,17 +5,25 @@
*
*/
+interface LintFlaw {
+ ruleNames: string[]
+ severity: string
+ lineNumber?: number
+ ruleDescription?: string
+ errorDetail?: string
+ context?: string
+ [key: string]: unknown
+}
+
export function printAnnotationResults(
- // Using 'any' type as results structure is dynamic and comes from various linting tools with different formats
- results: any,
+ results: Record,
{
skippableRules = [],
skippableFlawProperties = [],
}: { skippableRules?: string[]; skippableFlawProperties?: string[] } = {},
) {
for (const [file, flaws] of Object.entries(results)) {
- // Using 'any' type for flaws as they have varying structures depending on the linting rule
- for (const flaw of flaws as any) {
+ for (const flaw of flaws) {
if (intersection(flaw.ruleNames, skippableRules)) {
continue
}
@@ -57,7 +65,6 @@ export function printAnnotationResults(
}
}
-// Using 'any' types for generic array intersection utility function
-function intersection(arr1: any[], arr2: any[]) {
- return arr1.some((item: any) => arr2.includes(item))
+function intersection(arr1: string[], arr2: string[]) {
+ return arr1.some((item) => arr2.includes(item))
}
diff --git a/src/content-linter/lib/helpers/utils.ts b/src/content-linter/lib/helpers/utils.ts
index 2a28b651a8ec..3e4d478542e1 100644
--- a/src/content-linter/lib/helpers/utils.ts
+++ b/src/content-linter/lib/helpers/utils.ts
@@ -17,18 +17,14 @@ export function addFixErrorDetail(
addError(onError, lineNumber, `Expected: ${expected}`, ` Actual: ${actual}`, range, fixInfo)
}
-export function forEachInlineChild(
+export function forEachInlineChild(
params: RuleParams,
type: string,
- // Handler uses `any` for function parameter variance reasons. TypeScript's contravariance rules for function
- // parameters mean that a function accepting a specific type cannot be assigned to a parameter of type `unknown`.
- // Therefore, `unknown` cannot be used here, as different linting rules pass tokens with varying structures
- // beyond the base MarkdownToken interface, and some handlers are async.
- handler: (child: any, token?: any) => void | Promise,
+ handler: (child: T, token?: MarkdownToken) => void | Promise,
): void {
filterTokens(params, 'inline', (token: MarkdownToken) => {
for (const child of token.children!.filter((c) => c.type === type)) {
- handler(child, token)
+ handler(child as unknown as T, token)
}
})
}
diff --git a/src/content-linter/lib/linting-rules/internal-links-no-lang.ts b/src/content-linter/lib/linting-rules/internal-links-no-lang.ts
index f24d783a590c..1df11e4d4c38 100644
--- a/src/content-linter/lib/linting-rules/internal-links-no-lang.ts
+++ b/src/content-linter/lib/linting-rules/internal-links-no-lang.ts
@@ -2,7 +2,7 @@ import { filterTokens } from 'markdownlint-rule-helpers'
import { addFixErrorDetail, getRange } from '../helpers/utils'
import { languageKeys } from '@/languages/lib/languages'
-import type { RuleParams, RuleErrorCallback, Rule } from '../../types'
+import type { RuleParams, RuleErrorCallback, Rule, MarkdownToken } from '../../types'
export const internalLinksNoLang: Rule = {
names: ['GHD002', 'internal-links-no-lang'],
@@ -10,9 +10,8 @@ export const internalLinksNoLang: Rule = {
tags: ['links', 'url'],
parser: 'markdownit',
function: (params: RuleParams, onError: RuleErrorCallback) => {
- // Using 'any' type for token as markdownlint-rule-helpers doesn't provide TypeScript types
- filterTokens(params, 'inline', (token: any) => {
- for (const child of token.children) {
+ filterTokens(params, 'inline', (token: MarkdownToken) => {
+ for (const child of token.children!) {
if (child.type !== 'link_open') continue
// Example child.attrs:
@@ -21,8 +20,8 @@ export const internalLinksNoLang: Rule = {
// ['rel', 'canonical'],
// ]
// Attribute arrays are tuples of [attributeName, attributeValue] from markdownit parser
- const hrefsMissingSlashes = child.attrs
- // The attribute could also be `target` or `rel`
+ const hrefsMissingSlashes = child
+ .attrs! // The attribute could also be `target` or `rel`
.filter((attr: [string, string]) => attr[0] === 'href')
.filter((attr: [string, string]) => attr[1].startsWith('/') || !attr[1].startsWith('//'))
// Filter out link paths that start with language code
diff --git a/src/frame/middleware/find-page.ts b/src/frame/middleware/find-page.ts
index a78feb5c3dc1..4b384444485f 100644
--- a/src/frame/middleware/find-page.ts
+++ b/src/frame/middleware/find-page.ts
@@ -25,7 +25,7 @@ export default async function findPage(
isDev = process.env.NODE_ENV === 'development',
contentRoot = CONTENT_ROOT,
}: FindPageOptions = {},
-): Promise {
+): Promise {
// Filter out things like `/will/redirect` or `/_next/data/...`
if (!req.pagePath || !languagePrefixPathRegex.test(req.pagePath)) {
return next()
@@ -36,7 +36,7 @@ export default async function findPage(
}
// Using any for page because it's dynamically assigned properties (like version) that aren't in the Page type
- let page: any = req.context.pages[req.pagePath]
+ let page = req.context.pages[req.pagePath] as Page | undefined
if (page && isDev && englishPrefixRegex.test(req.pagePath)) {
// The .applicableVersions and .permalinks properties are computed
// when the page is read in from disk. But when the initial tree
@@ -67,19 +67,20 @@ export default async function findPage(
req.context?.currentVersion &&
!page.applicableVersions.includes(req.context.currentVersion)
) {
- return res
+ res
.status(404)
.send(
`After re-reading the page, '${req.context?.currentVersion}' is no longer an applicable version. ` +
'A restart is required.',
)
+ return
}
}
if (page && req.context) {
req.context.page = page
// Note: Page doesn't have a version property, this might be setting it dynamically
- ;(req.context.page as any).version = req.context.currentVersion
+ ;(req.context.page as Page & { version: string }).version = req.context.currentVersion || ''
// We can't depend on `page.hidden` because the dedicated search
// results page is a hidden page but it needs to offer all possible
diff --git a/src/frame/middleware/resolve-carousels.ts b/src/frame/middleware/resolve-carousels.ts
index ecd80e156a4e..c079cb23aad8 100644
--- a/src/frame/middleware/resolve-carousels.ts
+++ b/src/frame/middleware/resolve-carousels.ts
@@ -1,4 +1,4 @@
-import type { ExtendedRequest, ResolvedArticle } from '@/types'
+import type { ExtendedRequest, Page, ResolvedArticle } from '@/types'
import type { Response, NextFunction } from 'express'
import findPage from '@/frame/lib/find-page'
import { renderContent } from '@/content-render/index'
@@ -6,6 +6,12 @@ import Permalink from '@/frame/lib/permalink'
import { createLogger } from '@/observability/logger/index'
+// The Page class has rawCarousels and carousels properties that aren't on the Page type
+interface PageCarouselProps {
+ rawCarousels?: Record
+ carousels?: Record
+}
+
const logger = createLogger('middleware:resolve-carousels')
/**
@@ -24,7 +30,7 @@ function tryResolveArticlePath(
rawPath: string,
pageRelativePath: string | undefined,
req: ExtendedRequest,
-): any {
+): Page | undefined {
const { pages, redirects } = req.context!
const currentLanguage = req.context!.currentLanguage || 'en'
@@ -86,7 +92,7 @@ function tryResolveArticlePath(
/**
* Get the path for a page (without language/version)
*/
-function getPageHref(page: any): string {
+function getPageHref(page: Page): string {
if (page.relativePath) {
return Permalink.relativePathToSuffix(page.relativePath)
}
@@ -103,7 +109,7 @@ async function resolveCarousels(
): Promise {
try {
const page = req.context?.page
- const rawCarousels = (page as any)?.rawCarousels
+ const rawCarousels = (page as unknown as PageCarouselProps)?.rawCarousels
// Handle carousels format
if (rawCarousels && typeof rawCarousels === 'object') {
@@ -158,7 +164,7 @@ async function resolveCarousels(
// Store resolved carousels on the page
if (page && Object.keys(resolvedCarousels).length > 0) {
- ;(page as any).carousels = resolvedCarousels
+ ;(page as unknown as PageCarouselProps).carousels = resolvedCarousels
}
}
} catch (error) {
diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx
index 8a7472c7760e..7f948d4d9f63 100644
--- a/src/pages/_error.tsx
+++ b/src/pages/_error.tsx
@@ -2,6 +2,18 @@ import type { NextPageContext } from 'next'
import { GenericError } from '@/frame/components/GenericError'
+interface ExpressRequestExtensions {
+ FailBot?: {
+ report: (
+ err: Error,
+ context: Record,
+ ) => Array> | undefined
+ }
+ method?: string
+ query?: Record
+ language?: string
+}
+
function Error() {
return
}
@@ -34,7 +46,7 @@ Error.getInitialProps = async (ctx: NextPageContext) => {
// do which mutate the Express request object by attaching
// callables to it. This way it's only ever present in SSR executed
// code and doesn't need any custom webpack configuration.
- const expressRequest = req as any
+ const expressRequest = req as unknown as ExpressRequestExtensions
const FailBot = expressRequest.FailBot
if (FailBot) {
try {
@@ -42,7 +54,7 @@ Error.getInitialProps = async (ctx: NextPageContext) => {
// they don't contain an PII.
const OK_HEADER_KEYS = ['user-agent', 'referer', 'accept-encoding', 'accept-language']
const reported = FailBot.report(err, {
- path: req.url,
+ path: req.url || '',
request: JSON.stringify(
{
method: expressRequest.method,
diff --git a/src/rest/scripts/utils/create-rest-examples.ts b/src/rest/scripts/utils/create-rest-examples.ts
index ed4107542947..557b2ad95057 100644
--- a/src/rest/scripts/utils/create-rest-examples.ts
+++ b/src/rest/scripts/utils/create-rest-examples.ts
@@ -328,11 +328,11 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
// This is a fallback to allow using the `example` property in
// the schema. If we start to enforce using examples vs. example using
// a linter, we can remove the check for `example`.
- // For now, we'll use the key default, which is a common default
- // example name in the OpenAPI schema.
+ // We key by statusCode so that operations with multiple success
+ // responses (e.g. 200 + 201) get unique keys instead of colliding.
if (operation.responses[statusCode].content[contentType].example) {
examples = {
- default: {
+ [statusCode]: {
value: operation.responses[statusCode].content[contentType].example,
},
}
diff --git a/src/rest/tests/create-rest-examples.ts b/src/rest/tests/create-rest-examples.ts
index ed1b531065ed..bdb85fd0e494 100644
--- a/src/rest/tests/create-rest-examples.ts
+++ b/src/rest/tests/create-rest-examples.ts
@@ -1,6 +1,9 @@
import { describe, expect, test } from 'vitest'
-import getCodeSamples, { mergeExamples } from '../scripts/utils/create-rest-examples'
+import getCodeSamples, {
+ mergeExamples,
+ getResponseExamples,
+} from '../scripts/utils/create-rest-examples'
import {
operation,
noContent,
@@ -63,4 +66,31 @@ describe('rest example requests and responses', () => {
)
}
})
+
+ test('response examples have unique keys when multiple status codes use inline example', () => {
+ const multiStatusOp = {
+ responses: {
+ 200: {
+ description: 'Response when already exists',
+ content: {
+ 'application/json': {
+ example: { id: 1, status: 'existing' },
+ },
+ },
+ },
+ 201: {
+ description: 'Response when created',
+ content: {
+ 'application/json': {
+ example: { id: 1, status: 'created' },
+ },
+ },
+ },
+ },
+ }
+ const examples = getResponseExamples(multiStatusOp)
+ const keys = examples.map((e) => e.key)
+ expect(keys).toEqual(['200', '201'])
+ expect(new Set(keys).size).toBe(keys.length)
+ })
})
diff --git a/src/search/lib/search-request-params/get-search-from-request-params.ts b/src/search/lib/search-request-params/get-search-from-request-params.ts
index af3fd98b0cf6..1a4535200baf 100644
--- a/src/search/lib/search-request-params/get-search-from-request-params.ts
+++ b/src/search/lib/search-request-params/get-search-from-request-params.ts
@@ -37,7 +37,7 @@ export function getSearchFromRequestParams(
continue
}
- let value = req.query[key]
+ let value: unknown = req.query[key]
if (!value || (typeof value === 'string' && !value.trim())) {
if (default_ === undefined) {
validationErrors.push({ error: `No truthy value for key '${key}'`, key })
diff --git a/src/search/lib/search-request-params/search-params-objects.ts b/src/search/lib/search-request-params/search-params-objects.ts
index 2db04bbb0571..ce133c8aff12 100644
--- a/src/search/lib/search-request-params/search-params-objects.ts
+++ b/src/search/lib/search-request-params/search-params-objects.ts
@@ -44,8 +44,8 @@ const SHARED_PARAMS_OBJ: SearchRequestQueryParams[] = [
{
key: 'version',
default_: 'free-pro-team',
- validate: (version: string) => {
- if (!versionToIndexVersionMap[version]) {
+ validate: (version) => {
+ if (!versionToIndexVersionMap[version as string]) {
throw new ValidationError(`'${version}' not in ${allIndexVersionKeys.join(', ')}`)
}
return true
@@ -56,28 +56,36 @@ const SHARED_PARAMS_OBJ: SearchRequestQueryParams[] = [
const GENERAL_SEARCH_PARAMS_OBJ: SearchRequestQueryParams[] = [
...SHARED_PARAMS_OBJ,
{ key: 'query' },
- { key: 'language', default_: 'en', validate: (v) => v in languages },
+ { key: 'language', default_: 'en', validate: (v) => (v as string) in languages },
{
key: 'size',
default_: DEFAULT_SIZE,
- cast: (v) => parseInt(v, 10),
- validate: (v) => v >= 0 && v <= MAX_SIZE,
+ cast: (v) => parseInt(v as string, 10),
+ validate: (v) => (v as number) >= 0 && (v as number) <= MAX_SIZE,
},
{
key: 'page',
default_: DEFAULT_PAGE,
- cast: (v) => parseInt(v, 10),
- validate: (v) => v >= 1 && v <= MAX_PAGE,
+ cast: (v) => parseInt(v as string, 10),
+ validate: (v) => (v as number) >= 1 && (v as number) <= MAX_PAGE,
+ },
+ {
+ key: 'sort',
+ default_: DEFAULT_SORT,
+ validate: (v) => POSSIBLE_SORTS.includes(v as (typeof POSSIBLE_SORTS)[number]),
},
- { key: 'sort', default_: DEFAULT_SORT, validate: (v) => POSSIBLE_SORTS.includes(v as any) },
{
key: 'highlights',
default_: DEFAULT_HIGHLIGHT_FIELDS,
cast: (v) => (Array.isArray(v) ? v : [v]),
multiple: true,
validate: (v) => {
- for (const highlight of v) {
- if (!POSSIBLE_HIGHLIGHT_FIELDS.includes(highlight)) {
+ for (const highlight of v as string[]) {
+ if (
+ !POSSIBLE_HIGHLIGHT_FIELDS.includes(
+ highlight as (typeof POSSIBLE_HIGHLIGHT_FIELDS)[number],
+ )
+ ) {
throw new ValidationError(`highlight value '${highlight}' is not valid`)
}
}
@@ -92,7 +100,9 @@ const GENERAL_SEARCH_PARAMS_OBJ: SearchRequestQueryParams[] = [
cast: toArray,
multiple: true,
validate: (values) =>
- values.every((value: string) => V1_ADDITIONAL_INCLUDES.includes(value as any)),
+ (values as string[]).every((value: string) =>
+ V1_ADDITIONAL_INCLUDES.includes(value as (typeof V1_ADDITIONAL_INCLUDES)[number]),
+ ),
},
{
key: 'toplevel',
@@ -105,7 +115,10 @@ const GENERAL_SEARCH_PARAMS_OBJ: SearchRequestQueryParams[] = [
default_: [],
cast: toArray,
multiple: true,
- validate: (values) => values.every((value: string) => V1_AGGREGATES.includes(value as any)),
+ validate: (values) =>
+ (values as string[]).every((value: string) =>
+ V1_AGGREGATES.includes(value as (typeof V1_AGGREGATES)[number]),
+ ),
},
]
@@ -114,14 +127,14 @@ const SHARED_AUTOCOMPLETE_PARAMS_OBJ: SearchRequestQueryParams[] = [
{
key: 'size',
default_: DEFAULT_AUTOCOMPLETE_SIZE,
- cast: (size: string) => parseInt(size, 10),
- validate: (size: number) => size >= 0 && size <= MAX_AUTOCOMPLETE_SIZE,
+ cast: (size) => parseInt(size as string, 10),
+ validate: (size) => (size as number) >= 0 && (size as number) <= MAX_AUTOCOMPLETE_SIZE,
},
{
key: 'version',
default_: 'free-pro-team',
- validate: (version: string) => {
- if (!versionToIndexVersionMap[version]) {
+ validate: (version) => {
+ if (!versionToIndexVersionMap[version as string]) {
throw new ValidationError(`'${version}' not in ${allIndexVersionKeys.join(', ')}`)
}
return true
@@ -131,13 +144,13 @@ const SHARED_AUTOCOMPLETE_PARAMS_OBJ: SearchRequestQueryParams[] = [
const AI_SEARCH_AUTOCOMPLETE_PARAMS_OBJ: SearchRequestQueryParams[] = [
...SHARED_AUTOCOMPLETE_PARAMS_OBJ,
- { key: 'language', default_: 'en', validate: (language: string) => language === 'en' },
+ { key: 'language', default_: 'en', validate: (language) => language === 'en' },
]
-function toBoolean(value: any): boolean {
+function toBoolean(value: unknown): boolean {
return value === 'true' || value === '1'
}
-function toArray(value: any): any[] {
+function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [value]
}
diff --git a/src/search/lib/search-request-params/types.ts b/src/search/lib/search-request-params/types.ts
index 5a669925a26f..e24132e0fde2 100644
--- a/src/search/lib/search-request-params/types.ts
+++ b/src/search/lib/search-request-params/types.ts
@@ -38,9 +38,9 @@ export interface ComputedSearchQueryParamsMap {
export interface SearchRequestQueryParams {
key: keyof ComputedSearchQueryParams
- default_?: any
- cast?: (value: any) => any
- validate?: (value: any) => boolean
+ default_?: unknown
+ cast?: (value: unknown) => unknown
+ validate?: (value: unknown) => boolean
multiple?: boolean
}
diff --git a/src/webhooks/lib/index.ts b/src/webhooks/lib/index.ts
index c9870e33f21c..854437daa54c 100644
--- a/src/webhooks/lib/index.ts
+++ b/src/webhooks/lib/index.ts
@@ -6,8 +6,29 @@ import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
export const WEBHOOK_DATA_DIR = 'src/webhooks/data'
export const WEBHOOK_SCHEMA_FILENAME = 'schema.json'
+interface WebhookBodyParameter {
+ name?: string
+ type?: string
+ description?: string
+ isRequired?: boolean
+ childParamsGroups?: unknown[]
+ [key: string]: unknown
+}
+
+interface WebhookActionData {
+ bodyParameters?: WebhookBodyParameter[]
+ summaryHtml?: string
+ descriptionHtml?: string
+ availability?: string[]
+ payloadExample?: unknown
+ [key: string]: unknown
+}
+
+type WebhookCategory = Record
+type WebhookData = Record
+
// cache for webhook data per version
-const webhooksCache = new Map>>()
+const webhooksCache = new Map>()
// cache for webhook data for when you first visit the webhooks page where we
// show all webhooks for the current version but only 1 action type per webhook
// and also no nested parameters
@@ -16,13 +37,7 @@ const initialWebhooksCache = new Map()
interface InitialWebhook {
name: string
actionTypes: string[]
- data: {
- bodyParameters?: Array<{
- childParamsGroups?: any[]
- [key: string]: any
- }>
- [key: string]: any
- }
+ data: WebhookActionData
}
// return the webhoook data as described for `initialWebhooksCache` for the given
@@ -69,13 +84,13 @@ export async function getInitialPageWebhooks(version: string): Promise | undefined> {
+): Promise {
const webhooks = await getWebhooks(version)
return webhooks[webhookCategory]
}
// returns all the webhook data for the given version
-export async function getWebhooks(version: string): Promise> {
+export async function getWebhooks(version: string): Promise {
const openApiVersion = getOpenApiVersion(version)
if (!webhooksCache.has(openApiVersion)) {
// The `readCompressedJsonFileFallback()` function
@@ -85,7 +100,7 @@ export async function getWebhooks(version: string): Promise>
Promise.resolve(
readCompressedJsonFileFallback(
path.join(WEBHOOK_DATA_DIR, openApiVersion, WEBHOOK_SCHEMA_FILENAME),
- ) as Record,
+ ) as WebhookData,
),
)
}
diff --git a/src/webhooks/pages/webhook-events-and-payloads.tsx b/src/webhooks/pages/webhook-events-and-payloads.tsx
index 96c48ea8d29d..479dee096a8d 100644
--- a/src/webhooks/pages/webhook-events-and-payloads.tsx
+++ b/src/webhooks/pages/webhook-events-and-payloads.tsx
@@ -87,7 +87,7 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
// Get data for initial webhooks page (i.e. only 1 action type per webhook and
// no nested parameters)
- const webhooks = (await getInitialPageWebhooks(currentVersion)) as WebhookAction[]
+ const webhooks = (await getInitialPageWebhooks(currentVersion)) as unknown as WebhookAction[]
// Build the minitocs for the webhooks page which is based on the webhook
// categories in addition to the Markdown in the webhook-events-and-payloads.md
diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts
index aeaddad00428..e3d6d514f857 100644
--- a/src/workflows/tests/actions-workflows.ts
+++ b/src/workflows/tests/actions-workflows.ts
@@ -13,17 +13,32 @@ const actionHashRegexp = /^[A-Za-z0-9-/]+@[0-9a-f]{40}$/
const checkoutRegexp = /^[actions/checkout]+@(v\d+(\.\d+)*|[0-9a-f]{40})$/
const permissionsRegexp = /(read|write)/
+interface WorkflowTriggers {
+ schedule?: Array<{ cron: string }>
+ [key: string]: unknown
+}
+
type WorkflowMeta = {
filename: string
fullpath: string
data: {
name: string
- on: Record
- permissions: Record
- jobs: Record
+ on: WorkflowTriggers
+ permissions: Record
+ jobs: Record
}
}
+interface WorkflowJob {
+ if?: string
+ steps: WorkflowStep[]
+}
+
+interface WorkflowStep {
+ uses?: string
+ [key: string]: unknown
+}
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const workflowsDir = path.join(__dirname, '../../../.github/workflows')
const workflows: WorkflowMeta[] = fs
@@ -69,12 +84,12 @@ const alertWorkflows = workflows
// to generate list, console.log(new Set(workflows.map(({ data }) => Object.keys(data.on)).flat()))
const dailyWorkflows = scheduledWorkflows.filter(({ data }) =>
- data.on.schedule.find(({ cron }: { cron: string }) => /^20 \d{1,2} /.test(cron)),
+ data.on.schedule!.find(({ cron }: { cron: string }) => /^20 \d{1,2} /.test(cron)),
)
// Weekly workflows have a single day-of-week digit (e.g. "20 16 * * 1")
const weeklyWorkflows = dailyWorkflows.filter(({ data }) =>
- data.on.schedule.find(({ cron }: { cron: string }) => /^20 16 \* \* \d$/.test(cron)),
+ data.on.schedule!.find(({ cron }: { cron: string }) => /^20 16 \* \* \d$/.test(cron)),
)
describe('GitHub Actions workflows', () => {
@@ -87,7 +102,7 @@ describe('GitHub Actions workflows', () => {
test.each(scheduledWorkflows)(
'schedule workflow runs at 20 minutes past $filename',
({ data }) => {
- for (const { cron } of data.on.schedule) {
+ for (const { cron } of data.on.schedule!) {
expect(cron).toMatch(/^20/)
}
},
@@ -96,15 +111,15 @@ describe('GitHub Actions workflows', () => {
test.each(dailyWorkflows)(
'daily scheduled workflows run at 16:20 UTC / 8:20 PST $filename',
({ data }) => {
- for (const { cron } of data.on.schedule) {
- const hour = cron.match(/^20 ([^*\s]+)/)[1]
+ for (const { cron } of data.on.schedule!) {
+ const hour = cron.match(/^20 ([^*\s]+)/)![1]
expect(hour).toEqual('16')
}
},
)
test.each(dailyWorkflows)('daily scheduled workflows only run Mon-Fri $filename', ({ data }) => {
- for (const { cron } of data.on.schedule) {
+ for (const { cron } of data.on.schedule!) {
const fields = cron.trim().split(/\s+/)
const dayOfWeek = fields[4]
// Day-of-week must be 1-5 (Mon-Fri) or a range within 1-5
@@ -113,7 +128,7 @@ describe('GitHub Actions workflows', () => {
})
test.each(weeklyWorkflows)('weekly scheduled workflows run on Monday $filename', ({ data }) => {
- for (const { cron } of data.on.schedule) {
+ for (const { cron } of data.on.schedule!) {
const fields = cron.trim().split(/\s+/)
const dayOfWeek = fields[4]
// Day-of-week must be 1 (Monday)
@@ -141,9 +156,7 @@ describe('GitHub Actions workflows', () => {
({ filename, data }) => {
for (const [name, job] of Object.entries(data.jobs)) {
if (
- !job.steps.find(
- (step: Record) => step.uses === './.github/actions/slack-alert',
- )
+ !job.steps.find((step: WorkflowStep) => step.uses === './.github/actions/slack-alert')
) {
throw new Error(`Job ${filename} # ${name} missing slack alert on fail`)
}
@@ -157,8 +170,7 @@ describe('GitHub Actions workflows', () => {
for (const [name, job] of Object.entries(data.jobs)) {
if (
!job.steps.find(
- (step: Record) =>
- step.uses === './.github/actions/create-workflow-failure-issue',
+ (step: WorkflowStep) => step.uses === './.github/actions/create-workflow-failure-issue',
)
) {
throw new Error(`Job ${filename} # ${name} missing create-workflow-failure-issue on fail`)
@@ -171,7 +183,7 @@ describe('GitHub Actions workflows', () => {
'performs a checkout before calling composite action $filename',
({ filename, data }) => {
for (const [name, job] of Object.entries(data.jobs)) {
- if (!job.steps.find((step: Record) => checkoutRegexp.test(step.uses))) {
+ if (!job.steps.find((step: WorkflowStep) => checkoutRegexp.test(step.uses || ''))) {
throw new Error(
`Job ${filename} # ${name} missing a checkout before calling the composite action`,
)