From ee2ca94d89380a87eb1026cb1ab39005a2876dba Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Thu, 5 Mar 2026 08:29:21 -0800 Subject: [PATCH 01/12] [2026-03-05] Pick a model when tagging Copilot coding agent with @copilot in a pull request comment (#59621) --- .../coding-agent/changing-the-ai-model.md | 2 +- .../coding-agent/make-changes-to-an-existing-pr.md | 6 ++++-- .../use-copilot-agents/coding-agent/review-copilot-prs.md | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/content/copilot/how-tos/use-copilot-agents/coding-agent/changing-the-ai-model.md b/content/copilot/how-tos/use-copilot-agents/coding-agent/changing-the-ai-model.md index a0fb945c5c41..e8c700529621 100644 --- a/content/copilot/how-tos/use-copilot-agents/coding-agent/changing-the-ai-model.md +++ b/content/copilot/how-tos/use-copilot-agents/coding-agent/changing-the-ai-model.md @@ -20,7 +20,7 @@ In supported entrypoints, you can select the model used by {% data variables.cop You may find that different models perform better, or provide more useful responses, depending on the type of tasks you give {% data variables.product.prodname_copilot_short %}. > [!NOTE] -> Model selection for {% data variables.copilot.copilot_coding_agent %} is only supported when assigning an issue to {% data variables.product.prodname_copilot_short %} on {% data variables.product.prodname_dotcom_the_website %}, or when starting a task from the agents tab, agents panel, {% data variables.product.prodname_mobile %} or the Raycast launcher. Where a model picker is not available, Auto will be used automatically. See [AUTOTITLE](/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr). +> Model selection for {% data variables.copilot.copilot_coding_agent %} is only supported when assigning an issue to {% data variables.product.prodname_copilot_short %} on {% data variables.product.prodname_dotcom_the_website %}, when mentioning `@copilot` in a pull request comment on {% data variables.product.prodname_dotcom_the_website %}, or when starting a task from the agents tab, agents panel, {% data variables.product.prodname_mobile %} or the Raycast launcher. Where a model picker is not available, Auto will be used automatically. See [AUTOTITLE](/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr). ## Supported models diff --git a/content/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr.md b/content/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr.md index aa59e6e3dddd..4aa1192ce54f 100644 --- a/content/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr.md +++ b/content/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr.md @@ -16,7 +16,7 @@ category: ## Introduction -You can ask {% data variables.product.prodname_copilot_short %} to make changes to an existing pull request created by a human developer. +You can ask {% data variables.product.prodname_copilot_short %} to make changes to an existing pull request created by a human developer by mentioning `@copilot` in a comment. {% data variables.product.prodname_copilot_short %} will create a child pull request, using the existing pull request's branch as the base branch. Once it has finished work on the changes you requested, it requests your review on the child pull request. @@ -27,7 +27,9 @@ You can ask {% data variables.product.prodname_copilot_short %} to make changes ## Asking {% data variables.product.prodname_copilot_short %} to make changes 1. Navigate to the pull request that you want {% data variables.product.prodname_copilot_short %} to make changes to. -1. Leave a comment or review mentioning {% data variables.product.prodname_copilot_short %} with `@copilot`. +1. Write a comment or review mentioning {% data variables.product.prodname_copilot_short %} with `@copilot`. +1. Optionally, when leaving a pull request comment (not a review or review comment) through the {% data variables.product.github %} web interface, select a model using the model picker. +1. Submit your comment or review. {% data variables.product.prodname_copilot_short %} will open a child pull request, using the existing pull request's branch as the base branch. diff --git a/content/copilot/how-tos/use-copilot-agents/coding-agent/review-copilot-prs.md b/content/copilot/how-tos/use-copilot-agents/coding-agent/review-copilot-prs.md index 45c94de8f06b..59e515b74a96 100644 --- a/content/copilot/how-tos/use-copilot-agents/coding-agent/review-copilot-prs.md +++ b/content/copilot/how-tos/use-copilot-agents/coding-agent/review-copilot-prs.md @@ -30,6 +30,8 @@ After {% data variables.product.prodname_copilot_short %} has finished working o You can ask {% data variables.product.prodname_copilot_short %} to make changes by mentioning `@copilot` in pull request comments, or you can check out {% data variables.product.prodname_copilot_short %}'s branch and make changes yourself. +Optionally, when submitting a pull request comment (not a review or review comment) through the {% data variables.product.github %} web interface, you can select a model using the model picker. By default, {% data variables.product.prodname_copilot_short %} will use the model originally used to create the pull request. + > [!TIP] > We recommend you batch your review comments instead of submitting them individually. From e073f5aee6cda247e36216cb2ea3776a5c4d9bb0 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Thu, 5 Mar 2026 09:00:12 -0800 Subject: [PATCH 02/12] Remove incorrect information saying that CCA Auto mode is only for Pro and Pro+ (#60028) --- content/copilot/concepts/auto-model-selection.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/content/copilot/concepts/auto-model-selection.md b/content/copilot/concepts/auto-model-selection.md index 431dab601326..b7dbce996bc8 100644 --- a/content/copilot/concepts/auto-model-selection.md +++ b/content/copilot/concepts/auto-model-selection.md @@ -62,8 +62,6 @@ During the {% data variables.release-phases.public_preview %}, if you're using a ## {% data variables.copilot.copilot_auto_model_selection_short_cap_a %} in {% data variables.copilot.copilot_coding_agent %} -> [!NOTE] {% data variables.copilot.copilot_auto_model_selection_short_cap_a %} for {% data variables.copilot.copilot_coding_agent %} is generally available and currently only available for {% data variables.copilot.copilot_pro %} and {% data variables.copilot.copilot_pro_plus %} plans. - When you select **Auto** in {% data variables.copilot.copilot_coding_agent %}, {% data variables.copilot.copilot_auto_model_selection_short_cap_a %} currently chooses from the following list of models, subject to your policies and subscription type: {% data reusables.copilot.copilot-coding-agent-auto-models %} From e69361fbec2cf5ed367a28c5ca5b195169815670 Mon Sep 17 00:00:00 2001 From: Evan Bonsignori Date: Thu, 5 Mar 2026 10:10:19 -0800 Subject: [PATCH 03/12] Add REST API changelog sync and fix GHES multi-version release bugs (#59834) --- .../about-the-rest-api/breaking-changes.md | 4 +- .../rest-api/breaking-changes-changelog.md | 18 + .../deprecate/update-automated-pipelines.ts | 52 ++- src/rest/scripts/update-files.ts | 2 + src/rest/scripts/utils/sync-changelogs.ts | 190 +++++++++ src/rest/tests/sync-changelogs.ts | 397 ++++++++++++++++++ src/versions/lib/all-versions.ts | 2 +- 7 files changed, 643 insertions(+), 22 deletions(-) create mode 100644 data/reusables/rest-api/breaking-changes-changelog.md create mode 100644 src/rest/scripts/utils/sync-changelogs.ts create mode 100644 src/rest/tests/sync-changelogs.ts diff --git a/content/rest/about-the-rest-api/breaking-changes.md b/content/rest/about-the-rest-api/breaking-changes.md index 151b02462eac..ec257cd46225 100644 --- a/content/rest/about-the-rest-api/breaking-changes.md +++ b/content/rest/about-the-rest-api/breaking-changes.md @@ -24,6 +24,4 @@ When you update your integration to specify the new API version in the `X-GitHub Once your integration is updated, test your integration to verify that it works with the new API version. -## Breaking changes for {{ initialRestVersioningReleaseDate }} - -Version `{{ initialRestVersioningReleaseDate }}` is the first version of the {% data variables.product.github %} REST API after date-based versioning was introduced. This version does not include any breaking changes. +{% data reusables.rest-api.breaking-changes-changelog %} diff --git a/data/reusables/rest-api/breaking-changes-changelog.md b/data/reusables/rest-api/breaking-changes-changelog.md new file mode 100644 index 000000000000..fbaa9f0493fa --- /dev/null +++ b/data/reusables/rest-api/breaking-changes-changelog.md @@ -0,0 +1,18 @@ + +{% ifversion fpt %} +{% if query.apiVersion == nil or "2022-11-28" <= query.apiVersion %} +## Version 2022-11-28 + +Version `2022-11-28` is the first version of the GitHub Free, Pro & Team REST API after date-based versioning was introduced. This version does not include any breaking changes. + +{% endif %} +{% endif %} + +{% ifversion ghec %} +{% if query.apiVersion == nil or "2022-11-28" <= query.apiVersion %} +## Version 2022-11-28 + +Version `2022-11-28` is the first version of the GitHub Enterprise Cloud REST API after date-based versioning was introduced. This version does not include any breaking changes. + +{% endif %} +{% endif %} diff --git a/src/ghes-releases/scripts/deprecate/update-automated-pipelines.ts b/src/ghes-releases/scripts/deprecate/update-automated-pipelines.ts index 3e3fb3489181..ca294175a8a9 100755 --- a/src/ghes-releases/scripts/deprecate/update-automated-pipelines.ts +++ b/src/ghes-releases/scripts/deprecate/update-automated-pipelines.ts @@ -124,28 +124,44 @@ export async function updateAutomatedPipelines() { } // Get a list of data directories to create (release) and create them - // This should only happen if a relase is being added. + // This should only happen if a release is being added. const addFiles = difference(expectedDirectory, existingDataDir) - if (addFiles.length > numberedReleaseBaseNames.length) { - throw new Error( - 'Only one new release per numbered release version should be added at a time. Check that the lib/enterprise-server-releases.ts is correct.', - ) + + // Verify all new directories belong to the current release + for (const dir of addFiles) { + if (!dir.includes(currentReleaseNumber)) { + throw new Error( + `Unexpected directory to add: ${dir}. Only directories for the current release ` + + `(${currentReleaseNumber}) should be added. Check that the lib/enterprise-server-releases.ts is correct.`, + ) + } } for (const base of numberedReleaseBaseNames) { - const dirToAdd = addFiles.find((item) => item.startsWith(base)) - if (!dirToAdd) continue - // The suppported array is ordered from most recent (index 0) to oldest - // Index 1 will be the release prior to the most recent release - const lastRelease = supported[1] - const previousDirName = existingDataDir.filter((directory) => directory.includes(lastRelease)) - - console.log( - `Copying src/${pipeline}/data/${previousDirName} to src/${pipeline}/data/${dirToAdd}`, - ) - await cp(`src/${pipeline}/data/${previousDirName}`, `src/${pipeline}/data/${dirToAdd}`, { - recursive: true, - }) + // Find ALL directories to add for this base name (may be multiple + // when a release has more than one calendar-date version). + const dirsToAdd = addFiles.filter((item) => item.startsWith(base)) + for (const dirToAdd of dirsToAdd) { + // Derive the previous release's corresponding directory by replacing + // the current release number with the previous one. This correctly + // maps each calendar-date variant to its predecessor, e.g.: + // ghes-3.20-2022-11-28 → ghes-3.19-2022-11-28 + // ghes-3.20-2026-03-10 → ghes-3.19-2026-03-10 + const previousDirName = dirToAdd.replace(currentReleaseNumber, previousReleaseNumber) + if (!existingDataDir.includes(previousDirName)) { + throw new Error( + `Cannot find previous release directory '${previousDirName}' to copy from ` + + `when creating '${dirToAdd}' in src/${pipeline}/data/.`, + ) + } + + console.log( + `Copying src/${pipeline}/data/${previousDirName} to src/${pipeline}/data/${dirToAdd}`, + ) + await cp(`src/${pipeline}/data/${previousDirName}`, `src/${pipeline}/data/${dirToAdd}`, { + recursive: true, + }) + } } } diff --git a/src/rest/scripts/update-files.ts b/src/rest/scripts/update-files.ts index c9d1abb44c92..477d016ebd90 100755 --- a/src/rest/scripts/update-files.ts +++ b/src/rest/scripts/update-files.ts @@ -21,6 +21,7 @@ import { allVersions } from '@/versions/lib/all-versions' import { syncWebhookData } from '../../webhooks/scripts/sync' import { syncGitHubAppsData } from '../../github-apps/scripts/sync' import { syncRestRedirects } from './utils/get-redirects' +import { syncChangelogs } from './utils/sync-changelogs' import { MODELS_GATEWAY_ROOT, injectModelsSchema } from './utils/inject-models-schema' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -130,6 +131,7 @@ async function main() { if (pipelines.includes('rest')) { console.log(`\n▶️ Generating REST data files...\n`) await syncRestData(TEMP_OPENAPI_DIR, restSchemas, sourceRepoDirectory, injectModelsSchema) + await syncChangelogs(sourceRepoDirectory, VERSION_NAMES) } if (pipelines.includes('webhooks')) { diff --git a/src/rest/scripts/utils/sync-changelogs.ts b/src/rest/scripts/utils/sync-changelogs.ts new file mode 100644 index 000000000000..5471068ddddc --- /dev/null +++ b/src/rest/scripts/utils/sync-changelogs.ts @@ -0,0 +1,190 @@ +import { readFile, writeFile } from 'fs/promises' +import { existsSync } from 'fs' +import path from 'path' + +import { allVersions } from '@/versions/lib/all-versions' + +const REST_API_DESCRIPTION_ROOT = 'rest-api-description' +const OUTPUT_PATH = 'data/reusables/rest-api/breaking-changes-changelog.md' + +interface VersionMapping { + sourceDir: string + ifversionExpr: string +} + +interface VersionSection { + version: string + content: string +} + +// Build a list of { sourceDir, ifversionExpr } tuples from allVersions. +// For example: +// fpt → source dir "api.github.com", ifversion "fpt" +// ghec → source dir "ghec", ifversion "ghec" +// ghes-3.14 → source dir "ghes-3.14", ifversion "ghes = 3.14" +function buildVersionMappings(versionNames: Record): VersionMapping[] { + // Build reverse lookup: docs short name → source directory name + // e.g. "fpt" → "api.github.com", "ghec" → "ghec" + const reverseMapping: Record = {} + for (const [sourceDir, docsName] of Object.entries(versionNames)) { + reverseMapping[docsName] = sourceDir + } + + const mappings: VersionMapping[] = [] + const seen = new Set() + + for (const versionObj of Object.values(allVersions)) { + const key = versionObj.openApiVersionName + if (seen.has(key)) continue + seen.add(key) + + let sourceDir: string + let ifversionExpr: string + + if (versionObj.shortName === 'ghes') { + // GHES versions: source dir is like "ghes-3.14", ifversion is "ghes = 3.14" + sourceDir = `ghes-${versionObj.currentRelease}` + ifversionExpr = `ghes = ${versionObj.currentRelease}` + } else { + // Non-GHES: look up source dir from reverse mapping + sourceDir = reverseMapping[versionObj.shortName] || versionObj.shortName + ifversionExpr = versionObj.shortName + } + + mappings.push({ sourceDir, ifversionExpr }) + } + + return mappings +} + +// Resolve the changelog file path based on whether we're using +// rest-api-description or the local github repo. +export function getChangelogPath(sourceRepoDir: string, releaseDir: string): string { + if (sourceRepoDir === REST_API_DESCRIPTION_ROOT) { + return path.join(REST_API_DESCRIPTION_ROOT, 'descriptions-next', releaseDir, 'CHANGELOG.md') + } + // Local github repo dev workflow + return path.join( + sourceRepoDir, + 'app', + 'api', + 'description', + 'changelogs', + releaseDir, + 'CHANGELOG.md', + ) +} + +// Parse a CHANGELOG.md into an array of { version, content } objects +// by splitting on `## Version YYYY-MM-DD` headings. +// Strips the top-level `# REST API Breaking Changes for ...` title and intro paragraph. +export function parseVersionSections(markdown: string): VersionSection[] { + const lines = markdown.split('\n') + const sections: VersionSection[] = [] + let currentVersion: string | null = null + let currentLines: string[] = [] + let pastHeader = false + + for (const line of lines) { + // Skip the top-level title (# REST API Breaking Changes ...) + if (!pastHeader && line.startsWith('# ')) { + pastHeader = true + continue + } + + // Skip intro paragraph lines before the first ## Version heading + const versionMatch = line.match(/^## Version (\d{4}-\d{2}-\d{2})/) + if (versionMatch) { + // Save previous section if any + if (currentVersion) { + sections.push({ + version: currentVersion, + content: currentLines.join('\n').trim(), + }) + } + currentVersion = versionMatch[1] + currentLines = [line] + pastHeader = true + continue + } + + if (currentVersion) { + currentLines.push(line) + } + } + + // Save last section + if (currentVersion) { + sections.push({ + version: currentVersion, + content: currentLines.join('\n').trim(), + }) + } + + return sections +} + +// Main function: reads changelogs from each release directory, wraps them +// in product-version gating ({% ifversion %}) and API-version filtering +// ({% if query.apiVersion %}), and writes a combined data file. +export async function syncChangelogs( + sourceRepoDir: string, + versionNames: Record, + outputPath: string = OUTPUT_PATH, +): Promise { + console.log(`\n▶️ Generating REST API breaking changes changelog...\n`) + + const mappings = buildVersionMappings(versionNames) + const outputBlocks: string[] = [] + + for (const { sourceDir, ifversionExpr } of mappings) { + const changelogPath = getChangelogPath(sourceRepoDir, sourceDir) + + if (!existsSync(changelogPath)) { + console.log(` ⏭️ No changelog found for ${sourceDir}, skipping.`) + continue + } + + const markdown = await readFile(changelogPath, 'utf-8') + const sections = parseVersionSections(markdown) + + if (sections.length === 0) { + console.log(` ⏭️ No version sections found in changelog for ${sourceDir}, skipping.`) + continue + } + + const sectionBlocks = sections.map(({ version, content }) => { + return [ + `{% if query.apiVersion == nil or "${version}" <= query.apiVersion %}`, + content, + '', + '{% endif %}', + ].join('\n') + }) + + const releaseBlock = [ + `{% ifversion ${ifversionExpr} %}`, + sectionBlocks.join('\n'), + '{% endif %}', + ].join('\n') + + outputBlocks.push(releaseBlock) + console.log(` ✅ Processed changelog for ${sourceDir} (${sections.length} version sections)`) + } + + if (outputBlocks.length === 0) { + console.log(' ⚠️ No changelogs found. Skipping changelog generation.') + return + } + + // The generated Liquid uses quoted date strings in comparisons + // (e.g., "2022-11-28" <= query.apiVersion) which is valid Liquid but + // triggers the GHD016 lint rule that flags quoted conditional args. + // The upstream changelogs may also contain docs.github.com URLs and + // "deprecated" terminology that trigger docs-domain and GHD046 rules. + const lintDisable = + '\n' + const output = `${lintDisable + outputBlocks.join('\n\n')}\n` + await writeFile(outputPath, output) + console.log(`\n✅ Wrote ${outputPath}`) +} diff --git a/src/rest/tests/sync-changelogs.ts b/src/rest/tests/sync-changelogs.ts new file mode 100644 index 000000000000..904db6415476 --- /dev/null +++ b/src/rest/tests/sync-changelogs.ts @@ -0,0 +1,397 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises' +import { existsSync } from 'fs' +import path from 'path' +import os from 'os' + +import { + parseVersionSections, + getChangelogPath, + syncChangelogs, +} from '../scripts/utils/sync-changelogs' + +// Suppress console.log output during tests +beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}) +}) +afterEach(() => { + vi.restoreAllMocks() +}) + +// --------------------------------------------------------------------------- +// parseVersionSections +// --------------------------------------------------------------------------- +describe('parseVersionSections', () => { + test('parses a changelog with multiple version sections', () => { + const markdown = `# REST API Breaking Changes for GitHub Free, Pro & Team + +For more information, see API Versions. + +## Version 2026-06-10 + +- **Removed topics property** + Use the dedicated endpoint instead. + +## Version 2026-03-10 + +- **Removed permission property** + Use notification_setting instead. + +## Version 2022-11-28 + +No breaking changes.` + + const sections = parseVersionSections(markdown) + expect(sections).toHaveLength(3) + expect(sections[0].version).toBe('2026-06-10') + expect(sections[0].content).toContain('Removed topics property') + expect(sections[1].version).toBe('2026-03-10') + expect(sections[1].content).toContain('Removed permission property') + expect(sections[2].version).toBe('2022-11-28') + expect(sections[2].content).toContain('No breaking changes') + }) + + test('parses a changelog with a single version section', () => { + const markdown = `# REST API Breaking Changes + +## Version 2022-11-28 + +This version has no breaking changes.` + + const sections = parseVersionSections(markdown) + expect(sections).toHaveLength(1) + expect(sections[0].version).toBe('2022-11-28') + expect(sections[0].content).toContain('no breaking changes') + }) + + test('strips the top-level title and intro paragraph', () => { + const markdown = `# REST API Breaking Changes for GitHub Enterprise Cloud + +For more information about versioning, see API Versions. + +## Version 2022-11-28 + +Content here.` + + const sections = parseVersionSections(markdown) + expect(sections).toHaveLength(1) + expect(sections[0].content).not.toContain('Breaking Changes for') + expect(sections[0].content).not.toContain('For more information') + expect(sections[0].content).toContain('Content here') + }) + + test('preserves the ## Version heading in section content', () => { + const markdown = `# Title + +## Version 2026-03-10 + +Some changes.` + + const sections = parseVersionSections(markdown) + expect(sections[0].content).toMatch(/^## Version 2026-03-10/) + }) + + test('returns empty array for markdown with no version sections', () => { + const markdown = `# REST API Breaking Changes + +Just some intro text with no version headings.` + + const sections = parseVersionSections(markdown) + expect(sections).toHaveLength(0) + }) + + test('returns empty array for empty string', () => { + expect(parseVersionSections('')).toHaveLength(0) + }) + + test('handles multi-line content within a version section', () => { + const markdown = `# Title + +## Version 2026-03-10 + +- **Change one** + Detailed description spanning + multiple lines. + + With a paragraph break. + +- **Change two** + Another description.` + + const sections = parseVersionSections(markdown) + expect(sections).toHaveLength(1) + expect(sections[0].content).toContain('Change one') + expect(sections[0].content).toContain('multiple lines') + expect(sections[0].content).toContain('paragraph break') + expect(sections[0].content).toContain('Change two') + }) +}) + +// --------------------------------------------------------------------------- +// getChangelogPath +// --------------------------------------------------------------------------- +describe('getChangelogPath', () => { + test('returns descriptions-next path for rest-api-description source', () => { + const result = getChangelogPath('rest-api-description', 'api.github.com') + expect(result).toBe( + path.join('rest-api-description', 'descriptions-next', 'api.github.com', 'CHANGELOG.md'), + ) + }) + + test('returns descriptions-next path for ghec', () => { + const result = getChangelogPath('rest-api-description', 'ghec') + expect(result).toBe( + path.join('rest-api-description', 'descriptions-next', 'ghec', 'CHANGELOG.md'), + ) + }) + + test('returns descriptions-next path for ghes release dir', () => { + const result = getChangelogPath('rest-api-description', 'ghes-3.19') + expect(result).toBe( + path.join('rest-api-description', 'descriptions-next', 'ghes-3.19', 'CHANGELOG.md'), + ) + }) + + test('returns local github repo path for github source', () => { + const result = getChangelogPath('../github', 'api.github.com') + expect(result).toBe( + path.join( + '..', + 'github', + 'app', + 'api', + 'description', + 'changelogs', + 'api.github.com', + 'CHANGELOG.md', + ), + ) + }) +}) + +// --------------------------------------------------------------------------- +// syncChangelogs (integration tests using temp directories) +// --------------------------------------------------------------------------- +describe('syncChangelogs', () => { + let tmpDir: string + let outputPath: string + + const versionNames: Record = { + 'api.github.com': 'fpt', + ghec: 'ghec', + ghes: 'ghes', + } + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), 'sync-changelogs-test-')) + outputPath = path.join(tmpDir, 'breaking-changes-changelog.md') + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) + }) + + // Helper to create a changelog file in the github repo layout: + // /app/api/description/changelogs//CHANGELOG.md + async function createChangelog(githubDir: string, releaseDir: string, content: string) { + const changelogDir = path.join(githubDir, 'app', 'api', 'description', 'changelogs', releaseDir) + await mkdir(changelogDir, { recursive: true }) + await writeFile(path.join(changelogDir, 'CHANGELOG.md'), content) + } + + test('generates output with ifversion and apiVersion gating for fpt', async () => { + const githubDir = path.join(tmpDir, 'github') + await createChangelog( + githubDir, + 'api.github.com', + `# REST API Breaking Changes + +## Version 2026-03-10 + +- **Breaking change A** + +## Version 2022-11-28 + +No breaking changes.`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + + // Should have ifversion fpt wrapping + expect(output).toContain('{% ifversion fpt %}') + expect(output).toContain('{% endif %}') + + // Should have apiVersion filtering for each version section + expect(output).toContain('{% if query.apiVersion == nil or "2026-03-10" <= query.apiVersion %}') + expect(output).toContain('{% if query.apiVersion == nil or "2022-11-28" <= query.apiVersion %}') + + // Should include the actual content + expect(output).toContain('Breaking change A') + expect(output).toContain('No breaking changes') + }) + + test('generates GHES sections with ghes = X.Y ifversion syntax', async () => { + // Find a real GHES version from allVersions to use + const { allVersions } = await import('@/versions/lib/all-versions') + const ghesVersion = Object.values(allVersions).find((v) => v.shortName === 'ghes') + if (!ghesVersion) return + + const release = ghesVersion.currentRelease + const ghesSourceDir = `ghes-${release}` + + const githubDir = path.join(tmpDir, 'github') + await createChangelog( + githubDir, + ghesSourceDir, + `# REST API Breaking Changes for GHES ${release} + +## Version 2022-11-28 + +No breaking changes.`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + expect(output).toContain(`{% ifversion ghes = ${release} %}`) + }) + + test('skips release directories with no changelog file', async () => { + const githubDir = path.join(tmpDir, 'github') + // Only create a changelog for fpt, not ghec or ghes + await createChangelog( + githubDir, + 'api.github.com', + `# Breaking Changes + +## Version 2022-11-28 + +No breaking changes.`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + expect(output).toContain('{% ifversion fpt %}') + expect(output).not.toContain('{% ifversion ghec %}') + }) + + test('skips changelog files with no version sections', async () => { + const githubDir = path.join(tmpDir, 'github') + + // fpt has valid sections + await createChangelog( + githubDir, + 'api.github.com', + `# Breaking Changes + +## Version 2022-11-28 + +Content.`, + ) + + // ghec has a changelog but no version sections + await createChangelog( + githubDir, + 'ghec', + `# Breaking Changes + +This file has no version headings yet.`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + expect(output).toContain('{% ifversion fpt %}') + expect(output).not.toContain('{% ifversion ghec %}') + }) + + test('does not write output when no changelogs are found', async () => { + const githubDir = path.join(tmpDir, 'github') + await mkdir(githubDir, { recursive: true }) + + await syncChangelogs(githubDir, versionNames, outputPath) + + expect(existsSync(outputPath)).toBe(false) + }) + + test('combines multiple release changelogs into a single output', async () => { + const githubDir = path.join(tmpDir, 'github') + + await createChangelog( + githubDir, + 'api.github.com', + `# Breaking Changes for FPT + +## Version 2026-03-10 + +- FPT change + +## Version 2022-11-28 + +No breaking changes.`, + ) + + await createChangelog( + githubDir, + 'ghec', + `# Breaking Changes for GHEC + +## Version 2022-11-28 + +No breaking changes.`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + + // Both product versions should be present + expect(output).toContain('{% ifversion fpt %}') + expect(output).toContain('{% ifversion ghec %}') + + // FPT should have two apiVersion blocks, GHEC should have one. + // Extract the fpt block: everything between {% ifversion fpt %} and the + // next {% ifversion (which starts the ghec block). + const afterFpt = output.split('{% ifversion fpt %}')[1] + const fptBlock = afterFpt.split('{% ifversion ghec %}')[0] + expect(fptBlock).toContain('"2026-03-10"') + expect(fptBlock).toContain('"2022-11-28"') + expect(fptBlock).toContain('FPT change') + }) + + test('version sections are ordered as they appear in the changelog (newest first)', async () => { + const githubDir = path.join(tmpDir, 'github') + + await createChangelog( + githubDir, + 'api.github.com', + `# Breaking Changes + +## Version 2026-06-10 + +Change C + +## Version 2026-03-10 + +Change B + +## Version 2022-11-28 + +Change A`, + ) + + await syncChangelogs(githubDir, versionNames, outputPath) + + const output = await readFile(outputPath, 'utf-8') + + // Versions should appear in the same order as the changelog (newest first) + const idx2026_06 = output.indexOf('"2026-06-10"') + const idx2026_03 = output.indexOf('"2026-03-10"') + const idx2022 = output.indexOf('"2022-11-28"') + expect(idx2026_06).toBeLessThan(idx2026_03) + expect(idx2026_03).toBeLessThan(idx2022) + }) +}) diff --git a/src/versions/lib/all-versions.ts b/src/versions/lib/all-versions.ts index 76fb644bc3aa..92e7e387a2e7 100644 --- a/src/versions/lib/all-versions.ts +++ b/src/versions/lib/all-versions.ts @@ -117,7 +117,7 @@ const apiVersions: RestApiConfig['api-versions'] = JSON.parse( for (const key of Object.keys(apiVersions)) { const docsVersion = getDocsVersion(key) - allVersions[docsVersion].apiVersions.push(...apiVersions[key].sort()) + allVersions[docsVersion].apiVersions.push(...apiVersions[key].sort().reverse()) // Create a copy of the array to avoid mutating the original when using pop() const sortedVersions = [...apiVersions[key].sort()] allVersions[docsVersion].latestApiVersion = sortedVersions.pop() || '' From e7219154e65f1304ebee5a153f6ae0244c5e0099 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 10:34:53 -0800 Subject: [PATCH 04/12] Track article body response size in Datadog (#59850) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/article-api/middleware/article.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/article-api/middleware/article.ts b/src/article-api/middleware/article.ts index 713c5eb6790a..b9eb40a97e55 100644 --- a/src/article-api/middleware/article.ts +++ b/src/article-api/middleware/article.ts @@ -59,6 +59,7 @@ router.get( } incrementArticleLookup(req, 'full', cacheInfo) + recordBodySize(req, bodyContent) defaultCacheControl(res) return res.json({ @@ -100,6 +101,7 @@ router.get( } incrementArticleLookup(req, 'body') + recordBodySize(req, bodyContent) defaultCacheControl(res) return res.type('text/markdown').send(bodyContent) @@ -202,4 +204,13 @@ function incrementArticleLookup( statsd.increment('api.article.lookup', 1, tags) } +function recordBodySize(req: ExtendedRequestWithPageInfo, body: string) { + const sizeBytes = Buffer.byteLength(body, 'utf8') + const tags = [ + `pathname:${req.pageinfo.pathname}`.slice(0, 200), + `language:${req.pageinfo.page?.languageCode || 'en'}`, + ] + statsd.distribution('api.article.body_size_bytes', sizeBytes, tags) +} + export default router From 2e3c5c2a379adb508c749a4664f516079b51e316 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 10:44:15 -0800 Subject: [PATCH 05/12] Support Accept: text/markdown content negotiation and extend .md to all page types (#59999) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frame/middleware/cache-control.ts | 10 ++++-- src/frame/middleware/render-page.ts | 18 +++++----- src/frame/tests/server.ts | 34 +++++++++++++++++++ .../middleware/handle-invalid-paths.ts | 15 ++------ src/shielding/tests/shielding.ts | 22 +++--------- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/frame/middleware/cache-control.ts b/src/frame/middleware/cache-control.ts index 7132b94329d7..fd4f79ecbf57 100644 --- a/src/frame/middleware/cache-control.ts +++ b/src/frame/middleware/cache-control.ts @@ -84,20 +84,26 @@ export function defaultCacheControl(res: Response): void { defaultBrowserCacheControl(res) } +// Vary on content type for pages that support content negotiation (HTML vs markdown) +export function contentTypeCacheControl(res: Response): void { + defaultCacheControl(res) + res.append('vary', 'accept') +} + // Vary on language when needed // x-user-language is a custom request header derived from req.cookie:user_language // accept-language is truncated to one of our available languages // https://bit.ly/3u5UeRN export function languageCacheControl(res: Response): void { defaultCacheControl(res) - res.set('vary', 'accept-language, x-user-language') + res.append('vary', 'accept-language, x-user-language') } // Vary on both language and version for homepage redirects // x-user-version is a custom request header derived from req.cookie:user_version export function languageAndVersionCacheControl(res: Response): void { defaultCacheControl(res) - res.set('vary', 'accept-language, x-user-language, x-user-version') + res.append('vary', 'accept-language, x-user-language, x-user-version') } // Long cache control for versioned assets: images, CSS, JS... diff --git a/src/frame/middleware/render-page.ts b/src/frame/middleware/render-page.ts index 2dbc8b0415f1..03902f45ec38 100644 --- a/src/frame/middleware/render-page.ts +++ b/src/frame/middleware/render-page.ts @@ -10,7 +10,7 @@ import statsd from '@/observability/lib/statsd' import type { ExtendedRequest } from '@/types' import { allVersions } from '@/versions/lib/all-versions' import { minimumNotFoundHtml } from '../lib/constants' -import { defaultCacheControl } from './cache-control' +import { contentTypeCacheControl, defaultCacheControl } from './cache-control' import { isConnectionDropped } from './halt-on-dropped-connection' import { nextHandleRequest } from './next' @@ -90,6 +90,12 @@ export default async function renderPage(req: ExtendedRequest, res: Response) { // Stop processing if the connection was already dropped if (isConnectionDropped(req, res)) return + // Content negotiation: serve markdown when the client prefers it over HTML. + // Agents like Claude Code send Accept headers that omit text/html. + if (req.accepts(['text/html', 'text/markdown']) === 'text/markdown') { + context.markdownRequested = true + } + if (!req.context) throw new Error('request not contextualized') req.context.renderedPage = await buildRenderedPage(req) req.context.miniTocItems = buildMiniTocItems(req) @@ -145,15 +151,11 @@ export default async function renderPage(req: ExtendedRequest, res: Response) { } if (context.markdownRequested) { - if (!page.autogenerated && page.documentType === 'article') { - return res.type('text/markdown').send(req.context.renderedPage) - } else { - const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\.md$/, '')) - return res.redirect(newUrl) - } + contentTypeCacheControl(res) + return res.type('text/markdown').send(req.context.renderedPage) } - defaultCacheControl(res) + contentTypeCacheControl(res) return nextHandleRequest(req, res) } diff --git a/src/frame/tests/server.ts b/src/frame/tests/server.ts index 6db4e28cc070..bb62313f5f40 100644 --- a/src/frame/tests/server.ts +++ b/src/frame/tests/server.ts @@ -278,6 +278,40 @@ describe('server', () => { expect(res.headers['cache-control']).toMatch(/max-age=\d+/) }) }) + + describe('Accept: text/markdown content negotiation', () => { + test('returns markdown when Accept header prefers text/markdown', async () => { + const res = await get('/en', { + headers: { + accept: 'text/markdown', + }, + }) + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/markdown') + expect(res.headers.vary).toContain('accept') + }) + + test('returns HTML when Accept header prefers text/html', async () => { + const res = await get('/en', { + headers: { + accept: 'text/html,application/xhtml+xml', + }, + }) + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/html') + expect(res.headers.vary).toContain('accept') + }) + + test('returns HTML when Accept header is */*', async () => { + const res = await get('/en', { + headers: { + accept: '*/*', + }, + }) + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/html') + }) + }) }) describe('static routes', () => { diff --git a/src/shielding/middleware/handle-invalid-paths.ts b/src/shielding/middleware/handle-invalid-paths.ts index f2def92c8879..ccf9ace62ae5 100644 --- a/src/shielding/middleware/handle-invalid-paths.ts +++ b/src/shielding/middleware/handle-invalid-paths.ts @@ -85,21 +85,12 @@ export default function handleInvalidPaths( if (req.path.endsWith('/index.md')) { defaultCacheControl(res) - // The originalUrl is the full URL including query string. - // E.g. `/en/foo.md?bar=baz` const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\/index\.md$/, '')) return res.redirect(newUrl) } else if (req.path.endsWith('.md')) { - // encode the query params but also make them pretty so we can see - // them as `/` and `@` in the address bar - // e.g. /api/article/body?pathname=/en/enterprise-server@3.16/admin... - // NOT: /api/article/body?pathname=%2Fen%2Fenterprise-server%403.16%2Fadmin... - const encodedPath = encodeURIComponent(req.path.replace(/\.md$/, '')) - .replace(/%2F/g, '/') - .replace(/%40/g, '@') - const newUrl = `/api/article/body?pathname=${encodedPath}` - res.redirect(newUrl) - return + req.url = req.url.replace(/\.md($|\?)/, '$1') + req.headers.accept = 'text/markdown' + return next() } return next() } diff --git a/src/shielding/tests/shielding.ts b/src/shielding/tests/shielding.ts index f7539951e1e3..12208dfa7ccd 100644 --- a/src/shielding/tests/shielding.ts +++ b/src/shielding/tests/shielding.ts @@ -72,24 +72,12 @@ describe('index.md and .md suffixes', () => { } }) - test('any URL that ends with /.md redirects', async () => { - // With language prefix - { - const res = await get('/en/get-started/hello.md') - expect(res.statusCode).toBe(302) - expect(res.headers.location).toBe('/api/article/body?pathname=/en/get-started/hello') - } - // Without language prefix + test('any URL that ends with .md serves markdown directly', async () => { + // .md is stripped and request flows through with Accept: text/markdown { - const res = await get('/get-started/hello.md') - expect(res.statusCode).toBe(302) - expect(res.headers.location).toBe('/api/article/body?pathname=/get-started/hello') - } - // With query string - { - const res = await get('/get-started/hello.md?foo=bar') - expect(res.statusCode).toBe(302) - expect(res.headers.location).toBe('/api/article/body?pathname=/get-started/hello') + const res = await get('/en/get-started.md') + // Should not redirect — serves markdown directly (or 404 if page doesn't exist) + expect(res.statusCode).not.toBe(302) } }) }) From b807b04959917d5039aad47b0bbc358104e2d169 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 10:56:39 -0800 Subject: [PATCH 06/12] Fix sync-llms-txt branch detection swallowing 404 errors (#59969) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sync-llms-txt-to-github.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sync-llms-txt-to-github.yml b/.github/workflows/sync-llms-txt-to-github.yml index cce898f3f529..41809db7b4c4 100644 --- a/.github/workflows/sync-llms-txt-to-github.yml +++ b/.github/workflows/sync-llms-txt-to-github.yml @@ -9,7 +9,7 @@ on: - 'data/llms-txt-config.yml' - 'src/workflows/generate-llms-txt.ts' schedule: - - cron: '20 16 * * 1-5' # Weekdays at ~9:20am Pacific + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read @@ -75,8 +75,7 @@ jobs: REPO="github/github" echo "Checking if branch '$BRANCH' exists..." - BRANCH_SHA=$(gh api "repos/$REPO/git/ref/heads/$BRANCH" --jq '.object.sha' 2>/dev/null || true) - if [ -n "$BRANCH_SHA" ]; then + if BRANCH_SHA=$(gh api "repos/$REPO/git/ref/heads/$BRANCH" --jq '.object.sha' 2>/dev/null); then echo "Branch exists at $BRANCH_SHA" else echo "Branch does not exist, creating from default branch..." @@ -99,11 +98,11 @@ jobs: CONTENT=$(base64 -w 0 /tmp/llms.txt) echo "Checking for existing file SHA on branch..." - EXISTING_SHA=$(gh api "repos/$REPO/contents/public/llms.txt?ref=$BRANCH" \ - --jq '.sha' 2>/dev/null || true) - if [ -n "$EXISTING_SHA" ]; then + if EXISTING_SHA=$(gh api "repos/$REPO/contents/public/llms.txt?ref=$BRANCH" \ + --jq '.sha' 2>/dev/null); then echo "Existing file SHA: $EXISTING_SHA" else + EXISTING_SHA="" echo "No existing file on branch (new file)" fi @@ -128,9 +127,8 @@ jobs: REPO="github/github" echo "Checking for existing PR from '$BRANCH'..." - EXISTING_PR=$(gh pr list --repo "$REPO" --head "$BRANCH" \ - --json number --jq '.[0].number' 2>/dev/null || true) - if [ -n "$EXISTING_PR" ]; then + if EXISTING_PR=$(gh pr list --repo "$REPO" --head "$BRANCH" \ + --json number --jq '.[0].number' 2>/dev/null) && [ -n "$EXISTING_PR" ]; then echo "PR #$EXISTING_PR already exists, updated with new commit" exit 0 fi From 8a02308929bc91c5aa2cb30c88e93d2b8e1d3674 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 11:16:02 -0800 Subject: [PATCH 07/12] Fix link check workflow failures (#59963) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/link-check-internal.yml | 1 + .../copilot-in-github-desktop.md | 2 +- src/links/lib/extract-links.ts | 5 ++-- src/links/scripts/check-links-external.ts | 28 +++++++++++++++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml index fd68a6a93e7b..6aa411a2ca5e 100644 --- a/.github/workflows/link-check-internal.yml +++ b/.github/workflows/link-check-internal.yml @@ -108,6 +108,7 @@ jobs: needs: [setup-matrix, check-internal-links] runs-on: ubuntu-latest permissions: + contents: read issues: write steps: - name: Checkout diff --git a/content/copilot/responsible-use/copilot-in-github-desktop.md b/content/copilot/responsible-use/copilot-in-github-desktop.md index 04c56c3d5419..e8798a457434 100644 --- a/content/copilot/responsible-use/copilot-in-github-desktop.md +++ b/content/copilot/responsible-use/copilot-in-github-desktop.md @@ -39,7 +39,7 @@ The feature is intended to supplement rather than replace a human's work to draf ### Provide feedback -If you encounter any issues or limitations with {% data variables.copilot.copilot_desktop_short %}, you can provide feedback by creating an issue in the [{% data variables.product.prodname_desktop %} open source repository](https://github.com/desktop/desktop/issues/new?template=bug_report.yaml ). This can help the developers to improve the tool and address any concerns or limitations. +If you encounter any issues or limitations with {% data variables.copilot.copilot_desktop_short %}, you can provide feedback by creating an issue in the [{% data variables.product.prodname_desktop %} open source repository](https://github.com/desktop/desktop/issues/new?template=bug_report.yaml). This can help the developers to improve the tool and address any concerns or limitations. ## Limitations of {% data variables.copilot.copilot_desktop_short %} diff --git a/src/links/lib/extract-links.ts b/src/links/lib/extract-links.ts index d6a5c79f32a1..328f3c4d9481 100644 --- a/src/links/lib/extract-links.ts +++ b/src/links/lib/extract-links.ts @@ -16,8 +16,9 @@ import type { Context, Page } from '@/types' // Link patterns for Markdown const INTERNAL_LINK_PATTERN = /\]\(\/[^)]+\)/g const AUTOTITLE_LINK_PATTERN = /\[AUTOTITLE\]\(([^)]+)\)/g -// Handles one level of balanced parentheses in URLs (e.g., Wikipedia links) -const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/(?:[^()\s]+|\([^()]*\))*)\)/g +// Handles one level of balanced parentheses in URLs (e.g., Wikipedia links). +// Uses an unrolled loop to avoid catastrophic backtracking on malformed URLs. +const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/[^()\s]*(?:\([^()]*\)[^()\s]*)*)\)/g const IMAGE_LINK_PATTERN = /!\[[^\]]*\]\(([^)]+)\)/g // Anchor link patterns (for same-page links) diff --git a/src/links/scripts/check-links-external.ts b/src/links/scripts/check-links-external.ts index a5c5d89b87dc..526ff928d077 100644 --- a/src/links/scripts/check-links-external.ts +++ b/src/links/scripts/check-links-external.ts @@ -155,10 +155,20 @@ async function extractAllExternalLinks(): Promise 1000) { + console.warn(` ⚠️ Slow extraction: ${file} took ${(fileMs / 1000).toFixed(1)}s`) + } for (const link of result.externalLinks) { // Only check HTTPS links @@ -173,8 +183,16 @@ async function extractAllExternalLinks(): Promise Date: Thu, 5 Mar 2026 11:27:21 -0800 Subject: [PATCH 08/12] Make create-tree resilient to missing early-access children (#59968) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frame/lib/create-tree.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/frame/lib/create-tree.ts b/src/frame/lib/create-tree.ts index 1cb18aa80d7d..96227b716875 100644 --- a/src/frame/lib/create-tree.ts +++ b/src/frame/lib/create-tree.ts @@ -40,14 +40,18 @@ export default async function createTree( } // Throw an error if we can't find a content file associated with the children: entry. // But don't throw an error if the user is running the site locally and hasn't cloned the Early Access repo. - if (originalPath === 'content/early-access') { + // Also don't throw for missing children *within* early-access content — a broken + // early-access article should not block every docs-internal PR from merging. + const msg = `Cannot find a content file at ${originalPath}. Check the 'children' frontmatter in the parent index.md.` + + if ( + originalPath === 'content/early-access' || + originalPath.startsWith('content/early-access/') + ) { + console.warn(`Warning: ${msg}`) return } - throw new Error( - `Cannot find a content file at ${originalPath}. Fix the children frontmatter entry "/${path.basename( - originalPath, - )}" in ${path.dirname(originalPath)}/index.md.\n`, - ) + throw new Error(msg) } } From 7d46096e5aecf16b4c568c6e29d27896fb7de75e Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 11:29:10 -0800 Subject: [PATCH 09/12] Consolidate all weekly workflows to Monday (#60006) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../delete-orphan-translation-files.yml | 2 +- .github/workflows/enterprise-dates.yml | 2 +- .../workflows/enterprise-release-issue.yml | 2 +- .github/workflows/link-check-external.yml | 2 +- .../workflows/link-check-github-github.yml | 2 +- .github/workflows/link-check-internal.yml | 2 +- .../lint-entire-content-data-markdown.yml | 2 +- .github/workflows/moda-allowed-ips.yml | 2 +- .github/workflows/needs-sme-stale-check.yaml | 2 +- .github/workflows/no-response.yaml | 2 +- .github/workflows/orphaned-features-check.yml | 2 +- .github/workflows/orphaned-files-check.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/triage-stale-check.yml | 2 +- .../validate-github-github-docs-urls.yml | 2 +- src/workflows/tests/actions-workflows.ts | 21 +++++++++---------- 16 files changed, 25 insertions(+), 26 deletions(-) diff --git a/.github/workflows/delete-orphan-translation-files.yml b/.github/workflows/delete-orphan-translation-files.yml index 3615b25161ff..ceadbc0b8bc9 100644 --- a/.github/workflows/delete-orphan-translation-files.yml +++ b/.github/workflows/delete-orphan-translation-files.yml @@ -14,7 +14,7 @@ name: Delete orphan translation files on: workflow_dispatch: schedule: - - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/enterprise-dates.yml b/.github/workflows/enterprise-dates.yml index ebc927c2fa3c..bb49bd1d01e6 100644 --- a/.github/workflows/enterprise-dates.yml +++ b/.github/workflows/enterprise-dates.yml @@ -11,7 +11,7 @@ name: Enterprise date updater on: workflow_dispatch: schedule: - - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: write diff --git a/.github/workflows/enterprise-release-issue.yml b/.github/workflows/enterprise-release-issue.yml index 3b6e87ad768b..ecbd72a911e9 100644 --- a/.github/workflows/enterprise-release-issue.yml +++ b/.github/workflows/enterprise-release-issue.yml @@ -7,7 +7,7 @@ name: Open Enterprise release or deprecation issue on: workflow_dispatch: schedule: - - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/link-check-external.yml b/.github/workflows/link-check-external.yml index 9330a48e8feb..cade990f9704 100644 --- a/.github/workflows/link-check-external.yml +++ b/.github/workflows/link-check-external.yml @@ -2,7 +2,7 @@ name: 'Link Check: External' on: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST workflow_dispatch: inputs: max_urls: diff --git a/.github/workflows/link-check-github-github.yml b/.github/workflows/link-check-github-github.yml index 24249502c199..3f30cefcc4c0 100644 --- a/.github/workflows/link-check-github-github.yml +++ b/.github/workflows/link-check-github-github.yml @@ -7,7 +7,7 @@ name: 'Link Check: github/github' on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml index 6aa411a2ca5e..6f5cf874d988 100644 --- a/.github/workflows/link-check-internal.yml +++ b/.github/workflows/link-check-internal.yml @@ -2,7 +2,7 @@ name: 'Link Check: Internal' on: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST workflow_dispatch: inputs: version: diff --git a/.github/workflows/lint-entire-content-data-markdown.yml b/.github/workflows/lint-entire-content-data-markdown.yml index b99f5ff3385c..15f532cae63d 100644 --- a/.github/workflows/lint-entire-content-data-markdown.yml +++ b/.github/workflows/lint-entire-content-data-markdown.yml @@ -7,7 +7,7 @@ name: 'Lint entire content and data markdown files' on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST — link & content quality theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/moda-allowed-ips.yml b/.github/workflows/moda-allowed-ips.yml index 35609ce02f33..9d416f41fa97 100644 --- a/.github/workflows/moda-allowed-ips.yml +++ b/.github/workflows/moda-allowed-ips.yml @@ -6,7 +6,7 @@ name: Update Moda allowed IPs on: schedule: - - cron: '20 16 * * 4' # Run every Thursday at 16:20 UTC / 8:20 PST — infrastructure & releases theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST workflow_dispatch: permissions: diff --git a/.github/workflows/needs-sme-stale-check.yaml b/.github/workflows/needs-sme-stale-check.yaml index afcd5ec7755b..388708ebab91 100644 --- a/.github/workflows/needs-sme-stale-check.yaml +++ b/.github/workflows/needs-sme-stale-check.yaml @@ -6,7 +6,7 @@ name: Stale check for issues or PRs with "needs SME" label on: schedule: - - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml index 007faee4a4c7..451d668b10bd 100644 --- a/.github/workflows/no-response.yaml +++ b/.github/workflows/no-response.yaml @@ -12,7 +12,7 @@ on: types: [created] schedule: - - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/orphaned-features-check.yml b/.github/workflows/orphaned-features-check.yml index 0c951646a1e9..d3ec2d401f20 100644 --- a/.github/workflows/orphaned-features-check.yml +++ b/.github/workflows/orphaned-features-check.yml @@ -7,7 +7,7 @@ name: 'Orphaned features check' on: workflow_dispatch: schedule: - - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST pull_request: paths: - .github/workflows/orphaned-features-check.yml diff --git a/.github/workflows/orphaned-files-check.yml b/.github/workflows/orphaned-files-check.yml index e72147eb7005..d425621ac707 100644 --- a/.github/workflows/orphaned-files-check.yml +++ b/.github/workflows/orphaned-files-check.yml @@ -7,7 +7,7 @@ name: 'Orphaned files check' on: workflow_dispatch: schedule: - - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST pull_request: paths: - .github/workflows/orphaned-assets-check.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6ae4c489f342..4ae6f6d399b6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ name: Stale check for stalled pull requests in the docs-internal repository on: schedule: - - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST push: paths: - .github/workflows/stale.yml diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml index e154f9132bbe..63e081fe0c06 100644 --- a/.github/workflows/triage-stale-check.yml +++ b/.github/workflows/triage-stale-check.yml @@ -6,7 +6,7 @@ name: Stale check for no activity on: schedule: - - cron: '20 16 * * 2' # Run every Tuesday at 16:20 UTC / 8:20 PST — staleness & triage theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST permissions: contents: read diff --git a/.github/workflows/validate-github-github-docs-urls.yml b/.github/workflows/validate-github-github-docs-urls.yml index 96afda1e0194..cb76eb5e73c0 100644 --- a/.github/workflows/validate-github-github-docs-urls.yml +++ b/.github/workflows/validate-github-github-docs-urls.yml @@ -7,7 +7,7 @@ name: Validate github/github docs URLs on: workflow_dispatch: schedule: - - cron: '20 16 * * 3' # Run every Wednesday at 16:20 UTC / 8:20 PST — orphan & hygiene cleanup theme + - cron: '20 16 * * 1' # Run every Monday at 16:20 UTC / 8:20 PST # See https://gh.io/AAsyyao before uncommenting: # pull_request: # paths: diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts index 053884f0d4c1..00439d6e4f0b 100644 --- a/src/workflows/tests/actions-workflows.ts +++ b/src/workflows/tests/actions-workflows.ts @@ -104,22 +104,21 @@ describe('GitHub Actions workflows', () => { test.each(dailyWorkflows)('daily scheduled workflows only run Mon-Fri $filename', ({ data }) => { for (const { cron } of data.on.schedule) { - const dayOfWeek = cron.split(' ')[4] + 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 expect(dayOfWeek).toMatch(/^[1-5](-[1-5])?$/) } }) - test.each(weeklyWorkflows)( - 'weekly scheduled workflows only run Mon-Fri $filename', - ({ data }) => { - for (const { cron } of data.on.schedule) { - const dayOfWeek = cron.split(' ')[4] - // Day-of-week must be a single day 1 (Mon) through 5 (Fri) - expect(dayOfWeek).toMatch(/^[1-5]$/) - } - }, - ) + test.each(weeklyWorkflows)('weekly scheduled workflows run on Monday $filename', ({ data }) => { + for (const { cron } of data.on.schedule) { + const fields = cron.trim().split(/\s+/) + const dayOfWeek = fields[4] + // Day-of-week must be 1 (Monday) + expect(dayOfWeek).toBe('1') + } + }) test.each(workflows)( 'contains contents:read permissions when permissions are used $filename', From 1236101a08a40bd08dde1b08080158cec8c97322 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 11:35:09 -0800 Subject: [PATCH 10/12] Add usage guidance to llms.txt for LLM content discovery (#59849) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frame/middleware/llms-txt.ts | 16 ++++++++++------ src/frame/tests/llms-txt.ts | 26 +++++++++++++++++--------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/frame/middleware/llms-txt.ts b/src/frame/middleware/llms-txt.ts index 1ae9ea740d6b..cca0299a180a 100644 --- a/src/frame/middleware/llms-txt.ts +++ b/src/frame/middleware/llms-txt.ts @@ -53,13 +53,17 @@ function generateBasicLlmsTxt(): string { > Help for wherever you are on your GitHub journey. -## Docs Content +## How to Use -- [Page List API](${BASE_API_URL}/en/free-pro-team@latest) -- [Versions API](${BASE_API_URL}/versions): \`curl "https://docs.github.com/api/pagelist/versions"\` -- [Languages API](${BASE_API_URL}/languages): \`curl "https://docs.github.com/api/pagelist/languages"\` -- [Article API](https://docs.github.com/api/article): \`curl "https://docs.github.com/api/article?pathname=/en/get-started/start-your-journey/about-github-and-git"\` -- [Search API](https://docs.github.com/api/search): \`curl "https://docs.github.com/api/search?query=actions&language=en&version=free-pro-team@latest"\` +To find a specific article, use the **Search API** with a query. To browse all available pages, use the **Page List API** to get a list of paths, then fetch individual articles with the **Article API**. The \`/api/article/body\` endpoint returns clean markdown, ideal for LLM consumption. + +## APIs + +- [Page List API](${BASE_API_URL}/en/free-pro-team@latest): Returns all article paths for a given version. Use this to discover what content is available. +- [Article API](https://docs.github.com/api/article): Fetches a single article as JSON (metadata and markdown body). Use \`/api/article/body\` for markdown only. Example: \`/api/article/body?pathname=/en/get-started/start-your-journey/about-github-and-git\` +- [Search API](https://docs.github.com/api/search/v1): Full-text search across all articles. Returns matching pages with context. Example: \`/api/search/v1?query=actions&language=en&version=free-pro-team@latest&client_name=curl\` +- [Versions API](${BASE_API_URL}/versions): Lists all available documentation versions. +- [Languages API](${BASE_API_URL}/languages): Lists all available languages. ## Translations diff --git a/src/frame/tests/llms-txt.ts b/src/frame/tests/llms-txt.ts index be882f2f4e41..d1111f430bd6 100644 --- a/src/frame/tests/llms-txt.ts +++ b/src/frame/tests/llms-txt.ts @@ -20,6 +20,17 @@ describe('llms.txt endpoint', () => { expect(content).toMatch(/^# .*GitHub.*Docs/m) }) + test('includes how to use guidance', async () => { + const res = await get('/llms.txt') + const content = res.body + + // Should explain the workflow for LLMs + expect(content).toMatch(/Search API/) + expect(content).toMatch(/Page List API/) + expect(content).toMatch(/Article API/) + expect(content).toMatch(/markdown/i) + }) + test('includes programmatic access section', async () => { const res = await get('/llms.txt') const content = res.body @@ -27,6 +38,7 @@ describe('llms.txt endpoint', () => { // Should mention the existing APIs expect(content).toMatch(/Article API/i) expect(content).toMatch(/Page List API/i) + expect(content).toMatch(/Search API/i) expect(content).toMatch(/api\/article/i) expect(content).toMatch(/api\/pagelist\/en\/free-pro-team@latest/i) }) @@ -36,7 +48,8 @@ describe('llms.txt endpoint', () => { const content = res.body // Should have all the main sections we expect - expect(content).toMatch(/## Docs Content/i) + expect(content).toMatch(/## How to Use/i) + expect(content).toMatch(/## APIs/i) expect(content).toMatch(/## Translations/i) expect(content).toMatch(/## Versions/i) }) @@ -75,7 +88,6 @@ describe('llms.txt endpoint', () => { // Should prominently feature the pagelist API as the main content source expect(content).toMatch(/Page List API.*api\/pagelist\/en\/free-pro-team@latest/i) - expect(content).not.toMatch(/Machine-readable list/i) // Removed descriptions }) test.each(['free-pro-team@latest', 'enterprise-cloud@latest'])( @@ -100,7 +112,7 @@ describe('llms.txt endpoint', () => { expect(content).toMatch(/api\/pagelist\/en\/enterprise-server@\d+\.\d+/) }) - test('follows llms.txt specification structure and has reasonable length', async () => { + test('follows llms.txt specification structure', async () => { const res = await get('/llms.txt') const content = res.body @@ -116,10 +128,6 @@ describe('llms.txt endpoint', () => { // Check for markdown links expect(content).toMatch(/\[.+\]\(.+\)/m) - // Should include translations and versions but still be reasonable - expect(content.length).toBeGreaterThan(500) - expect(content.length).toBeLessThan(5000) - // Split into lines for structure analysis const lines = content.split('\n') @@ -131,8 +139,8 @@ describe('llms.txt endpoint', () => { const hasBlockquote = lines.some((line: string) => line.trim().startsWith('>')) expect(hasBlockquote).toBe(true) - // Should have multiple H2 sections (Docs Content, Translations, Versions) + // Should have multiple H2 sections (How to Use, APIs, Translations, Versions) const h2Sections = lines.filter((line: string) => line.trim().startsWith('## ')) - expect(h2Sections.length).toBeGreaterThanOrEqual(3) + expect(h2Sections.length).toBeGreaterThanOrEqual(4) }) }) From 2f23568762d64c5811da769825d64612a452c12b Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 11:36:36 -0800 Subject: [PATCH 11/12] Add issue creation to slack-alert composite action (#60063) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../create-workflow-failure-issue/action.yml | 93 +++++++++++++++++++ .github/workflows/codeql.yml | 5 + .github/workflows/content-pipelines.yml | 5 + .github/workflows/create-changelog-pr.yml | 5 + .../delete-orphan-translation-files.yml | 5 + .github/workflows/docs-review-collect.yml | 5 + .github/workflows/enterprise-dates.yml | 5 + .../workflows/enterprise-release-issue.yml | 5 + .../workflows/index-autocomplete-search.yml | 5 + .github/workflows/index-general-search.yml | 15 +++ .github/workflows/keep-caches-warm.yml | 5 + .github/workflows/link-check-external.yml | 5 + .../workflows/link-check-github-github.yml | 5 + .github/workflows/link-check-internal.yml | 15 +++ .../lint-entire-content-data-markdown.yml | 5 + .github/workflows/moda-allowed-ips.yml | 5 + .github/workflows/orphaned-features-check.yml | 5 + .github/workflows/orphaned-files-check.yml | 5 + .github/workflows/repo-sync.yml | 5 + .github/workflows/stale.yml | 5 + .github/workflows/sync-audit-logs.yml | 5 + .github/workflows/sync-graphql.yml | 10 ++ .github/workflows/sync-llms-txt-to-github.yml | 5 + .github/workflows/sync-openapi.yml | 5 + .github/workflows/sync-secret-scanning.yml | 5 + .../validate-github-github-docs-urls.yml | 5 + src/workflows/tests/actions-workflows.ts | 16 ++++ 27 files changed, 259 insertions(+) create mode 100644 .github/actions/create-workflow-failure-issue/action.yml diff --git a/.github/actions/create-workflow-failure-issue/action.yml b/.github/actions/create-workflow-failure-issue/action.yml new file mode 100644 index 000000000000..0e0176b89d36 --- /dev/null +++ b/.github/actions/create-workflow-failure-issue/action.yml @@ -0,0 +1,93 @@ +name: Create workflow failure issue +description: Create or update a GitHub issue in docs-engineering when a workflow fails, for automated diagnosis by an agentic workflow. + +inputs: + token: + description: A token with issues write permission on the target repo + required: true + repo: + description: The repository to create the issue in + default: github/docs-engineering + required: false + +runs: + using: composite + steps: + - name: Check for existing open issue + id: check-existing + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + ISSUE_REPO: ${{ inputs.repo }} + WORKFLOW_NAME: ${{ github.workflow }} + run: | + existing=$(gh issue list \ + --repo "$ISSUE_REPO" \ + --label "workflow-failure" \ + --search "in:title [Workflow Failure] $WORKFLOW_NAME" \ + --state open \ + --json number \ + --jq '.[0].number // empty' 2>/dev/null || true) + echo "existing_issue=$existing" >> "$GITHUB_OUTPUT" + + - name: Comment on existing issue + if: steps.check-existing.outputs.existing_issue != '' + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + ISSUE_REPO: ${{ inputs.repo }} + ISSUE_NUMBER: ${{ steps.check-existing.outputs.existing_issue }} + WORKFLOW_NAME: ${{ github.workflow }} + SOURCE_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + EVENT_NAME: ${{ github.event_name }} + GIT_REF: ${{ github.ref }} + run: | + body=$(cat < 0 && github.event_name != 'workflow_dispatch' needs: update_graphql_files @@ -96,3 +101,8 @@ jobs: These change types are not in CHANGES_TO_REPORT and were silently ignored. Consider reviewing if they should be added to the changelog. See workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - uses: ./.github/actions/create-workflow-failure-issue + if: ${{ failure() && github.event_name != 'workflow_dispatch' }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/sync-llms-txt-to-github.yml b/.github/workflows/sync-llms-txt-to-github.yml index 41809db7b4c4..9de6fbb62ebd 100644 --- a/.github/workflows/sync-llms-txt-to-github.yml +++ b/.github/workflows/sync-llms-txt-to-github.yml @@ -167,3 +167,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() && github.event_name != 'workflow_dispatch' }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/sync-openapi.yml b/.github/workflows/sync-openapi.yml index f564aac181ee..4d1762b6f651 100644 --- a/.github/workflows/sync-openapi.yml +++ b/.github/workflows/sync-openapi.yml @@ -123,3 +123,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() && github.event_name != 'workflow_dispatch' }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/sync-secret-scanning.yml b/.github/workflows/sync-secret-scanning.yml index a0ec485db4d6..a51bdfc881db 100644 --- a/.github/workflows/sync-secret-scanning.yml +++ b/.github/workflows/sync-secret-scanning.yml @@ -84,3 +84,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() && github.event_name != 'workflow_dispatch' }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/.github/workflows/validate-github-github-docs-urls.yml b/.github/workflows/validate-github-github-docs-urls.yml index cb76eb5e73c0..db17a734203a 100644 --- a/.github/workflows/validate-github-github-docs-urls.yml +++ b/.github/workflows/validate-github-github-docs-urls.yml @@ -129,3 +129,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() && github.event_name == 'schedule' }} + with: + token: ${{ secrets.DOCS_BOT_PAT_BASE }} diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts index 00439d6e4f0b..c491ccc979ec 100644 --- a/src/workflows/tests/actions-workflows.ts +++ b/src/workflows/tests/actions-workflows.ts @@ -150,6 +150,22 @@ describe('GitHub Actions workflows', () => { }, ) + test.each(alertWorkflows)( + 'scheduled workflows create failure issue on fail $filename', + ({ filename, data }) => { + for (const [name, job] of Object.entries(data.jobs)) { + if ( + !job.steps.find( + (step: Record) => + step.uses === './.github/actions/create-workflow-failure-issue', + ) + ) { + throw new Error(`Job ${filename} # ${name} missing create-workflow-failure-issue on fail`) + } + } + }, + ) + test.each(alertWorkflows)( 'performs a checkout before calling composite action $filename', ({ filename, data }) => { From cb1ba5aac91cc5b368a76f037aacd04d3dff0197 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 5 Mar 2026 11:38:35 -0800 Subject: [PATCH 12/12] Add agent discovery workflow to .github/instructions (#60000) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/all.instructions.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/instructions/all.instructions.md b/.github/instructions/all.instructions.md index a32182f60098..373e3d77ad1b 100644 --- a/.github/instructions/all.instructions.md +++ b/.github/instructions/all.instructions.md @@ -29,3 +29,12 @@ When you create a pull request: 3. Label with "llm-generated". 4. If an issue exists, include "fixes owner/repo#issue" or "towards owner/repo#issue" as appropriate. 5. Always create PRs in **draft mode** using `--draft` flag. + +## Accessing docs.github.com content programmatically + +When you need to read GitHub Docs, use these endpoints on `docs.github.com` in order of preference: + +1. `/llms.txt` — Start here. Returns a structured overview of the site with links to pagelist endpoints for each product version. +2. `/api/pagelist/:lang/:version` — Returns a list of all pages for a given language and version (e.g., `/api/pagelist/en/free-pro-team@latest`). Use `/api/pagelist/versions` and `/api/pagelist/languages` for available options. +3. `/api/search/v1?query=...&language=...&version=...&client_name=...` — Search docs content (e.g., `/api/search/v1?query=actions&language=en&version=free-pro-team@latest&client_name=copilot`). +4. `/api/article/body?pathname=...` — Returns the rendered markdown body of a page. Handles all page types including REST, GraphQL, and webhook reference pages.