diff --git a/.github/actions/node-npm-setup/action.yml b/.github/actions/node-npm-setup/action.yml
index 8e4bb2999881..372fc0132c03 100644
--- a/.github/actions/node-npm-setup/action.yml
+++ b/.github/actions/node-npm-setup/action.yml
@@ -13,7 +13,8 @@ runs:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
with:
path: node_modules
- key: ${{ runner.os }}-node_modules-${{ hashFiles('package*.json') }}-${{ hashFiles('.github/actions/node-npm-setup/action.yml') }}
+ # The postinstall script applies patches/ during npm ci, so patch changes must miss the cache.
+ key: ${{ runner.os }}-node_modules-${{ hashFiles('package*.json', 'patches/**') }}-${{ hashFiles('.github/actions/node-npm-setup/action.yml') }}
# The npm download cache only helps npm ci, so skip its 153 MB restore when node_modules hits.
- name: Setup Node.js
diff --git a/.github/workflows/orphaned-features-check.yml b/.github/workflows/orphaned-features-check.yml
index 0fa5fb4604f9..70f9780859d6 100644
--- a/.github/workflows/orphaned-features-check.yml
+++ b/.github/workflows/orphaned-features-check.yml
@@ -14,6 +14,7 @@ on:
- 'package*.json'
- 'src/data-directory/scripts/find-orphaned-features/**'
- .github/actions/clone-translations/action.yml
+ - .github/actions/get-docs-early-access/action.yml
- .github/actions/node-npm-setup/action.yml
permissions:
@@ -38,6 +39,11 @@ jobs:
- uses: ./.github/actions/node-npm-setup
+ # Early access docs use data/features too, so include them before deciding that a feature is unused.
+ - uses: ./.github/actions/get-docs-early-access
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
+
- name: Check for orphaned features
env:
# The gh CLI reads GITHUB_TOKEN.
diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml
index 8057b2770d3b..b3022cb9e60e 100644
--- a/.github/workflows/repo-sync.yml
+++ b/.github/workflows/repo-sync.yml
@@ -12,6 +12,12 @@ permissions:
contents: write
pull-requests: write
+# Queue overlapping runs. A second run closes every open repo-sync pull request, including one
+# the first run is still waiting to merge. Cancelling mid-merge would leave that pull request open.
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: false
+
jobs:
repo-sync:
if: github.repository == 'github/docs-internal' || github.repository == 'github/docs'
diff --git a/.github/workflows/reviewers-dependabot.yml b/.github/workflows/reviewers-dependabot.yml
index dc3b615c89a6..c2eb715a9c9a 100644
--- a/.github/workflows/reviewers-dependabot.yml
+++ b/.github/workflows/reviewers-dependabot.yml
@@ -12,8 +12,32 @@ on:
- reopened
- synchronize
paths:
- - 'data/reusable/dependabot/**'
- - 'content/code-security/dependabot/**'
+ - 'data/reusables/dependabot/**'
+ - 'data/reusables/**/*dependabot*'
+ - 'content/code-security/**/*dependabot*'
+ - 'content/code-security/**/*dependabot*/**'
+ # Dependabot articles whose paths do not contain "dependabot"
+ - 'content/code-security/concepts/supply-chain-security/malware-alerts.md'
+ - 'content/code-security/concepts/supply-chain-security/multi-ecosystem-updates.md'
+ - 'content/code-security/how-tos/secure-at-scale/configure-enterprise-security/configure-specific-tools/configure-limited-internet-access.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-access-to-private-registries.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-on-github-hosted-runners.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-on-self-hosted-runners.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-private-registries.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/controlling-dependencies-updated.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/list-configured-dependencies.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/prioritize-with-preset-rules.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/remove-access-to-public-registries.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/auto-update-actions.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-malware-alerts.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-security-updates.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-version-updates.md'
+ - 'content/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configuring-multi-ecosystem-updates.md'
+ - 'content/code-security/reference/supply-chain-security/criteria-for-preset-rules.md'
+ - 'content/code-security/reference/supply-chain-security/supported-ecosystems-and-manifests-for-dependency-scope.md'
+ - 'content/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories.md'
+ - 'content/code-security/tutorials/secure-your-dependencies/optimizing-pr-creation-version-updates.md'
- 'content/rest/dependabot/**'
- '.github/workflows/reviewers-dependabot.yml'
diff --git a/.github/workflows/sync-audit-logs.yml b/.github/workflows/sync-audit-logs.yml
index f19d5753d4b7..768d60a13205 100644
--- a/.github/workflows/sync-audit-logs.yml
+++ b/.github/workflows/sync-audit-logs.yml
@@ -47,61 +47,81 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }}
run: |
- echo "Creating a new branch if needed..."
branchname=audit-logs-schema-update-${{ steps.audit-log-allowlists.outputs.COMMIT_SHA }}
- remotesha=$(git ls-remote --heads origin $branchname)
- if [ -n "$remotesha" ]; then
- echo "Branch $branchname already exists in 'github/docs-internal'. Exiting..."
- exit 0
- fi
- git checkout -b $branchname
- echo "Created a new branch $branchname"
-
- echo "Preparing commit..."
- git config --global user.name "docs-bot"
- git config --global user.email "77750099+docs-bot@users.noreply.github.com"
- git add -A .
- echo "Prepared commit"
-
- echo "Check if there are changes..."
- if git diff-index --cached --quiet HEAD -- . ':(exclude)src/audit-logs/lib/config.json'
- then
- echo "No real changes (only the SHA in config.json moved). Exiting…"
+
+ # People close sync PRs that fail to deploy or merge, so do not reopen this SHA after a close.
+ # Reuse an open PR from an earlier run so a rerun does not restart its CI.
+ # --head matches fork branches too, so only trust same-repo PRs that docs-bot opened.
+ existing_pr=$(gh pr list --repo github/docs-internal --head "$branchname" --state all --json number,state,author,isCrossRepository \
+ --jq 'map(select(.author.login == "docs-bot" and (.isCrossRepository | not))) | .[0] | select(.) | "\(.number) \(.state)"')
+ if [ -n "$existing_pr" ] && [ "${existing_pr#* }" != "OPEN" ]; then
+ echo "Pull request #${existing_pr% *} for branch $branchname is ${existing_pr#* }. Exiting..."
exit 0
+ elif [ -n "$existing_pr" ]; then
+ pr=${existing_pr% *}
+ echo "Pull request #$pr already exists for branch $branchname. Checking that it holds only the regenerated data..."
+ # Others can push to the branch, so approve only if it matches this run's output.
+ git add -A .
+ git fetch --depth=1 origin "$branchname"
+ pr_files=$(gh api --paginate "repos/github/docs-internal/pulls/$pr/files" --jq '.[].filename')
+ unexpected_files=$(echo "$pr_files" | grep -v -e '^src/audit-logs/data/' -e '^src/audit-logs/lib/config\.json$' || true)
+ if [ -n "$unexpected_files" ] || ! git diff --cached --quiet FETCH_HEAD -- src/audit-logs/data src/audit-logs/lib/config.json; then
+ echo "Pull request #$pr does not match the regenerated audit log data. Fix or close it by hand."
+ echo "Unexpected files: ${unexpected_files:-none}"
+ exit 1
+ fi
+ echo "Pull request #$pr matches the regenerated data. Skipping to approval."
+ else
+ git checkout -b $branchname
+ echo "Created a new branch $branchname"
+
+ echo "Preparing commit..."
+ git config --global user.name "docs-bot"
+ git config --global user.email "77750099+docs-bot@users.noreply.github.com"
+ git add -A .
+ echo "Prepared commit"
+
+ echo "Check if there are changes..."
+ if git diff-index --cached --quiet HEAD -- . ':(exclude)src/audit-logs/lib/config.json'
+ then
+ echo "No real changes (only the SHA in config.json moved). Exiting…"
+ exit 0
+ fi
+ echo "Changes detected, proceeding"
+
+ echo "Creating commit..."
+ git commit -m "Add updated audit log event data"
+ echo "Created commit"
+
+ # Force-push reruns over failed attempts; this workflow exclusively owns these branches.
+ echo "Pushing commit..."
+ git push --force -u origin $branchname
+ echo "Pushed commit"
+
+ echo "Creating pull request..."
+ pr=$(gh pr create \
+ --title "Update audit log event data" \
+ --body '👋 Docs First Responder. This PR updates the audit log event data with the latest changes, synced from github/audit-log-allowlists.
+ Make sure the PR builds successfully and there are no gross errors (for example, a file is deleted). You do not need to validate the contents (that is the responsibility of product teams).
+ If CI does not pass or other problems arise, contact #technical-content on slack.' \
+ --repo github/docs-internal \
+ --label audit-log-pipeline,workflow-generated \
+ --head=$branchname)
+ echo "Created pull request $pr"
fi
- echo "Changes detected, proceeding"
-
- echo "Creating commit..."
- git commit -m "Add updated audit log event data"
- echo "Created commit"
-
- echo "Pushing commit..."
- git push origin $branchname
- echo "Pushed commit"
-
- echo "Creating pull request..."
- gh pr create \
- --title "Update audit log event data" \
- --body '👋 Docs First Responder. This PR updates the audit log event data with the latest changes, synced from github/audit-log-allowlists.
- Make sure the PR builds successfully and there are no gross errors (for example, a file is deleted). You do not need to validate the contents (that is the responsibility of product teams).
- If CI does not pass or other problems arise, contact #technical-content on slack.' \
- --repo github/docs-internal \
- --label audit-log-pipeline,workflow-generated \
- --head=$branchname
- echo "Created pull request"
# docs-bot cannot approve its own PR, so GITHUB_TOKEN approves it.
echo "Approving pull request..."
unset GITHUB_TOKEN
gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}"
- gh pr review --approve
+ gh pr review "$pr" --approve
echo "Approved pull request"
# GITHUB_TOKEN cannot enable auto-merge here, so docs-bot enables it.
echo "Setting pull request to auto merge..."
unset GITHUB_TOKEN
gh auth login --with-token <<< "${{ secrets.DOCS_BOT_PAT_BASE }}"
- gh pr merge --auto
+ gh pr merge "$pr" --auto
echo "Set pull request to auto merge"
- uses: ./.github/actions/create-workflow-failure-issue
diff --git a/content/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-jfrog.md b/content/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-jfrog.md
index 1f12a985cf17..8255568a3b20 100644
--- a/content/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-jfrog.md
+++ b/content/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-jfrog.md
@@ -87,7 +87,7 @@ jobs:
```yaml
{% data reusables.actions.actions-not-certified-by-github-comment %}
- name: Sign in to Artifactory Docker registry
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: {% raw %}${{ env.JF_URL }}{% endraw %}
username: {% raw %}${{ steps.setup-jfrog-cli.outputs.oidc-user }}{% endraw %}
diff --git a/content/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens.md b/content/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens.md
index a063fb2aa1ef..89e67caeea35 100644
--- a/content/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens.md
+++ b/content/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens.md
@@ -30,7 +30,7 @@ jobs:
- uses: {% data reusables.actions.action-checkout %}
- run: your-application
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
```
The organization's **Allow use of Copilot CLI billed to the organization** policy must be enabled. This approach needs no GitHub App or stored authentication secret. For details, see [AUTOTITLE](/copilot/how-tos/copilot-cli/use-copilot-cli-in-actions).
diff --git a/data/ui.yml b/data/ui.yml
index a88b9607bc29..131b00dd395f 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -210,7 +210,7 @@ rest:
banner:
api_versioned: The REST API is now versioned.
api_version_info: For more information, see "About API versioning."
- ghes_api_versioned: After a site administrator upgrades your Enterprise Server instance to {{ firstGhesReleaseWithApiVersions.versionTitle }} or later, the REST API will be versioned. To learn how to find your instance's version, see "About versions of GitHub Docs".
+ ghes_api_versioned: After a site administrator upgrades your Enterprise Server instance to {{ firstGhesReleaseWithApiVersions.versionTitle }} or later, the REST API will be versioned. To learn how to find your instance's version, see "About versions of GitHub Docs".
redirect_notice: We've recently moved some of the REST API documentation.
redirect_repo: If you can't find what you're looking for, you might try the new {{ newRestPagesLinks }} REST API pages.
redirect_enterprise: If you can't find what you're looking for, you might try the {{ actionsPageLink }} REST API page.
diff --git a/src/article-api/lib/summarize-schema.ts b/src/article-api/lib/summarize-schema.ts
index ccf7c1cafc5f..93e0010eb08b 100644
--- a/src/article-api/lib/summarize-schema.ts
+++ b/src/article-api/lib/summarize-schema.ts
@@ -142,6 +142,22 @@ function renderProperties(
if (propType === 'array' && prop.items) {
const itemTitle = prop.items.title
+ const itemKey = (['oneOf', 'anyOf', 'allOf'] as const).find((k) => prop.items![k])
+ if (itemKey) {
+ const label = `${itemTitle ? `array of \`${itemTitle}\`` : 'array of items'}${isNullable ? ' or null' : ''}`
+ if (shouldReference(itemTitle, depth < MAX_DEPTH, seen)) {
+ lines.push(`${prefix}* \`${name}\`: ${reqStr}${label} (see above)`)
+ } else {
+ lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`)
+ if (prop.items.properties && depth < MAX_DEPTH) {
+ lines.push(renderProperties(prop.items, indent + 1, depth + 1, seen))
+ }
+ lines.push(
+ renderCompositionVariants(itemKey, prop.items[itemKey]!, indent + 1, depth, seen),
+ )
+ }
+ continue
+ }
const canExpand = Boolean(prop.items.properties) && depth < MAX_DEPTH
if (shouldReference(itemTitle, canExpand, seen)) {
lines.push(
diff --git a/src/article-api/middleware/article.ts b/src/article-api/middleware/article.ts
index 8b71f9590350..fe0ca5192a6b 100644
--- a/src/article-api/middleware/article.ts
+++ b/src/article-api/middleware/article.ts
@@ -12,7 +12,7 @@ import {
import { getArticleBody } from './article-body'
import { getMetadata } from './article-pageinfo'
import {
- makeLanguageSurrogateKey,
+ makeContentSurrogateKeys,
setFastlySurrogateKey,
} from '@/frame/middleware/set-fastly-surrogate-key'
import statsd from '@/observability/lib/statsd'
@@ -60,6 +60,7 @@ router.get(
recordBodySize(req, bodyContent)
defaultCacheControl(res)
+ setArticleSurrogateKeys(req, res)
return res.json({
meta,
body: bodyContent,
@@ -102,12 +103,11 @@ router.get(
recordBodySize(req, bodyContent)
defaultCacheControl(res)
+ setArticleSurrogateKeys(req, res)
return res.type('text/markdown').send(bodyContent)
}),
)
-// /api/article/meta sets a language surrogate key because /api URLs lack a language segment.
-// Fastly needs the key for staggered language purges.
/**
* Get metadata about an article.
* @route GET /api/article/meta
@@ -147,16 +147,21 @@ router.get(
incrementArticleLookup(req, 'meta')
defaultCacheControl(res)
-
- setFastlySurrogateKey(
- res,
- makeLanguageSurrogateKey(req.pageinfo?.page?.languageCode || 'en'),
- true,
- )
+ setArticleSurrogateKeys(req, res)
return res.json(meta)
}),
)
+// /api URLs lack a language segment, so every Article API route sets language and page keys.
+// The page key matches the key that deploy purges build from changed content files.
+function setArticleSurrogateKeys(req: ExtendedRequestWithPageInfo, res: Response) {
+ const keys = makeContentSurrogateKeys({
+ langCode: req.pageinfo.page?.languageCode || 'en',
+ relativePath: req.pageinfo.page?.relativePath,
+ })
+ setFastlySurrogateKey(res, keys.join(' '), true)
+}
+
// Keep Datadog metric tags consistent across Article API endpoints.
// Datadog tags max at 200 characters, so path and source tags are truncated.
// See https://docs.datadoghq.com/getting_started/tagging/#define-tags
diff --git a/src/article-api/templates/webhooks-page.template.md b/src/article-api/templates/webhooks-page.template.md
index fd1aaf10bdb4..ed282118a35a 100644
--- a/src/article-api/templates/webhooks-page.template.md
+++ b/src/article-api/templates/webhooks-page.template.md
@@ -41,17 +41,23 @@ Events below list only their additional parameters beyond these common ones.
**Action type:** {% for actionType in webhook.actionTypes %}`{{ actionType }}`{% unless forloop.last %}, {% endunless %}{% endfor %}
{% endif %}
-{% if webhook.description %}
-{{ webhook.description }}
+{% for action in webhook.actions %}
+{% if webhook.actions.size > 1 %}
+#### `{{ action.name }}`
{% endif %}
-{% if webhook.bodyParameters.size > 0 %}
-#### Webhook payload object parameters
+{% if action.description %}
+{{ action.description }}
+
+{% endif %}
+{% if action.bodyParameters.size > 0 %}
+{% if webhook.actions.size > 1 %}#####{% else %}####{% endif %} Webhook payload object parameters
| Name | Type | Description |
|------|------|-------------|
-{% for param in webhook.bodyParameters %}| `{{ param.name }}` | `{{ param.type }}` | {% if param.isRequired %}**Required.** {% endif %}{{ param.description }} |
+{% for param in action.bodyParameters %}| `{{ param.name }}` | `{{ param.type }}` | {% if param.isRequired %}**Required.** {% endif %}{{ param.description }} |
{% endfor %}
{% endif %}
{% endfor %}
+{% endfor %}
diff --git a/src/article-api/tests/article-body.ts b/src/article-api/tests/article-body.ts
index 3e097f04f0ed..2a0461a11499 100644
--- a/src/article-api/tests/article-body.ts
+++ b/src/article-api/tests/article-body.ts
@@ -23,6 +23,14 @@ describe('article body api', () => {
expect(res.headers['content-type']).toContain('text/markdown')
})
+ test('sets the page surrogate key that deploy purges use', async () => {
+ const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
+ expect(res.statusCode).toBe(200)
+ const keys = res.headers['surrogate-key'].split(/\s/g)
+ expect(keys).toContain('language:en')
+ expect(keys).toContain('language:en,path:get-started/start-your-journey/hello-world.md')
+ })
+
test('body includes title and intro', async () => {
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
expect(res.statusCode).toBe(200)
diff --git a/src/article-api/tests/pageinfo.ts b/src/article-api/tests/pageinfo.ts
index 2fac904119a1..00d34d71be2c 100644
--- a/src/article-api/tests/pageinfo.ts
+++ b/src/article-api/tests/pageinfo.ts
@@ -1,7 +1,10 @@
import { beforeAll, describe, expect, test } from 'vitest'
import { get } from '@/tests/helpers/e2etest'
-import { makeLanguageSurrogateKey } from '@/frame/middleware/set-fastly-surrogate-key'
+import {
+ makeLanguageSurrogateKey,
+ makePageSurrogateKey,
+} from '@/frame/middleware/set-fastly-surrogate-key'
import { latest } from '@/versions/lib/enterprise-server-releases'
const makeURL = (pathname: string): string =>
@@ -51,7 +54,10 @@ describe('pageinfo api', () => {
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-control']).toContain('public')
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
- expect(res.headers['surrogate-key']).toBe(makeLanguageSurrogateKey('en'))
+ // The page key must match the purge key derived from the source file.
+ expect(res.headers['surrogate-key']).toBe(
+ `${makeLanguageSurrogateKey('en')} ${makePageSurrogateKey('en', 'get-started/start-your-journey/index.md')}`,
+ )
})
// Fullwidth colons make Japanese frontmatter scalar; English fallback prevents empty titles.
@@ -62,6 +68,17 @@ describe('pageinfo api', () => {
expect(meta.title).toBe('Broken frontmatter translation fallback')
})
+ // Japanese frontmatter has an empty title and a numeric intro, so each key falls back to English.
+ test('mistyped translation frontmatter values fall back to English', async () => {
+ const res = await get(makeURL('/ja/get-started/foo/mistyped-frontmatter-translation'))
+ expect(res.statusCode).toBe(200)
+ const meta = JSON.parse(res.body) as PageMetadata
+ expect(meta.title).toBe('Mistyped frontmatter translation fallback')
+ expect(meta.intro).toBe(
+ 'Demonstrates falling back to English when a translated frontmatter value has the wrong type.',
+ )
+ })
+
test('a pathname that does not exist', async () => {
const res = await get(makeURL('/en/never/heard/of'))
expect(res.statusCode).toBe(404)
diff --git a/src/article-api/tests/secret-scanning-transformer.test.ts b/src/article-api/tests/secret-scanning-transformer.test.ts
index 8ec8886c2400..cd0717355cc2 100644
--- a/src/article-api/tests/secret-scanning-transformer.test.ts
+++ b/src/article-api/tests/secret-scanning-transformer.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, test, vi, beforeEach } from 'vitest'
+import octicons from '@primer/octicons'
import { SecretScanningTransformer } from '@/article-api/transformers/secret-scanning-transformer'
import shortVersionsMiddleware from '@/versions/middleware/short-versions'
@@ -76,3 +77,31 @@ describe('SecretScanningTransformer Liquid evaluation', () => {
expect(context.secretScanningData![0].hasExtendedMetadata).toBe(true)
})
})
+
+describe('SecretScanningTransformer icon replacement', () => {
+ const transformer = new SecretScanningTransformer()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ test('replaces octicon SVGs with check and cross marks', async () => {
+ vi.mocked(getSecretScanningData).mockResolvedValue([])
+ const supported = octicons.check.toSVG({ 'aria-label': 'Supported' })
+ const notSupported = octicons.x.toSVG({ 'aria-label': 'Not supported' })
+ const page = {
+ ...stubPage,
+ render: vi
+ .fn()
+ .mockResolvedValue(
+ `| User alerts | ${supported} | ${notSupported} | ${supported} (if partner) |`,
+ ),
+ } as unknown as Page
+ const context = await buildContext('free-pro-team@latest')
+
+ const result = await transformer.transform(page, '/test', context)
+
+ expect(result).toContain('| User alerts | ✓ | ✗ | ✓ (if partner) |')
+ expect(result).not.toContain('