diff --git a/.github/workflows/all-documents.yml b/.github/workflows/all-documents.yml index 1f0101e770ed..0a9d3c0b4ed0 100644 --- a/.github/workflows/all-documents.yml +++ b/.github/workflows/all-documents.yml @@ -1,8 +1,6 @@ name: All documents script -# **What it does**: Verifies that the all-documents script works. -# **Why we have it**: Code quality and sustainability. -# **Who does it impact**: docs-engineering +# Catches all-documents crashes. This workflow has no output assertions. on: pull_request: @@ -38,5 +36,3 @@ jobs: echo "" echo "Look at the first 50 lines of the file..." cat all-documents.json | jq | head -n 50 - - # We're essentially expecting it to not crash and fail. diff --git a/.github/workflows/article-api-docs.yml b/.github/workflows/article-api-docs.yml index 8bb96924ab4d..1243e53a5e25 100644 --- a/.github/workflows/article-api-docs.yml +++ b/.github/workflows/article-api-docs.yml @@ -1,8 +1,6 @@ name: 'Check article-api docs' -# **What it does**: Makes sure changes to the article api are documented. -# **Why we have it**: So what's documented doesn't fall behind -# **Who does it impact**: Docs engineering, CGS team +# Keeps generated article API docs from falling behind middleware changes. on: workflow_dispatch: @@ -10,7 +8,6 @@ on: paths: - 'src/article-api/middleware/article.ts' - 'src/article-api/middleware/pagelist.ts' - # Self-test - .github/workflows/article-api-docs.yml permissions: @@ -34,8 +31,6 @@ jobs: if [ -n "$(git status --porcelain)" ]; then git status git diff - - # Some whitespace for the sake of the message below echo "" echo "" diff --git a/.github/workflows/auto-add-ready-for-doc-review.yml b/.github/workflows/auto-add-ready-for-doc-review.yml index a96829d134cf..a0816f5316a9 100644 --- a/.github/workflows/auto-add-ready-for-doc-review.yml +++ b/.github/workflows/auto-add-ready-for-doc-review.yml @@ -1,8 +1,6 @@ name: Auto-add ready-for-doc-review label -# **What it does**: Automatically adds the "ready-for-doc-review" label to DIY docs PRs that contain content or data changes when they are opened in a non-draft state or converted from draft to ready for review. -# **Why we have it**: To ensure DIY docs PRs are automatically added to the docs-content review board without requiring manual labeling. -# **Who does it impact**: Contributors making content changes and docs-content reviewers. +# Sends DIY content changes to the docs-content review board without manual labeling. on: pull_request: @@ -34,8 +32,7 @@ jobs: github-token: ${{ secrets.DOCS_BOT_PAT_BASE }} script: | try { - // Team is addressed by numeric ID (org github = 9919, team docs = 325922) - // because IDs survive team renames and slugs do not. + // 9919 is the github org and 325922 the docs team; numeric IDs survive renames. await github.request('GET /organizations/{org_id}/team/{team_id}/memberships/{username}', { org_id: 9919, team_id: 325922, diff --git a/.github/workflows/auto-close-dependencies.yml b/.github/workflows/auto-close-dependencies.yml index a073fa3328fa..c0c6a4d5c104 100644 --- a/.github/workflows/auto-close-dependencies.yml +++ b/.github/workflows/auto-close-dependencies.yml @@ -1,10 +1,6 @@ name: Auto Close Open Source Dependency Updates -# **What it does**: -# - close-external: Automatically close dependabot's pull requests in the open-source repository. -# **Why we have it**: -# - close-external: To avoid duplicating updates against the internal repository. -# **Who does it impact**: It helps docs engineering focus on higher value work. +# Closes Dependabot dependency updates in github/docs because the internal repo owns them. on: pull_request: @@ -48,7 +44,7 @@ jobs: run: | gh pr comment "$PR_URL" --body "This dependency update will be handled internally by our engineering team." - # Because we get far too much spam ;_; + # Lock conversations to stop repeated dependency-update comments. - name: Lock conversations uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 env: diff --git a/.github/workflows/benchmark-pages.yml b/.github/workflows/benchmark-pages.yml index a23b3c37784d..aa0e0edf696a 100644 --- a/.github/workflows/benchmark-pages.yml +++ b/.github/workflows/benchmark-pages.yml @@ -1,13 +1,11 @@ name: 'Weekly page benchmark' -# **What it does**: Benchmarks all pages via the article API, flags errors and slow pages -# **Why we have it**: Catch perf regressions and broken pages before users hit them -# **Who does it impact**: Docs engineering +# Catches slow or broken article API pages before users hit them. on: workflow_dispatch: schedule: - - cron: '20 16 * * 1' # Every Monday at 16:20 UTC / 8:20 PST + - cron: '20 16 * * 1' # Mondays at 16:20 UTC. permissions: contents: read diff --git a/.github/workflows/changelog-agent.yml b/.github/workflows/changelog-agent.yml index 71e73de5c3ae..acf2240f0de7 100644 --- a/.github/workflows/changelog-agent.yml +++ b/.github/workflows/changelog-agent.yml @@ -1,11 +1,7 @@ name: Changelog agent ā draft entry when a qualified PR merges -# **What it does**: When a PR merges that closes a docs-content issue with a -# parent issue, uses an LLM to draft a changelog entry, opens a PR in -# github/docs-content, and DMs the author in Slack for review. -# **Why we have it**: Automates the changelog drafting process so authors -# don't have to remember to write a changelog entry manually. -# **Who does it impact**: docs-content team members. +# Drafts internal changelog entries for merged PRs that close a docs-content child issue. +# Authors review generated PRs before publication. on: pull_request: @@ -81,7 +77,6 @@ jobs: script: | const author = '${{ steps.resolve_pr.outputs.pr_author }}'; - // Fetch github-to-slack.json from docs-content via API let mapping = {}; try { const { data } = await github.rest.repos.getContent({ @@ -96,7 +91,6 @@ jobs: return; } - // Remove non-user keys (like _comment) const teamMembers = Object.keys(mapping).filter(k => !k.startsWith('_')); if (!teamMembers.includes(author)) { @@ -119,8 +113,7 @@ jobs: script: | const body = process.env.PR_BODY || ''; - // Match closing keywords followed by docs-content issue references. - // Supports: closes github/docs-content#123, fixes https://github.com/github/docs-content/issues/123 + // Finds docs-content issues closed by the source PR body. const patterns = [ /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+github\/docs-content#(\d+)/gi, /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+https:\/\/github\.com\/github\/docs-content\/issues\/(\d+)/gi, @@ -168,7 +161,6 @@ jobs: return; } - // Query for parent issue via GraphQL const query = ` query($nodeId: ID!) { node(id: $nodeId) { @@ -218,7 +210,6 @@ jobs: core.setOutput('parent_assignees', (parent.assignees?.nodes || []).map(a => a.login).join(',')); core.setOutput('parent_repo', parent.repository.nameWithOwner); - // Also store the docs-content issue details core.setOutput('dc_issue_title', issue.title); core.setOutput('dc_issue_body', issue.body || ''); @@ -236,7 +227,6 @@ jobs: const prNumber = parseInt('${{ steps.resolve_pr.outputs.pr_number }}', 10); const prAuthor = '${{ steps.resolve_pr.outputs.pr_author }}'; - // Get approved reviewers (exclude bots and PR author) const { data: reviews } = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, @@ -249,7 +239,6 @@ jobs: .map(r => r.user.login) )]; - // Get changed files (paths only, limit to 50) const { data: files } = await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, @@ -297,7 +286,6 @@ jobs: with: github-token: ${{ secrets.DOCS_BOT_PAT_BASE }} script: | - // Fetch changelog-internal.md from docs-content const { data } = await github.rest.repos.getContent({ owner: 'github', repo: 'docs-content', @@ -305,7 +293,6 @@ jobs: }); const changelog = Buffer.from(data.content, 'base64').toString('utf-8'); - // Extract the first 3 entries (each starts with **date**) const lines = changelog.split('\n'); let count = 0; let examples = []; @@ -420,12 +407,8 @@ jobs: uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3 with: provider: copilot - # Must be an explicit empty string, not omitted. This action defaults - # `model` to "gpt-4.1" and always forwards it as --model, and that slug - # is retired, so omitting the input fails with: - # Error: Model "gpt-4.1" from --model flag is not available. - # An empty string makes the action skip --model entirely and lets the - # Copilot CLI pick its own current default. See actions/ai-inference#271. + # Keep this empty string. Omitting it makes actions/ai-inference pass + # its retired gpt-4.1 default, which fails. model: '' prompt-file: prompt.txt system-prompt-file: system-prompt.txt @@ -471,7 +454,6 @@ jobs: const branchName = `changelog-agent-${{ steps.resolve_pr.outputs.pr_number }}`; const filePath = 'docs-content-docs/docs-content-workflows/changelog-internal.md'; - // Get the current changelog file from docs-content const { data: fileData } = await github.rest.repos.getContent({ owner: 'github', repo: 'docs-content', @@ -480,11 +462,9 @@ jobs: let changelog = Buffer.from(fileData.content, 'base64').toString('utf-8'); - // Build the new entry const entry = `**${process.env.DATE_STR}**\n\n${process.env.DRAFT}\n\n
'
+versions:
+ fpt: '*'
+ ghes: '*'
+ ghec: '*'
+contentType: how-tos
+---
+
+## An emoji image
+
+Typing `:strawberry:` renders the emoji
diff --git a/src/fixtures/fixtures/content/get-started/images/images-in-lists.md b/src/fixtures/fixtures/content/get-started/images/images-in-lists.md
index 237ad23da21c..897001107654 100644
--- a/src/fixtures/fixtures/content/get-started/images/images-in-lists.md
+++ b/src/fixtures/fixtures/content/get-started/images/images-in-lists.md
@@ -14,3 +14,10 @@ contentType: how-tos
1. French press is also great

3. Drip coffee is not so great.
+
+## A numbered list with a linked image
+
+1. Pour-over takes patience.
+1. The linked image renders like this:
+
+ [](https://github.com)
diff --git a/src/fixtures/fixtures/content/get-started/images/index.md b/src/fixtures/fixtures/content/get-started/images/index.md
index ef16588d1a04..a6b7262d8d46 100644
--- a/src/fixtures/fixtures/content/get-started/images/index.md
+++ b/src/fixtures/fixtures/content/get-started/images/index.md
@@ -10,4 +10,5 @@ children:
- /images-in-lists
- /link-to-image
- /retina-image
+ - /emoji-and-decorative-images
---
diff --git a/src/fixtures/tests/images.ts b/src/fixtures/tests/images.ts
index b9aee2601786..796d79eb2104 100644
--- a/src/fixtures/tests/images.ts
+++ b/src/fixtures/tests/images.ts
@@ -37,6 +37,7 @@ describe('render Markdown image tags', () => {
expect(src).toMatch(/^\/assets\/cb-\w+\/images\/_fixtures\/screenshot\.png$/)
const alt = imgs.attr('alt')
expect(alt).toBe('This is the alt text')
+ expect(imgs.attr('class')).toMatch(/Image/)
const res = await get(srcset!.split(' ')[0], { responseType: 'buffer' })
expect(res.statusCode).toBe(200)
@@ -69,6 +70,16 @@ describe('render Markdown image tags', () => {
expect(imageSpan.length).toBe(1)
})
+ // A div inside a paragraph is invalid HTML and breaks hydration.
+ test('linked image in a list paragraph has no wrapper', async () => {
+ const $: CheerioAPI = await getDOM('/get-started/images/images-in-lists')
+
+ const link = $('#article-contents ol > li > p > a[href="https://github.com"]')
+ expect(link.length).toBe(1)
+ expect($('div', link).length).toBe(0)
+ expect($('img', link).attr('alt')).toBe('Linked test image')
+ })
+
test("links directly to images aren't rewritten", async () => {
const $: CheerioAPI = await getDOM('/get-started/images/link-to-image')
// The fixture has one article link; header links are out of scope.
@@ -79,4 +90,34 @@ describe('render Markdown image tags', () => {
const res = await head(links.attr('href')!)
expect(res.statusCode).toBe(200)
})
+
+ test('emoji images stay plain img elements', async () => {
+ const $: CheerioAPI = await getDOM('/get-started/images/emoji-and-decorative-images')
+ const emoji = $(
+ '#article-contents img[src^="https://github.githubassets.com/images/icons/emoji"]',
+ )
+ expect(emoji.length).toBe(1)
+ expect(emoji.attr('alt')).toBe(':strawberry:')
+ expect(emoji.attr('class')).toBeUndefined()
+ })
+
+ test('images without alt text render an empty alt', async () => {
+ const $: CheerioAPI = await getDOM('/get-started/images/emoji-and-decorative-images')
+ const imgs = $('#article-contents img[src*="/images/_fixtures/screenshot.png"]')
+ expect(imgs.length).toBe(1)
+ expect(imgs.attr('alt')).toBe('')
+ expect(imgs.attr('class')).toMatch(/Image/)
+ })
+
+ test('images in RenderedHTML intros use the Brand Image component', async () => {
+ const $: CheerioAPI = await getDOM('/get-started/images/emoji-and-decorative-images')
+ const lead = $('[data-container="lead"]')
+ const image = $('img[src*="/images/_fixtures/electrocat.png"]', lead)
+ expect(image.length).toBe(1)
+ expect(image.attr('alt')).toBe('Intro image')
+ expect(image.attr('class')).toMatch(/Image/)
+ const emoji = $('img[src^="https://github.githubassets.com/images/icons/emoji"]', lead)
+ expect(emoji.length).toBe(1)
+ expect(emoji.attr('class')).toBeUndefined()
+ })
})
diff --git a/src/frame/components/article/ViewMarkdownButton.module.scss b/src/frame/components/article/ViewMarkdownButton.module.scss
index 46dbbabec6fc..1c253f3c58c1 100644
--- a/src/frame/components/article/ViewMarkdownButton.module.scss
+++ b/src/frame/components/article/ViewMarkdownButton.module.scss
@@ -23,7 +23,11 @@
// @primer/react ActionMenu.Button, so several rules here make them agree. Where
// a doubled class or !important appears, it beats a library rule; the specificity
// that forced it is noted.
-.button {
+//
+// The doubled classes beat @primer/react ButtonBase rules, which have one-class
+// specificity. Without them the winner depends on stylesheet load order, and a
+// dev hot reload or chunk reorder brings back the chevron's border and radius.
+.button.button {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -75,7 +79,7 @@
// The pill's left end is rounded outside and square where it meets the chevron.
// The radius token is 624.9375rem rather than 9999px, and nothing clips it.
-.copyButton {
+.copyButton.copyButton {
border-radius: var(--brand-borderRadius-full, 624.9375rem) 0 0
var(--brand-borderRadius-full, 624.9375rem);
// Less padding on the seam side so the label sits closer to the chevron than
@@ -92,7 +96,7 @@
// The pill's right end: square at the seam, rounded outside. Width matches the
// height for an even chevron target.
-.dropdownButton {
+.dropdownButton.dropdownButton {
border-radius: 0 var(--brand-borderRadius-full, 624.9375rem)
var(--brand-borderRadius-full, 624.9375rem) 0;
width: 28px;
diff --git a/src/frame/components/hooks/useHasAccount.ts b/src/frame/components/hooks/useHasAccount.ts
index 589eaf4997b1..047e78748a0c 100644
--- a/src/frame/components/hooks/useHasAccount.ts
+++ b/src/frame/components/hooks/useHasAccount.ts
@@ -2,15 +2,9 @@ import { useState, useEffect } from 'react'
import Cookies from '@/frame/components/lib/cookies'
import { COLOR_MODE_COOKIE_NAME, PREFERRED_COLOR_MODE_COOKIE_NAME } from '@/frame/lib/constants'
-// Measure if the user has a github.com account and signed in during this session.
-// The github.com sends the color_mode cookie every request when you sign in,
-// but does not delete the color_mode cookie on sign out.
-// You do not need to change your color mode settings to get this cookie,
-// this applies to every user regardless of if they changed this setting.
-// To test this, try a private browser tab.
-// We are using the color_mode cookie because it is not HttpOnly.
-// For users that haven't changed their session cookies recently,
-// we also can check for the browser-set `preferred_color_mode` cookie.
+// github.com sends color_mode on every signed-in request and leaves it on sign-out.
+// Every account gets that client-readable cookie, regardless of color-mode settings.
+// preferred_color_mode covers sessions that still use the browser-set cookie.
export function useHasAccount() {
const [hasAccount, setHasAccount] = useState