Files
vectorize-io__hindsight/hindsight-docs/scripts/check-integrations.mjs
Nicolò Boschi 6f2799569f docs: make every integration changelog reachable, and compact the Integrations Hub (#4005)
* docs: link every integration changelog from its doc page

The release script has written a changelog page per integration since 0.4.20
(src/pages/changelog/integrations/<slug>.md), but nothing reliably linked them:
16 of 61 doc pages carried a "View Changelog" link, and the only index was a
hand-written table at the bottom of /changelog listing 6 of the 48 pages, stale
since the release that introduced them. coding-agents was one of the 31 pages
with a changelog nobody could reach except by guessing the URL.

Adds the link to the 30 pages missing one, plus a third invariant in
check-integrations.mjs so the next one can't ship unlinked: every changelog page
whose integration has a gallery entry must be linked from its doc page.

coding-agents.md is generated, so its link goes in the README as an absolute
hindsight.vectorize.io URL — the sync script rewrites our own absolute URLs to
site-relative, so it resolves on npm, GitHub and the docs site. It sits outside
the skill:begin regions, so SKILL.md is unchanged.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN

* docs(integrations): compact grouped gallery with Docs/Changelog buttons

The Integrations Hub listed 60 three-line cards with a repeated "Hindsight Team"
byline and an OFFICIAL badge on 52 of them, running 6092px tall — scanning it
meant scrolling past what you came for. Cards are now a compact row (icon, name,
two-line description) grouped by category, with the featured three using the same
row tinted rather than a second card design: 4352px, and the shape of the
catalogue is visible at a glance.

Each card carries Docs and Changelog buttons. Which integrations have a changelog
comes from a new build-time plugin that indexes
src/pages/changelog/integrations/*.md and joins integrations.json on the link
slug (vercel-ai-sdk -> /sdks/integrations/ai-sdk -> ai-sdk.md), so an integration
grows the button on its first release with nothing to update by hand.

The card is a <div> now — nested anchors are invalid — so the name and the pills
are the links, not the whole card. /changelog's first line points here instead of
carrying its own listing.

Also drops the 14 CSS classes the old card owned, and shares CATEGORY_LABELS /
groupByCategory from src/lib/integrations.ts so the taxonomy has one definition.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN
2026-09-02 10:21:08 +02:00

148 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Integrations single-source-of-truth guardrails.
*
* src/data/integrations.json is the single source: it drives the /integrations
* gallery and (via the swizzled DocPage/Layout/Sidebar component) the
* Integrations sidebar category on every docs version. This script enforces the
* two invariants that keep it honest:
*
* 1. Forward — every entry with an internal `/sdks/integrations/<slug>` link
* has a doc page at docs-integrations/<slug>.{md,mdx}. (The sidebar is
* injected at render time, so it isn't covered by Docusaurus' build-time
* broken-link check — this is what catches a missing page.)
*
* 3. Discoverable — every integration with a changelog page links to it from
* its doc page. The changelog pages are written by the release script and
* were otherwise reachable only by guessing the URL.
*
* 2. Reverse — every *released* integration (a published git tag
* `integrations/<name>/vX.Y.Z`) appears in integrations.json, so a release
* can't silently skip the gallery/sidebar. Degrades to a skip when tags
* aren't available (shallow checkout); CI fetches tags (fetch-depth: 0).
*
* Run: node scripts/check-integrations.mjs
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const docsDir = join(__dirname, '..');
const integrationsJson = join(docsDir, 'src', 'data', 'integrations.json');
const integrationsDocsDir = join(docsDir, 'docs-integrations');
const changelogDir = join(docsDir, 'src', 'pages', 'changelog', 'integrations');
// Released integrations that intentionally have no gallery/doc page.
// cloudflare-oauth-proxy is internal infrastructure (an OAuth proxy Worker,
// `"private": true`), not a user-facing framework integration.
// coding-agents is released but deliberately unlisted for now: the package ships
// so it can be installed and tested end to end, while its page stays out of the
// gallery and sidebar until it is announced. Remove from this set (and restore
// its integrations.json entry + drop `unlisted` from the doc page) to publish it.
const EXCLUDED = new Set(['cloudflare-oauth-proxy']);
const { integrations } = JSON.parse(readFileSync(integrationsJson, 'utf8'));
const internal = integrations.filter((entry) => entry.link.startsWith('/sdks/integrations/'));
let failed = false;
// ─── 1. Forward: every internal entry has a doc page ──────────────────────────
const missingPages = [];
for (const entry of internal) {
const slug = entry.link.replace('/sdks/integrations/', '');
const hasDoc = ['md', 'mdx'].some((ext) => existsSync(join(integrationsDocsDir, `${slug}.${ext}`)));
if (!hasDoc) {
missingPages.push({ id: entry.id, slug });
}
}
if (missingPages.length > 0) {
failed = true;
console.error('[integrations] ❌ integrations.json entries with no doc page:\n');
for (const { id, slug } of missingPages) {
console.error(` ${id} — expected docs-integrations/${slug}.{md,mdx}`);
}
console.error('\nAdd the doc page, or remove or externalize the entry in integrations.json.\n');
} else {
console.log(`[integrations] ✅ All ${internal.length} integration entries have a doc page.`);
}
// ─── 2. Reverse: every released integration is in integrations.json ───────────
const documented = new Set(internal.map((entry) => entry.link.replace('/sdks/integrations/', '')));
function releasedIntegrations() {
let raw;
try {
raw = execFileSync('git', ['tag', '-l', 'integrations/*'], { encoding: 'utf8' });
} catch {
return null; // git unavailable
}
const names = new Set();
for (const tag of raw.split('\n')) {
const m = tag.match(/^integrations\/(.+)\/v\d/);
if (m) names.add(m[1]);
}
return names;
}
const released = releasedIntegrations();
if (released === null || released.size === 0) {
console.warn(
'[integrations] ⚠️ No integration tags found (shallow checkout?). ' +
'Skipping reverse check — fetch tags (fetch-depth: 0) to enforce in CI.',
);
} else {
const missingEntries = [...released]
.filter((name) => !EXCLUDED.has(name) && !documented.has(name))
.sort();
if (missingEntries.length > 0) {
failed = true;
console.error('[integrations] ❌ Released integrations missing from integrations.json:\n');
for (const name of missingEntries) {
console.error(` ${name} — released as integrations/${name}/vX.Y.Z but no entry in integrations.json`);
}
console.error(
'\nAdd each to integrations.json (with an internal `link`), or — if not user-facing — ' +
'add it to the EXCLUDED set in this script.\n',
);
} else {
console.log(`[integrations] ✅ All ${released.size - EXCLUDED.size} released integrations are present in integrations.json.`);
}
}
// ─── 3. Discoverable: a changelog page is linked from its doc page ────────────
const changelogSlugs = readdirSync(changelogDir)
.filter((f) => f.endsWith('.md') && f !== 'index.md')
.map((f) => f.slice(0, -'.md'.length));
const unlinked = [];
let checkedChangelogs = 0;
for (const slug of changelogSlugs) {
if (!documented.has(slug)) continue; // no gallery entry ⇒ not user-facing (see EXCLUDED)
checkedChangelogs++;
const docPath = ['md', 'mdx']
.map((ext) => join(integrationsDocsDir, `${slug}.${ext}`))
.find((p) => existsSync(p));
if (!docPath) continue; // already reported by check 1
if (!readFileSync(docPath, 'utf8').includes(`/changelog/integrations/${slug}`)) {
unlinked.push(slug);
}
}
if (unlinked.length > 0) {
failed = true;
console.error('[integrations] ❌ Changelog pages not linked from their doc page:\n');
for (const slug of unlinked) {
console.error(` ${slug} — add [View Changelog →](/changelog/integrations/${slug})`);
}
console.error(
'\nThe /changelog listing finds these pages automatically, but the doc page is where ' +
'someone using the integration looks first.\n',
);
} else {
console.log(`[integrations] ✅ All ${checkedChangelogs} changelog pages are linked from their doc page.`);
}
process.exit(failed ? 1 : 0);