Skip to content

Commit c986794

Browse files
committed
feat(docs): fail CI when generated integration docs are stale
1 parent be20df9 commit c986794

8 files changed

Lines changed: 119 additions & 34 deletions

File tree

apps/docs/content/docs/en/integrations/ashby.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ Trigger workflow when a new job is created
14931493
|`title` | string | Job title |
14941494
|`confidential` | boolean | Whether the job is confidential |
14951495
|`status` | string | Job status \(Open, Closed, Draft, Archived\) |
1496-
|`employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract\) |
1496+
|`employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
14971497

14981498

14991499
---

apps/docs/content/docs/en/integrations/logrocket.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,4 @@ Register a release version in LogRocket so uploaded source maps can decode stack
188188
| --------- | ---- | ----------- |
189189
| `version` | string | Release version that was registered |
190190

191+

apps/docs/content/docs/en/integrations/netsuite.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Manage NetSuite records, queries, datasets, batches, and async jobs
55

66
import { BlockInfoCard } from "@/components/ui/block-info-card"
77

8-
<BlockInfoCard
8+
<BlockInfoCard
99
type="netsuite"
1010
color="#FFFFFF"
1111
/>

apps/docs/content/docs/en/integrations/snowflake.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Query data and manage warehouses and tasks in Snowflake
55

66
import { BlockInfoCard } from "@/components/ui/block-info-card"
77

8-
<BlockInfoCard
8+
<BlockInfoCard
99
type="snowflake"
1010
color="#FFFFFF"
1111
/>

apps/docs/content/docs/en/integrations/zoho_desk.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Manage Zoho Desk tickets, comments, threads, and contacts
55

66
import { BlockInfoCard } from "@/components/ui/block-info-card"
77

8-
<BlockInfoCard
8+
<BlockInfoCard
99
type="zoho_desk"
1010
color="#FFFFFF"
1111
/>
@@ -522,3 +522,4 @@ Trigger a workflow when a Zoho Desk event occurs (ticket, comment, thread, conta
522522
| `orgId` | string | Zoho Desk organization ID |
523523
| `payload` | json | The full resource that changed \(ticket, comment, thread, etc.\). Comment and thread events gain a derived plain-text `contentText` alongside the raw `content` + `contentType`; ticket events gain `descriptionText` alongside `description`. |
524524
| `prevState` | json | Previous state of the resource \(update events only\) |
525+

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"tool-metadata:generate": "bun run scripts/sync-tool-metadata.ts",
6363
"tool-metadata:check": "bun run scripts/sync-tool-metadata.ts --check",
6464
"integration-catalog:check": "bun run scripts/check-integration-catalog.ts",
65+
"docs:check": "bun run scripts/generate-docs.ts --check",
6566
"mship-tools:generate": "bun run scripts/sync-tool-catalog.ts",
6667
"mship-tools:check": "bun run scripts/sync-tool-catalog.ts --check",
6768
"trace-spans-contract:generate": "bun run scripts/sync-trace-spans-contract.ts",

scripts/generate-docs.ts

Lines changed: 111 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -358,23 +358,71 @@ interface IconRef {
358358
source: string
359359
}
360360

361+
/**
362+
* Check mode (`--check`): render every generated artifact in memory and compare
363+
* it against the committed file instead of writing, so CI can fail on docs
364+
* drift the same way `tool-metadata:check` fails on stale tool metadata. Check
365+
* mode performs no filesystem mutations.
366+
*
367+
* The pipeline writes some pages twice per run — the block pass writes the base
368+
* page, then the trigger pass reads it back and appends/merges the Triggers
369+
* section — so check mode keeps an in-memory overlay of everything "written"
370+
* this run (`emittedByPath`), readers consult the overlay before disk
371+
* (`readGeneratedFile`), and staleness is judged once at the end against each
372+
* artifact's FINAL content. Comparing at emit time would flag the intermediate
373+
* block-pass content of every trigger-owning page as a false positive.
374+
*
375+
* Known limitation: `updateMetaJson` derives the sidebar from the mdx files on
376+
* disk, so in check mode a brand-new block's missing page is reported directly
377+
* while the corresponding meta.json entry is not — regenerating fixes both.
378+
*/
379+
let CHECK_ONLY = false
380+
const staleArtifacts: string[] = []
381+
const emittedByPath = new Map<string, string>()
382+
383+
/** Writes a generated artifact, or in check mode records its final content for the end-of-run comparison. */
384+
function emitGeneratedFile(filePath: string, content: string): void {
385+
if (CHECK_ONLY) {
386+
emittedByPath.set(filePath, content)
387+
return
388+
}
389+
fs.writeFileSync(filePath, content)
390+
}
391+
392+
/** Reads a generated artifact as the pipeline would see it mid-run: overlay first in check mode, then disk. */
393+
function readGeneratedFile(filePath: string): string | null {
394+
const emitted = emittedByPath.get(filePath)
395+
if (emitted !== undefined) return emitted
396+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null
397+
}
398+
399+
/** Compares every overlay entry against the committed file; returns repo-relative stale paths. */
400+
function collectStaleEmissions(): string[] {
401+
const stale: string[] = []
402+
for (const [filePath, content] of emittedByPath) {
403+
const committed = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null
404+
if (committed !== content) stale.push(path.relative(rootDir, filePath))
405+
}
406+
return stale
407+
}
408+
361409
/**
362410
* Copy the icons.tsx file from the main sim app to the docs app
363411
* This ensures icons are rendered consistently across both apps
364412
*/
365413
function copyIconsFile(): void {
366414
try {
367-
console.log('Copying icons from sim app to docs app...')
415+
if (!CHECK_ONLY) console.log('Copying icons from sim app to docs app...')
368416

369417
if (!fs.existsSync(ICONS_PATH)) {
370418
console.error(`Source icons file not found: ${ICONS_PATH}`)
371419
return
372420
}
373421

374422
const iconsContent = fs.readFileSync(ICONS_PATH, 'utf-8')
375-
fs.writeFileSync(DOCS_ICONS_PATH, iconsContent)
423+
emitGeneratedFile(DOCS_ICONS_PATH, iconsContent)
376424

377-
console.log('✓ Icons successfully copied to docs app')
425+
if (!CHECK_ONLY) console.log('✓ Icons successfully copied to docs app')
378426
} catch (error) {
379427
console.error('Error copying icons file:', error)
380428
}
@@ -579,8 +627,8 @@ ${mappingEntries}
579627
}
580628
`
581629

582-
fs.writeFileSync(iconMappingPath, content)
583-
console.log('✓ Icon mapping file written to docs app')
630+
emitGeneratedFile(iconMappingPath, content)
631+
if (!CHECK_ONLY) console.log('✓ Icon mapping file written to docs app')
584632
} catch (error) {
585633
console.error('Error writing icon mapping:', error)
586634
}
@@ -938,8 +986,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
938986
${mappingEntries}
939987
}
940988
`
941-
fs.writeFileSync(iconMappingPath, content)
942-
console.log('✓ Integration icon mapping written')
989+
emitGeneratedFile(iconMappingPath, content)
990+
if (!CHECK_ONLY) console.log('✓ Integration icon mapping written')
943991
} catch (error) {
944992
console.error('Error writing integration icon mapping:', error)
945993
}
@@ -1122,6 +1170,11 @@ async function writeIntegrationsJson(iconMapping: Record<string, IconRef>): Prom
11221170
return
11231171
}
11241172

1173+
if (CHECK_ONLY) {
1174+
staleArtifacts.push(path.relative(rootDir, jsonPath))
1175+
return
1176+
}
1177+
11251178
const updatedAt = new Date().toISOString().slice(0, 10)
11261179
fs.writeFileSync(jsonPath, `${serialize({ updatedAt, integrations })}\n`)
11271180
console.log(`✓ Integration data written: ${integrations.length} integrations → ${jsonPath}`)
@@ -3117,10 +3170,7 @@ async function generateBlockDoc(blockPath: string) {
31173170
const displayType = stripVersionSuffix(blockConfig.type)
31183171
const outputFilePath = path.join(DOCS_OUTPUT_PATH, `${displayType}.mdx`)
31193172

3120-
let existingContent: string | null = null
3121-
if (fs.existsSync(outputFilePath)) {
3122-
existingContent = fs.readFileSync(outputFilePath, 'utf-8')
3123-
}
3173+
const existingContent = readGeneratedFile(outputFilePath)
31243174

31253175
const manualSections = existingContent ? extractManualContent(existingContent) : {}
31263176

@@ -3131,10 +3181,14 @@ async function generateBlockDoc(blockPath: string) {
31313181
finalContent = mergeWithManualContent(markdown, existingContent, manualSections)
31323182
}
31333183

3134-
fs.writeFileSync(outputFilePath, finalContent)
3135-
const logType =
3136-
displayType !== blockConfig.type ? `${displayType} (from ${blockConfig.type})` : displayType
3137-
console.log(`✓ Generated docs for ${logType}`)
3184+
emitGeneratedFile(outputFilePath, finalContent)
3185+
if (!CHECK_ONLY) {
3186+
const logType =
3187+
displayType !== blockConfig.type
3188+
? `${displayType} (from ${blockConfig.type})`
3189+
: displayType
3190+
console.log(`✓ Generated docs for ${logType}`)
3191+
}
31383192
}
31393193
} catch (error) {
31403194
console.error(`Error processing ${blockPath}:`, error)
@@ -3376,6 +3430,13 @@ function cleanupStaleToolDocs(validToolDocs: Set<string>): void {
33763430
continue
33773431
}
33783432

3433+
if (CHECK_ONLY) {
3434+
staleArtifacts.push(
3435+
`${path.relative(rootDir, docPath)} (stale page — regeneration would delete it)`
3436+
)
3437+
continue
3438+
}
3439+
33793440
fs.unlinkSync(docPath)
33803441
console.log(`✓ Removed stale tool doc: ${blockType}.mdx`)
33813442
removedCount++
@@ -3900,14 +3961,16 @@ async function generateAllTriggerDocs(): Promise<void> {
39003961
continue
39013962
}
39023963

3903-
const existing = fs.existsSync(outputFilePath)
3904-
? fs.readFileSync(outputFilePath, 'utf-8')
3905-
: null
3964+
const existing = readGeneratedFile(outputFilePath)
39063965

39073966
if (existing?.includes('\n## Actions')) {
39083967
// Actions page generated this run by the block pass — append the Triggers section.
39093968
if (!existing.includes('\n## Triggers')) {
3910-
fs.appendFileSync(outputFilePath, `\n${buildTriggersSection(triggers)}`)
3969+
if (CHECK_ONLY) {
3970+
emittedByPath.set(outputFilePath, `${existing}\n${buildTriggersSection(triggers)}`)
3971+
} else {
3972+
fs.appendFileSync(outputFilePath, `\n${buildTriggersSection(triggers)}`)
3973+
}
39113974
}
39123975
} else {
39133976
// Trigger-only service (no actions block) — (re)write the standalone page,
@@ -3923,13 +3986,15 @@ async function generateAllTriggerDocs(): Promise<void> {
39233986
Object.keys(manualSections).length > 0
39243987
? mergeWithManualContent(markdown, existing, manualSections)
39253988
: markdown
3926-
fs.writeFileSync(outputFilePath, finalContent)
3989+
emitGeneratedFile(outputFilePath, finalContent)
39273990
}
39283991

39293992
generatedProviders.push(blockType)
3930-
console.log(
3931-
`✓ Triggers for ${formatTriggerProviderName(provider)} (${triggers.length} trigger${triggers.length === 1 ? '' : 's'})`
3932-
)
3993+
if (!CHECK_ONLY) {
3994+
console.log(
3995+
`✓ Triggers for ${formatTriggerProviderName(provider)} (${triggers.length} trigger${triggers.length === 1 ? '' : 's'})`
3996+
)
3997+
}
39333998
}
39343999

39354000
console.log(`✓ Trigger sections merged into ${generatedProviders.length} integration pages`)
@@ -3990,21 +4055,37 @@ function updateMetaJson() {
39904055
pages: items,
39914056
}
39924057

3993-
fs.writeFileSync(metaJsonPath, `${JSON.stringify(metaJson, null, 2)}\n`)
3994-
console.log(`Updated meta.json with ${items.length} entries`)
4058+
emitGeneratedFile(metaJsonPath, `${JSON.stringify(metaJson, null, 2)}\n`)
4059+
if (!CHECK_ONLY) console.log(`Updated meta.json with ${items.length} entries`)
39954060
}
39964061

39974062
if (import.meta.main) {
3998-
console.log('Starting documentation generator...')
4063+
CHECK_ONLY = process.argv.includes('--check')
4064+
console.log(
4065+
CHECK_ONLY
4066+
? 'Checking generated documentation freshness...'
4067+
: 'Starting documentation generator...'
4068+
)
39994069
generateAllBlockDocs()
40004070
.then((success) => {
4001-
if (success) {
4002-
console.log('Documentation generation completed successfully')
4003-
process.exit(0)
4004-
} else {
4071+
if (!success) {
40054072
console.error('Documentation generation failed')
40064073
process.exit(1)
40074074
}
4075+
if (CHECK_ONLY) {
4076+
const stale = [...collectStaleEmissions(), ...staleArtifacts]
4077+
if (stale.length > 0) {
4078+
console.error(
4079+
`Generated integration docs are stale:\n- ${stale.join('\n- ')}\n` +
4080+
'Run `bun run scripts/generate-docs.ts` and commit the result.'
4081+
)
4082+
process.exit(1)
4083+
}
4084+
console.log('✓ Generated integration docs are in sync')
4085+
process.exit(0)
4086+
}
4087+
console.log('Documentation generation completed successfully')
4088+
process.exit(0)
40084089
})
40094090
.catch((error) => {
40104091
console.error('Fatal error:', error)

scripts/run-audits.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const EXCLUDED: Record<string, string> = {
2727
const EXTRA_AUDITS = [
2828
'tool-metadata:check',
2929
'integration-catalog:check',
30+
'docs:check',
3031
'skills:check',
3132
'agent-stream-docs:check',
3233
] as const

0 commit comments

Comments
 (0)