diff --git a/content/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow.md b/content/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow.md index 74bb9881d499..c783a76061fd 100644 --- a/content/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow.md +++ b/content/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow.md @@ -18,21 +18,11 @@ redirect_from: {% data reusables.actions.enterprise-github-hosted-runners %} -## About workflow triggers +## Prerequisites -{% data reusables.actions.about-triggers %} +To learn more about workflows and triggering workflows, see [AUTOTITLE](/actions/concepts/workflows-and-actions/workflows). -Workflow triggers are defined with the `on` key. For more information, see [AUTOTITLE](/actions/using-workflows/workflow-syntax-for-github-actions#on). - -The following steps occur to trigger a workflow run: - -1. An event occurs on your repository. The event has an associated commit SHA and Git ref. -1. {% data variables.product.github %} searches the `.github/workflows` directory in the root of your repository for workflow files that are present in the associated commit SHA or Git ref of the event. -1. A workflow run is triggered for any workflows that have `on:` values that match the triggering event. Some events also require the workflow file to be present on the default branch of the repository in order to run. - - Each workflow run will use the version of the workflow that is present in the associated commit SHA or Git ref of the event. When a workflow runs, {% data variables.product.github %} sets the `GITHUB_SHA` (commit SHA) and `GITHUB_REF` (Git ref) environment variables in the runner environment. For more information, see [AUTOTITLE](/actions/learn-github-actions/variables). - -### Triggering a workflow from a workflow +## Triggering a workflow from a workflow {% data reusables.actions.actions-do-not-trigger-workflows %} For more information, see [AUTOTITLE](/actions/security-guides/automatic-token-authentication). diff --git a/content/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot.md b/content/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot.md index ddce43061e35..4bb01dabc320 100644 --- a/content/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot.md +++ b/content/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot.md @@ -160,7 +160,8 @@ Examples of how to configure access to the private registries supported by {% da * [`cargo-registry`](#cargo-registry) * [`composer-repository`](#composer-repository) * [`docker-registry`](#docker-registry) -* [`git`](#git) +* [`git`](#git){% ifversion dependabot-helm-support %} +* [`helm-registry`](#helm-registry){% endif %} * [`hex-organization`](#hex-organization) * [`hex-repository`](#hex-repository) * [`maven-repository`](#maven-repository) diff --git a/content/copilot/how-tos/context/model-context-protocol/extending-copilot-chat-with-mcp.md b/content/copilot/how-tos/context/model-context-protocol/extending-copilot-chat-with-mcp.md index 95cb7888365c..e69269d927c6 100644 --- a/content/copilot/how-tos/context/model-context-protocol/extending-copilot-chat-with-mcp.md +++ b/content/copilot/how-tos/context/model-context-protocol/extending-copilot-chat-with-mcp.md @@ -15,8 +15,8 @@ redirect_from: >[!NOTE] > * MCP support is generally available (GA) in {% data variables.copilot.copilot_chat_short %} for {% data variables.product.prodname_vscode %}. -> * MCP support for Copilot in {% data variables.product.prodname_vs %}, JetBrains, Eclipse, and Xcode is in {% data variables.release-phases.public_preview %} and is subject to change. -> * The [AUTOTITLE](/free-pro-team@latest/site-policy/github-terms/github-pre-release-license-terms) apply to your use of this product. +> * MCP support for {% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vs %}, JetBrains, Eclipse, and Xcode is in {% data variables.release-phases.public_preview %} and is subject to change. +> * The [AUTOTITLE](/free-pro-team@latest/site-policy/github-terms/github-pre-release-license-terms) apply only to {% data variables.product.prodname_copilot_short %} in IDEs where MCP support is still in preview. GA terms apply when using MCP for {% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_vscode %}. {% vscode %} diff --git a/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-enterprise.md b/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-enterprise.md index 656b987a0a17..ee3104489333 100644 --- a/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-enterprise.md +++ b/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-enterprise.md @@ -41,6 +41,6 @@ Enable {% data variables.product.prodname_copilot_short %} for some or all organ If your enterprise is on {% data variables.enterprise.data_residency_site %}, users must perform some additional setup to authenticate to their account from their development environment. See [AUTOTITLE](/copilot/managing-copilot/configure-personal-settings/using-github-copilot-with-an-account-on-ghecom). -## 5. Drive {% data variables.product.prodname_copilot_short %} adoption +## Next steps -Planning and implementing an effective enablement process is essential to drive adoption of {% data variables.product.prodname_copilot_short %} in your enterprise. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/driving-copilot-adoption-in-your-company). +{% data reusables.copilot.setup-next-steps %} diff --git a/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-organization.md b/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-organization.md index ce0a4ac9aac9..2e2b8fb94bff 100644 --- a/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-organization.md +++ b/content/copilot/how-tos/set-up/setting-up-github-copilot-for-your-organization.md @@ -35,22 +35,10 @@ You may also need to install custom SSL certificates on your members' machines. ## 4. Grant access to members -Enable {% data variables.product.prodname_copilot_short %} for some or all members of your organization. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-access-to-github-copilot-in-your-organization/granting-access-to-copilot-for-members-of-your-organization). - -To help drive adoption of {% data variables.product.prodname_copilot_short %} in your organization, think about what teams or members are most excited about {% data variables.product.prodname_copilot_short %} or could benefit the most from {% data variables.product.prodname_copilot_short %}. You may want to enable {% data variables.product.prodname_copilot_short %} for those members before enabling {% data variables.product.prodname_copilot_short %} for your whole organization. This can help you discover blockers, demonstrate early success, and set your organization up for a successful {% data variables.product.prodname_copilot_short %} rollout. - -{% data reusables.copilot.self-serve-license-link %} +Enable {% data variables.product.prodname_copilot_short %} for some or all members of your organization. You may want to identify teams or members who are most likely to benefit from {% data variables.product.prodname_copilot_short %} and enable {% data variables.product.prodname_copilot_short %} for them first to discover potential blockers and demonstrate early success. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-access-to-github-copilot-in-your-organization/granting-access-to-copilot-for-members-of-your-organization). If your organization is part of an enterprise on {% data variables.enterprise.data_residency_site %}, users must perform some additional setup to authenticate to their account from their development environment. See [AUTOTITLE](/copilot/managing-copilot/configure-personal-settings/using-github-copilot-with-an-account-on-ghecom). -## 5. Drive {% data variables.product.prodname_copilot_short %} adoption - -Planning and implementing an effective enablement process is essential to drive adoption of {% data variables.product.prodname_copilot_short %} in your organization. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/driving-copilot-adoption-in-your-company). - -## 6. Enhance the {% data variables.product.prodname_copilot_short %} experience - -Enhance the {% data variables.product.prodname_copilot_short %} experience for your organization by: +## Next steps -* **Setting up knowledge bases** for use with {% data variables.copilot.copilot_chat_short %} _({% data variables.copilot.copilot_enterprise_short %} only)_. See [AUTOTITLE](/enterprise-cloud@latest/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-github-copilot-features-in-your-organization/managing-copilot-knowledge-bases). -* **Installing {% data variables.copilot.copilot_extensions_short %}** to integrate other tools with {% data variables.copilot.copilot_chat_short %}. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-github-copilot-features-in-your-organization/installing-github-copilot-extensions-for-your-organization). -* **Adding {% data variables.copilot.copilot_coding_agent %}** as a team member to work asynchronously on issues. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/enabling-developers/using-copilot-coding-agent-in-org). +{% data reusables.copilot.setup-next-steps %} diff --git a/data/release-notes/enterprise-server/3-14/0.yml b/data/release-notes/enterprise-server/3-14/0.yml index ea8ca5911db1..940e6a8c1174 100644 --- a/data/release-notes/enterprise-server/3-14/0.yml +++ b/data/release-notes/enterprise-server/3-14/0.yml @@ -168,6 +168,10 @@ sections: - | When using a JSON Web Token (JWT) to authenticate or request an installation token, developers of GitHub Apps can use the app's client ID for the JWT's `iss` claim. The application ID remains valid, but is considered deprecated. + changes: + - | + The API endpoint for setting and removing organization membership for a user (`PUT /orgs/{org}/memberships/{username}` and `DELETE /orgs/{org}/memberships/{username}`) requires `admin:org` permissions for classic tokens. Previously, the changes were allowed with the `read:org, repo` permissions. [Updated: 2025-07-16] + known_issues: - | Complete SCIM payloads are written to the audit log, including SCIM attributes that are not required or supported per [API docs](/rest/enterprise-admin/scim?apiVersion=2022-11-28#supported-scim-user-attributes). Customers using Okta with SCIM may notice that a placeholder password attribute is among the data passed to audit logs in its current configuration. This placeholder data is associated with Okta’s password synchronization feature that is not expected or required by GitHub. See [okta-scim](https://developer.okta.com/docs/api/openapi/okta-scim/guides/scim-20/#create-the-user) for more information. @@ -244,5 +248,7 @@ sections: These release notes previously did not include a note for the deprecation of team discussions. - | These release notes previously indicated as a known issue that on GitHub Enterprise Server 3.14.0, repositories originally imported using `ghe-migrator` will not correctly track Advanced Security contributions. - + The fix for this problem was already included in GitHub Enterprise Server [3.12](/admin/release-notes#3.12.0-bugs). [Updated: 2025-04-11] + - | + The release notes previously did not include a note for the change in permissions required for managing user organization membership. [Updated: 2025-07-16] diff --git a/data/reusables/actions/about-triggers.md b/data/reusables/actions/about-triggers.md index ff95af94a641..8cff08eae938 100644 --- a/data/reusables/actions/about-triggers.md +++ b/data/reusables/actions/about-triggers.md @@ -6,3 +6,13 @@ Workflow triggers are events that cause a workflow to run. These events can be: * Manual For example, you can configure your workflow to run when a push is made to the default branch of your repository, when a release is created, or when an issue is opened. + +Workflow triggers are defined with the `on` key. For more information, see [AUTOTITLE](/actions/using-workflows/workflow-syntax-for-github-actions#on). + +The following steps occur to trigger a workflow run: + +1. An event occurs on your repository. The event has an associated commit SHA and Git ref. +1. {% data variables.product.github %} searches the `.github/workflows` directory in the root of your repository for workflow files that are present in the associated commit SHA or Git ref of the event. +1. A workflow run is triggered for any workflows that have `on:` values that match the triggering event. Some events also require the workflow file to be present on the default branch of the repository in order to run. + +Each workflow run will use the version of the workflow that is present in the associated commit SHA or Git ref of the event. When a workflow runs, {% data variables.product.github %} sets the `GITHUB_SHA` (commit SHA) and `GITHUB_REF` (Git ref) environment variables in the runner environment. For more information, see [AUTOTITLE](/actions/learn-github-actions/variables). diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index 09f1b78266db..5f861a8dc84d 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -69,6 +69,8 @@ | GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format | | GHD043 | link-quotation | Internal link titles must not be surrounded by quotations | error | links, url | | GHD044 | octicon-aria-labels | Octicons should always have an aria-label attribute even if aria-hidden. | warning | accessibility, octicons | +| GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases | | GHD048 | british-english-quotes | Periods and commas should be placed inside quotation marks (American English style) | warning | punctuation, quotes, style, consistency | | GHD050 | multiple-emphasis-patterns | Do not use more than one emphasis/strong, italics, or uppercase for a string | warning | formatting, emphasis, style | -| GHD049 | note-warning-formatting | Note and warning tags should be formatted according to style guide | warning | formatting, callouts, notes, warnings, style | \ No newline at end of file +| GHD049 | note-warning-formatting | Note and warning tags should be formatted according to style guide | warning | formatting, callouts, notes, warnings, style | +| GHD051 | frontmatter-versions-whitespace | Versions frontmatter should not contain unnecessary whitespace | warning | frontmatter, versions | \ No newline at end of file diff --git a/data/reusables/copilot/setup-next-steps.md b/data/reusables/copilot/setup-next-steps.md new file mode 100644 index 000000000000..7bb6f25d2d08 --- /dev/null +++ b/data/reusables/copilot/setup-next-steps.md @@ -0,0 +1,4 @@ +* **Explore self-service license management options**. Many successful rollouts use a self-service model where developers can claim a license without approval. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/setting-up-a-self-serve-process-for-github-copilot-licenses). +* **Learn how to plan and implement an effective enablement process to drive {% data variables.product.prodname_copilot_short %} adoption**. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/driving-copilot-adoption-in-your-company). +* **Enhance the development experience by enabling and training developers on the latest features**. For example, share context with {% data variables.copilot.copilot_spaces %}, enable {% data variables.copilot.copilot_code-review_short %} on pull requests, and allow developers to experiment with prompts using {% data variables.product.prodname_github_models %}. For an example showing how these features fit together, see [AUTOTITLE](/copilot/tutorials/rolling-out-github-copilot-at-scale/enabling-developers/integrating-agentic-ai). +* **Add {% data variables.copilot.copilot_coding_agent %} as a team member for asynchronous issue work**. See [AUTOTITLE](/copilot/rolling-out-github-copilot-at-scale/enabling-developers/using-copilot-coding-agent-in-org). diff --git a/data/reusables/repositories/ruleset-beta-note.md b/data/reusables/repositories/ruleset-beta-note.md index ccdd55dd0187..cec89fb6175d 100644 --- a/data/reusables/repositories/ruleset-beta-note.md +++ b/data/reusables/repositories/ruleset-beta-note.md @@ -1,2 +1,2 @@ > [!NOTE] -> {% ifversion ghes > 3.16 %}Only changes made to a ruleset after you have upgraded to {% data variables.product.prodname_ghe_server %} 3.17.0, or a later version, are included in the ruleset history.{% elsif fpt or ghec %}Only changes made to a ruleset after the {% data variables.release-phases.public_preview %} release, on October 11, 2023, are included in the ruleset history.{% endif %} +> {% ifversion ghes > 3.16 %}Only changes made to a ruleset after you have upgraded to {% data variables.product.prodname_ghe_server %} 3.17.0, or a later version, are included in the ruleset history.{% endif %} diff --git a/data/reusables/repositories/ruleset-history-conceptual.md b/data/reusables/repositories/ruleset-history-conceptual.md index 7839f710c1df..bbed5c55006a 100644 --- a/data/reusables/repositories/ruleset-history-conceptual.md +++ b/data/reusables/repositories/ruleset-history-conceptual.md @@ -1 +1,3 @@ +Ruleset history lists events triggered by changes that affect your rulesets within the last 180 days. + You can view all the changes to a ruleset and revert back to a specific iteration. You can also download a JSON file containing the ruleset's configuration at a specific iteration. The bypass list of a ruleset is excluded from the exported JSON file. diff --git a/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js new file mode 100644 index 000000000000..befc1d5ee882 --- /dev/null +++ b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js @@ -0,0 +1,94 @@ +import { addError } from 'markdownlint-rule-helpers' +import { getFrontmatter } from '@/content-linter/lib/helpers/utils' + +export const frontmatterVersionsWhitespace = { + names: ['GHD051', 'frontmatter-versions-whitespace'], + description: 'Versions frontmatter should not contain unnecessary whitespace', + tags: ['frontmatter', 'versions'], + function: (params, onError) => { + const fm = getFrontmatter(params.lines) + if (!fm || !fm.versions) return + + const versionsObj = fm.versions + if (typeof versionsObj !== 'object') return + + // Find the frontmatter section in the file + const fmStartIndex = params.lines.findIndex((line) => line.trim() === '---') + if (fmStartIndex === -1) return + + // Check each version entry for whitespace issues + Object.entries(versionsObj).forEach(([key, value]) => { + if (typeof value !== 'string') return + + const hasUnwantedWhitespace = checkForUnwantedWhitespace(value) + if (hasUnwantedWhitespace) { + // Find the line containing this version key + const versionLineIndex = params.lines.findIndex((line, index) => { + return index > fmStartIndex && line.trim().startsWith(`${key}:`) && line.includes(value) + }) + + if (versionLineIndex !== -1) { + const line = params.lines[versionLineIndex] + const lineNumber = versionLineIndex + 1 + const cleanedValue = getCleanedValue(value) + + // Create fix info to remove unwanted whitespace + const fixInfo = { + editColumn: line.indexOf(value) + 1, + deleteCount: value.length, + insertText: cleanedValue, + } + + addError( + onError, + lineNumber, + `Versions frontmatter should not contain leading or trailing whitespace. Found: '${value}', expected: '${cleanedValue}'`, + line, + [line.indexOf(value) + 1, value.length], + fixInfo, + ) + } + } + }) + }, +} + +/** + * Check if a version string has unwanted whitespace + * Allows whitespace in complex expressions like '<3.6 >3.8' + * but disallows leading/trailing whitespace + */ +function checkForUnwantedWhitespace(value) { + // Don't flag if the value is just whitespace or empty + if (!value || value.trim() === '') return false + + // Check for leading or trailing whitespace + if (value !== value.trim()) return true + + // Allow whitespace around operators in complex expressions + // This regex matches patterns like '<3.6 >3.8', '>=2.19', etc. + const hasOperators = /[<>=]/.test(value) + if (hasOperators) { + // For operator expressions, we're more lenient about internal whitespace + // Only flag if there's leading/trailing whitespace (already checked above) + return false + } + + // For simple version strings (like 'fpt', 'ghec'), no internal whitespace should be allowed + // This catches cases like 'f pt' where there's whitespace in the middle + return /\s/.test(value) +} + +/** + * Get the cleaned version of a value by removing appropriate whitespace + */ +function getCleanedValue(value) { + // For values with operators, just trim leading/trailing whitespace + const hasOperators = /[<>=]/.test(value) + if (hasOperators) { + return value.trim() + } + + // For simple version strings, remove all whitespace + return value.replace(/\s/g, '') +} diff --git a/src/content-linter/lib/linting-rules/index.js b/src/content-linter/lib/linting-rules/index.js index dc47e77a4b61..918e76924d06 100644 --- a/src/content-linter/lib/linting-rules/index.js +++ b/src/content-linter/lib/linting-rules/index.js @@ -1,42 +1,56 @@ import searchReplace from 'markdownlint-rule-search-replace' import markdownlintGitHub from '@github/markdownlint-github' -import { codeFenceLineLength } from './code-fence-line-length' -import { imageAltTextEndPunctuation } from './image-alt-text-end-punctuation' -import { imageFileKebabCase } from './image-file-kebab-case' -import { incorrectAltTextLength } from './image-alt-text-length' -import { internalLinksNoLang } from './internal-links-no-lang' -import { internalLinksSlash } from './internal-links-slash' -import { imageAltTextExcludeStartWords } from './image-alt-text-exclude-start-words' -import { listFirstWordCapitalization } from './list-first-word-capitalization' -import { linkPunctuation } from './link-punctuation' -import { earlyAccessReferences, frontmatterEarlyAccessReferences } from './early-access-references' -import { frontmatterHiddenDocs } from './frontmatter-hidden-docs' -import { frontmatterVideoTranscripts } from './frontmatter-video-transcripts' -import { yamlScheduledJobs } from './yaml-scheduled-jobs' -import { internalLinksOldVersion } from './internal-links-old-version' -import { hardcodedDataVariable } from './hardcoded-data-variable' -import { githubOwnedActionReferences } from './github-owned-action-references' -import { liquidQuotedConditionalArg } from './liquid-quoted-conditional-arg' -import { liquidDataReferencesDefined, liquidDataTagFormat } from './liquid-data-tags' -import { frontmatterSchema } from './frontmatter-schema' -import { codeAnnotations } from './code-annotations' -import { codeAnnotationCommentSpacing } from './code-annotation-comment-spacing' -import { frontmatterLiquidSyntax, liquidSyntax } from './liquid-syntax' -import { liquidIfTags, liquidIfVersionTags } from './liquid-versioning' -import { raiReusableUsage } from './rai-reusable-usage' -import { imageNoGif } from './image-no-gif' -import { expiredContent, expiringSoon } from './expired-content' -import { tableLiquidVersioning } from './table-liquid-versioning' -import { tableColumnIntegrity } from './table-column-integrity' -import { thirdPartyActionPinning } from './third-party-action-pinning' -import { liquidTagWhitespace } from './liquid-tag-whitespace' -import { linkQuotation } from './link-quotation' -import { octiconAriaLabels } from './octicon-aria-labels' -import { liquidIfversionVersions } from './liquid-ifversion-versions' -import { britishEnglishQuotes } from './british-english-quotes' -import { multipleEmphasisPatterns } from './multiple-emphasis-patterns' -import { noteWarningFormatting } from './note-warning-formatting' +import { codeFenceLineLength } from '@/content-linter/lib/linting-rules/code-fence-line-length' +import { imageAltTextEndPunctuation } from '@/content-linter/lib/linting-rules/image-alt-text-end-punctuation' +import { imageFileKebabCase } from '@/content-linter/lib/linting-rules/image-file-kebab-case' +import { incorrectAltTextLength } from '@/content-linter/lib/linting-rules/image-alt-text-length' +import { internalLinksNoLang } from '@/content-linter/lib/linting-rules/internal-links-no-lang' +import { internalLinksSlash } from '@/content-linter/lib/linting-rules/internal-links-slash' +import { imageAltTextExcludeStartWords } from '@/content-linter/lib/linting-rules/image-alt-text-exclude-start-words' +import { listFirstWordCapitalization } from '@/content-linter/lib/linting-rules/list-first-word-capitalization' +import { linkPunctuation } from '@/content-linter/lib/linting-rules/link-punctuation' +import { + earlyAccessReferences, + frontmatterEarlyAccessReferences, +} from '@/content-linter/lib/linting-rules/early-access-references' +import { frontmatterHiddenDocs } from '@/content-linter/lib/linting-rules/frontmatter-hidden-docs' +import { frontmatterVideoTranscripts } from '@/content-linter/lib/linting-rules/frontmatter-video-transcripts' +import { yamlScheduledJobs } from '@/content-linter/lib/linting-rules/yaml-scheduled-jobs' +import { internalLinksOldVersion } from '@/content-linter/lib/linting-rules/internal-links-old-version' +import { hardcodedDataVariable } from '@/content-linter/lib/linting-rules/hardcoded-data-variable' +import { githubOwnedActionReferences } from '@/content-linter/lib/linting-rules/github-owned-action-references' +import { liquidQuotedConditionalArg } from '@/content-linter/lib/linting-rules/liquid-quoted-conditional-arg' +import { + liquidDataReferencesDefined, + liquidDataTagFormat, +} from '@/content-linter/lib/linting-rules/liquid-data-tags' +import { frontmatterSchema } from '@/content-linter/lib/linting-rules/frontmatter-schema' +import { codeAnnotations } from '@/content-linter/lib/linting-rules/code-annotations' +import { codeAnnotationCommentSpacing } from '@/content-linter/lib/linting-rules/code-annotation-comment-spacing' +import { + frontmatterLiquidSyntax, + liquidSyntax, +} from '@/content-linter/lib/linting-rules/liquid-syntax' +import { + liquidIfTags, + liquidIfVersionTags, +} from '@/content-linter/lib/linting-rules/liquid-versioning' +import { raiReusableUsage } from '@/content-linter/lib/linting-rules/rai-reusable-usage' +import { imageNoGif } from '@/content-linter/lib/linting-rules/image-no-gif' +import { expiredContent, expiringSoon } from '@/content-linter/lib/linting-rules/expired-content' +import { tableLiquidVersioning } from '@/content-linter/lib/linting-rules/table-liquid-versioning' +import { tableColumnIntegrity } from '@/content-linter/lib/linting-rules/table-column-integrity' +import { thirdPartyActionPinning } from '@/content-linter/lib/linting-rules/third-party-action-pinning' +import { liquidTagWhitespace } from '@/content-linter/lib/linting-rules/liquid-tag-whitespace' +import { linkQuotation } from '@/content-linter/lib/linting-rules/link-quotation' +import { octiconAriaLabels } from '@/content-linter/lib/linting-rules/octicon-aria-labels' +import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions' +import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology' +import { britishEnglishQuotes } from '@/content-linter/lib/linting-rules/british-english-quotes' +import { multipleEmphasisPatterns } from '@/content-linter/lib/linting-rules/multiple-emphasis-patterns' +import { noteWarningFormatting } from '@/content-linter/lib/linting-rules/note-warning-formatting' +import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace' const noDefaultAltText = markdownlintGitHub.find((elem) => elem.names.includes('no-default-alt-text'), @@ -88,8 +102,10 @@ export const gitHubDocsMarkdownlint = { liquidTagWhitespace, linkQuotation, octiconAriaLabels, + outdatedReleasePhaseTerminology, britishEnglishQuotes, multipleEmphasisPatterns, noteWarningFormatting, + frontmatterVersionsWhitespace, ], } diff --git a/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.js b/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.js new file mode 100644 index 000000000000..dd799faff65a --- /dev/null +++ b/src/content-linter/lib/linting-rules/outdated-release-phase-terminology.js @@ -0,0 +1,109 @@ +import { addError, ellipsify } from 'markdownlint-rule-helpers' + +import { getRange } from '@/content-linter/lib/helpers/utils' +import frontmatter from '@/frame/lib/read-frontmatter' + +// Mapping of outdated terms to their new replacements +// Order matters - longer phrases must come first to avoid partial matches +const TERMINOLOGY_REPLACEMENTS = [ + // Beta variations → public preview (longer phrases first) + ['limited public beta', 'public preview'], + ['public beta', 'public preview'], + ['private beta', 'private preview'], + ['beta', 'public preview'], + + // Alpha → private preview + ['alpha', 'private preview'], + + // Deprecated variations → closing down + ['deprecation', 'closing down'], + ['deprecated', 'closing down'], + + // Sunset → retired + ['sunset', 'retired'], +] + +// Precompile RegExp objects for better performance +const COMPILED_REGEXES = TERMINOLOGY_REPLACEMENTS.map(([outdatedTerm, replacement]) => ({ + regex: new RegExp(`(? + (match.index >= existing.start && match.index < existing.end) || + (match.index + match[0].length > existing.start && + match.index + match[0].length <= existing.end) || + (match.index <= existing.start && match.index + match[0].length >= existing.end), + ) + + if (!overlaps) { + foundMatches.push({ + start: match.index, + end: match.index + match[0].length, + text: match[0], + replacement: replacement, + outdatedTerm: outdatedTerm, + }) + } + } + } + + // Sort matches by position for consistent ordering + return foundMatches.sort((a, b) => a.start - b.start) +} + +export const outdatedReleasePhaseTerminology = { + names: ['GHD046', 'outdated-release-phase-terminology'], + description: + 'Outdated release phase terminology should be replaced with current GitHub terminology', + tags: ['terminology', 'consistency', 'release-phases'], + severity: 'error', + function: (params, onError) => { + // Skip autogenerated files + const frontmatterString = params.frontMatterLines.join('\n') + const fm = frontmatter(frontmatterString).data + if (fm && fm.autogenerated) return + + // Check all lines for outdated terminology + for (let i = 0; i < params.lines.length; i++) { + const line = params.lines[i] + const lineNumber = i + 1 + + // Find all matches on this line + const foundMatches = findOutdatedTerminologyMatches(line) + + // Report all found matches + for (const matchInfo of foundMatches) { + const range = getRange(line, matchInfo.text) + const errorMessage = `Replace outdated terminology "${matchInfo.text}" with "${matchInfo.replacement}"` + + // Provide a fix suggestion + const fixInfo = { + editColumn: matchInfo.start + 1, + deleteCount: matchInfo.text.length, + insertText: matchInfo.replacement, + } + + addError(onError, lineNumber, errorMessage, ellipsify(line), range, fixInfo) + } + } + }, +} diff --git a/src/content-linter/style/github-docs.js b/src/content-linter/style/github-docs.js index 5da71383d4af..19b3d3e4c650 100644 --- a/src/content-linter/style/github-docs.js +++ b/src/content-linter/style/github-docs.js @@ -206,6 +206,12 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, + 'outdated-release-phase-terminology': { + // GHD046 + severity: 'warning', + 'partial-markdown-files': true, + 'yml-files': true, + }, 'table-column-integrity': { // GHD047 severity: 'warning', @@ -280,6 +286,12 @@ export const githubDocsFrontmatterConfig = { severity: 'error', 'partial-markdown-files': false, }, + 'frontmatter-versions-whitespace': { + // GHD051 + severity: 'warning', + 'partial-markdown-files': false, + 'yml-files': false, + }, } // Configures rules from the `github/markdownlint-github` repo diff --git a/src/content-linter/tests/unit/frontmatter-versions-whitespace.js b/src/content-linter/tests/unit/frontmatter-versions-whitespace.js new file mode 100644 index 000000000000..ac2b7c22d99e --- /dev/null +++ b/src/content-linter/tests/unit/frontmatter-versions-whitespace.js @@ -0,0 +1,276 @@ +import { describe, expect, test } from 'vitest' + +import { runRule } from '@/content-linter/lib/init-test' +import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace' + +// Configure the test fixture to not split frontmatter and content +const fmOptions = { markdownlintOptions: { frontMatter: null } } + +// Valid cases - should pass +const validCases = [ + { + name: 'valid-simple-versions', + content: `--- +title: Test +versions: + fpt: '*' + ghec: '*' + ghes: '>=3.0' +--- + +# Test + +This is a test. +`, + }, + { + name: 'valid-complex-operators', + content: `--- +title: Test +versions: + ghes: '<3.6 >3.8' + fpt: '*' +--- + +# Test + +This is a test. +`, + }, + { + name: 'valid-no-versions', + content: `--- +title: Test +type: tutorial +--- + +# Test + +This is a test. +`, + }, +] + +// Invalid cases - should fail +const invalidCases = [ + { + name: 'trailing-whitespace', + content: `--- +title: Test +versions: + fpt: '* ' + ghec: '*' +--- + +# Test + +This is a test. +`, + expectedErrors: 1, + expectedMessage: + "Versions frontmatter should not contain leading or trailing whitespace. Found: '* ', expected: '*'", + }, + { + name: 'leading-whitespace', + content: `--- +title: Test +versions: + fpt: ' *' + ghec: '*' +--- + +# Test + +This is a test. +`, + expectedErrors: 1, + expectedMessage: + "Versions frontmatter should not contain leading or trailing whitespace. Found: ' *', expected: '*'", + }, + { + name: 'both-leading-and-trailing', + content: `--- +title: Test +versions: + fpt: ' * ' + ghec: '*' +--- + +# Test + +This is a test. +`, + expectedErrors: 1, + expectedMessage: + "Versions frontmatter should not contain leading or trailing whitespace. Found: ' * ', expected: '*'", + }, + { + name: 'multiple-version-whitespace-issues', + content: `--- +title: Test +versions: + fpt: '* ' + ghec: ' *' + ghes: '>=3.0' +--- + +# Test + +This is a test. +`, + expectedErrors: 2, + }, + { + name: 'internal-whitespace-simple-version', + content: `--- +title: Test +versions: + fpt: 'f pt' + ghec: '*' +--- + +# Test + +This is a test. +`, + expectedErrors: 1, + expectedMessage: + "Versions frontmatter should not contain leading or trailing whitespace. Found: 'f pt', expected: 'fpt'", + }, +] + +describe(frontmatterVersionsWhitespace.names.join(' - '), () => { + describe('valid cases', () => { + validCases.forEach(({ name, content }) => { + test(`${name} should pass`, async () => { + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(0) + }) + }) + }) + + describe('invalid cases', () => { + invalidCases.forEach(({ name, content, expectedErrors, expectedMessage }) => { + test(`${name} should fail`, async () => { + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(expectedErrors) + + if (expectedMessage) { + expect(result.content[0].errorDetail).toBe(expectedMessage) + } + }) + }) + }) + + describe('fixable errors', () => { + test('should provide fix information for trailing whitespace', async () => { + const content = `--- +title: Test +versions: + fpt: '* ' +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(1) + expect(result.content[0].fixInfo).toBeDefined() + expect(result.content[0].fixInfo.insertText).toBe('*') + }) + + test('should provide fix information for leading whitespace', async () => { + const content = `--- +title: Test +versions: + fpt: ' *' +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(1) + expect(result.content[0].fixInfo).toBeDefined() + expect(result.content[0].fixInfo.insertText).toBe('*') + }) + }) + + describe('edge cases', () => { + test('should handle non-string version values', async () => { + const content = `--- +title: Test +versions: + fpt: '*' + feature: ['foo', 'bar'] +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(0) + }) + + test('should handle missing versions frontmatter', async () => { + const content = `--- +title: Test +type: tutorial +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(0) + }) + + test('should allow complex operator expressions with spaces', async () => { + const content = `--- +title: Test +versions: + ghes: '<3.6 >3.8' + fpt: '*' +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(0) + }) + + test('should allow operator expressions with spaces around operators', async () => { + const content = `--- +title: Test +versions: + ghes: '>= 3.0' + fpt: '*' +--- + +# Test +` + const result = await runRule(frontmatterVersionsWhitespace, { + strings: { content }, + ...fmOptions, + }) + expect(result.content.length).toBe(0) + }) + }) +}) diff --git a/src/content-linter/tests/unit/outdated-release-phase-terminology.js b/src/content-linter/tests/unit/outdated-release-phase-terminology.js new file mode 100644 index 000000000000..166a0126140c --- /dev/null +++ b/src/content-linter/tests/unit/outdated-release-phase-terminology.js @@ -0,0 +1,154 @@ +import { describe, expect, test } from 'vitest' + +import { runRule } from '@/content-linter/lib/init-test' +import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology' + +describe(outdatedReleasePhaseTerminology.names.join(' - '), () => { + test('Using outdated beta terminology causes error', async () => { + const markdown = [ + 'This feature is in beta.', + 'The public beta is available now.', + 'We are running a limited public beta.', + ].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(3) + expect(errors[0].lineNumber).toBe(1) + expect(errors[0].errorDetail).toContain( + 'Replace outdated terminology "beta" with "public preview"', + ) + expect(errors[1].lineNumber).toBe(2) + expect(errors[1].errorDetail).toContain( + 'Replace outdated terminology "public beta" with "public preview"', + ) + expect(errors[2].lineNumber).toBe(3) + expect(errors[2].errorDetail).toContain( + 'Replace outdated terminology "limited public beta" with "public preview"', + ) + }) + + test('Using outdated private beta and alpha terminology causes error', async () => { + const markdown = ['The private beta starts next month.', 'This alpha version has bugs.'].join( + '\n', + ) + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(2) + expect(errors[0].lineNumber).toBe(1) + expect(errors[0].errorDetail).toContain( + 'Replace outdated terminology "private beta" with "private preview"', + ) + expect(errors[1].lineNumber).toBe(2) + expect(errors[1].errorDetail).toContain( + 'Replace outdated terminology "alpha" with "private preview"', + ) + }) + + test('Using outdated deprecated terminology causes error', async () => { + const markdown = ['This feature is deprecated.', 'The deprecation timeline is available.'].join( + '\n', + ) + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(2) + expect(errors[0].lineNumber).toBe(1) + expect(errors[0].errorDetail).toContain( + 'Replace outdated terminology "deprecated" with "closing down"', + ) + expect(errors[1].lineNumber).toBe(2) + expect(errors[1].errorDetail).toContain( + 'Replace outdated terminology "deprecation" with "closing down"', + ) + }) + + test('Using outdated sunset terminology causes error', async () => { + const markdown = ['This API will sunset in 2024.'].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(1) + expect(errors[0].lineNumber).toBe(1) + expect(errors[0].errorDetail).toContain('Replace outdated terminology "sunset" with "retired"') + }) + + test('Case insensitive matching works', async () => { + const markdown = [ + 'This BETA feature is great.', + 'The Alpha version is ready.', + 'This is DEPRECATED.', + 'We will SUNSET this feature.', + ].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(4) + expect(errors[0].errorDetail).toContain( + 'Replace outdated terminology "BETA" with "public preview"', + ) + expect(errors[1].errorDetail).toContain( + 'Replace outdated terminology "Alpha" with "private preview"', + ) + expect(errors[2].errorDetail).toContain( + 'Replace outdated terminology "DEPRECATED" with "closing down"', + ) + expect(errors[3].errorDetail).toContain('Replace outdated terminology "SUNSET" with "retired"') + }) + + test('Word boundaries prevent false positives in compound words', async () => { + const markdown = [ + 'The alphabet contains letters.', + 'We use betaflight software.', + 'The deprecated-api endpoint is different.', + ].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(0) + }) + + test('Context-sensitive terms are flagged (human review needed)', async () => { + const markdown = ['A beautiful sunset view.', 'The API will sunset next year.'].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(2) + expect(errors[0].errorDetail).toContain('Replace outdated terminology "sunset" with "retired"') + expect(errors[1].errorDetail).toContain('Replace outdated terminology "sunset" with "retired"') + }) + + test('Multiple occurrences on same line are all caught', async () => { + const markdown = ['This beta feature replaces the deprecated alpha version.'].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(3) + expect(errors[0].errorDetail).toContain( + 'Replace outdated terminology "beta" with "public preview"', + ) + expect(errors[1].errorDetail).toContain( + 'Replace outdated terminology "deprecated" with "closing down"', + ) + expect(errors[2].errorDetail).toContain( + 'Replace outdated terminology "alpha" with "private preview"', + ) + }) + + test('New terminology does not cause errors', async () => { + const markdown = [ + 'This feature is in public preview.', + 'The private preview is available now.', + 'This feature is closing down.', + 'The API has been retired.', + ].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { strings: { markdown } }) + const errors = result.markdown + expect(errors.length).toBe(0) + }) + + test('Autogenerated files are skipped', async () => { + const frontmatter = ['---', 'title: Test', 'autogenerated: rest', '---'].join('\n') + const markdown = ['This feature is in beta.'].join('\n') + const result = await runRule(outdatedReleasePhaseTerminology, { + strings: { + markdown: frontmatter + '\n' + markdown, + }, + }) + const errors = result.markdown + expect(errors.length).toBe(0) + }) +}) diff --git a/src/content-render/scripts/reconcile-filenames-with-ids.js b/src/content-render/scripts/reconcile-filenames-with-ids.js index f64d186b6250..94796101fc67 100755 --- a/src/content-render/scripts/reconcile-filenames-with-ids.js +++ b/src/content-render/scripts/reconcile-filenames-with-ids.js @@ -38,22 +38,25 @@ contentFiles.forEach((oldFullPath) => { // skip pages with frontmatter flag if (data.allowTitleToDifferFromFilename) return - // slugify the title of each article - // where title = Foo bar - // and slug = foo-bar + // Slugify the title of each article, where: + // * title = Foo bar + // * slug = foo-bar + // Also allow for the slugified shortTitle to match the filename. slugger.reset() - const slug = slugger.slug(decode(data.title)) + const slugTitle = slugger.slug(decode(data.title)) + const slugShortTitle = slugger.slug(decode(data.shortTitle)) + const allowedSlugs = [slugTitle, slugShortTitle] // get the basename of each file // where file = content/foo-bar.md // and basename = foo-bar const basename = path.basename(oldFullPath, '.md') - // if slug and basename match, return early - if (basename === slug) return + // If the basename is one of the allowed slugs, we're all set here. + if (allowedSlugs.includes(basename)) return // otherwise rename the file using the slug - const newFullPath = oldFullPath.replace(basename, slug) + const newFullPath = oldFullPath.replace(basename, slugShortTitle || slugTitle) const oldContentPath = path.relative(process.cwd(), oldFullPath) const newContentPath = path.relative(process.cwd(), newFullPath) diff --git a/src/frame/tests/pages.js b/src/frame/tests/pages.js index 486af4cae232..b5a1b1d0a2ef 100644 --- a/src/frame/tests/pages.js +++ b/src/frame/tests/pages.js @@ -89,7 +89,7 @@ describe('pages module', () => { expect(duplicates.length, message).toBe(0) }) - test('every English page has a filename that matches its slugified title', async () => { + test('every English page has a filename that matches its slugified title or shortTitle', async () => { const nonMatches = pages .filter((page) => { slugger.reset() @@ -97,7 +97,8 @@ describe('pages module', () => { page.languageCode === 'en' && // only check English !page.relativePath.includes('index.md') && // ignore TOCs !page.allowTitleToDifferFromFilename && // ignore docs with override - slugger.slug(decode(page.title)) !== path.basename(page.relativePath, '.md') + slugger.slug(decode(page.title)) !== path.basename(page.relativePath, '.md') && + slugger.slug(decode(page.shortTitle || '')) !== path.basename(page.relativePath, '.md') ) }) // make the output easier to read diff --git a/src/secret-scanning/data/public-docs.yml b/src/secret-scanning/data/public-docs.yml index 018845f0fc72..b1c420ab8be2 100644 --- a/src/secret-scanning/data/public-docs.yml +++ b/src/secret-scanning/data/public-docs.yml @@ -506,7 +506,7 @@ isPrivateWithGhas: true hasPushProtection: true hasValidityCheck: false - isduplicate: false + isduplicate: true - provider: Azure supportedSecret: Azure Event Grid Key Identifiable secretType: azure_event_grid_key_identifiable diff --git a/src/secret-scanning/lib/config.json b/src/secret-scanning/lib/config.json index a38969e06434..41ddff7381ba 100644 --- a/src/secret-scanning/lib/config.json +++ b/src/secret-scanning/lib/config.json @@ -1,5 +1,5 @@ { - "sha": "4ee8d2a67d1b6fb5cd0b70eda806cae6d5090507", - "blob-sha": "8dddddad0d62725930bd1d5c4e3108d413220b88", + "sha": "8b3254a87c5c709c467646b6c2773020b00b8be2", + "blob-sha": "1c2582c53c06065252060d41509997eeb763f4a5", "targetFilename": "code-security/secret-scanning/introduction/supported-secret-scanning-patterns" } \ No newline at end of file