Skip to content

Commit 389e795

Browse files
louis-preclaude
andcommitted
ci: Add format-code-sample-tabs to CI format workflow
Adds a codegen script that auto-fixes code sample tab order and titles in guide and brand-guide markdown files. Runs as part of the Format CI workflow, which auto-commits fixes on non-main branches. The canonical tab order and title mapping are defined in a shared module (codegen/lib/code-sample-tab-order.ts), imported by both the API reference codegen and the tab formatter — single source of truth. The script: - Reorders tabs to match API reference canonical order (JavaScript → cURL → Python → Ruby → PHP → C# → Java → Go → Seam CLI) - Normalizes tab titles ("cURL (bash)" → "cURL", "Javascript" → "JavaScript") - Ensures GitBook tab syncing works across all code blocks on a page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dc0adaf commit 389e795

5 files changed

Lines changed: 132 additions & 3 deletions

File tree

.github/workflows/format.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
passphrase: ${{ secrets.GPG_PASSPHRASE }}
3030
- name: Setup
3131
uses: ./.github/actions/setup
32+
- name: Format code sample tabs
33+
run: npm run format-code-sample-tabs
3234
- name: Format
3335
run: npm run format
3436
- name: Commit

codegen/format-code-sample-tabs.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { readFileSync, writeFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
4+
import { globSync } from 'glob'
5+
6+
import { tabTitleFixes, tabTitleOrder } from './lib/code-sample-tab-order.js'
7+
8+
const tabsBlockPattern = /\{%\s*tabs\s*%\}[\s\S]*?\{%\s*endtabs\s*%\}/g
9+
10+
const tabPattern =
11+
/\{%\s*tab\s+title="([^"]+)"\s*%\}([\s\S]*?)\{%\s*endtab\s*%\}/g
12+
13+
function canonicalIndex(title: string): number {
14+
const normalized = tabTitleFixes[title] ?? title
15+
return tabTitleOrder.get(normalized) ?? tabTitleOrder.size + 1
16+
}
17+
18+
function formatTabsBlock(block: string): { result: string; changed: boolean } {
19+
const tabs: Array<{ title: string; content: string }> = []
20+
for (const match of block.matchAll(tabPattern)) {
21+
tabs.push({ title: match[1] ?? '', content: match[2] ?? '' })
22+
}
23+
24+
if (tabs.length < 2) return { result: block, changed: false }
25+
26+
const originalTitles = tabs.map((t) => t.title)
27+
const sorted = [...tabs].sort(
28+
(a, b) => canonicalIndex(a.title) - canonicalIndex(b.title),
29+
)
30+
const sortedTitles = sorted.map((t) => t.title)
31+
const needsRename = originalTitles.some((t) => t in tabTitleFixes)
32+
const needsReorder = originalTitles.some((t, i) => t !== sortedTitles[i])
33+
34+
if (!needsReorder && !needsRename) return { result: block, changed: false }
35+
36+
const lines = ['{% tabs %}']
37+
for (const tab of sorted) {
38+
const title = tabTitleFixes[tab.title] ?? tab.title
39+
lines.push(`{% tab title="${title}" %}`)
40+
lines.push(tab.content.trimEnd())
41+
lines.push('{% endtab %}')
42+
lines.push('')
43+
}
44+
while (lines.at(-1) === '') lines.pop()
45+
lines.push('{% endtabs %}')
46+
47+
return { result: lines.join('\n'), changed: true }
48+
}
49+
50+
const dirs = ['docs/guides', 'docs/brand-guides']
51+
const files = dirs.flatMap((dir) => globSync(join(dir, '**/*.md')))
52+
53+
let totalChanged = 0
54+
55+
for (const file of files) {
56+
const content = readFileSync(file, 'utf-8')
57+
let changed = false
58+
59+
const updated = content.replace(tabsBlockPattern, (block) => {
60+
const { result, changed: blockChanged } = formatTabsBlock(block)
61+
if (blockChanged) changed = true
62+
return result
63+
})
64+
65+
if (changed) {
66+
writeFileSync(file, updated)
67+
totalChanged++
68+
}
69+
}
70+
71+
if (totalChanged > 0) {
72+
// eslint-disable-next-line no-console
73+
console.log(`Formatted tabs in ${String(totalChanged)} file(s).`)
74+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { SdkName } from '@seamapi/blueprint'
2+
3+
/**
4+
* Canonical tab order for code samples across all documentation.
5+
* Used by the API reference codegen (api-endpoint.ts) and the
6+
* format-code-sample-tabs script to keep guide tabs in sync.
7+
*/
8+
export const supportedSdkOrder: SdkName[] = [
9+
'javascript',
10+
'curl',
11+
'python',
12+
'ruby',
13+
'php',
14+
'csharp',
15+
'java',
16+
'go',
17+
'seam_cli',
18+
]
19+
20+
/**
21+
* Maps SdkName values to their canonical display titles as used in
22+
* GitBook tab headers. Sourced from @seamapi/blueprint CodeSample titles.
23+
*/
24+
export const sdkDisplayTitle: Record<SdkName, string> = {
25+
javascript: 'JavaScript',
26+
curl: 'cURL',
27+
python: 'Python',
28+
ruby: 'Ruby',
29+
php: 'PHP',
30+
csharp: 'C#',
31+
java: 'Java',
32+
go: 'Go',
33+
seam_cli: 'Seam CLI',
34+
}
35+
36+
/** Reverse lookup: display title → canonical index. */
37+
export const tabTitleOrder: Map<string, number> = new Map(
38+
supportedSdkOrder.map((sdk, i) => [sdkDisplayTitle[sdk], i]),
39+
)
40+
41+
/** Known non-canonical title variants that should be normalized. */
42+
export const tabTitleFixes: Record<string, string> = {
43+
'cURL (bash)': 'cURL',
44+
Bash: 'cURL',
45+
Javascript: 'JavaScript',
46+
javascript: 'JavaScript',
47+
}

codegen/lib/layout/api-endpoint.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
} from '@seamapi/blueprint'
1414
import { capitalCase } from 'change-case'
1515

16+
import { supportedSdkOrder } from '../code-sample-tab-order.js'
1617
import type { PathMetadata } from '../path-metadata.js'
1718
import {
1819
type ApiRouteResource,
@@ -23,14 +24,18 @@ import {
2324
resourceSampleFilter,
2425
} from './api-route.js'
2526

26-
const supportedSdks: SdkName[] = [
27+
const apiReferenceSdks = new Set<SdkName>([
2728
'javascript',
2829
'curl',
2930
'python',
3031
'ruby',
3132
'php',
3233
'seam_cli',
33-
]
34+
])
35+
36+
const supportedSdks = supportedSdkOrder.filter((sdk) =>
37+
apiReferenceSdks.has(sdk),
38+
)
3439

3540
export interface ApiEndpointLayoutContext {
3641
description: string

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"lint": "eslint .",
2525
"postlint": "prettier --check --ignore-path .prettierignore .",
2626
"format": "prettier --write --ignore-path .prettierignore .",
27-
"preformat": "eslint --fix ."
27+
"preformat": "eslint --fix .",
28+
"format-code-sample-tabs": "tsx codegen/format-code-sample-tabs.ts"
2829
},
2930
"engines": {
3031
"node": ">=22.11.0",

0 commit comments

Comments
 (0)