Skip to content

Commit df769e7

Browse files
committed
feat(docs): fail CI when generated integration docs are stale
1 parent 852906e commit df769e7

8 files changed

Lines changed: 123 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: 3 additions & 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
/>
@@ -651,3 +651,5 @@ Retrieve REST web-services concurrency limits for the NetSuite account and integ
651651
|`accountUnallocatedConcurrencyLimit` | number | Account concurrency not allocated to integrations |
652652
|`integrationConcurrencyLimit` | number | Concurrency allocated to this integration |
653653
|`integrationLimitType` | string | Limit assignment: integrationSpecific, accountLimit, or internal |
654+
655+

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

Lines changed: 3 additions & 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
/>
@@ -1316,3 +1316,5 @@ Call a stored procedure with explicitly typed Snowflake bindings.
13161316
|`rowsDeleted` | number | Rows deleted by the statement |
13171317
|`duplicateRowsUpdated` | number | Duplicate rows updated by the statement |
13181318
|`rowsAffected` | number | Total inserted, updated, and deleted rows |
1319+
1320+

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}`)
@@ -3041,10 +3094,7 @@ async function generateBlockDoc(blockPath: string) {
30413094
const displayType = stripVersionSuffix(blockConfig.type)
30423095
const outputFilePath = path.join(DOCS_OUTPUT_PATH, `${displayType}.mdx`)
30433096

3044-
let existingContent: string | null = null
3045-
if (fs.existsSync(outputFilePath)) {
3046-
existingContent = fs.readFileSync(outputFilePath, 'utf-8')
3047-
}
3097+
const existingContent = readGeneratedFile(outputFilePath)
30483098

30493099
const manualSections = existingContent ? extractManualContent(existingContent) : {}
30503100

@@ -3055,10 +3105,14 @@ async function generateBlockDoc(blockPath: string) {
30553105
finalContent = mergeWithManualContent(markdown, existingContent, manualSections)
30563106
}
30573107

3058-
fs.writeFileSync(outputFilePath, finalContent)
3059-
const logType =
3060-
displayType !== blockConfig.type ? `${displayType} (from ${blockConfig.type})` : displayType
3061-
console.log(`✓ Generated docs for ${logType}`)
3108+
emitGeneratedFile(outputFilePath, finalContent)
3109+
if (!CHECK_ONLY) {
3110+
const logType =
3111+
displayType !== blockConfig.type
3112+
? `${displayType} (from ${blockConfig.type})`
3113+
: displayType
3114+
console.log(`✓ Generated docs for ${logType}`)
3115+
}
30623116
}
30633117
} catch (error) {
30643118
console.error(`Error processing ${blockPath}:`, error)
@@ -3300,6 +3354,13 @@ function cleanupStaleToolDocs(validToolDocs: Set<string>): void {
33003354
continue
33013355
}
33023356

3357+
if (CHECK_ONLY) {
3358+
staleArtifacts.push(
3359+
`${path.relative(rootDir, docPath)} (stale page — regeneration would delete it)`
3360+
)
3361+
continue
3362+
}
3363+
33033364
fs.unlinkSync(docPath)
33043365
console.log(`✓ Removed stale tool doc: ${blockType}.mdx`)
33053366
removedCount++
@@ -3824,14 +3885,16 @@ async function generateAllTriggerDocs(): Promise<void> {
38243885
continue
38253886
}
38263887

3827-
const existing = fs.existsSync(outputFilePath)
3828-
? fs.readFileSync(outputFilePath, 'utf-8')
3829-
: null
3888+
const existing = readGeneratedFile(outputFilePath)
38303889

38313890
if (existing?.includes('\n## Actions')) {
38323891
// Actions page generated this run by the block pass — append the Triggers section.
38333892
if (!existing.includes('\n## Triggers')) {
3834-
fs.appendFileSync(outputFilePath, `\n${buildTriggersSection(triggers)}`)
3893+
if (CHECK_ONLY) {
3894+
emittedByPath.set(outputFilePath, `${existing}\n${buildTriggersSection(triggers)}`)
3895+
} else {
3896+
fs.appendFileSync(outputFilePath, `\n${buildTriggersSection(triggers)}`)
3897+
}
38353898
}
38363899
} else {
38373900
// Trigger-only service (no actions block) — (re)write the standalone page,
@@ -3847,13 +3910,15 @@ async function generateAllTriggerDocs(): Promise<void> {
38473910
Object.keys(manualSections).length > 0
38483911
? mergeWithManualContent(markdown, existing, manualSections)
38493912
: markdown
3850-
fs.writeFileSync(outputFilePath, finalContent)
3913+
emitGeneratedFile(outputFilePath, finalContent)
38513914
}
38523915

38533916
generatedProviders.push(blockType)
3854-
console.log(
3855-
`✓ Triggers for ${formatTriggerProviderName(provider)} (${triggers.length} trigger${triggers.length === 1 ? '' : 's'})`
3856-
)
3917+
if (!CHECK_ONLY) {
3918+
console.log(
3919+
`✓ Triggers for ${formatTriggerProviderName(provider)} (${triggers.length} trigger${triggers.length === 1 ? '' : 's'})`
3920+
)
3921+
}
38573922
}
38583923

38593924
console.log(`✓ Trigger sections merged into ${generatedProviders.length} integration pages`)
@@ -3914,21 +3979,37 @@ function updateMetaJson() {
39143979
pages: items,
39153980
}
39163981

3917-
fs.writeFileSync(metaJsonPath, `${JSON.stringify(metaJson, null, 2)}\n`)
3918-
console.log(`Updated meta.json with ${items.length} entries`)
3982+
emitGeneratedFile(metaJsonPath, `${JSON.stringify(metaJson, null, 2)}\n`)
3983+
if (!CHECK_ONLY) console.log(`Updated meta.json with ${items.length} entries`)
39193984
}
39203985

39213986
if (import.meta.main) {
3922-
console.log('Starting documentation generator...')
3987+
CHECK_ONLY = process.argv.includes('--check')
3988+
console.log(
3989+
CHECK_ONLY
3990+
? 'Checking generated documentation freshness...'
3991+
: 'Starting documentation generator...'
3992+
)
39233993
generateAllBlockDocs()
39243994
.then((success) => {
3925-
if (success) {
3926-
console.log('Documentation generation completed successfully')
3927-
process.exit(0)
3928-
} else {
3995+
if (!success) {
39293996
console.error('Documentation generation failed')
39303997
process.exit(1)
39313998
}
3999+
if (CHECK_ONLY) {
4000+
const stale = [...collectStaleEmissions(), ...staleArtifacts]
4001+
if (stale.length > 0) {
4002+
console.error(
4003+
`Generated integration docs are stale:\n- ${stale.join('\n- ')}\n` +
4004+
'Run `bun run scripts/generate-docs.ts` and commit the result.'
4005+
)
4006+
process.exit(1)
4007+
}
4008+
console.log('✓ Generated integration docs are in sync')
4009+
process.exit(0)
4010+
}
4011+
console.log('Documentation generation completed successfully')
4012+
process.exit(0)
39324013
})
39334014
.catch((error) => {
39344015
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)