docs: vendor and stage Mattermost Agents docs for Docusaurus build (#37627)

* docs: vendor and stage Mattermost Agents docs for Docusaurus build

The migration plan externalized Agents links, but the Docusaurus sidebar
generator already declares an `agents` TOP_LEVEL category and stub pages
link to `/agents/docs/*` paths with no content behind them. Vendor the
mattermost-plugin-agents docs/ folder as a submodule and stage it into
main/agents/docs/ at build time (mirroring how Sphinx handles the same
submodule via conf.py excludes), so the Agents sidebar category and
internal links resolve to real, versioned content instead of dead ends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: strip leading license comment before MDX-staging Agents docs

Three feature pages (channel_summaries.md, custom_prompts.md,
managing_agents.md) lead with a raw <!-- Copyright ... --> HTML comment
before the # Title line, which broke both title extraction (regex only
matched a title at the very start of the file) and the Docusaurus build
(MDX doesn't parse bare HTML comments the way Markdown does, so it failed
to compile with "Unexpected character '!'"). Strip the license comment
before further processing instead of trying to convert it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: fail loudly on missing Agents submodule, fix usage comment path

- A missing/uninitialized vendor/mattermost-plugin-agents submodule
  previously logged a warning and returned a successful zero-count
  result, letting CI/prestart/prebuild continue with an empty (or, on a
  reused workspace, stale) Agents section instead of failing. Now it
  clears any previously staged output first, then throws so the build
  fails clearly when the submodule isn't present.
- Fixed the usage comment referencing the nonexistent docs-site/scripts
  path instead of the actual docs/site/scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: checkout submodules in docs-ci.yaml build check

This is the third docs-building workflow (alongside docs-cd.yml and
docs-preview-template.yml) that runs npm run build on docs/site, and it
was missed when submodules: true was added to the other two — its
"Build docs site" step failed on this PR because
vendor/mattermost-plugin-agents wasn't checked out, which
stage-agents-docs.mjs now correctly treats as a hard failure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: inline Agents content instead of a separate top-level LHS section

Sphinx never gives Agents its own top-level nav entry: it `.. include::`s
admin_guide.md/user_guide.md directly into the existing curated pages
(administration-guide/configure/agents-admin-guide.rst and
end-user-guide/agents.rst), and only nests a handful of files
(providers, aws_bedrock_setup, sovereign_ai, usage_tips) as small hidden-
toctree children of those same two pages. load-testing.md,
upgrading_to_2.0.md, and all of features/*.md are never referenced in any
Sphinx toctree at all.

The previous vendoring approach staged all 12 files as a brand-new,
disconnected top-level "Agents" category, so reaching the actual guide
meant leaving the curated page, opening an unrelated top-level section,
and drilling into "Docs" — the opposite of what Sphinx does today.

This reproduces Sphinx's structure instead:
- stage-agents-docs.mjs now categorizes the 12 vendored files: admin_guide/
  user_guide are staged twice — once as a normal (but `unlisted: true`)
  doc for direct-link parity with Sphinx's orphan pages, and once as a
  Docusaurus Markdown partial (leading underscore, auto-excluded from
  routing/sidebars) for inline import. providers/aws_bedrock_setup/
  sovereign_ai/usage_tips are staged as normal listed docs. Everything
  else is staged unlisted, matching Sphinx never surfacing them in nav.
- Added generic relative-markdown-link rewriting (mirroring the existing
  image-path rewriting) so cross-links between vendored files
  (`../admin_guide.md#anchor`, `features/channel_summaries.md`, etc.)
  resolve correctly regardless of nesting depth or where the linking
  content ends up rendered.
- agents-admin-guide.mdx and end-user-guide/agents.mdx now `import` and
  render the partials inline instead of linking out, and no longer link
  to a separate "Docs" section.
- gen-documentation-sidebar.mjs: removed the standalone `agents`
  TOP_LEVEL category. Added `{doc: '<full id>'}` support to
  buildAdminConfigureItem for cross-directory group items, and a new
  ADMIN_CONFIGURE_GROUPS.agents group nesting providers/aws_bedrock_setup/
  sovereign_ai under agents-admin-guide. Added a standalone
  promoteDocToCategory helper (End User Guide has no full manual-grouping
  override) to nest usage_tips under end-user-guide/agents.
- Documented both patterns (cross-directory group items, Markdown-partial
  inlining) in docs/site/README.md's "Manual grouping overrides" section.

Validated: stage-agents-docs.mjs + gen-documentation-sidebar.mjs both run
clean with zero warnings; a scoped @mdx-js/mdx compile check across all
16 affected files (2 curated pages + 12 staged docs + 2 partials) passes
with zero failures. Full `docusaurus build` still needs the OpenAPI
prebuild's Go toolchain, unavailable in this sandbox — already covered by
docs-ci.yaml in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(agents): self-host the plugin-v2 upgrade guide link

Now that mattermost-plugin-agents/docs is vendored and staged into
/agents/docs/*, point the two "how to upgrade to Agents v2" references
(v11 changelog, important upgrade notes) at our own
/agents/docs/upgrading_to_2.0 page instead of an external GitHub blob
link — the same self-hosting fix already applied to the old dangling
sample-file links.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(agents): tighten stage-agents-docs.mjs header comment

Same explanation, fewer words — trims the file-level comment from ~45
to ~33 lines without dropping the rationale (why staging instead of a
direct submodule checkout, and how the three-way listed/unlisted/
inline-partial split reproduces Sphinx's nav).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Eva Sarafianou
2026-07-24 16:56:13 +03:00
committed by GitHub
co-authored by Cursor
parent 1cc20031fe
commit 5fbb2a32d6
13 changed files with 313 additions and 8 deletions
+1
View File
@@ -27,6 +27,7 @@ jobs:
- name: cd/checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: true
persist-credentials: false
- name: cd/setup-node
+1
View File
@@ -25,6 +25,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: true
persist-credentials: false
- name: Set up Node
+6
View File
@@ -193,6 +193,12 @@ docs/pdf/build/
docs/pdf/node_modules/
docs/site/openapi/
# Agents docs, staged from the mattermost-plugin-agents submodule at build
# time by docs/site/scripts/stage-agents-docs.mjs (see docs/vendor/) — never
# tracked as source, regenerated on every prestart/prebuild.
docs/main/agents/docs/
docs/site/static/images/agents/
# OpenAPI make build artifacts
api/v4/html/static/mattermost-openapi-v4.yaml
api/v4/html/index.html
+4
View File
@@ -0,0 +1,4 @@
[submodule "docs/vendor/mattermost-plugin-agents"]
path = docs/vendor/mattermost-plugin-agents
url = https://github.com/mattermost/mattermost-plugin-agents.git
branch = master
@@ -1,9 +1,11 @@
---
title: "Mattermost Agents Admin Guide"
---
import AgentsAdminGuideBody from '../../agents/docs/_admin_guide_partial.mdx';
<PlanAvailability slug="all-commercial" />
{/* TODO: include /agents/docs/admin_guide.md could not be resolved */}
<AgentsAdminGuideBody />
## Additional configuration guides
@@ -466,7 +466,7 @@ DROP INDEX CONCURRENTLY IF EXISTS idx_propertyfields_protected;</code></pre></td
<td>The <code>role_updated</code> WebSocket events are scoped to the affected team/channel instead of broadcasting globally, improving performance and reducing unnecessary network traffic. No manual action is required from administrators as the migration runs automatically during the upgrade. No downtime is expected, and no special planning is needed for the upgrade as the migration can run during normal operations. All installations are affected by this change, but the impact is minimal since the <code>roles</code> table is small with well under 10k rows even on large installations. The upgrade includes automatic database migrations to add a <code>schemeid</code> column to the <code>roles</code> table for efficient role-scheme lookups. The migrations use PostgreSQL's non-blocking techniques: first adding the column with a brief ACCESS EXCLUSIVE lock (catalog-only operation), then backfilling existing data with a ROW EXCLUSIVE lock, and finally creating an index concurrently with a SHARE UPDATE EXCLUSIVE lock that doesn't block other operations. The <code>roles</code> table size is bounded and unaffected by posts, reactions, or other high-volume data. The migrations are fully backwards-compatible.</td>
</tr>
<tr>
<td>v11.7 includes Agents plugin v2. Please see <a href="https://github.com/mattermost/mattermost-plugin-agents/blob/master/docs/upgrading_to_2.0.md">this guide</a> on how to upgrade the Mattermost Agents plugin from a v1.x release to v2.0.0. It covers the supported version path, the migrations that run automatically on first start of v2.0.0, the breaking changes and default-behavior flips that admins should know about before the upgrade window, and the verification steps to confirm the upgrade succeeded.</td>
<td>v11.7 includes Agents plugin v2. Please see <a href="/agents/docs/upgrading_to_2.0">this guide</a> on how to upgrade the Mattermost Agents plugin from a v1.x release to v2.0.0. It covers the supported version path, the migrations that run automatically on first start of v2.0.0, the breaking changes and default-behavior flips that admins should know about before the upgrade window, and the verification steps to confirm the upgrade succeeded.</td>
</tr>
<tr>
<td><p>v11.7.6 migration removes orphaned rows from the <code>threadmemberships</code> table — specifically, rows where the associated user is no longer a member of the channel that the thread belongs to. The cleanup is performed via a filtered <code>DELETE</code> using a three-way join across <code>threadmemberships</code>, <code>threads</code>, and <code>channelmembers</code>. No schema objects (tables, columns, or indexes) are added or removed; only data rows are affected. Note for large instances: this migration issues a single unbatched delete that scans the entirety of <code>threadmemberships</code> with a join against <code>threads</code> and <code>channelmembers</code>. On databases with tens of millions of rows or significant historical channel-membership churn, this query may run for an extended period, block autovacuum on <code>threadmemberships</code>, increase WAL pressure, and contribute to replication lag. Administrators of large deployments should test execution time on a representative dataset before upgrading and consider scheduling the upgrade during a low-traffic window. The down migration contains no rollback SQL because the deleted rows cannot be recovered; this migration is irreversible. The migrations are fully backwards-compatible and no database downtime is expected for this upgrade. The SQL queries included are:</p><pre><code>-- Drop ThreadMembership rows whose user is no longer a member of the thread's channel.
+3 -1
View File
@@ -1,9 +1,11 @@
---
title: "AI Agents"
---
import AgentsUserGuideBody from '../agents/docs/_user_guide_partial.mdx';
<PlanAvailability slug="all-commercial" />
{/* TODO: include /agents/docs/user_guide.md could not be resolved */}
<AgentsUserGuideBody />
<Note>
@@ -437,7 +437,7 @@ See [this blog post](https://mattermost.com/blog/mattermost-v11-8-0-is-now-avail
**Breaking Changes**
- FIPS builds require a minimum of 14 characters for passwords, atmos/camo proxy configuration, and shared channel secrets. Shorter passwords for existing users will no longer be valid and require a password reset. Non-FIPS builds are unaffected.
- v11.7 includes Agents plugin v2. Please see [this guide](https://github.com/mattermost/mattermost-plugin-agents/blob/master/docs/upgrading_to_2.0.md) on how to upgrade the Mattermost Agents plugin from a v1.x release to v2.0.0. It covers the supported version path, the migrations that run automatically on first start of v2.0.0, the breaking changes and default-behavior flips that admins should know about before the upgrade window, and the verification steps to confirm the upgrade succeeded.
- v11.7 includes Agents plugin v2. Please see [this guide](/agents/docs/upgrading_to_2.0) on how to upgrade the Mattermost Agents plugin from a v1.x release to v2.0.0. It covers the supported version path, the migrations that run automatically on first start of v2.0.0, the breaking changes and default-behavior flips that admins should know about before the upgrade window, and the verification steps to confirm the upgrade succeeded.
</Important>
+32
View File
@@ -83,6 +83,38 @@ leaving it at the root.
existing one (Integrations Guide is the simplest example), then wire it
into `main()` alongside the existing `dir === '...'` checks.
**Nesting a doc from one section under a page in another section:** most
group `items` are plain basenames relative to that section's own
directory, but `buildAdminConfigureItem` also accepts `{doc: '<full id>'}`
for cross-directory references (their label is read directly from the
target file's frontmatter via `docLabelById`, since it won't be in that
section's `leafLabels` map). This is how `ADMIN_CONFIGURE_GROUPS.agents`
nests the vendored Agents plugin pages (`main/agents/docs/`, staged by
`stage-agents-docs.mjs` — not one of the `TOP_LEVEL` sections, so it has
no top-level nav entry of its own) under
`administration-guide/configure/agents-admin-guide`. The same page is
also nested for End User Guide's `end-user-guide/agents` doc, but since
that section has no manual grouping override at all, it uses the smaller
standalone `promoteDocToCategory` helper instead of a full `*_GROUPS`
override — copy that pattern for other one-off single-doc nestings rather
than building a whole grouping override for a section that's otherwise
fine auto-generated.
**Inlining another doc's content onto a page** (rather than just linking
or nesting it): Docusaurus's built-in [Markdown
partials](https://docusaurus.io/docs/next/create-doc#markdown-partials)
feature — any `.md`/`.mdx` file with a leading underscore in its name is
excluded from the docs plugin's routing/sidebars and can be `import`ed
into another MDX file and rendered as `<Component />`. `stage-agents-docs.mjs`
uses this to reproduce Sphinx's `.. include:: /agents/docs/admin_guide.md`
behavior: it stages `admin_guide.md`/`user_guide.md` as normal (but
`unlisted: true`) docs for direct-link parity, *and* as
`_admin_guide_partial.mdx`/`_user_guide_partial.mdx` partials that
`administration-guide/configure/agents-admin-guide.mdx` and
`end-user-guide/agents.mdx` import and render inline — so those two pages
show the full vendored guide content directly, with zero extra clicks,
instead of just linking out to a separate page.
The API reference section (`docs/api/reference/`, also gitignored) has the
same requirement: `docusaurus-plugin-openapi-docs` needs `docusaurus
gen-api-docs mattermost` run before it has any pages to render. `prestart`
+3 -2
View File
@@ -6,12 +6,13 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"stage:agents-docs": "node scripts/stage-agents-docs.mjs",
"build:sidebars": "node scripts/gen-documentation-sidebar.mjs && node scripts/gen-developer-sidebar.mjs",
"build:openapi:spec": "node scripts/build-openapi.mjs",
"build:openapi:docs": "docusaurus gen-api-docs mattermost",
"build:openapi": "npm run build:openapi:spec && npm run build:openapi:docs",
"prestart": "npm run build:sidebars && ( [ -f openapi/mattermost-openapi-v4.yaml ] || npm run build:openapi:spec ) && npm run build:openapi:docs",
"prebuild": "npm run build:sidebars && npm run build:openapi",
"prestart": "npm run stage:agents-docs && npm run build:sidebars && ( [ -f openapi/mattermost-openapi-v4.yaml ] || npm run build:openapi:spec ) && npm run build:openapi:docs",
"prebuild": "npm run stage:agents-docs && npm run build:sidebars && npm run build:openapi",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -56,7 +56,6 @@ const TOP_LEVEL = [
{dir: 'end-user-guide', label: 'End User Guide'},
{dir: 'integrations-guide', label: 'Integrations Guide'},
{dir: 'get-help', label: 'Get Help'},
{dir: 'agents', label: 'Agents'},
];
// ---------------------------------------------------------------------------
@@ -380,6 +379,23 @@ const ADMIN_CONFIGURE_GROUPS = {
'optimize-your-workspace',
],
},
// Nests the Agents plugin's own provider/setup pages (vendored from the
// mattermost-plugin-agents submodule, staged by stage-agents-docs.mjs
// into main/agents/docs/) under the admin guide landing page, instead of
// a standalone top-level "Agents" section — mirrors Sphinx, which hides
// these behind a small toctree on administration-guide/configure/
// agents-admin-guide.rst rather than giving Agents its own nav entry.
// Items use the {doc: '<full id>'} form since they live outside
// administration-guide/configure/.
agents: {
label: 'AI Agents Configuration',
landing: 'agents-admin-guide',
items: [
{doc: 'agents/docs/providers'},
{doc: 'agents/docs/aws_bedrock_setup'},
{doc: 'agents/docs/sovereign_ai'},
],
},
};
// Top-level Configure order. Strings are doc basenames relative to
@@ -389,7 +405,7 @@ const ADMIN_CONFIGURE_GROUPS = {
const ADMIN_CONFIGURE_ORDER = [
{group: 'settingsReference'},
{group: 'search'},
'agents-admin-guide',
{group: 'agents'},
{group: 'calls'},
{group: 'storage'},
{group: 'email'},
@@ -903,11 +919,28 @@ function buildDeploymentSidebar(autoCat) {
// Administration Guide — builder (regroups the "Configure" sub-category).
// ---------------------------------------------------------------------------
// Resolves the label for a fully-qualified doc id (one that lives outside
// the section currently being built, e.g. an Agents doc nested under
// Administration Guide → Configure) by reading its own frontmatter
// directly, since it won't be present in that section's `leafLabels` map.
function docLabelById(id) {
for (const ext of ['.mdx', '.md']) {
const abs = join(SRC, `${id}${ext}`);
if (existsSync(abs)) {
return readFm(abs, 'sidebar_label') || readFm(abs, 'title') || humanize(id.split('/').pop());
}
}
throw new Error(`doc id not found on disk: ${id}`);
}
function buildAdminConfigureItem(spec, leafLabels) {
if (typeof spec === 'string') {
const id = `administration-guide/configure/${spec}`;
return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
}
if (spec.doc) {
return {type: 'doc', id: spec.doc, label: docLabelById(spec.doc)};
}
const g = ADMIN_CONFIGURE_GROUPS[spec.group];
if (!g) throw new Error(`unknown admin configure group: ${spec.group}`);
const items = g.items.map((it) => buildAdminConfigureItem(it, leafLabels));
@@ -1092,6 +1125,46 @@ function buildIntegrationsSidebar(autoCat) {
};
}
// ---------------------------------------------------------------------------
// End User Guide — nests the Agents plugin's usage-tips page under the
// existing "AI Agents" doc, the same way Configure nests Agents' admin-side
// pages (see ADMIN_CONFIGURE_GROUPS.agents above). End User Guide is
// otherwise fully filesystem-driven, so this is a narrow, targeted
// promotion rather than a full manual-grouping override.
// ---------------------------------------------------------------------------
// Finds the {type: 'doc', id: docId} leaf anywhere in `items` and replaces
// it in place with a category that links to that same doc and nests
// `children` (each a fully-qualified doc id) underneath it. Returns true if
// the promotion was applied, so callers can warn when it wasn't.
function promoteDocToCategory(items, docId, children) {
for (let i = 0; i < items.length; i++) {
const it = items[i];
if (it.type === 'doc' && it.id === docId) {
items[i] = {
type: 'category',
label: it.label,
collapsed: true,
link: {type: 'doc', id: docId},
items: children.map((childId) => ({type: 'doc', id: childId, label: docLabelById(childId)})),
};
return true;
}
if (it.type === 'category' && it.items && promoteDocToCategory(it.items, docId, children)) {
return true;
}
}
return false;
}
function buildEndUserGuideSidebar(autoCat) {
const promoted = promoteDocToCategory(autoCat.items, 'end-user-guide/agents', ['agents/docs/usage_tips']);
if (!promoted) {
console.warn('[sidebar] WARN: End User Guide "agents" doc not found — Agents usage-tips nesting was not applied.');
}
return autoCat;
}
// ---------------------------------------------------------------------------
// Entry point.
// ---------------------------------------------------------------------------
@@ -1115,6 +1188,8 @@ function main() {
cat = buildAdminGuideSidebar(cat);
} else if (dir === 'integrations-guide') {
cat = buildIntegrationsSidebar(cat);
} else if (dir === 'end-user-guide') {
cat = buildEndUserGuideSidebar(cat);
}
sidebar.push(cat);
}
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env node
// Stage the mattermost-plugin-agents submodule's docs/ subfolder into
// main/agents/docs/ so the two curated Agents pages (administration-guide/
// configure/agents-admin-guide.mdx and end-user-guide/agents.mdx) and a
// handful of nested reference pages have real content to link to.
//
// Why staging instead of checking out the submodule directly under docs/:
// submodules can't do a sparse "just this subfolder" checkout, and the repo
// also has README/LICENSE/Go source/etc. we don't want as pages. So it's
// vendored out-of-tree at vendor/mattermost-plugin-agents, and this script
// copies+transforms only its docs/** into main/agents/docs/ before the
// sidebar is generated — same effect as Sphinx's conf.py exclude/redirect
// rules on its own full-repo submodule checkout.
//
// Navigation mirrors Sphinx exactly (source/end-user-guide/agents.rst,
// source/administration-guide/configure/agents-admin-guide.rst): no
// top-level Agents nav section. admin_guide.md/user_guide.md are `..
// include::`d into the two curated pages; providers/aws_bedrock_setup/
// sovereign_ai/usage_tips get their own nested pages via a small hidden
// toctree; everything else (load-testing, upgrading_to_2.0, features/*) is
// direct-URL-only, never in any toctree. Reproduced as a three-way split:
// - INLINE_PARTIALS: staged as an unlisted doc (direct-link parity) AND
// as a Markdown partial (leading underscore) the curated pages
// `import` and render inline, reproducing `.. include::`.
// - NAV_CHILDREN: normal listed docs, nested under the curated pages by
// gen-documentation-sidebar.mjs via cross-directory {doc: ...} items.
// - Everything else: unlisted docs — built and linkable, absent from
// the sidebar.
//
// Re-run safely at any time (e.g. after `git submodule update --remote`) —
// it fully regenerates its output directories.
//
// Usage: node docs/site/scripts/stage-agents-docs.mjs
import {
readFileSync, writeFileSync, mkdirSync, readdirSync, statSync,
existsSync, rmSync, copyFileSync,
} from 'node:fs';
import {join, resolve, relative, dirname, extname, posix} from 'node:path';
import {fileURLToPath} from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const SITE_ROOT = resolve(HERE, '..');
const REPO_ROOT = resolve(SITE_ROOT, '..');
const VENDOR_DOCS = join(REPO_ROOT, 'vendor', 'mattermost-plugin-agents', 'docs');
const DEST_DOCS = join(REPO_ROOT, 'main', 'agents', 'docs');
const DEST_IMAGES = join(SITE_ROOT, 'static', 'images', 'agents');
// Files whose body is inlined into a curated page via a Markdown partial
// import, in addition to being staged as their own unlisted page.
const INLINE_PARTIALS = new Set(['admin_guide', 'user_guide']);
// Files that get their own listed page, nested under a curated page by
// gen-documentation-sidebar.mjs. Everything else staged ends up unlisted.
const NAV_CHILDREN = new Set(['providers', 'aws_bedrock_setup', 'sovereign_ai', 'usage_tips']);
function rmrf(p) {
if (existsSync(p)) rmSync(p, {recursive: true, force: true});
}
function walk(dir, exclude = []) {
const out = [];
for (const name of readdirSync(dir)) {
if (exclude.includes(name)) continue;
const abs = join(dir, name);
if (statSync(abs).isDirectory()) out.push(...walk(abs, exclude));
else out.push(abs);
}
return out;
}
// Several vendored files lead with a raw `<!-- Copyright ... -->` HTML
// comment. Plain Markdown tolerates that, but MDX doesn't parse bare HTML
// comments the same way (it wants `{/* ... */}`), so strip it before any
// further processing rather than trying to convert it — license headers
// aren't meaningful on rendered docs pages anyway.
function stripLeadingLicenseComment(body) {
return body.replace(/^<!--[\s\S]*?-->\r?\n\r?\n?/, '');
}
// Extract a leading `# Title` line as Docusaurus frontmatter `title`, since
// migrated pages elsewhere in main/ carry the title in frontmatter rather
// than as an in-body H1 (Docusaurus renders the frontmatter title as the
// page's H1 automatically).
function extractTitle(body) {
const m = body.match(/^# (.+)\r?\n\r?\n?/);
if (!m) return {title: null, body};
return {title: m[1].trim(), body: body.slice(m[0].length)};
}
// Vendored markdown references sibling images as `img/foo.png` or
// `../img/foo.png` (relative to its own location under docs/). Those PNGs
// are copied to static/images/agents/, so rewrite refs to the site's
// standard absolute `/images/<dir>/<file>` convention.
function rewriteImagePaths(body) {
return body.replace(/(!\[[^\]]*]\()(?:\.\.\/)?img\/([^)\s]+)(\))/g, '$1/images/agents/$2$3');
}
// Vendored markdown cross-links sibling docs with relative paths, e.g.
// `[...](../admin_guide.md#license-requirements)` or
// `[...](features/channel_summaries.md)`. Resolve those relative to the
// linking file's own directory under docs/ and rewrite to the site's
// absolute `/agents/docs/<id>` doc convention, so links keep working
// regardless of where the linking content ends up rendered (including
// when inlined into a curated page in a completely different directory).
function rewriteRelativeMdLinks(body, fileRelDir) {
return body.replace(/(\[[^\]]*]\()(?!https?:\/\/|\/|#)([^)\s#]+)\.md(#[^)\s]+)?(\))/g, (_m, pre, target, anchor, post) => {
const resolved = posix.normalize(posix.join(fileRelDir, target));
return `${pre}/agents/docs/${resolved}${anchor || ''}${post}`;
});
}
function stageDocs() {
// Clear previous output unconditionally, even on failure below, so a
// reused workspace (stale checkout, missing submodule init) never ships
// docs left over from a prior run instead of failing loudly.
rmrf(DEST_DOCS);
rmrf(DEST_IMAGES);
if (!existsSync(VENDOR_DOCS)) {
throw new Error(
`[stage-agents-docs] submodule content not found at ${VENDOR_DOCS}. ` +
'Run `git submodule update --init --remote docs/vendor/mattermost-plugin-agents` first.',
);
}
const mdFiles = walk(VENDOR_DOCS, ['img']).filter((f) => extname(f) === '.md');
let partialCount = 0;
for (const src of mdFiles) {
const rel = relative(VENDOR_DOCS, src); // e.g. "admin_guide.md" or "features/channel_summaries.md"
const relDir = posix.dirname(rel.replace(/\\/g, '/'));
const baseName = basenameNoExt(rel);
const raw = stripLeadingLicenseComment(readFileSync(src, 'utf8'));
const {title, body} = extractTitle(raw);
const transformed = rewriteRelativeMdLinks(rewriteImagePaths(body), relDir === '.' ? '' : relDir);
const dest = join(DEST_DOCS, rel);
mkdirSync(dirname(dest), {recursive: true});
const listed = NAV_CHILDREN.has(baseName);
const inlined = INLINE_PARTIALS.has(baseName);
const frontmatterLines = [];
if (title) frontmatterLines.push(`title: "${title.replace(/"/g, '\\"')}"`);
if (!listed) frontmatterLines.push('unlisted: true');
const frontmatter = frontmatterLines.length ? `---\n${frontmatterLines.join('\n')}\n---\n\n` : '';
writeFileSync(dest, frontmatter + transformed);
if (inlined) {
// Markdown partials (leading underscore) are auto-excluded from
// Docusaurus routing/sidebars and can be `import`ed + rendered
// inline elsewhere — this is what reproduces Sphinx's
// `.. include:: /agents/docs/*.md` behavior.
const partialDest = join(dirname(dest), `_${baseName}_partial.mdx`);
writeFileSync(partialDest, transformed);
partialCount++;
}
}
const imgDir = join(VENDOR_DOCS, 'img');
let imageCount = 0;
if (existsSync(imgDir)) {
mkdirSync(DEST_IMAGES, {recursive: true});
for (const name of readdirSync(imgDir)) {
copyFileSync(join(imgDir, name), join(DEST_IMAGES, name));
imageCount++;
}
}
return {files: mdFiles.length, partials: partialCount, images: imageCount};
}
function basenameNoExt(relPath) {
const base = posix.basename(relPath.replace(/\\/g, '/'));
return base.replace(/\.md$/, '');
}
const {files, partials, images} = stageDocs();
console.log(`[stage-agents-docs] staged ${files} doc(s) (${partials} with inline partials), ${images} image(s) into ${relative(REPO_ROOT, DEST_DOCS)}`);