From e69f2c4b1239541495b6ebe95599604570d4395d Mon Sep 17 00:00:00 2001 From: Steve Gontzes Date: Tue, 24 Mar 2026 10:46:56 -0400 Subject: [PATCH] Compile MDX documentation to HTML during release and send to registry API The registry UI renders documentation via dangerouslySetInnerHTML from pre-compiled HTML to avoid CSP unsafe-eval restrictions. This adds the compilation step to the release workflow so new releases get both raw MDX and compiled HTML. - Add mdx-compile.mjs and package.json to cmd/record-release for Node deps - Add "Compile documentation to HTML" workflow step (runs before record-release) - Add -docs-html flag to record-release binary - Add documentationHtml field to RecordReleaseRequest --- .github/workflows/release.yaml | 23 ++++ cmd/record-release/main.go | 91 +++++++------ cmd/record-release/mdx-compile.mjs | 204 +++++++++++++++++++++++++++++ cmd/record-release/package.json | 11 ++ 4 files changed, 291 insertions(+), 38 deletions(-) create mode 100755 cmd/record-release/mdx-compile.mjs create mode 100644 cmd/record-release/package.json diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2b35388..d49fc65 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1331,6 +1331,23 @@ jobs: mkdir -p _output echo "$MERGED_MANIFEST" | jq . > _output/manifest.json + - name: Compile documentation to HTML + id: compile-docs + if: steps.read-docs.outputs.has_docs == 'true' + working-directory: _workflows + run: | + cd cmd/record-release && npm ci --ignore-scripts 2>/dev/null + cd ../.. + node cmd/record-release/mdx-compile.mjs < ../_connector/docs/connector.mdx > /tmp/docs.html 2>/dev/null + if [ -s /tmp/docs.html ]; then + echo "has_html=true" >> "$GITHUB_OUTPUT" + echo "Compiled docs to HTML ($(wc -c < /tmp/docs.html) bytes)" + else + echo "has_html=false" >> "$GITHUB_OUTPUT" + echo "MDX compilation produced empty output, skipping HTML" + fi + continue-on-error: true + - name: Record release via registry API if: steps.registry-oidc.outcome == 'success' working-directory: _workflows @@ -1342,6 +1359,11 @@ jobs: DOCS_FLAG="-docs ../_connector/docs/connector.mdx" fi + DOCS_HTML_FLAG="" + if [ "${{ steps.compile-docs.outputs.has_html }}" = "true" ]; then + DOCS_HTML_FLAG="-docs-html /tmp/docs.html" + fi + CHANGELOG_FLAG="" if [ -s /tmp/changelog.md ]; then CHANGELOG_FLAG="-changelog /tmp/changelog.md" @@ -1372,6 +1394,7 @@ jobs: -workflow-run-id "${{ github.run_id }}" \ -registry-url "https://dist.conductorone.com" \ $DOCS_FLAG \ + $DOCS_HTML_FLAG \ $CHANGELOG_FLAG \ $CONFIG_SCHEMA_FLAG \ $CAPABILITIES_FLAG \ diff --git a/cmd/record-release/main.go b/cmd/record-release/main.go index 642c37b..3dbd659 100644 --- a/cmd/record-release/main.go +++ b/cmd/record-release/main.go @@ -19,21 +19,22 @@ import ( // RecordReleaseRequest is the JSON body sent to the registry API. type RecordReleaseRequest struct { - Org string `json:"org"` - Name string `json:"name"` - Version string `json:"version"` - RepositoryURL string `json:"repositoryUrl"` - CommitSha string `json:"commitSha"` - WorkflowRunID string `json:"workflowRunId"` - Documentation string `json:"documentation,omitempty"` - Changelog string `json:"changelog,omitempty"` - ConfigSchema string `json:"configSchema,omitempty"` - Capabilities string `json:"capabilities,omitempty"` - SignatureURL string `json:"signatureUrl,omitempty"` - CertificateURL string `json:"certificateUrl,omitempty"` - Assets map[string]*ReleaseAsset `json:"assets,omitempty"` - Images map[string]*ReleaseImage `json:"images,omitempty"` - ReleasedAt string `json:"releasedAt,omitempty"` + Org string `json:"org"` + Name string `json:"name"` + Version string `json:"version"` + RepositoryURL string `json:"repositoryUrl"` + CommitSha string `json:"commitSha"` + WorkflowRunID string `json:"workflowRunId"` + Documentation string `json:"documentation,omitempty"` + DocumentationHTML string `json:"documentationHtml,omitempty"` + Changelog string `json:"changelog,omitempty"` + ConfigSchema string `json:"configSchema,omitempty"` + Capabilities string `json:"capabilities,omitempty"` + SignatureURL string `json:"signatureUrl,omitempty"` + CertificateURL string `json:"certificateUrl,omitempty"` + Assets map[string]*ReleaseAsset `json:"assets,omitempty"` + Images map[string]*ReleaseImage `json:"images,omitempty"` + ReleasedAt string `json:"releasedAt,omitempty"` } // ReleaseAsset is the transformed asset for the registry API. @@ -71,14 +72,15 @@ func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { func main() { var ( - manifestPath string - docsPath string - org string - name string - version string - repositoryURL string - commitSha string - workflowRunID string + manifestPath string + docsPath string + docsHTMLPath string + org string + name string + version string + repositoryURL string + commitSha string + workflowRunID string registryURL string changelogPath string configSchemaPath string @@ -88,6 +90,7 @@ func main() { flag.StringVar(&manifestPath, "manifest", "", "Path to merged manifest.json file (required)") flag.StringVar(&docsPath, "docs", "", "Path to docs/connector.mdx file (optional)") + flag.StringVar(&docsHTMLPath, "docs-html", "", "Path to pre-compiled HTML documentation file (optional)") flag.StringVar(&org, "org", "", "GitHub organization (required)") flag.StringVar(&name, "name", "", "Repository/connector name (required)") flag.StringVar(&version, "version", "", "Release version tag (required)") @@ -172,6 +175,17 @@ func main() { } } + // Read optional pre-compiled HTML documentation + var documentationHTML string + if docsHTMLPath != "" { + htmlBytes, err := os.ReadFile(docsHTMLPath) + if err != nil { + fmt.Fprintf(os.Stderr, "record-release: warning: could not read docs-html file: %v\n", err) + } else { + documentationHTML = string(htmlBytes) + } + } + // Read optional changelog / release notes var changelog string if changelogPath != "" { @@ -234,21 +248,22 @@ func main() { // Build request body req := &RecordReleaseRequest{ - Org: org, - Name: name, - Version: version, - RepositoryURL: repositoryURL, - CommitSha: commitSha, - WorkflowRunID: workflowRunID, - Documentation: documentation, - Changelog: changelog, - ConfigSchema: configSchema, - Capabilities: capabilities, - SignatureURL: manifest.GetSignatureHref(), - CertificateURL: manifest.GetCertificateHref(), - Assets: assets, - Images: images, - ReleasedAt: releasedAt, + Org: org, + Name: name, + Version: version, + RepositoryURL: repositoryURL, + CommitSha: commitSha, + WorkflowRunID: workflowRunID, + Documentation: documentation, + DocumentationHTML: documentationHTML, + Changelog: changelog, + ConfigSchema: configSchema, + Capabilities: capabilities, + SignatureURL: manifest.GetSignatureHref(), + CertificateURL: manifest.GetCertificateHref(), + Assets: assets, + Images: images, + ReleasedAt: releasedAt, } bodyBytes, err := json.Marshal(req) diff --git a/cmd/record-release/mdx-compile.mjs b/cmd/record-release/mdx-compile.mjs new file mode 100755 index 0000000..2541f78 --- /dev/null +++ b/cmd/record-release/mdx-compile.mjs @@ -0,0 +1,204 @@ +#!/usr/bin/env node +// +// MDX-to-HTML compiler for connector documentation. +// +// Reads MDX from stdin, compiles it to static HTML using the same Mintlify +// component mappings as the registry UI, writes HTML to stdout. +// +// Usage: +// cat docs/connector.mdx | node ui/mdx-compile.mjs +// echo "$MDX_CONTENT" | node ui/mdx-compile.mjs +// +// Exit codes: +// 0 - success (HTML on stdout) +// 1 - compilation error (message on stderr) + +import { compile, run } from "@mdx-js/mdx"; +import * as runtime from "react/jsx-runtime"; +import React from "react"; +import ReactDOMServer from "react-dom/server"; +import remarkGfm from "remark-gfm"; +import remarkFrontmatter from "remark-frontmatter"; + +// ── Mintlify component mappings (HTML equivalents) ────────────────── + +function Tip({ children }) { + return React.createElement( + "div", + { className: "mdx-alert mdx-alert-tip" }, + React.createElement("div", { className: "mdx-alert-icon" }, "\u2139\uFE0F"), + React.createElement("div", { className: "mdx-alert-content" }, children), + ); +} + +function Warning({ children }) { + return React.createElement( + "div", + { className: "mdx-alert mdx-alert-warning" }, + React.createElement("div", { className: "mdx-alert-icon" }, "\u26A0\uFE0F"), + React.createElement("div", { className: "mdx-alert-content" }, children), + ); +} + +function Note({ children }) { + return React.createElement( + "div", + { className: "mdx-alert mdx-alert-note" }, + React.createElement("div", { className: "mdx-alert-icon" }, "\u2139\uFE0F"), + React.createElement("div", { className: "mdx-alert-content" }, children), + ); +} + +function Icon({ icon, color }) { + if (icon === "square-check") { + return React.createElement( + "span", + { + className: "mdx-icon mdx-icon-check", + style: { color: color || "#4caf50" }, + }, + "\u2611", + ); + } + return null; +} + +function Frame({ children, caption }) { + return React.createElement( + "div", + { className: "mdx-frame" }, + children, + caption + ? React.createElement( + "div", + { className: "mdx-frame-caption" }, + caption, + ) + : null, + ); +} + +function Card({ children, title }) { + return React.createElement( + "div", + { className: "mdx-card" }, + title ? React.createElement("h4", null, title) : null, + children, + ); +} + +function Tabs({ children }) { + const childArray = Array.isArray(children) ? children : [children]; + const tabs = childArray.filter((c) => c?.props?.title); + + if (tabs.length === 0) return React.createElement(React.Fragment, null, children); + + return React.createElement( + "div", + { className: "mdx-tabs" }, + React.createElement( + "div", + { className: "mdx-tabs-nav", role: "tablist" }, + tabs.map((tab, i) => + React.createElement( + "button", + { + key: i, + className: `mdx-tab-btn${i === 0 ? " mdx-tab-active" : ""}`, + "data-tab-index": i, + role: "tab", + type: "button", + }, + tab.props.title, + ), + ), + ), + tabs.map((tab, i) => + React.createElement( + "div", + { + key: i, + className: `mdx-tab-panel${i === 0 ? " mdx-tab-visible" : ""}`, + "data-tab-index": i, + role: "tabpanel", + }, + tab.props.children, + ), + ), + ); +} + +function Tab({ children }) { + return React.createElement(React.Fragment, null, children); +} + +function Steps({ children }) { + const childArray = Array.isArray(children) ? children : [children]; + const steps = childArray.filter((c) => c?.props); + return React.createElement( + "ol", + { className: "mdx-steps" }, + steps.map((child, i) => + React.createElement( + "li", + { key: i, className: "mdx-step" }, + child?.props?.children, + ), + ), + ); +} + +function Step({ children }) { + return React.createElement(React.Fragment, null, children); +} + +// ── Component map ─────────────────────────────────────────────────── + +const components = { + Tip, + Warning, + Note, + Icon, + Frame, + Card, + Tabs, + Tab, + Steps, + Step, +}; + +// ── Main ──────────────────────────────────────────────────────────── + +async function main() { + let content = ""; + for await (const chunk of process.stdin) { + content += chunk; + } + + if (!content.trim()) { + process.exit(0); + } + + try { + const compiled = await compile(content, { + outputFormat: "function-body", + remarkPlugins: [remarkGfm, remarkFrontmatter], + }); + + const { default: MDXContent } = await run(String(compiled), { + ...runtime, + baseUrl: "file:///", + }); + + const html = ReactDOMServer.renderToStaticMarkup( + React.createElement(MDXContent, { components }), + ); + + process.stdout.write(html); + } catch (err) { + process.stderr.write(`mdx-compile: ${err.message}\n`); + process.exit(1); + } +} + +main(); diff --git a/cmd/record-release/package.json b/cmd/record-release/package.json new file mode 100644 index 0000000..4387fb2 --- /dev/null +++ b/cmd/record-release/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@mdx-js/mdx": "^3.1.1", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.1" + } +}