diff --git a/assets/images/help/copilot/code-review/automatic-code-review-personal.png b/assets/images/help/copilot/code-review/automatic-code-review-personal.png
index c92788db8ee6..01abb750d206 100644
Binary files a/assets/images/help/copilot/code-review/automatic-code-review-personal.png and b/assets/images/help/copilot/code-review/automatic-code-review-personal.png differ
diff --git a/assets/images/help/copilot/coding-agent/approve-and-run-workflows.png b/assets/images/help/copilot/coding-agent/approve-and-run-workflows.png
index caf23ecd47b8..db566bb3ad0e 100644
Binary files a/assets/images/help/copilot/coding-agent/approve-and-run-workflows.png and b/assets/images/help/copilot/coding-agent/approve-and-run-workflows.png differ
diff --git a/assets/images/help/copilot/coding-agent/assign-to-copilot.png b/assets/images/help/copilot/coding-agent/assign-to-copilot.png
index c1a9e80417a7..850ad4fc89d5 100644
Binary files a/assets/images/help/copilot/coding-agent/assign-to-copilot.png and b/assets/images/help/copilot/coding-agent/assign-to-copilot.png differ
diff --git a/assets/images/help/copilot/coding-agent/comment-to-agent-on-pr.png b/assets/images/help/copilot/coding-agent/comment-to-agent-on-pr.png
index 5759875e849e..4b8d26fab03f 100644
Binary files a/assets/images/help/copilot/coding-agent/comment-to-agent-on-pr.png and b/assets/images/help/copilot/coding-agent/comment-to-agent-on-pr.png differ
diff --git a/assets/images/help/copilot/coding-agent/copilot-finished-work.png b/assets/images/help/copilot/coding-agent/copilot-finished-work.png
index 19ef6fd27269..8d2c73f06dcf 100644
Binary files a/assets/images/help/copilot/coding-agent/copilot-finished-work.png and b/assets/images/help/copilot/coding-agent/copilot-finished-work.png differ
diff --git a/assets/images/help/copilot/coding-agent/copilot-started-work.png b/assets/images/help/copilot/coding-agent/copilot-started-work.png
index 1cb95cc75ea5..790723642261 100644
Binary files a/assets/images/help/copilot/coding-agent/copilot-started-work.png and b/assets/images/help/copilot/coding-agent/copilot-started-work.png differ
diff --git a/assets/images/help/copilot/coding-agent/firewall-warning.png b/assets/images/help/copilot/coding-agent/firewall-warning.png
index ed0c5d559745..285d93ee2e09 100644
Binary files a/assets/images/help/copilot/coding-agent/firewall-warning.png and b/assets/images/help/copilot/coding-agent/firewall-warning.png differ
diff --git a/assets/images/help/copilot/coding-agent/issue-assigned-to-copilot.png b/assets/images/help/copilot/coding-agent/issue-assigned-to-copilot.png
index 9d4ee4674038..d70b2d8f066e 100644
Binary files a/assets/images/help/copilot/coding-agent/issue-assigned-to-copilot.png and b/assets/images/help/copilot/coding-agent/issue-assigned-to-copilot.png differ
diff --git a/assets/images/help/copilot/coding-agent/issue-link-to-pr.png b/assets/images/help/copilot/coding-agent/issue-link-to-pr.png
index 2fd080b2f886..1ea05e0dc422 100644
Binary files a/assets/images/help/copilot/coding-agent/issue-link-to-pr.png and b/assets/images/help/copilot/coding-agent/issue-link-to-pr.png differ
diff --git a/assets/images/help/copilot/coding-agent/log-stop-session.png b/assets/images/help/copilot/coding-agent/log-stop-session.png
index bb6b42548c59..4c3dc631305e 100644
Binary files a/assets/images/help/copilot/coding-agent/log-stop-session.png and b/assets/images/help/copilot/coding-agent/log-stop-session.png differ
diff --git a/assets/images/help/copilot/coding-agent/log-view-session.png b/assets/images/help/copilot/coding-agent/log-view-session.png
index 558486f6ec81..69a8cdd0c883 100644
Binary files a/assets/images/help/copilot/coding-agent/log-view-session.png and b/assets/images/help/copilot/coding-agent/log-view-session.png differ
diff --git a/assets/images/help/copilot/sdlc-guide/agent-mode.png b/assets/images/help/copilot/sdlc-guide/agent-mode.png
index 4d3081cd089f..abf6dfdb7c55 100644
Binary files a/assets/images/help/copilot/sdlc-guide/agent-mode.png and b/assets/images/help/copilot/sdlc-guide/agent-mode.png differ
diff --git a/assets/images/help/copilot/sdlc-guide/agent-pr.png b/assets/images/help/copilot/sdlc-guide/agent-pr.png
index 5c2b36e55ead..a8af44c8705c 100644
Binary files a/assets/images/help/copilot/sdlc-guide/agent-pr.png and b/assets/images/help/copilot/sdlc-guide/agent-pr.png differ
diff --git a/assets/images/help/copilot/sdlc-guide/autofix.png b/assets/images/help/copilot/sdlc-guide/autofix.png
index 879597bb6a18..07e514621f3c 100644
Binary files a/assets/images/help/copilot/sdlc-guide/autofix.png and b/assets/images/help/copilot/sdlc-guide/autofix.png differ
diff --git a/assets/images/help/copilot/sdlc-guide/issue-creation.png b/assets/images/help/copilot/sdlc-guide/issue-creation.png
index 47ac48174af5..1b7645ed39b0 100644
Binary files a/assets/images/help/copilot/sdlc-guide/issue-creation.png and b/assets/images/help/copilot/sdlc-guide/issue-creation.png differ
diff --git a/assets/images/help/copilot/sdlc-guide/model-compare.png b/assets/images/help/copilot/sdlc-guide/model-compare.png
index 11d5cb488344..8faf4f34035c 100644
Binary files a/assets/images/help/copilot/sdlc-guide/model-compare.png and b/assets/images/help/copilot/sdlc-guide/model-compare.png differ
diff --git a/assets/images/help/copilot/tell-me-about-repo.png b/assets/images/help/copilot/tell-me-about-repo.png
index 8810bf044346..58592bd2a60d 100644
Binary files a/assets/images/help/copilot/tell-me-about-repo.png and b/assets/images/help/copilot/tell-me-about-repo.png differ
diff --git a/assets/images/help/copilot/vsc-mcp-server-restart.png b/assets/images/help/copilot/vsc-mcp-server-restart.png
index 818744c4661e..0d347888cfa8 100644
Binary files a/assets/images/help/copilot/vsc-mcp-server-restart.png and b/assets/images/help/copilot/vsc-mcp-server-restart.png differ
diff --git a/assets/images/help/dependabot/dependabot-alert-fix-summary.png b/assets/images/help/dependabot/dependabot-alert-fix-summary.png
index d2a7c1882918..e7c5bd6d413d 100644
Binary files a/assets/images/help/dependabot/dependabot-alert-fix-summary.png and b/assets/images/help/dependabot/dependabot-alert-fix-summary.png differ
diff --git a/assets/images/help/dependabot/dependabot-alert-timeline.png b/assets/images/help/dependabot/dependabot-alert-timeline.png
index 7e9d311b7e10..f78a5b96f45d 100644
Binary files a/assets/images/help/dependabot/dependabot-alert-timeline.png and b/assets/images/help/dependabot/dependabot-alert-timeline.png differ
diff --git a/assets/images/help/dependabot/dependabot-alert-vulnerability-details.png b/assets/images/help/dependabot/dependabot-alert-vulnerability-details.png
index 9f046fa1a20f..71fc0bdc3b2a 100644
Binary files a/assets/images/help/dependabot/dependabot-alert-vulnerability-details.png and b/assets/images/help/dependabot/dependabot-alert-vulnerability-details.png differ
diff --git a/assets/images/help/dependabot/dependabot-vnet-logs.png b/assets/images/help/dependabot/dependabot-vnet-logs.png
index 3124b4c54245..b47baf35268b 100644
Binary files a/assets/images/help/dependabot/dependabot-vnet-logs.png and b/assets/images/help/dependabot/dependabot-vnet-logs.png differ
diff --git a/assets/images/help/issues/issues-create-saved-view.png b/assets/images/help/issues/issues-create-saved-view.png
index ca805224137c..f2d0cdee4811 100644
Binary files a/assets/images/help/issues/issues-create-saved-view.png and b/assets/images/help/issues/issues-create-saved-view.png differ
diff --git a/assets/images/help/models/github-models-commit-changes.png b/assets/images/help/models/github-models-commit-changes.png
index 883084dbc672..72dec7e8b4ea 100644
Binary files a/assets/images/help/models/github-models-commit-changes.png and b/assets/images/help/models/github-models-commit-changes.png differ
diff --git a/assets/images/help/models/github-models-compare-toggle.png b/assets/images/help/models/github-models-compare-toggle.png
index af1d61bc3b79..f8d28fcd956b 100644
Binary files a/assets/images/help/models/github-models-compare-toggle.png and b/assets/images/help/models/github-models-compare-toggle.png differ
diff --git a/assets/images/help/models/github-models-datasets.png b/assets/images/help/models/github-models-datasets.png
index 0f09b205b9dd..bfc1e531a99e 100644
Binary files a/assets/images/help/models/github-models-datasets.png and b/assets/images/help/models/github-models-datasets.png differ
diff --git a/assets/images/help/models/github-models-system-prompt.png b/assets/images/help/models/github-models-system-prompt.png
index 4629597f676e..bfd3ed25c275 100644
Binary files a/assets/images/help/models/github-models-system-prompt.png and b/assets/images/help/models/github-models-system-prompt.png differ
diff --git a/assets/images/help/repository/code-scanning-alert-details-learners.png b/assets/images/help/repository/code-scanning-alert-details-learners.png
index e138f840385b..53d1b83f1d08 100644
Binary files a/assets/images/help/repository/code-scanning-alert-details-learners.png and b/assets/images/help/repository/code-scanning-alert-details-learners.png differ
diff --git a/assets/images/help/repository/code-scanning-alert-location-learners.png b/assets/images/help/repository/code-scanning-alert-location-learners.png
index 2f4f46d3c721..d2ddc819dcf8 100644
Binary files a/assets/images/help/repository/code-scanning-alert-location-learners.png and b/assets/images/help/repository/code-scanning-alert-location-learners.png differ
diff --git a/assets/images/help/repository/code-scanning-alert-timeline-learners.png b/assets/images/help/repository/code-scanning-alert-timeline-learners.png
index 3801669dc4bf..e0a16e688f55 100644
Binary files a/assets/images/help/repository/code-scanning-alert-timeline-learners.png and b/assets/images/help/repository/code-scanning-alert-timeline-learners.png differ
diff --git a/assets/images/social-cards/actions.png b/assets/images/social-cards/actions.png
index 51c7eabdf9f4..40647b6a4c22 100644
Binary files a/assets/images/social-cards/actions.png and b/assets/images/social-cards/actions.png differ
diff --git a/assets/images/social-cards/code-security.png b/assets/images/social-cards/code-security.png
index 8f077fb25d45..be6192f5dbe9 100644
Binary files a/assets/images/social-cards/code-security.png and b/assets/images/social-cards/code-security.png differ
diff --git a/assets/images/social-cards/copilot.png b/assets/images/social-cards/copilot.png
index df5171d2e6ee..758ba4baa438 100644
Binary files a/assets/images/social-cards/copilot.png and b/assets/images/social-cards/copilot.png differ
diff --git a/assets/images/social-cards/default.png b/assets/images/social-cards/default.png
index ae7acbe25623..7405f132c147 100644
Binary files a/assets/images/social-cards/default.png and b/assets/images/social-cards/default.png differ
diff --git a/assets/images/social-cards/issues.png b/assets/images/social-cards/issues.png
index 9cd8c2c1e0cc..906047323ca3 100644
Binary files a/assets/images/social-cards/issues.png and b/assets/images/social-cards/issues.png differ
diff --git a/content/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs.md b/content/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs.md
index af4aa0360ff7..0de56405137a 100644
--- a/content/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs.md
+++ b/content/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs.md
@@ -347,8 +347,6 @@ jobs:
## `vars` context
-{% data reusables.actions.configuration-variables-beta-note %}
-
The `vars` context contains custom configuration variables set at the organization, repository, and environment levels. For more information about defining configuration variables for use in multiple workflows, see [AUTOTITLE](/actions/learn-github-actions/variables#defining-variables-for-multiple-workflows).
### Example contents of the `vars` context
diff --git a/content/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks.md b/content/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks.md
index 893bbf24c8d2..534d01a01498 100644
--- a/content/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks.md
+++ b/content/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks.md
@@ -46,9 +46,10 @@ Forking a repository is similar to duplicating a repository, with the following
* Code pushed to a fork is visible to all repositories in the fork network, even after that fork is deleted.
* You can use a pull request to suggest changes from your fork to the upstream repository.
* You can bring changes from the upstream repository to your fork by synchronizing your fork with the upstream repository.
-* Forks have their own members, branches, tags, labels, policies, issues, pull requests, discussions, actions, projects, and wikis.
* Forks inherit the restrictions of their upstream repositories. For example, branch protection rules cannot be passed down if the upstream repository belongs to an organization on a {% data variables.product.prodname_free_team %} plan.
+Like duplicated repositories, forks have their own members, branches, tags, labels, policies, issues, pull requests, discussions, actions, projects, and wikis.
+
## Further reading
* [AUTOTITLE](/pull-requests/collaborating-with-pull-requests/getting-started/about-collaborative-development-models)
diff --git a/content/site-policy/acceptable-use-policies/github-appeal-and-reinstatement.md b/content/site-policy/acceptable-use-policies/github-appeal-and-reinstatement.md
index deed7e722ba3..d2544ee37529 100644
--- a/content/site-policy/acceptable-use-policies/github-appeal-and-reinstatement.md
+++ b/content/site-policy/acceptable-use-policies/github-appeal-and-reinstatement.md
@@ -53,4 +53,4 @@ We track Appeals and Reinstatements in our [Transparency Center](https://transpa
### Legal Rights
-If you believe that a final decision made on Appeal is still incorrect you may, in certain circumstances, have additional rights to seek review of the decision under your local law. For example, if you are located in the European Union, it might be possible to access an out-of-court dispute settlement process under the [Digital Services Act](https://eur-lex.europa.eu/eli/reg/2022/2065/oj#d1e2819-1-1).
+If you believe that a final decision made on Appeal is still incorrect you may, in certain circumstances, have additional rights to seek review of the decision under your local law. For example, if you are located in the European Union, it might be possible to access an out-of-court dispute settlement process under the [Digital Services Act](https://eur-lex.europa.eu/eli/reg/2022/2065/oj#d1e2819-1-1). This process reflects GitHub's commitment to internationally recognized human rights set out in the United Nations Guiding Principles on Business and Human Rights (UNGPs), privacy, and free expression.
diff --git a/data/reusables/actions/configuration-variables-beta-note.md b/data/reusables/actions/configuration-variables-beta-note.md
deleted file mode 100644
index 9aaebc0d63fe..000000000000
--- a/data/reusables/actions/configuration-variables-beta-note.md
+++ /dev/null
@@ -1,2 +0,0 @@
-> [!NOTE]
-> Configuration variables for {% data variables.product.prodname_actions %} are in {% data variables.release-phases.public_preview %} and subject to change.
diff --git a/data/ui.yml b/data/ui.yml
index d5e6fa9dea6b..0dab040b7da1 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -50,8 +50,8 @@ search:
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
- copy_answer: Copy answer
- copied_announcement: Copied!
+ share_answer: Copy answer URL
+ share_copied_announcement: Copied share URL!
thumbs_up: This answer was helpful
thumbs_down: This answer was not helpful
thumbs_announcement: Thank you for your feedback!
@@ -61,6 +61,9 @@ search:
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
+ response:
+ copy_code: Copy code to clipboard
+ copied_code: Copied!
failure:
general_title: There was an error loading search results.
ai_title: There was an error loading Copilot.
diff --git a/src/content-linter/lib/linting-rules/index.js b/src/content-linter/lib/linting-rules/index.js
index 27567239275a..97657338c75e 100644
--- a/src/content-linter/lib/linting-rules/index.js
+++ b/src/content-linter/lib/linting-rules/index.js
@@ -35,6 +35,7 @@ import { liquidTagWhitespace } from './liquid-tag-whitespace.js'
import { linkQuotation } from './link-quotation.js'
import { octiconAriaLabels } from './octicon-aria-labels.js'
import { liquidIfversionVersions } from './liquid-ifversion-versions.js'
+import { noteWarningFormatting } from './note-warning-formatting.js'
const noDefaultAltText = markdownlintGitHub.find((elem) =>
elem.names.includes('no-default-alt-text'),
@@ -84,5 +85,6 @@ export const gitHubDocsMarkdownlint = {
liquidTagWhitespace,
linkQuotation,
octiconAriaLabels,
+ noteWarningFormatting,
],
}
diff --git a/src/content-linter/lib/linting-rules/list-first-word-capitalization.js b/src/content-linter/lib/linting-rules/list-first-word-capitalization.js
index cb39281816e8..43c8b16ee91f 100644
--- a/src/content-linter/lib/linting-rules/list-first-word-capitalization.js
+++ b/src/content-linter/lib/linting-rules/list-first-word-capitalization.js
@@ -5,6 +5,9 @@ export const listFirstWordCapitalization = {
description: 'First word of list item should be capitalized',
tags: ['ul', 'ol'],
function: (params, onError) => {
+ // Skip site-policy directory as these are legal documents with specific formatting requirements
+ if (params.name && params.name.includes('content/site-policy/')) return
+
// We're going to look for a sequence of 3 tokens. If the markdown
// is a really small string, it might not even have that many tokens
// in it. Can bail early.
diff --git a/src/content-linter/lib/linting-rules/note-warning-formatting.js b/src/content-linter/lib/linting-rules/note-warning-formatting.js
new file mode 100644
index 000000000000..37abecd8205e
--- /dev/null
+++ b/src/content-linter/lib/linting-rules/note-warning-formatting.js
@@ -0,0 +1,220 @@
+import { addError } from 'markdownlint-rule-helpers'
+import { getRange } from '../helpers/utils.js'
+import frontmatter from '#src/frame/lib/read-frontmatter.js'
+
+export const noteWarningFormatting = {
+ names: ['GHD049', 'note-warning-formatting'],
+ description: 'Note and warning tags should be formatted according to style guide',
+ tags: ['formatting', 'callouts', 'notes', 'warnings', 'style'],
+ severity: 'warning',
+ function: (params, onError) => {
+ // Skip autogenerated files
+ const frontmatterString = params.frontMatterLines.join('\n')
+ const fm = frontmatter(frontmatterString).data
+ if (fm && fm.autogenerated) return
+
+ const lines = params.lines
+ let inLegacyNote = false
+ let noteStartLine = null
+ let noteContent = []
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i]
+ const lineNumber = i + 1
+
+ // Check for legacy {% note %} tags
+ if (line.trim() === '{% note %}') {
+ inLegacyNote = true
+ noteStartLine = lineNumber
+ noteContent = []
+
+ // Check for missing line break before {% note %}
+ const prevLine = i > 0 ? lines[i - 1] : ''
+ if (prevLine.trim() !== '') {
+ const range = getRange(line, '{% note %}')
+ addError(onError, lineNumber, 'Add a blank line before {% note %} tag', line, range, {
+ editColumn: 1,
+ deleteCount: 0,
+ insertText: '\n',
+ })
+ }
+ continue
+ }
+
+ // Check for end of legacy note
+ if (line.trim() === '{% endnote %}') {
+ if (inLegacyNote) {
+ inLegacyNote = false
+
+ // Check for missing line break after {% endnote %}
+ const nextLine = i < lines.length - 1 ? lines[i + 1] : ''
+ if (nextLine.trim() !== '') {
+ const range = getRange(line, '{% endnote %}')
+ addError(onError, lineNumber, 'Add a blank line after {% endnote %} tag', line, range, {
+ editColumn: line.length + 1,
+ deleteCount: 0,
+ insertText: '\n',
+ })
+ }
+
+ // Check note content formatting
+ validateNoteContent(noteContent, noteStartLine, onError)
+ }
+ continue
+ }
+
+ // Collect content inside legacy notes
+ if (inLegacyNote) {
+ noteContent.push({ text: line, lineNumber: lineNumber })
+ continue
+ }
+
+ // Check for new-style callouts > [!NOTE], > [!WARNING], > [!DANGER]
+ const calloutMatch = line.match(/^>\s*\[!(NOTE|WARNING|DANGER)\]\s*$/)
+ if (calloutMatch) {
+ const calloutType = calloutMatch[1]
+
+ // Check for missing line break before callout
+ const prevLine = i > 0 ? lines[i - 1] : ''
+ if (prevLine.trim() !== '') {
+ const range = getRange(line, line.trim())
+ addError(
+ onError,
+ lineNumber,
+ `Add a blank line before > [!${calloutType}] callout`,
+ line,
+ range,
+ {
+ editColumn: 1,
+ deleteCount: 0,
+ insertText: '\n',
+ },
+ )
+ }
+
+ // Find the end of this callout block and validate content
+ const calloutContent = []
+ let j = i + 1
+ while (j < lines.length && lines[j].startsWith('>')) {
+ if (lines[j].trim() !== '>') {
+ calloutContent.push({ text: lines[j], lineNumber: j + 1 })
+ }
+ j++
+ }
+
+ // Check for missing line break after callout
+ if (j < lines.length && lines[j].trim() !== '') {
+ const range = getRange(lines[j], lines[j].trim())
+ addError(
+ onError,
+ j + 1,
+ `Add a blank line after > [!${calloutType}] callout block`,
+ lines[j],
+ range,
+ {
+ editColumn: 1,
+ deleteCount: 0,
+ insertText: '\n',
+ },
+ )
+ }
+
+ validateCalloutContent(calloutContent, calloutType, lineNumber, onError)
+ i = j - 1 // Skip to end of callout block
+ continue
+ }
+
+ // Check for orphaned **Note:**/**Warning:**/**Danger:** outside callouts
+ const orphanedPrefixMatch = line.match(/\*\*(Note|Warning|Danger):\*\*/)
+ if (orphanedPrefixMatch && !inLegacyNote && !line.startsWith('>')) {
+ const range = getRange(line, orphanedPrefixMatch[0])
+ addError(
+ onError,
+ lineNumber,
+ `${orphanedPrefixMatch[1]} prefix should be inside a callout block`,
+ line,
+ range,
+ null, // No auto-fix as this requires human decision
+ )
+ }
+ }
+ },
+}
+
+/**
+ * Validate content inside legacy {% note %} blocks
+ */
+function validateNoteContent(noteContent, noteStartLine, onError) {
+ if (noteContent.length === 0) return
+
+ const contentLines = noteContent.filter((item) => item.text.trim() !== '')
+ if (contentLines.length === 0) return
+
+ // Count bullet points
+ const bulletLines = contentLines.filter((item) => item.text.trim().match(/^[*\-+]\s/))
+ if (bulletLines.length > 2) {
+ const range = getRange(bulletLines[2].text, bulletLines[2].text.trim())
+ addError(
+ onError,
+ bulletLines[2].lineNumber,
+ 'Do not include more than 2 bullet points inside a callout',
+ bulletLines[2].text,
+ range,
+ null, // No auto-fix as this requires content restructuring
+ )
+ }
+
+ // Check for missing prefix (only if it looks like a traditional note)
+ const firstContentLine = contentLines[0]
+ const allContent = contentLines.map((line) => line.text).join(' ')
+ const hasButtons =
+ allContent.includes(' item.text.trim() !== '>')
+ if (contentLines.length === 0) return
+
+ // Count bullet points
+ const bulletLines = contentLines.filter((item) => item.text.match(/^>\s*[*\-+]\s/))
+ if (bulletLines.length > 2) {
+ const range = getRange(bulletLines[2].text, bulletLines[2].text.trim())
+ addError(
+ onError,
+ bulletLines[2].lineNumber,
+ 'Do not include more than 2 bullet points inside a callout',
+ bulletLines[2].text,
+ range,
+ null, // No auto-fix as this requires content restructuring
+ )
+ }
+
+ // For new-style callouts, the prefix is handled by the [!NOTE] syntax itself
+ // so we don't need to check for manual **Note:** prefixes
+}
diff --git a/src/content-linter/tests/unit/list-first-word-captitalization.js b/src/content-linter/tests/unit/list-first-word-captitalization.js
index 780bac091924..b00b39fe880b 100644
--- a/src/content-linter/tests/unit/list-first-word-captitalization.js
+++ b/src/content-linter/tests/unit/list-first-word-captitalization.js
@@ -76,4 +76,25 @@ describe(listFirstWordCapitalization.names.join(' - '), () => {
const errors = result.markdown
expect(errors.length).toBe(0)
})
+
+ test('skips site-policy directory files', async () => {
+ const markdown = [
+ '- list item should normally be flagged',
+ '- another uncapitalized item',
+ '- a. this is alphabetic numbering',
+ '- b. this is also alphabetic numbering',
+ ].join('\n')
+
+ // Test normal behavior (should flag errors)
+ const normalResult = await runRule(listFirstWordCapitalization, { strings: { markdown } })
+ expect(normalResult.markdown.length).toBeGreaterThan(0)
+
+ // Test site-policy exclusion (should skip all errors)
+ const sitePolicyResult = await runRule(listFirstWordCapitalization, {
+ strings: {
+ 'content/site-policy/some-policy.md': markdown,
+ },
+ })
+ expect(sitePolicyResult['content/site-policy/some-policy.md'].length).toBe(0)
+ })
})
diff --git a/src/content-linter/tests/unit/note-warning-formatting.js b/src/content-linter/tests/unit/note-warning-formatting.js
new file mode 100644
index 000000000000..791abd68e159
--- /dev/null
+++ b/src/content-linter/tests/unit/note-warning-formatting.js
@@ -0,0 +1,324 @@
+import { describe, expect, test } from 'vitest'
+
+import { runRule } from '../../lib/init-test.js'
+import { noteWarningFormatting } from '../../lib/linting-rules/note-warning-formatting.js'
+
+describe(noteWarningFormatting.names.join(' - '), () => {
+ test('Correctly formatted legacy notes pass', async () => {
+ const markdown = [
+ 'This is a paragraph.',
+ '',
+ '{% note %}',
+ '',
+ '**Note:** This is a properly formatted note.',
+ '',
+ '{% endnote %}',
+ '',
+ 'Another paragraph follows.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Correctly formatted new-style callouts pass', async () => {
+ const markdown = [
+ 'This is a paragraph.',
+ '',
+ '> [!NOTE]',
+ '> This is a properly formatted callout note.',
+ '',
+ 'Another paragraph follows.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Missing line break before legacy note is flagged', async () => {
+ const markdown = [
+ 'This is a paragraph.',
+ '{% note %}',
+ '**Note:** This note needs a line break before it.',
+ '{% endnote %}',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(2)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Add a blank line before {% note %}')
+ }
+ })
+
+ test('Missing line break after legacy note is flagged', async () => {
+ const markdown = [
+ '',
+ '{% note %}',
+ '**Note:** This note needs a line break after it.',
+ '{% endnote %}',
+ 'This paragraph is too close.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(4)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Add a blank line after {% endnote %}')
+ }
+ })
+
+ test('Missing line break before new-style callout is flagged', async () => {
+ const markdown = [
+ 'This is a paragraph.',
+ '> [!WARNING]',
+ '> This warning needs a line break before it.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(2)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Add a blank line before > [!WARNING]')
+ }
+ })
+
+ test('Missing line break after new-style callout is flagged', async () => {
+ const markdown = [
+ '',
+ '> [!DANGER]',
+ '> This danger callout needs a line break after it.',
+ 'This paragraph is too close.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(4)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Add a blank line after > [!DANGER]')
+ }
+ })
+
+ test('Too many bullet points in legacy note is flagged', async () => {
+ const markdown = [
+ '',
+ '{% note %}',
+ '',
+ '**Note:** This note has too many bullets:',
+ '',
+ '* First bullet point',
+ '* Second bullet point',
+ '* Third bullet point (this should be flagged)',
+ '',
+ '{% endnote %}',
+ '',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(8)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Do not include more than 2 bullet points')
+ }
+ })
+
+ test('Too many bullet points in new-style callout is flagged', async () => {
+ const markdown = [
+ '',
+ '> [!NOTE]',
+ '> This callout has too many bullets:',
+ '>',
+ '> * First bullet point',
+ '> * Second bullet point',
+ '> * Third bullet point (this should be flagged)',
+ '',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(7)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Do not include more than 2 bullet points')
+ }
+ })
+
+ test('Missing prefix in legacy note is flagged and fixable', async () => {
+ const markdown = [
+ '',
+ '{% note %}',
+ '',
+ 'This note is missing the proper prefix.',
+ '',
+ '{% endnote %}',
+ '',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(4)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('should start with **Note:**')
+ }
+ expect(errors[0].fixInfo).toBeDefined()
+ if (errors[0].fixInfo) {
+ expect(errors[0].fixInfo.insertText).toBe('**Note:** ')
+ }
+ })
+
+ test('Orphaned note prefix outside callout is flagged', async () => {
+ const markdown = [
+ 'This is a regular paragraph.',
+ '',
+ '**Note:** This note prefix should be inside a callout block.',
+ '',
+ 'Another paragraph.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(3)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('should be inside a callout block')
+ }
+ })
+
+ test('Orphaned warning prefix outside callout is flagged', async () => {
+ const markdown = [
+ 'Regular content here.',
+ '',
+ '**Warning:** This warning should be in a proper callout.',
+ '',
+ 'More content.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(1)
+ expect(errors[0].lineNumber).toBe(3)
+ if (errors[0].detail) {
+ expect(errors[0].detail).toContain('Warning prefix should be inside a callout block')
+ }
+ })
+
+ test('Feedback forms in legacy notes are not flagged for missing prefix', async () => {
+ const markdown = [
+ '',
+ '{% note %}',
+ '',
+ 'Did you successfully complete this task?',
+ '',
+ 'Yes',
+ '',
+ '{% endnote %}',
+ '',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ // Should only flag missing line breaks, not missing prefix for feedback forms
+ expect(errors.length).toBe(0)
+ })
+
+ test('Multiple formatting issues are all caught', async () => {
+ const markdown = [
+ 'Paragraph without break.',
+ '{% note %}',
+ 'Missing prefix and has bullets:',
+ '* First bullet',
+ '* Second bullet',
+ '* Third bullet (too many)',
+ '{% endnote %}',
+ 'No break after note.',
+ '',
+ '**Danger:** Orphaned danger prefix.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(5)
+
+ // Check we get all expected error types by line numbers and error count
+ const errorLines = errors.map((e) => e.lineNumber).sort((a, b) => a - b)
+ expect(errorLines).toEqual([2, 3, 6, 7, 10])
+
+ // Verify we have the expected number of different types of errors:
+ // 1. Missing line break before note (line 2)
+ // 2. Missing prefix in note content (line 3)
+ // 3. Too many bullet points (line 6)
+ // 4. Missing line break after note (line 7)
+ // 5. Orphaned danger prefix (line 10)
+ expect(errors.length).toBe(5)
+ })
+
+ test('Mixed legacy and new-style callouts work correctly', async () => {
+ const markdown = [
+ 'Some content.',
+ '',
+ '{% note %}',
+ '**Note:** This is a legacy note.',
+ '{% endnote %}',
+ '',
+ 'More content.',
+ '',
+ '> [!WARNING]',
+ '> This is a new-style warning.',
+ '',
+ 'Final content.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Different callout types are handled correctly', async () => {
+ const markdown = [
+ '',
+ '> [!NOTE]',
+ '> This is a note callout.',
+ '',
+ '> [!WARNING]',
+ '> This is a warning callout.',
+ '',
+ '> [!DANGER]',
+ '> This is a danger callout.',
+ '',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Autogenerated files are skipped', async () => {
+ const frontmatter = ['---', 'title: API Reference', 'autogenerated: rest', '---'].join('\n')
+ const markdown = [
+ 'Content.',
+ '{% note %}',
+ 'Badly formatted note.',
+ '{% endnote %}',
+ 'More content.',
+ ].join('\n')
+ const result = await runRule(noteWarningFormatting, {
+ strings: {
+ markdown: frontmatter + '\n' + markdown,
+ },
+ })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Empty notes and callouts do not cause errors', async () => {
+ const markdown = ['', '{% note %}', '', '{% endnote %}', '', '> [!NOTE]', '>', ''].join('\n')
+ const result = await runRule(noteWarningFormatting, { strings: { markdown } })
+ const errors = result.markdown
+ expect(errors.length).toBe(0)
+ })
+
+ test('Warning severity is set correctly', () => {
+ expect(noteWarningFormatting.severity).toBe('warning')
+ })
+
+ test('Rule has correct metadata', () => {
+ expect(noteWarningFormatting.names).toEqual(['GHD049', 'note-warning-formatting'])
+ expect(noteWarningFormatting.description).toContain('style guide')
+ expect(noteWarningFormatting.tags).toContain('callouts')
+ expect(noteWarningFormatting.tags).toContain('notes')
+ expect(noteWarningFormatting.tags).toContain('warnings')
+ })
+})
diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml
index d5e6fa9dea6b..0dab040b7da1 100644
--- a/src/fixtures/fixtures/data/ui.yml
+++ b/src/fixtures/fixtures/data/ui.yml
@@ -50,8 +50,8 @@ search:
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
- copy_answer: Copy answer
- copied_announcement: Copied!
+ share_answer: Copy answer URL
+ share_copied_announcement: Copied share URL!
thumbs_up: This answer was helpful
thumbs_down: This answer was not helpful
thumbs_announcement: Thank you for your feedback!
@@ -61,6 +61,9 @@ search:
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
+ response:
+ copy_code: Copy code to clipboard
+ copied_code: Copied!
failure:
general_title: There was an error loading search results.
ai_title: There was an error loading Copilot.
diff --git a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx
index 189a0bf1d9e2..78c8edf8142f 100644
--- a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx
+++ b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx
@@ -1,35 +1,91 @@
+import React from 'react'
import ReactMarkdown from 'react-markdown'
import type { Components } from 'react-markdown'
-import cx from 'classnames'
import remarkGfm from 'remark-gfm'
+import cx from 'classnames'
+import { IconButton } from '@primer/react'
+import { CopyIcon, CheckIcon } from '@primer/octicons-react'
+import { announce } from '@primer/live-region-element'
+
+import { useTranslation } from '@/languages/components/useTranslation'
+import useCopyClipboard from '@/rest/components/useClipboard'
+import { EventType } from '@/events/types'
+import { sendEvent } from '@/events/components/events'
export type MarkdownContentPropsT = {
children: string
className?: string
openLinksInNewTab?: boolean
includeQueryParams?: boolean
+ codeBlocksCopyable?: boolean
eventGroupKey?: string
eventGroupId?: string
as?: keyof JSX.IntrinsicElements
tabIndex?: number
}
-// For content that comes in a Markdown string
-// e.g. a GPT Response
-
export const UnrenderedMarkdownContent = ({
children,
className,
openLinksInNewTab = true,
includeQueryParams = true,
+ codeBlocksCopyable = true,
eventGroupKey = '',
eventGroupId = '',
...restProps
}: MarkdownContentPropsT) => {
+ const { t } = useTranslation('search')
// Overrides for ReactMarkdown components
const components = {} as Components
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- components.a = ({ node, ...props }) => {
+ if (codeBlocksCopyable) {
+ // Override the default code block to make multiline code blocks copyable
+ components.code = ({ ...props }) => {
+ // get the literal text of the code block
+ let text = String(props.children)
+ // If the codeblock is not multiline, return inline code block without copy functionality
+ if (!text.includes('\n')) {
+ return {props.children}
+ } else {
+ // Otherwise it's multiline and we want to make it copyable
+ text = text.replace(/\n$/, '')
+ }
+
+ const [isCopied, copyToClipboard] = useCopyClipboard(text, {
+ successDuration: 2000,
+ })
+
+ return (
+
{props.children}
+