-
Notifications
You must be signed in to change notification settings - Fork 669
Description
Rush Publishing v2: Decoupled Versioning and Plugin-Based Publishing
| Document Metadata | Details |
|---|---|
| Author(s) | Sean Larkin |
| Status | Draft (WIP) |
| Team / Owner | Rush Stack |
| Created / Last Updated | 2026-02-12 |
1. Executive Summary
This RFC proposes decoupling Rush's version bumping system from npm publishing and introducing a plugin-based publish architecture. Today, the shouldPublish flag in rush.json simultaneously gates version bumping (changelogs, semver increments) and npm publishing -- making it impossible for non-npm artifacts like VS Code extensions to participate in the rush change / rush version workflow. The proposed solution introduces: (1) decoupling shouldPublish from npm-only semantics, (2) a publishTarget array that routes publishing to one or more backends, and (3) a publish plugin system (registerPublishProviderFactory) modeled after the existing build cache plugin pattern. Provider configuration is riggable per-project via config/publish.json (following the config/rush-project.json pattern) rather than a global options file. The npm publish provider is always built-in, and publishTarget: ["npm"] is inferred when omitted for backward compatibility. This enables the 4 VS Code extensions in vscode-extensions/ to use rush change and rush version --bump while being published as VSIX files through a dedicated plugin rather than npm publish.
2. Context and Motivation
2.1 Current State
Rush's version management pipeline is a three-stage workflow:
rush change --> rush version --bump --> rush publish
(collect) (compute + apply) (distribute)
The first two stages (rush change and rush version --bump) are already npm-agnostic -- they operate on JSON change files, package.json version fields, and CHANGELOG generation without touching any package registry. The third stage (rush publish) is hardcoded to npm publish.
Research reference: [research/docs/2026-02-10-rush-version-bump-system-and-vs-code-extension-versioning.md, Section 8] documents that ChangeManager, VersionManager, ChangelogGenerator, VersionPolicyConfiguration, and PublishUtilities.findChangeRequestsAsync() contain zero npm-specific code.
The architectural separation already exists in the codebase:
| Operation | Version Calculation | npm Publishing |
|---|---|---|
rush version --bump |
Yes | No |
rush publish --include-all |
No | Yes |
rush publish (change-based) |
Yes | Yes |
However, participation in any of these workflows requires shouldPublish: true (or a versionPolicyName), which is the sole gating mechanism.
Architecture diagram (current state):
flowchart TB
classDef gate fill:#e53e3e,stroke:#c53030,stroke-width:2px,color:#ffffff,font-weight:600
classDef agnostic fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#ffffff,font-weight:600
classDef npmSpecific fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#ffffff,font-weight:600
classDef project fill:#4a90e2,stroke:#357abd,stroke-width:2px,color:#ffffff,font-weight:600
subgraph Projects["rush.json projects"]
NpmPkg["npm packages<br>shouldPublish: true"]:::project
VscodeExt["VS Code extensions<br>shouldPublish: (absent)"]:::project
end
Gate{{"shouldPublish<br>gate"}}:::gate
subgraph VersionPipeline["Version Pipeline (npm-agnostic)"]
RushChange["rush change<br>ChangeAction.ts:375"]:::agnostic
RushVersion["rush version --bump<br>VersionManager.bumpAsync()"]:::agnostic
Changelog["CHANGELOG generation<br>ChangelogGenerator.ts:288"]:::agnostic
end
subgraph PublishPipeline["Publish Pipeline (npm-coupled)"]
RushPublish["rush publish<br>PublishAction.ts:34"]:::npmSpecific
NpmPublish["npm publish<br>_npmPublishAsync():439"]:::npmSpecific
end
NpmPkg -->|"passes"| Gate
VscodeExt -->|"BLOCKED"| Gate
Gate --> VersionPipeline
VersionPipeline --> PublishPipeline
2.2 The Problem
User Impact: VS Code extension developers in the rushstack monorepo cannot use rush change to document their changes, rush version --bump to increment versions, or generate CHANGELOGs. Version management for the 4 extensions (rushstack, debug-certificate-manager, playwright-local-browser-server, @rushstack/rush-vscode-command-webview) must be done manually.
Ecosystem Impact: GitHub Issue #3342 documents demand for non-npm publishing targets. Any monorepo with artifacts that aren't npm packages (Docker images, VS Code extensions, NuGet packages, Python packages, mobile apps) faces this same limitation.
Technical Debt: The shouldPublish flag conflates two orthogonal concerns. Research reference: [research/docs/2026-02-10-rush-version-bump-system-and-vs-code-extension-versioning.md, Section 5] catalogs 10 locations where shouldPublish gates version-bumping behavior, and 3 additional locations where it gates npm-specific publishing behavior -- all through the same boolean.
The specific coupling points are:
| Location | File:Line | Gates |
|---|---|---|
| Change file creation | ChangeAction.ts:375 |
Version bumping |
| Empty change file creation | ChangeManager.ts:27 |
Version bumping |
| Version bump skip | PublishUtilities.ts:373 |
Version bumping |
| Package change write | PublishUtilities.ts:408 |
Version bumping |
| Dependency propagation | PublishUtilities.ts:502 |
Version bumping |
| Changelog generation | ChangelogGenerator.ts:288 |
Version bumping |
| Dependency change tracking | VersionManager.ts:330 |
Version bumping |
| npm publish execution | PublishAction.ts:372 |
npm publishing |
| Git tag creation | PublishAction.ts:426 |
npm publishing |
| npm config injection | PublishAction.ts:443 |
npm publishing |
3. Goals and Non-Goals
3.1 Functional Goals
- VS Code extensions can participate in the
rush changeworkflow (prompted for change descriptions/bump types) - VS Code extensions receive version bumps via
rush version --bump(updatedpackage.jsonversion, CHANGELOG generation) -
rush publishcan dispatch to different publish backends (npm, VSIX) based on project configuration - A new
registerPublishProviderFactoryAPI onRushSessionallows plugins to register custom publish providers - An npm publish plugin ships as the default (always built-in) publish provider, preserving backward compatibility
- A VSIX publish plugin integrates with the existing
heft-vscode-extension-plugininfrastructure - Projects can opt into versioning without opting into any automated publishing (version-only mode via
publishTarget: ["none"]) -
publishTargetsupports arrays, enabling a single project to publish to multiple targets (e.g.,["npm", "internal-registry"]) - All publish provider configuration is riggable per-project via
config/publish.json(no global options file) - VS Code extension versions always match their
package.jsonversion (managed byrush version --bump)
3.2 Non-Goals (Out of Scope)
- We will NOT change the change file format (JSON structure in
common/changes/) - We will NOT modify version policy types (lockstep/individual remain as-is)
- We will NOT build a generic artifact publishing framework (only npm and VSIX in this iteration)
- We will NOT migrate existing
shouldPublish: trueprojects -- they continue to work identically - We will NOT modify the
rush version --bumpcommand semantics -- only its gating logic - We will NOT create a new
rush.jsonschema version -- changes are additive - We will NOT address the
heft-vscode-extension-pluginbuild pipeline (packaging/signing) -- only the publish dispatch
4. Proposed Solution (High-Level Design)
4.1 System Architecture Diagram
flowchart TB
classDef gate fill:#5a67d8,stroke:#4c51bf,stroke-width:2px,color:#ffffff,font-weight:600
classDef agnostic fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#ffffff,font-weight:600
classDef plugin fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#ffffff,font-weight:600
classDef project fill:#4a90e2,stroke:#357abd,stroke-width:2px,color:#ffffff,font-weight:600
classDef config fill:#718096,stroke:#4a5568,stroke-width:2px,color:#ffffff,font-weight:600
subgraph Projects["rush.json projects"]
NpmPkg["npm packages<br>shouldPublish: true<br>(publishTarget inferred: npm)"]:::project
VscodeExt["VS Code extensions<br>shouldPublish: true<br>publishTarget: [vsix]"]:::project
MultiTarget["Multi-target projects<br>shouldPublish: true<br>publishTarget: [npm, vsix]"]:::project
VersionOnly["Version-only projects<br>shouldPublish: true<br>publishTarget: [none]"]:::project
end
subgraph VersionPipeline["Version Pipeline (unchanged, npm-agnostic)"]
RushChange["rush change"]:::agnostic
RushVersion["rush version --bump"]:::agnostic
Changelog["CHANGELOG generation"]:::agnostic
end
subgraph PublishDispatch["Publish Dispatch (new)"]
RushPublish["rush publish<br>(orchestrator)"]:::gate
subgraph Plugins["Publish Provider Plugins"]
NpmPlugin["npm publish plugin<br>(always built-in)"]:::plugin
VsixPlugin["vsix publish plugin<br>(autoinstaller)"]:::plugin
CustomPlugin["custom plugin<br>(future)"]:::plugin
end
end
subgraph Configuration["Per-Project Configuration (riggable)"]
PublishJson["config/publish.json<br>(riggable via rig system)"]:::config
RushSession["RushSession<br>registerPublishProviderFactory()"]:::config
end
Projects --> VersionPipeline
VersionPipeline --> PublishDispatch
RushPublish -->|"publishTarget: npm<br>(default)"| NpmPlugin
RushPublish -->|"publishTarget: vsix"| VsixPlugin
RushPublish -->|"publishTarget: custom"| CustomPlugin
NpmPlugin -.->|"registers"| RushSession
VsixPlugin -.->|"registers"| RushSession
CustomPlugin -.->|"registers"| RushSession
PublishJson -.->|"configures per-project"| RushPublish
4.2 Architectural Pattern
The design follows four patterns already established in the Rush codebase:
-
Factory Registration Pattern (from build cache plugins): Publish providers register via
rushSession.registerPublishProviderFactory(name, factory), identical to howregisterCloudBuildCacheProviderFactoryworks today. The factory receives provider-specific configuration read from the project's riggable config file, mirroring how build cache plugin factories receive their section frombuild-cache.json. Research reference: [research/docs/2026-02-07-rush-plugin-architecture.md, Section 4.1] documentsRushSession's existing registration methods. -
Strategy Pattern (from version policies): Different publish targets are handled by different
IPublishProviderimplementations, similar to howLockStepVersionPolicyandIndividualVersionPolicyare strategy implementations ofVersionPolicy. Research reference: [research/docs/2026-02-10-rush-version-bump-system-and-vs-code-extension-versioning.md, Section 2] documents the version policy strategy pattern. -
Associated Commands Pattern (from existing plugins): Publish plugins declare
"associatedCommands": ["publish"]in their manifest, so they only load whenrush publishruns. Research reference: [research/docs/2026-02-07-existing-rush-plugins.md, Section "Plugin Infrastructure"] documents the associated commands mechanism. -
Riggable Configuration Pattern (from
rush-project.json): Provider options are stored in a per-projectconfig/publish.jsonfile that is resolved through the rig system usingProjectConfigurationFileandRigConfig.loadForProjectFolderAsync(). This follows the exact same pattern asconfig/rush-project.json-- projects can define their own config or inherit from their rig package. TheProjectConfigurationFileclass provides schema validation andpropertyInheritancefor merging rig defaults with project overrides.
4.3 Key Components
| Component | Responsibility | Technology | Justification |
|---|---|---|---|
shouldPublish gate refactor |
Enable versioning for all shouldPublish projects regardless of publish target |
TypeScript, rush-lib | Minimal change: shouldPublish already gates versioning; we just need to stop conflating it with npm-only publishing |
publishTarget field (array) |
Routes projects to one or more publish providers | JSON array field in rush.json | Additive schema change; defaults to ["npm"] when omitted for backward compatibility; supports multi-target publishing |
config/publish.json (riggable) |
Per-project provider configuration with rig inheritance | JSON config + ProjectConfigurationFile |
Follows config/rush-project.json pattern; no global options file; rig packages can provide shared defaults |
IPublishProvider interface |
Contract for publish provider implementations | TypeScript interface in rush-lib | Mirrors the ICloudBuildCacheProvider pattern |
registerPublishProviderFactory() |
Plugin registration API on RushSession |
TypeScript method | Follows exact pattern of registerCloudBuildCacheProviderFactory() |
rush-npm-publish-plugin |
Default npm publish provider (extracted from PublishAction) |
Rush plugin (always built-in) | Preserves existing behavior; same loading mechanism as S3/Azure cache plugins; listed in publishOnlyDependencies |
rush-vscode-publish-plugin |
VSIX publish provider using @vscode/vsce |
Rush plugin (autoinstaller) | Leverages existing heft-vscode-extension-plugin patterns |
PublishAction refactor |
Dispatch to registered providers instead of hardcoded npm | TypeScript, rush-lib | The publish action becomes an orchestrator rather than an implementor |
5. Detailed Design
5.1 rush.json Schema Changes
The rush.json project entry schema (libraries/rush-lib/src/schemas/rush.schema.json) gains one new optional field:
{
"publishTarget": {
"description": "Specifies the publish targets for this project. Determines which publish provider plugins handle publishing. Each entry maps to a registered publish provider. Common values: 'npm', 'vsix', 'none'. When set to ['none'], the project participates in versioning but is not published by any provider. When omitted, defaults to ['npm'] for backward compatibility.",
"oneOf": [
{ "type": "string" },
{
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
]
}
}Default behavior: When publishTarget is omitted, it defaults to ["npm"] for backward compatibility. Projects with shouldPublish: true and no publishTarget behave exactly as they do today. A string value is normalized to a single-element array (e.g., "vsix" becomes ["vsix"]).
Example rush.json entries:
5.2 RushConfigurationProject Changes
File: libraries/rush-lib/src/api/RushConfigurationProject.ts
Add a new publishTargets property:
// In IRushConfigurationProjectJson (line ~29)
export interface IRushConfigurationProjectJson {
// ... existing fields ...
publishTarget?: string | string[]; // NEW
}
// In RushConfigurationProject class
private readonly _publishTargets: ReadonlyArray<string>;
// In constructor (after line ~327)
const rawTarget = projectJson.publishTarget;
if (rawTarget === undefined) {
this._publishTargets = ['npm']; // Infer npm when omitted
} else if (typeof rawTarget === 'string') {
this._publishTargets = [rawTarget]; // Normalize string to array
} else {
this._publishTargets = rawTarget;
}
// New public getter
/**
* Specifies the publish targets for this project. Determines which publish
* provider plugins handle publishing during `rush publish`.
*
* Common values: 'npm', 'vsix', 'none'.
* When the array contains 'none', the project participates in versioning
* but is not published by any provider.
* When omitted in rush.json, defaults to ['npm'] for backward compatibility.
*/
public get publishTargets(): ReadonlyArray<string> {
return this._publishTargets;
}The existing shouldPublish getter remains unchanged:
public get shouldPublish(): boolean {
return this._shouldPublish || !!this.versionPolicyName;
}This means shouldPublish continues to gate version bumping for all projects. The new publishTargets field only affects the rush publish command's dispatch logic.
Validation: In the constructor, add validations:
publishTarget: ["none"]is incompatible withversionPolicyNameon lockstep policies (since lockstep policies inherently couple versioning with publishing intent). Individual version policies can usepublishTarget: ["none"]."none"cannot be combined with other targets in the same array (e.g.,["npm", "none"]is invalid).- The
private: truevalidation is relaxed for non-npm targets:
// Updated validation in RushConfigurationProject constructor
if (this._shouldPublish && this.packageJson.private && this._publishTargets.includes('npm')) {
throw new Error(
`The project "${packageName}" specifies "shouldPublish": true with ` +
`publishTarget including "npm", but the package.json file specifies "private": true.`
);
}5.3 IPublishProvider Interface
New file: libraries/rush-lib/src/pluginFramework/IPublishProvider.ts
import type { RushConfigurationProject } from '../api/RushConfigurationProject';
import type { ILogger } from './logging/Logger';
/**
* Information about a project that needs to be published.
*/
export interface IPublishProjectInfo {
/** The Rush project configuration */
readonly project: RushConfigurationProject;
/** The new version string after bumping */
readonly newVersion: string;
/** The previous version string before bumping */
readonly previousVersion: string;
/** The change type that triggered this publish */
readonly changeType: string;
/** Provider-specific configuration for this project, loaded from config/publish.json */
readonly providerConfig: Record<string, unknown>;
}
/**
* Options passed to the publish provider's publishAsync method.
*/
export interface IPublishProviderPublishOptions {
/** Projects to publish, filtered to those matching this provider's target */
readonly projects: ReadonlyArray<IPublishProjectInfo>;
/** The npm tag to apply (e.g., 'latest', 'next') */
readonly tag?: string;
/** Whether this is a dry run */
readonly dryRun: boolean;
/** Logger instance for output */
readonly logger: ILogger;
}
/**
* Options passed to the publish provider's checkExistsAsync method.
*/
export interface IPublishProviderCheckExistsOptions {
/** The project to check */
readonly project: RushConfigurationProject;
/** The version to check for */
readonly version: string;
/** Provider-specific configuration for this project */
readonly providerConfig: Record<string, unknown>;
}
/**
* Options passed to the publish provider's packAsync method.
*/
export interface IPublishProviderPackOptions {
/** Projects to pack, filtered to those matching this provider's target */
readonly projects: ReadonlyArray<IPublishProjectInfo>;
/** The folder where packed artifacts should be placed (from --release-folder or default) */
readonly releaseFolder: string;
/** Whether this is a dry run */
readonly dryRun: boolean;
/** Logger instance for output */
readonly logger: ILogger;
}
/**
* Interface that publish provider plugins must implement.
* Modeled after ICloudBuildCacheProvider.
*/
export interface IPublishProvider {
/** Human-readable name for this provider */
readonly providerName: string;
/**
* Publishes the specified projects.
* @returns A map from package name to success/failure status.
*/
publishAsync(options: IPublishProviderPublishOptions): Promise<Map<string, boolean>>;
/**
* Packs the specified projects into distributable artifacts for this
* provider's target. Each provider defines what "packing" means:
* - npm: runs `<packageManager> pack` to produce a `.tgz` tarball
* - vsix: runs `vsce package` to produce a `.vsix` file
*
* Artifacts are written to the `releaseFolder` specified in options.
* Called when `rush publish --pack` is invoked.
*/
packAsync(options: IPublishProviderPackOptions): Promise<void>;
/**
* Checks whether a specific version of a project already exists in the
* target registry/marketplace.
*/
checkExistsAsync(options: IPublishProviderCheckExistsOptions): Promise<boolean>;
}
/**
* Factory function type for creating publish providers.
* The factory is called once per `rush publish` invocation.
* No global config is passed -- all provider configuration comes
* from per-project config/publish.json via IPublishProjectInfo.providerConfig.
*/
export type PublishProviderFactory = () => Promise<IPublishProvider>;5.4 RushSession Registration API
File: libraries/rush-lib/src/pluginFramework/RushSession.ts
Add registration methods following the existing pattern:
// New private field
private _publishProviderFactories: Map<string, PublishProviderFactory> = new Map();
/**
* Registers a factory function that creates an IPublishProvider for the
* specified publish target name.
*
* This mirrors the registerCloudBuildCacheProviderFactory pattern.
* The factory takes no arguments -- provider-specific configuration
* is loaded per-project from riggable config/publish.json and passed
* via IPublishProjectInfo.providerConfig.
*
* @example
* ```typescript
* rushSession.registerPublishProviderFactory('vsix', async () => {
* return new VsixPublishProvider();
* });
* ```
*/
public registerPublishProviderFactory(
publishTargetName: string,
factory: PublishProviderFactory
): void {
if (this._publishProviderFactories.has(publishTargetName)) {
throw new Error(
`A publish provider factory has already been registered for target "${publishTargetName}".`
);
}
this._publishProviderFactories.set(publishTargetName, factory);
}
/**
* Retrieves a previously registered publish provider factory.
*/
public getPublishProviderFactory(
publishTargetName: string
): PublishProviderFactory | undefined {
return this._publishProviderFactories.get(publishTargetName);
}5.5 PublishAction Refactor
File: libraries/rush-lib/src/cli/actions/PublishAction.ts
The PublishAction currently hardcodes npm publishing in _npmPublishAsync() (line 439) and _packageExistsAsync() (line 488). The refactor replaces these with provider dispatch:
Phase 1: Load Per-Project Configuration
Before dispatch, load each project's config/publish.json via the riggable config system:
// Load riggable publish config for a project
const publishConfig: IPublishJson | undefined =
await PUBLISH_CONFIGURATION_FILE.tryLoadConfigurationFileForProjectAsync(
terminal,
project.projectFolder,
rigConfig
);Phase 2: Provider Resolution
In _publishChangesAsync() (line 278) and _publishAllAsync() (line 361), after computing the set of projects to publish, group them by publish target. Since publishTargets is an array, a single project may appear in multiple groups:
// Group projects by publish target
const projectsByTarget: Map<string, IPublishProjectInfo[]> = new Map();
for (const [packageName, changeInfo] of allPackageChanges) {
const project = this.rushConfiguration.projectsByName.get(packageName)!;
if (!project.shouldPublish) continue;
const publishConfig = await this._loadPublishConfigAsync(project);
for (const target of project.publishTargets) {
if (target === 'none') continue; // Version-only projects skip publishing
// Extract the provider-specific section from the project's publish config
const providerConfig = publishConfig?.providers?.[target] ?? {};
let targetProjects = projectsByTarget.get(target);
if (!targetProjects) {
targetProjects = [];
projectsByTarget.set(target, targetProjects);
}
targetProjects.push({
project,
newVersion: changeInfo.newVersion!,
previousVersion: changeInfo.oldVersion,
changeType: ChangeType[changeInfo.changeType!],
providerConfig
});
}
}Phase 3: Provider Dispatch
// Dispatch to registered providers
for (const [targetName, projects] of projectsByTarget) {
const factory = this._rushSession.getPublishProviderFactory(targetName);
if (!factory) {
throw new Error(
`No publish provider registered for target "${targetName}". ` +
`Projects with this target: ${projects.map(p => p.project.packageName).join(', ')}. ` +
`Ensure a plugin is configured that registers a "${targetName}" publish provider.`
);
}
const provider = await factory();
const results = await provider.publishAsync({
projects,
tag: this._npmTag.value,
dryRun: !shouldCommit,
logger
});
// Process results...
}Phase 4: Pack Dispatch
When --pack is used, PublishAction dispatches to each provider's packAsync method instead of publishAsync. This replaces the hardcoded _npmPackAsync call:
// New method: _packProjectViaProvidersAsync
// Mirrors _publishProjectViaProvidersAsync but calls packAsync
const releaseFolder: string = this._releaseFolder.value
? this._releaseFolder.value
: path.join(this.rushConfiguration.commonTempFolder, 'artifacts', 'packages');
FileSystem.ensureFolder(releaseFolder);
for (const target of project.publishTargets) {
if (target === 'none') continue;
const provider = await this._getProviderAsync(target, project.packageName);
const providerConfig = await this._getProviderConfigAsync(project, target);
await provider.packAsync({
projects: [{ project, newVersion: version, previousVersion: version,
changeType: ChangeType.none, providerConfig }],
releaseFolder,
dryRun,
logger
});
}The _publishAllAsync branching at line 411 changes from:
if (this._pack.value) {
await this._npmPackAsync(packageName, packageConfig); // OLD: hardcoded npmto:
if (this._pack.value) {
await this._packProjectViaProvidersAsync(packageConfig); // NEW: routes through providersThe _npmPackAsync and _calculateTarballName private methods are deleted from PublishAction — their logic moves into NpmPublishProvider.packAsync.
Phase 5: Extract npm-specific code
The existing _npmPublishAsync(), _packageExistsAsync(), .npmrc-publish handling, and npm-specific flag construction move into the rush-npm-publish-plugin.
5.6 rush-npm-publish-plugin (Always Built-In)
New package: rush-plugins/rush-npm-publish-plugin/
This is a permanently built-in plugin (registered in PluginManager alongside the cache plugins) that extracts the existing npm publish logic from PublishAction. It is listed in rush-lib's publishOnlyDependencies (same mechanism as the S3, Azure, and HTTP build cache plugins). The npm provider will always ship with Rush -- it will never be moved to an autoinstaller since virtually all Rush users depend on npm publishing.
Structure:
rush-plugins/rush-npm-publish-plugin/
package.json
rush-plugin-manifest.json
src/
index.ts
RushNpmPublishPlugin.ts
NpmPublishProvider.ts
rush-plugin-manifest.json:
{
"plugins": [
{
"pluginName": "rush-npm-publish-plugin",
"description": "Default publish provider for npm registries",
"entryPoint": "lib/index.js",
"associatedCommands": ["publish"]
}
]
}RushNpmPublishPlugin.ts:
import type { IRushPlugin } from '@rushstack/rush-sdk';
import type { RushSession, RushConfiguration } from '@rushstack/rush-sdk';
const PLUGIN_NAME: string = 'rush-npm-publish-plugin';
export class RushNpmPublishPlugin implements IRushPlugin {
public readonly pluginName: string = PLUGIN_NAME;
public apply(rushSession: RushSession, rushConfiguration: RushConfiguration): void {
rushSession.hooks.initialize.tap(PLUGIN_NAME, () => {
rushSession.registerPublishProviderFactory('npm', async () => {
const { NpmPublishProvider } = await import('./NpmPublishProvider');
return new NpmPublishProvider(rushConfiguration);
});
});
}
}
export default RushNpmPublishPlugin;The NpmPublishProvider class encapsulates the existing logic from:
PublishAction._npmPublishAsync()(line 439) →publishAsyncPublishAction._packageExistsAsync()(line 488) →checkExistsAsyncPublishAction._npmPackAsync()(line 503) →packAsyncPublishAction._calculateTarballName()(line 531) → private helper inNpmPublishProviderPublishAction._addSharedNpmConfig()(npm config handling)npmrcUtilities.tsfor.npmrc-publishmanagement
The packAsync method runs <packageManager> pack in the project's publishFolder and moves the resulting .tgz tarball to the release folder specified in IPublishProviderPackOptions.releaseFolder. Tarball naming follows npm conventions (scoped packages remove @ and replace / with -; yarn prepends v before the version).
It reads npm-specific options (e.g., registryUrl) from IPublishProjectInfo.providerConfig, which comes from the project's riggable config/publish.json under the "npm" provider key.
5.7 rush-vscode-publish-plugin (Autoinstaller)
New package: rush-plugins/rush-vscode-publish-plugin/
This is an autoinstaller-based plugin that publishes VSIX files to the VS Code Marketplace using the @vscode/vsce tool.
Structure:
rush-plugins/rush-vscode-publish-plugin/
package.json
rush-plugin-manifest.json
src/
index.ts
RushVscodePublishPlugin.ts
VsixPublishProvider.ts
rush-plugin-manifest.json:
{
"plugins": [
{
"pluginName": "rush-vscode-publish-plugin",
"description": "Publish provider for VS Code extensions (VSIX files)",
"entryPoint": "lib/index.js",
"associatedCommands": ["publish"]
}
]
}VsixPublishProvider.ts:
The VsixPublishProvider implements IPublishProvider and delegates to @vscode/vsce for both publishing and packing. It reads VSIX-specific options from IPublishProjectInfo.providerConfig (sourced from the project's config/publish.json under the "vsix" key). It follows the same patterns as VSCodeExtensionPublishPlugin.ts in heft-plugins/heft-vscode-extension-plugin/.
The packAsync method runs vsce package --no-dependencies --out <releaseFolder>/<name>.vsix to produce a distributable VSIX file. The publishAsync method runs vsce publish --packagePath <vsix> to publish to the Marketplace:
export class VsixPublishProvider implements IPublishProvider {
public readonly providerName: string = 'vsix';
public async publishAsync(options: IPublishProviderPublishOptions): Promise<Map<string, boolean>> {
const results = new Map<string, boolean>();
for (const projectInfo of options.projects) {
const vsixConfig = projectInfo.providerConfig as IVsixProviderConfig;
const vsixPath = path.resolve(
projectInfo.project.projectFolder,
vsixConfig.vsixPathPattern ?? 'dist/vsix/extension.vsix'
);
if (options.dryRun) {
options.logger.terminal.writeLine(`[DRY RUN] Would publish ${vsixPath}`);
results.set(projectInfo.project.packageName, true);
continue;
}
// Delegate to @vscode/vsce CLI (same approach as heft-vscode-extension-plugin)
const args = ['publish', '--no-dependencies', '--packagePath', vsixPath];
if (vsixConfig.useAzureCredential !== false) {
args.push('--azure-credential');
}
const result = await Executable.waitForExitAsync(
Executable.spawn(process.execPath, [vsceScriptPath, ...args])
);
results.set(projectInfo.project.packageName, result.exitCode === 0);
}
return results;
}
public async checkExistsAsync(options: IPublishProviderCheckExistsOptions): Promise<boolean> {
// VS Code Marketplace doesn't have a simple "does this version exist" check
// like npm. Return false to always attempt publishing.
return false;
}
}5.8 Per-Project Riggable Configuration (config/publish.json)
Instead of a global common/config/rush/publish-config.json, publish provider configuration lives in a per-project riggable config file at config/publish.json. This follows the established pattern of config/rush-project.json which uses ProjectConfigurationFile with rig resolution.
Why riggable per-project config instead of global config:
- The
config/rush-project.jsonpattern is proven and well-understood in the codebase - Rig packages can provide shared defaults for all projects of a given type (e.g., the
heft-vscode-extension-rigcould provide VSIX publish defaults) - Projects can override rig defaults without modifying global state
- No cross-project configuration coupling -- each project is self-contained
- The build cache plugins use
build-cache.jsonas a global config, but publish providers have more variation per-project (e.g., different VSIX paths, different npm registries per scope)
Schema (publish.schema.json):
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Publish Configuration",
"description": "Per-project configuration for publish providers. Resolved through the rig system.",
"type": "object",
"properties": {
"providers": {
"description": "Provider-specific configuration keyed by publish target name.",
"type": "object",
"additionalProperties": {
"type": "object",
"description": "Configuration options for a specific publish provider. Schema varies by provider."
}
}
},
"additionalProperties": false
}Example: config/publish.json in a VS Code extension project:
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/publish.schema.json",
"providers": {
"vsix": {
"vsixPathPattern": "dist/vsix/extension.vsix",
"useAzureCredential": true
}
}
}Example: config/publish.json in an npm package project (optional -- defaults are sensible):
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/publish.schema.json",
"providers": {
"npm": {
"registryUrl": "https://registry.npmjs.org/"
}
}
}Example: config/publish.json in a rig package (provides defaults for all extension projects):
Located at rigs/heft-vscode-extension-rig/profiles/default/config/publish.json:
{
"providers": {
"vsix": {
"vsixPathPattern": "dist/vsix/extension.vsix",
"useAzureCredential": true
}
}
}Projects using this rig inherit these defaults via the standard config/rig.json mechanism. A project can override specific values in its own config/publish.json, and the ProjectConfigurationFile rig resolution merges them.
Loading via ProjectConfigurationFile:
const PUBLISH_CONFIGURATION_FILE: ProjectConfigurationFile<IPublishJson> =
new ProjectConfigurationFile<IPublishJson>({
projectRelativeFilePath: 'config/publish.json',
jsonSchemaObject: publishSchemaJson,
propertyInheritance: {
providers: {
inheritanceType: InheritanceType.custom,
inheritanceFunction: (
child: Record<string, unknown> | undefined,
parent: Record<string, unknown> | undefined
) => {
if (!child) return parent;
if (!parent) return child;
// Deep merge: child provider sections override parent
return { ...parent, ...child };
}
}
}
});Comparison with build cache plugin pattern:
| Aspect | Build Cache Plugins | Publish Plugins |
|---|---|---|
| Config location | common/config/rush/build-cache.json (global) |
config/publish.json (per-project, riggable) |
| Factory receives | Section from build-cache.json |
Nothing -- config via IPublishProjectInfo.providerConfig |
| Config resolution | Direct file read | ProjectConfigurationFile + RigConfig.loadForProjectFolderAsync() |
| Inheritance | None (single global file) | Rig inheritance with custom merge |
| Per-project variation | Same config for all projects | Different config per project/rig |
5.9 PublishAction Lifecycle Hooks
To enable the plugin system, new hooks are added to RushLifecycleHooks:
// In RushLifeCycle.ts
export class RushLifecycleHooks {
// ... existing hooks ...
/**
* Fires before the publish command begins processing.
* Allows plugins to perform setup (authentication, validation).
*/
public readonly beforePublish: AsyncSeriesHook<IPublishCommand> =
new AsyncSeriesHook<IPublishCommand>(['command']);
/**
* Fires after all publish providers have completed.
* Allows plugins to perform cleanup or reporting.
*/
public readonly afterPublish: AsyncSeriesHook<IPublishResult> =
new AsyncSeriesHook<IPublishResult>(['result']);
}These hooks are invoked by the refactored PublishAction:
// In PublishAction.runAsync()
await this._rushSession.hooks.beforePublish.promise({ actionName: this.actionName });
// ... dispatch to providers ...
await this._rushSession.hooks.afterPublish.promise({ results });5.10 VS Code Extension Project Configuration Changes
The 4 VS Code extension projects in rush.json gain shouldPublish: true and publishTarget: ["vsix"]:
// rush.json - vscode-extensions section
{
"packageName": "rushstack",
"projectFolder": "vscode-extensions/rush-vscode-extension",
"reviewCategory": "vscode-extensions",
"tags": ["vsix"],
"shouldPublish": true, // NEW: enables rush change / rush version
"publishTarget": ["vsix"] // NEW: routes to VSIX publish provider
},
{
"packageName": "@rushstack/rush-vscode-command-webview",
"projectFolder": "vscode-extensions/rush-vscode-command-webview",
"reviewCategory": "vscode-extensions",
"tags": ["vsix"],
"shouldPublish": true,
"publishTarget": ["vsix"]
},
{
"packageName": "debug-certificate-manager",
"projectFolder": "vscode-extensions/debug-certificate-manager-vscode-extension",
"reviewCategory": "vscode-extensions",
"tags": ["vsix"],
"shouldPublish": true,
"publishTarget": ["vsix"]
},
{
"packageName": "playwright-local-browser-server",
"projectFolder": "vscode-extensions/playwright-local-browser-server-vscode-extension",
"reviewCategory": "vscode-extensions",
"tags": ["vsix"],
"shouldPublish": true,
"publishTarget": ["vsix"]
}Note: @rushstack/vscode-shared remains shouldPublish: false since it is an internal library not published independently.
Validation concern: These extensions have "private": true in their package.json (since they are not npm packages). The current constructor validation at RushConfigurationProject.ts:331-336 throws when shouldPublish: true and package.json has "private": true. This validation is relaxed: the check only applies when publishTargets includes "npm" (see Section 5.2).
5.11 Version Synchronization for VS Code Extensions
The vsce tool reads the extension version directly from package.json via its readManifest() function (confirmed in @vscode/vsce@3.2.1 source at out/package.js:1027). This means:
rush version --bumpupdatespackage.jsonversion fields (already npm-agnostic)vsce packagereads the version frompackage.jsonand embeds it in the VSIX manifestvsce publish --packagePathpublishes the VSIX with the version from its manifest
Therefore, VS Code extension versions always match. The version in package.json is the single source of truth, managed by rush version --bump, and automatically picked up by vsce. No additional synchronization logic is needed -- the existing version pipeline handles this naturally.
This is a key advantage of reusing the existing version management pipeline rather than inventing a separate versioning system for non-npm artifacts.
5.12 Version Policy for VS Code Extensions
The VS Code extensions should use individual versioning (no version policy name needed). Each extension maintains its own version in its package.json and receives independent bumps based on its change files. This is the default behavior for shouldPublish: true projects without a versionPolicyName.
If a lockstep policy is desired in the future (e.g., to keep all extensions at the same version), a new lockstep policy could be added to common/config/rush/version-policies.json:
{
"policyName": "vscode-extensions",
"definitionName": "lockStepVersion",
"version": "1.0.0",
"nextBump": "patch",
"mainProject": "rushstack"
}This is an optional future enhancement, not required for the initial implementation.
5.13 Data Flow (Complete)
Developer makes changes to VS Code extension
|
v
rush change
(ChangeAction.ts)
- shouldPublish gate passes (shouldPublish: true)
- Prompts for comment + bump type
- Writes JSON to common/changes/rushstack/
|
v
rush version --bump
(VersionManager.bumpAsync())
- Reads change files
- Computes new version via semver.inc()
- Updates package.json version field <-- vsce reads this automatically
- Generates CHANGELOG.json and CHANGELOG.md
- Deletes processed change files
- Commits to git
|
v
rush publish [--pack]
(PublishAction - refactored)
1. Loads config/publish.json for each project (via rig system)
2. Groups projects by publishTargets (array -- project may appear in multiple groups)
3. For publishTargets: ["none"] --> skip
4. If --pack: dispatch to provider.packAsync()
5. Else: dispatch to provider.publishAsync()
|
+-- npm publish provider
| (rush-npm-publish-plugin, built-in)
| - publishAsync: npm publish --registry ...
| - packAsync: <packageManager> pack → .tgz → release folder
| - checkExistsAsync: npm view versions
| - .npmrc-publish handling
|
+-- VSIX publish provider
(rush-vscode-publish-plugin, autoinstaller)
- publishAsync: vsce publish --azure-credential
- packAsync: vsce package → .vsix → release folder
- checkExistsAsync: always false
6. Alternatives Considered
| Option | Pros | Cons | Reason for Rejection |
|---|---|---|---|
A: New shouldVersion flag separate from shouldPublish |
Clean semantic separation; no ambiguity | Breaks backward compatibility; requires migrating all shouldPublish entries; two flags to manage |
Doubles the configuration surface area. shouldPublish already works for the versioning case -- we just need to stop conflating it with npm-only publishing. |
B: Custom rush publish shell command |
No rush-lib changes; community workaround exists (DEV article) | Loses integration with change files; no _packageExistsAsync equivalent; duplicates version bump logic; no dry-run support |
Not a first-class solution; maintenance burden on each consumer. |
C: Heft-only publish pipeline (no rush publish involvement) |
VS Code extensions already have heft-vscode-extension-plugin; no rush-lib changes needed |
Loses rush change integration; no changelogs; version bumps are manual; no coordination with lockstep policies |
Throws away the entire version management pipeline for non-npm artifacts. |
D: Tag-based conditional publishing (check tags: ["vsix"] in PublishAction) |
Minimal code change; uses existing metadata | Hardcodes tag names in rush-lib; not extensible to future targets; violates single-responsibility principle | Tags are metadata, not behavior configuration. Using them as dispatch keys creates hidden coupling. |
E: Global publish-config.json (single config file in common/config/rush/) |
Simple; single location for all publish settings | Not riggable; no per-project variation; doesn't match the config/rush-project.json pattern; forces all projects to share a single config |
Per-project riggable config is more flexible and consistent with established patterns. Projects in different rigs may have very different publishing needs. |
F: publishTarget as single string |
Simpler schema; fewer edge cases | Cannot publish to multiple targets from one project; limits future extensibility | Array support is trivially more complex but enables important use cases (dual publishing, multi-registry). |
G: publishTarget field + plugin system + riggable config + array support (Selected) |
Extensible; backward compatible; leverages existing plugin and rig patterns; clean separation of concerns; additive schema change; supports multi-target publishing | More upfront work than alternatives; requires extracting npm code into a plugin | Selected. The plugin pattern is proven in the codebase (cache providers). The riggable config pattern is proven (rush-project.json). The combination cleanly separates version management from artifact distribution. |
7. Cross-Cutting Concerns
7.1 Backward Compatibility
This is the highest-priority concern. Every existing shouldPublish: true project must continue to work without any configuration changes.
Guarantees:
publishTargetdefaults to["npm"]when omitted -- inferred for all existing projects- The
rush-npm-publish-pluginis a permanently built-in plugin (registered byPluginManagerlike cache plugins) -- no user configuration required - The
shouldPublishgetter returns the same value for all existing projects rush change,rush version --bumpare semantically unchangedrush publishproduces identical behavior for npm-target projects- The
shouldPublish: true+private: truevalidation only changes for non-npm targets - Existing projects without
config/publish.jsoncontinue to work -- providers use sensible defaults when no per-project config is present
7.2 rush publish CLI Flag Compatibility
The existing rush publish flags must work correctly with the new dispatch:
| Flag | npm provider | VSIX provider | Behavior |
|---|---|---|---|
--include-all |
Publish all npm-target projects | Publish all VSIX-target projects | Each provider gets its filtered set |
--version-policy |
Filter by policy | Filter by policy | Same gating applies |
--tag |
npm dist-tag | Ignored (VSIX has no tag concept) | Provider-specific interpretation |
--npm-auth-token |
Used for registry auth | Ignored | Provider-specific |
--publish |
Enables actual publishing | Enables actual publishing | Universal flag |
--pack |
packAsync: <packageManager> pack → .tgz |
packAsync: vsce package → .vsix |
Dispatched to IPublishProvider.packAsync (required method) |
New provider-specific flags may be needed. These can be contributed by the plugins via the commandLineJsonFilePath mechanism in rush-plugin-manifest.json. Research reference: [research/docs/2026-02-07-rush-plugin-architecture.md, Section 6.2] documents how plugins contribute CLI commands.
7.3 Git Workflow
The existing two-commit workflow during rush version --bump (changelogs first, then package.json updates) is unaffected since it happens before publishing.
During rush publish, git tags are currently created by _gitAddTagsAsync() (line 421). This logic should be generalized:
- npm packages: tag format
<packageName>_v<version>(existing) - VSIX packages: tag format
<packageName>_v<version>(same format, since the package name is unique within the monorepo)
7.4 Error Handling
Each publish provider handles its own errors and returns a Map<string, boolean> from publishAsync(). The PublishAction orchestrator:
- Collects results from all providers
- Reports failures per-project
- Continues publishing other targets even if one target fails (e.g., if npm publish fails, VSIX publish still runs)
- Returns non-zero exit code if any publication failed
For multi-target projects (e.g., ["npm", "vsix"]), failure in one target does not prevent the other from running.
7.5 Observability
- Each provider logs to the terminal via the
ILoggerpassed in options - The
PublishActionlogs which provider is handling which projects - The
afterPublishhook enables telemetry plugins to report publish outcomes
8. Migration, Rollout, and Testing
8.1 Deployment Strategy
This is a multi-phase rollout designed to minimize risk:
- Phase 1: Schema +
publishTargetfield -- Add thepublishTargetfield (array support) to the schema andRushConfigurationProject. Default to["npm"]when omitted. No behavior change for any existing project. - Phase 2:
IPublishProviderinterface +RushSessionregistration -- Add the new interface and registration API. No plugins registered yet;PublishActionstill uses hardcoded npm logic as fallback. - Phase 3: Riggable
config/publish.json-- Add theProjectConfigurationFileforconfig/publish.jsonwith rig resolution and schema validation. No behavior change yet. - Phase 4: Extract
rush-npm-publish-plugin-- Move npm publishing logic into a built-in plugin. The plugin registers forpublishTarget: "npm". Add to rush-lib'spublishOnlyDependencies. IncludespublishAsync,packAsync(extracted from_npmPackAsync), andcheckExistsAsync. Behavior is identical to pre-refactor. - Phase 5: Refactor
PublishActionto dispatch --PublishActionuses registered providers instead of hardcoded logic for both--publishand--pack. The--packbranch calls_packProjectViaProvidersAsyncwhich dispatches toprovider.packAsync. Delete_npmPackAsyncand_calculateTarballName. The npm plugin handles all existing projects. Regression test. - Phase 6: Create
rush-vscode-publish-plugin-- Build the VSIX publish provider as an autoinstaller plugin. - Phase 7: Add rig defaults -- Add
config/publish.jsontoheft-vscode-extension-rigwith default VSIX provider configuration. - Phase 8: Enable VS Code extensions -- Set
shouldPublish: trueandpublishTarget: ["vsix"]on the 4 extension projects. Relax theprivate: truevalidation. Runrush changeto verify they appear in prompts. - Phase 9: Publish hooks -- Add
beforePublishandafterPublishhooks. These are additive and don't affect existing behavior.
8.2 Test Plan
Unit Tests:
RushConfigurationProject: TestpublishTargetsgetter with various configurations (omitted infers["npm"], string normalized to array, explicit array)RushConfigurationProject: Test thatshouldPublish: true+private: true+publishTarget: ["vsix"]does NOT throwRushConfigurationProject: Test thatshouldPublish: true+private: true+publishTarget: ["npm"]DOES throw (existing behavior)RushConfigurationProject: Test that["npm", "none"]is rejected as invalidRushSession: TestregisterPublishProviderFactoryandgetPublishProviderFactoryregistration and retrievalRushSession: Test duplicate registration throws error- Riggable config: Test
config/publish.jsonloading with rig resolution, property inheritance, and missing file fallback
Integration Tests:
PublishAction: Test dispatch to mock providers based onpublishTargetsPublishAction: Test that projects withpublishTarget: ["none"]are skippedPublishAction: Test that missing provider for a target throws descriptive errorPublishAction: Test--include-allwith mixed npm and VSIX targetsPublishAction: Test multi-target project dispatches to all its targetsPublishAction: Test that per-projectconfig/publish.jsonconfig is correctly passed to providers viaproviderConfigChangeAction: Test that VSIX-target projects appear inrush changepromptsVersionManager: Test thatrush version --bumpprocesses VSIX-target projects
Pack Tests (packAsync):
NpmPublishProvider: TestpackAsyncspawns<packageManager> packin the project'spublishFolderNpmPublishProvider: TestpackAsyncmoves tarball to release folder with correct name (scoped, unscoped, yarnvprefix)NpmPublishProvider: TestpackAsyncdry run mode (logs but does not spawn)VsixPublishProvider: TestpackAsyncspawnsvsce package --no-dependencies --out <releaseFolder>/<name>.vsixVsixPublishProvider: TestpackAsyncdry run mode (logs but does not spawn)PublishAction: Test--packdispatches toprovider.packAsyncinstead of the removed_npmPackAsyncPublishAction: Test--packwith multi-target project dispatches to all providers'packAsyncPublishAction: Test--release-folderis passed through toIPublishProviderPackOptions.releaseFolder
End-to-End Tests:
- Build a VS Code extension, run
rush change,rush version --bump, verify version increment inpackage.jsonand CHANGELOG generation - Run
rush publish --publishwith both npm and VSIX projects, verify each provider receives the correct project set - Run
rush publish --packand verify each provider'spackAsyncproduces its artifact type (.tgzfor npm,.vsixfor VSIX) - Verify a project with
publishTarget: ["npm", "vsix"]dispatches to both providers for both--publishand--pack
Test fixtures:
Create a test fixture repo (similar to existing fixtures in libraries/rush-lib/src/cli/test/) with:
- An npm package with
shouldPublish: true(defaultpublishTargetsinferred as["npm"]) - A VSIX project with
shouldPublish: true, publishTarget: ["vsix"] - A multi-target project with
shouldPublish: true, publishTarget: ["npm", "vsix"] - A version-only project with
shouldPublish: true, publishTarget: ["none"] - Rig packages with
config/publish.jsondefaults for testing inheritance
9. Resolved Design Decisions
The following questions from the original draft have been resolved:
-
publishTargetsupports arrays. A project can publish to multiple targets (e.g.,["npm", "internal-registry"]). A string value is normalized to a single-element array. This is a first-class feature, not a future enhancement. -
The npm plugin remains permanently built-in. It is listed in rush-lib's
publishOnlyDependenciesalongside the build cache plugins. Since virtually all Rush users depend on npm publishing, keeping it built-in avoids a disruptive migration and ensures zero-config for the common case. -
No global
publish-config.json. All publish provider configuration is per-project and riggable viaconfig/publish.json, resolved throughProjectConfigurationFile+RigConfig.loadForProjectFolderAsync(). This follows theconfig/rush-project.jsonpattern and enables rig packages to provide shared defaults. -
publishTarget: ["npm"]is inferred when omitted. WhenshouldPublish: trueand nopublishTargetis specified, the field defaults to["npm"]for backward compatibility. This means existing projects require zero configuration changes. -
VS Code extension versions always match. The
vscetool reads the version directly frompackage.jsonviareadManifest(). Sincerush version --bumpupdatespackage.json, the versions are naturally synchronized. No additional version synchronization logic is needed. -
The VSIX publish plugin lives in
rush-plugins/. It implementsIRushPlugin(not a Heft plugin), sorush-plugins/is the correct location. It shares the@vscode/vsce@3.2.1dependency version withheft-vscode-extension-plugin, managed by its own autoinstaller. -
packAsyncis a required method onIPublishProvider. The--packflag is dispatched through providers rather than hardcoded to npm. Each provider implementspackAsyncto produce its artifact type (npm →.tgz, vsix →.vsix). For multi-target projects, all providers'packAsyncmethods are called, producing all artifact types in the release folder. The release folder is passed viaIPublishProviderPackOptions.releaseFolder. Git tagging (--apply-git-tags-on-pack) remains in thePublishActionorchestrator. Detailed spec: [specs/rush-publish-pack-as-provider-hook.md]
10. Remaining Open Questions
-
How should
rush publish --include-allinteract withpublishTarget: ["none"]projects? The proposed behavior is to skip them entirely (they opted out of publishing). Should there be a warning? A--include-none-targetsoverride? -
How does this interact with
rush version --ensure-version-policy? Theensuremode aligns project versions to policy constraints. It currently skips non-shouldPublishprojects. Once VS Code extensions haveshouldPublish: true, they will participate inensureif assigned a version policy. This is desired behavior but should be validated. -
Should
config/publish.jsonsupport provider-level schema validation? The current design uses a genericadditionalProperties: { type: "object" }for provider sections. Should each registered provider contribute its own JSON schema for validation (similar to howoptionsSchemaworks in plugin manifests)? -
How should multi-target publishing interact withResolved:--pack?packAsyncis a required method onIPublishProvider. Each provider produces its artifact type. A project withpublishTarget: ["npm", "vsix"]produces both.tgzand.vsixfiles. See [specs/rush-publish-pack-as-provider-hook.md].
Metadata
Metadata
Assignees
Labels
Type
Projects
Status