diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 13f96abe16..33293849ec 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -114,6 +114,9 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} LINEAR_TOKEN: ${{ secrets.LINEAR_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # vsce needs VSCE_PAT to authenticate to the VS Code Marketplace + # during the vscode-ext step of the core chain. + VSCE_PAT: ${{ secrets.VSCE_PAT }} GITHUB_REF_NAME: stable run: node scripts/release-local-stable.mjs diff --git a/.github/workflows/release-vscode-ext.yml b/.github/workflows/release-vscode-ext.yml index c4e0771bb2..e378ac9654 100644 --- a/.github/workflows/release-vscode-ext.yml +++ b/.github/workflows/release-vscode-ext.yml @@ -1,11 +1,14 @@ -# 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. +# Note: VS Code Marketplace doesn't support semver prerelease versions, so +# main pushes only build a .vsix and attach it to the GitHub release. name: 📦 Release vscode-ext on: push: branches: - main - - stable paths: - 'apps/vscode-ext/**' - 'packages/superdoc/**' diff --git a/scripts/__tests__/release-local.test.mjs b/scripts/__tests__/release-local.test.mjs index e394bbbc03..a25f0b4f54 100644 --- a/scripts/__tests__/release-local.test.mjs +++ b/scripts/__tests__/release-local.test.mjs @@ -176,11 +176,12 @@ test('stable orchestrator prunes before snapshot and reports would-release previ ); }); -test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (superdoc, react) in order', async () => { +test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (superdoc, react, vscode-ext) 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)'); + assertOrder(content, "name: 'react'", "name: 'vscode-ext'", 'scripts/release-local-stable.mjs (react before vscode-ext)'); 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', @@ -189,10 +190,14 @@ test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (s content.includes("name: 'react'"), 'scripts/release-local-stable.mjs: orchestrator must release react after superdoc so consumers see them ship together', ); + assert.ok( + content.includes("name: 'vscode-ext'") && content.includes("vsCodeExtensionId: 'superdoc-dev.superdoc-vscode-ext'"), + 'scripts/release-local-stable.mjs: orchestrator must release vscode-ext with its marketplace extension id so publishComplete checks the marketplace, not npm', + ); assert.equal( - content.includes("name: 'esign'") || content.includes("name: 'template-builder'") || content.includes("name: 'vscode-ext'"), + content.includes("name: 'esign'") || content.includes("name: 'template-builder'"), false, - 'scripts/release-local-stable.mjs: vscode-ext, esign, and template-builder are added in follow-up PRs', + 'scripts/release-local-stable.mjs: esign and template-builder are not yet brought into the orchestrator', ); }); @@ -263,13 +268,12 @@ test('stable release workflows serialize on the shared release-stable concurrenc ); // Per-package workflows that still auto-fire on stable directly. - // 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. + // superdoc, react, and vscode-ext 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-esign.yml', '.github/workflows/release-template-builder.yml', - '.github/workflows/release-vscode-ext.yml', ]; for (const file of perPackageStableWorkflows) { const content = await readRepoFile(file); @@ -284,6 +288,7 @@ test('stable release workflows serialize on the shared release-stable concurrenc const orchestratorOnlyOnStable = [ '.github/workflows/release-superdoc.yml', '.github/workflows/release-react.yml', + '.github/workflows/release-vscode-ext.yml', ]; for (const file of orchestratorOnlyOnStable) { const content = await readRepoFile(file); diff --git a/scripts/release-local-stable.mjs b/scripts/release-local-stable.mjs index 888c10e3e8..f985f3abbd 100644 --- a/scripts/release-local-stable.mjs +++ b/scripts/release-local-stable.mjs @@ -16,12 +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 (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. + * Core chain (superdoc -> react -> vscode-ext): + * superdoc is the npm core; react consumes it; vscode-ext bundles the + * editor and ships a .vsix to the VS Code Marketplace. They release in + * order so downstream packages are 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. * * Per-package adapters live on the descriptor (resumePublish, * preparePythonSnapshot). The recovery engine is generic; new packages @@ -421,11 +421,14 @@ async function generateGitHubReleaseNotes({ tag, targetCommit, previousTag }) { } function getExpectedReleaseAssets(pkg, workspaceRoot) { - if (pkg.name !== 'vscode-ext') { + // Only vscode-ext currently has release assets (the .vsix). Other + // descriptors don't set vsCodeExtensionId, so they get an empty list and + // ensureGitHubReleaseAssets becomes a no-op. + if (!pkg.vsCodeExtensionId) { return []; } - const extensionDir = join(workspaceRoot, 'apps/vscode-ext'); + const extensionDir = join(workspaceRoot, pkg.packageCwd); let assets = readdirSync(extensionDir) .filter((entry) => entry.endsWith('.vsix')) .map((entry) => join(extensionDir, entry)); @@ -477,7 +480,7 @@ function isGitHubReleaseComplete(pkg, release) { return false; } - if (pkg.name === 'vscode-ext') { + if (pkg.vsCodeExtensionId) { return Array.isArray(release.assets) && release.assets.some((asset) => asset.name.endsWith('.vsix')); } @@ -506,7 +509,7 @@ async function ensureGitHubRelease(pkg, { tag, targetCommit, previousTag, worksp }); } - if (pkg.name === 'vscode-ext') { + if (pkg.vsCodeExtensionId) { await ensureGitHubReleaseAssets(release, pkg, workspaceRoot); release = await getGitHubReleaseByTag(tag); } @@ -680,6 +683,27 @@ function prepareSdkPythonSnapshot(workspaceRoot, tag) { return copySdkPythonArtifacts(workspaceRoot, tag); } +function resumeVscodeExtPublish(workspaceRoot, _distTag, options = {}) { + const { skipBuild = workspaceRoot === REPO_ROOT } = options; + // The vscode-ext webview imports `superdoc` and `superdoc/style.css`, both + // of which resolve to packages/superdoc/dist. In a tagged snapshot only + // `pnpm install` has run, so build superdoc first or esbuild's webview + // bundling fails to resolve those imports. In REPO_ROOT the workflow's + // `Build packages` step already produced the dist. + if (!skipBuild) { + runInWorkspace(workspaceRoot, 'pnpm', ['run', 'build:superdoc']); + } + // VS Code Marketplace publish needs the VSCE_PAT env var (passed in by the + // orchestrator workflow). `pnpm run package` recreates the .vsix from + // whatever is on disk and `vsce publish --skip-duplicate` is idempotent + // against the marketplace, so recovery and primary release use the same + // commands. The .vsix asset upload to the GitHub release is handled by + // ensureGitHubReleaseAssets earlier in recoverPackageRelease. + const extensionRoot = join(workspaceRoot, 'apps/vscode-ext'); + runInWorkspace(extensionRoot, 'pnpm', ['run', 'package']); + runInWorkspace(extensionRoot, 'pnpm', ['run', 'publish:vsce']); +} + function resumeReactPublish(workspaceRoot, distTag, options = {}) { const { skipBuild = workspaceRoot === REPO_ROOT } = options; // react's `prepublishOnly` runs `vite build`, whose `dts` plugin rolls up @@ -901,6 +925,18 @@ const packages = [ npmPackages: ['@superdoc-dev/react'], resumePublish: resumeReactPublish, }, + { + name: 'vscode-ext', + chain: 'core', + packageCwd: 'apps/vscode-ext', + tagPrefix: 'vscode-v', + tagPattern: 'vscode-v*', + // VS Code Marketplace is the publish target instead of npm; the script's + // generic release-state inspection already special-cases vsCodeExtensionId + // for publishComplete and .vsix asset upload. + vsCodeExtensionId: 'superdoc-dev.superdoc-vscode-ext', + resumePublish: resumeVscodeExtPublish, + }, ]; /**