diff --git a/.github/workflows/release-react.yml b/.github/workflows/release-react.yml index 6249177163..a669b88ad5 100644 --- a/.github/workflows/release-react.yml +++ b/.github/workflows/release-react.yml @@ -1,11 +1,12 @@ -# Auto-releases on push to main (@next) and stable (@latest). +# Auto-releases on push to main (@next). +# Stable releases are orchestrated centrally by release-stable.yml so that +# every stable release shares one concurrency slot and one git push lane. name: 📦 Release react on: push: branches: - main - - stable paths: # React declares `superdoc` in dependencies (not peerDependencies), so # existing consumers with lockfiles won't pick up a new core version diff --git a/scripts/__tests__/release-local.test.mjs b/scripts/__tests__/release-local.test.mjs index c1e8c81b82..e394bbbc03 100644 --- a/scripts/__tests__/release-local.test.mjs +++ b/scripts/__tests__/release-local.test.mjs @@ -176,18 +176,23 @@ test('stable orchestrator prunes before snapshot and reports would-release previ ); }); -test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (superdoc) in order', async () => { +test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (superdoc, react) in order', async () => { const content = await readRepoFile('scripts/release-local-stable.mjs'); assertOrder(content, "name: 'cli'", "name: 'sdk'", 'scripts/release-local-stable.mjs (cli before sdk)'); assertOrder(content, "name: 'sdk'", "name: 'mcp'", 'scripts/release-local-stable.mjs (sdk before mcp)'); + assertOrder(content, "name: 'superdoc'", "name: 'react'", 'scripts/release-local-stable.mjs (superdoc before react)'); assert.ok( content.includes("name: 'superdoc'"), 'scripts/release-local-stable.mjs: orchestrator must release superdoc so the v* tag drives docs-stable promotion in the same workflow', ); + assert.ok( + content.includes("name: 'react'"), + 'scripts/release-local-stable.mjs: orchestrator must release react after superdoc so consumers see them ship together', + ); assert.equal( - content.includes("name: 'esign'") || content.includes("name: 'react'") || content.includes("name: 'template-builder'") || content.includes("name: 'vscode-ext'"), + content.includes("name: 'esign'") || content.includes("name: 'template-builder'") || content.includes("name: 'vscode-ext'"), false, - 'scripts/release-local-stable.mjs: react, vscode-ext, esign, and template-builder are added in follow-up PRs', + 'scripts/release-local-stable.mjs: vscode-ext, esign, and template-builder are added in follow-up PRs', ); }); @@ -258,11 +263,10 @@ test('stable release workflows serialize on the shared release-stable concurrenc ); // Per-package workflows that still auto-fire on stable directly. - // superdoc is excluded because release-stable.yml drives its stable - // releases now. The remaining workflows have not yet been brought into - // the orchestrator. + // superdoc and react are excluded because release-stable.yml drives + // their stable releases now. The remaining workflows have not yet been + // brought into the orchestrator. const perPackageStableWorkflows = [ - '.github/workflows/release-react.yml', '.github/workflows/release-esign.yml', '.github/workflows/release-template-builder.yml', '.github/workflows/release-vscode-ext.yml', @@ -275,14 +279,20 @@ test('stable release workflows serialize on the shared release-stable concurrenc ); } - // superdoc no longer auto-fires on stable - the orchestrator is its - // single stable release path. - const superdocWorkflow = await readRepoFile('.github/workflows/release-superdoc.yml'); - assert.equal( - /branches:\s*\n\s*-\s*main\s*\n\s*-\s*stable/.test(superdocWorkflow), - false, - '.github/workflows/release-superdoc.yml: stable releases are driven by release-stable.yml; this workflow only fires on main', - ); + // Workflows that no longer auto-fire on stable - the orchestrator is + // their single stable release path. + const orchestratorOnlyOnStable = [ + '.github/workflows/release-superdoc.yml', + '.github/workflows/release-react.yml', + ]; + for (const file of orchestratorOnlyOnStable) { + const content = await readRepoFile(file); + assert.equal( + /branches:\s*\n\s*-\s*main\s*\n\s*-\s*stable/.test(content), + false, + `${file}: stable releases are driven by release-stable.yml; this workflow only fires on main`, + ); + } }); test('MCP releaserc builds the package before publish so the tarball ships dist/', async () => { diff --git a/scripts/release-local-stable.mjs b/scripts/release-local-stable.mjs index 13a4893a0a..888c10e3e8 100644 --- a/scripts/release-local-stable.mjs +++ b/scripts/release-local-stable.mjs @@ -16,11 +16,12 @@ * These three share artifacts (SDK packages CLI native binaries; MCP * imports SDK + engine code), so they must release in this order. * - * Core chain: - * Currently superdoc only. react and vscode-ext still ship from their - * per-package stable workflows; pulling them into this chain is a - * separate refactor and is what makes docs-stable promotion (keyed off - * superdoc's v* tag) live in this workflow. + * Core chain (superdoc -> react): + * superdoc is the npm core; react consumes it. They release in order so + * react is never published against an older superdoc than what just + * shipped. docs-stable promotion is keyed off superdoc's v* tag and + * lives in this workflow as a result. vscode-ext still ships from its + * per-package stable workflow and joins the chain in a separate refactor. * * Per-package adapters live on the descriptor (resumePublish, * preparePythonSnapshot). The recovery engine is generic; new packages @@ -679,6 +680,25 @@ function prepareSdkPythonSnapshot(workspaceRoot, tag) { return copySdkPythonArtifacts(workspaceRoot, tag); } +function resumeReactPublish(workspaceRoot, distTag, options = {}) { + const { skipBuild = workspaceRoot === REPO_ROOT } = options; + // react's `prepublishOnly` runs `vite build`, whose `dts` plugin rolls up + // types imported from `superdoc`. That import resolves through + // packages/superdoc/dist via the workspace symlink. The snapshot only ran + // `pnpm install`, so build superdoc first; in REPO_ROOT it is already on + // disk from the workflow's `Build packages` step. + if (!skipBuild) { + runInWorkspace(workspaceRoot, 'pnpm', ['run', 'build:superdoc']); + } + runInWorkspace(workspaceRoot, 'node', [ + join(workspaceRoot, 'scripts/npm-publish-package.cjs'), + '--package-dir', + 'packages/react', + '--tag', + distTag, + ]); +} + function resumeSuperdocPublish(workspaceRoot, distTag, options = {}) { const { skipBuild = workspaceRoot === REPO_ROOT } = options; const args = [join(workspaceRoot, 'scripts/publish-superdoc.cjs'), '--dist-tag', distTag]; @@ -872,6 +892,15 @@ const packages = [ npmPackages: SUPERDOC_NPM_PACKAGES, resumePublish: resumeSuperdocPublish, }, + { + name: 'react', + chain: 'core', + packageCwd: 'packages/react', + tagPrefix: 'react-v', + tagPattern: 'react-v*', + npmPackages: ['@superdoc-dev/react'], + resumePublish: resumeReactPublish, + }, ]; /**