Proposal: Extract `@semantic-release/core` package to enable composable release pipelines
Extract the core engine (orchestration, plugin system, git operations, config loading) into a standalone @semantic-release/core package. The existing semantic-release package becomes a thin wrapper that provides the CLI and bundles the four default plugins.
This lets users compose custom release pipelines without being forced to depend on @semantic-release/commit-analyzer, @semantic-release/release-notes-generator, @semantic-release/npm, and @semantic-release/github.
Motivation
Today, semantic-release bundles three distinct concerns into one package:
| Concern | Source | Dependencies |
|---|---|---|
| Core engine | index.js, lib | env-ci, semver, cosmiconfig, hook-std, git-log-parser, etc. |
| CLI | cli.js, semantic-release.js | yargs, marked-terminal |
| Default plugins | (bundled as deps) | @semantic-release/commit-analyzer, release-notes-generator, npm, github |
Users who want to build a custom release workflow.... e.g., a framework-specific CLI, or a pipeline that publishes to a private registry instead of npm/GitHub; still have to install all four default plugins as transitive dependencies, even if they never use them.
Use cases this unblocks
- Custom CLIs: Teams building internal release tooling on top of the semantic-release engine without inheriting the default CLI or plugins.
- Alternative plugin sets: Using a different commit analyzer (I don't know, you never can tell) or publishing to GitLab/Bitbucket without pulling in the GitHub plugin.
- Lighter installs: CI environments that only need the engine + 1-2 plugins instead of the full default stack.
- Framework wrappers: Tools or platform-specific release managers that embed semantic-release as a library.
Proposed Design
Package split
@semantic-release/core ← NEW: engine + plugin system (no CLI, no default plugins)
semantic-release ← existing: CLI + re-exports core + default plugins as deps@semantic-release/core
Will Contain:
index.js- therun()orchestration function and the public API (export default)lib- all modules (git operations, plugin loading/pipeline, branch handling, config, verification, etc.)index.d.ts- TypeScript types
Will NOT Contain:
cli.js/semantic-release.js- the CLI- Any of the four default plugin dependencies
Key change: The default plugins array in get-config.js#L75-L80 becomes [] instead of the current hardcoded list:
// Current (in semantic-release)
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github",
],
// In @semantic-release/core
plugins: [],The core would throw a clear error if no plugins are configured and no analyzeCommits step is provided (since that step is required: true in plugins.js).
semantic-release (existing package)
Becomes a thin wrapper:
- Depends on
@semantic-release/core - Re-exports the core API for full backward compatibility
- Provides the CLI (
yargs,marked-terminal) - Bundles the 4 default plugins as dependencies
- Injects the default plugin list so existing users see zero behavior change
Usage of @semantic-release/core
import semanticRelease from "@semantic-release/core";
const result = await semanticRelease({
plugins: [
"@my-org/commit-analyzer",
"@semantic-release/release-notes-generator",
"@my-org/deploy-plugin",
],
});Feasibility
The architecture already supports this cleanly:
- Core has zero imports from CLI code. index.js and lib never import from cli.js or bin.
- CLI only imports core via dynamic
import("./index.js")- a clean boundary already exists. - Default plugins are referenced only as strings in get-config.js, not imported directly. The plugin system resolves them by name at runtime via
import-from-esm. - The plugin pipeline is fully generic. index.js and plugins.js don't know or care which specific plugins are loaded.
The only coupling point that needs attention is terminalOutput() in index.js, which imports marked-terminal - a CLI-oriented dependency that should either move to the CLI layer or become injectable.
Migration & Backward Compatibility
semantic-releaseusers: Zero breaking changes. The package continues to work identically, it just delegates to@semantic-release/coreinternally.- Programmatic API users: No change -
semantic-releasere-exports the same default function.
Open Questions
- Should we just still pack a default commit-analyzer with
@semantic-release/core, like we already do anyways in considering how imperative commit analysis is to software release at all? but does it not beat the point on no plugins in core? https://github.com/semantic-release/semantic-release/blob/e9f4aba2a23800b5c3c769b2ca150962fd199fc2/lib/definitions/plugins.js#L15-L18 - Should
@semantic-release/coreship its own minimal CLI? Or should it be library-only, leaving all CLI concerns to consumers? - Should the
marked-terminalrendering be part of core? It's used for error output formatting in index.js, not just the CLI. We could make it a soft/optional dependency or inject a formatter function.
Source: semantic-release/semantic-release