semantic-release/semantic-release
github.com/semantic-release/semantic-release ↗ · 85 files · 465 KB
📦🚀 semantic-release Fully automated version management and package publishing semantic-release automates the whole package release workflow including: determining the next version number, generating the release notes, and publishing the package.
Overview
semantic-release is an ESM Node.js CLI and JavaScript API that automates package releases for maintainers running CI against Git repositories. It derives release versions from commit analysis, generates notes, persists release and channel metadata in Git tags and notes, and delegates publishing and notifications to plugins. Its distinctive capability is coordinating regular, maintenance, and prerelease branches with distribution channels while enforcing version ranges.
System map
The system is a one-shot release coordinator with CLI and programmatic entrypoints. Core modules own configuration, branch policy, Git state, and lifecycle sequencing; external plugins own package and hosting integrations. The published package contains executable JavaScript and TypeScript declarations, while Git tags and notes provide durable release state.
- package.json exposes bin/semantic-release.js as the semantic-release executable, index.js as the main module, and index.d.ts as its declarations; the package uses ESM.
- The executable rejects unsupported Node versions and checks Git against minimum 2.7.1; package engines require ^22.14.0 or >=24.10.0.
- cli.js parses release options with yargs, dynamically imports index.js, and maps successful execution to exit code 0 and errors to 1.
- index.js owns the run context and release sequence, with dedicated modules for configuration, verification, versions, commits, authentication, and branches.
- getConfig searches for release configuration with cosmiconfig, reads package metadata upward, falls back to remote.origin.url, and constructs plugin pipelines.
- Default integrations are commit-analyzer, release-notes-generator, npm, and github; they are dependencies loaded through the plugin system rather than implemented in the core.
- Git release storage uses tags plus JSON notes under semantic-release-prefixed refs; current writes use a separate note ref for each tagged release, and reads include older note naming.
- Development uses npm scripts for formatting, lockfile validation, engine compatibility, publish validation, and AVA suites; the release workflow runs npx semantic-release@25.0.1 on configured branch pushes.
Feature catalog
Users configure a single release command or invoke the asynchronous API. Features cover commit-driven versions, notes, branch channels, maintenance ranges, prereleases, and promotion of previously released versions. Plugins provide configurable preparation, publishing, verification, and notification behavior.
- The CLI exposes branches, repository URL, tag format, plugins, shareable configurations, CI verification, debugging, dry-run, help, and lifecycle-specific plugin options.
- The JavaScript API accepts configurable cwd, env, stdout, and stderr and normally returns lastRelease, commits, nextRelease, and releases; skipped runs return false.
- Commit analysis excludes messages containing [skip release] or [release skip], case-insensitively, and selects the highest patch/minor/major result across analyzers.
- First releases start at 1.0.0; prerelease branches append their identifier and start at .1, while later releases increment according to analysis and channel history.
- Branch configuration supports remote-name patterns, interpolated branch properties, maintenance branches, up to three regular release branches, and unique prerelease identifiers.
- Maintenance and regular branches receive computed version ranges and accepted release types; prerelease branches receive an identifier and default channel based on their name.
- Merged releases from higher non-prerelease branches can be added to the current distribution channel using addChannel, without creating a new version.
- Dry-run permits verification, commit analysis, and note generation, skips preparation/publishing/channel/notification hooks, and prints generated notes in the terminal.
Data flow
Configuration and CI detection establish a mutable core context for each invocation. Remote branch names, merged tags, Git notes, and commit history become normalized release inputs. Plugin pipelines transform those inputs into release decisions and outputs, which the core persists to Git before provider publication.
- Configuration merges discovered values with CLI/API options, reduces shareable configurations in order, then overlays explicit options and non-null defaults.
- envCi derives the execution branch from the supplied environment and cwd; run uses prBranch for pull requests and branch otherwise.
- getGitAuthUrl normalizes hosted shortcuts and git+HTTP URLs, tries existing authentication, and falls back to selected environment credentials embedded in a formatted URL.
- Branch discovery expands remote patterns, fetches each matching branch and notes, loads tags, classifies branch types, normalizes ranges, and aggregates validation errors.
- get-tags builds a regex from tagFormat, checks parsed versions with semver, attaches note channels, and treats tags without parsed notes as belonging to the default channel.
- getLastRelease selects the highest eligible tag; getCommits reads from its resolved head to nextRelease.gitHead or HEAD and trims parsed messages and tag decorations.
- Plugin wrappers bind cloned global/local configuration and clone context data while preserving streams and supplying a scoped logger; pipeline input changes occur through getNextInput.
- Publication writes the tag and channel note and pushes both before provider hooks; publish results combine nextRelease metadata, provider output, and plugin identity before entering context.releases.
Behavior traces
The inspected flows include CLI startup, new-version publication, and promotion of an existing release to another channel. Additional branches explain dry-run behavior, skipped releases, preparation commits, and failures. Execution order comes from implementation rather than the README's simplified step table.
- Startup: bin checks Node and launches execa(git --version), then calls cli; cli parses options, enables debug if requested, imports index.js, and the API installs masking, detects CI, creates a logger, calls getConfig, then run.
- Eligibility: run forces dry-run outside known CI unless noCi is set, skips CI pull requests unless noCi, then calls verify, getGitAuthUrl, and getBranches; a branch absent from normalized release branches returns false.
- Authorization: run calls verifyAuth; if that fails, isBranchUpToDate can return false and skip publication, otherwise the error becomes EGITNOPERMISSION before verifyConditions runs.
- New version: getLastRelease → getTagHead when available → getCommits → analyzeCommits → getGitHead → getNextVersion → makeTag; non-prerelease versions outside branch.range throw EINVALIDNEXTVERSION.
- Publication: verifyRelease → generateNotes → prepare → tag → addNote → push → pushNotes → publish → success; the API finally returns selected context fields.
- Preparation commits: after each prepare plugin, getGitHead detects a changed HEAD, updates nextRelease.gitHead, regenerates notes, and passes updated release data to the following prepare plugin.
- Channel promotion: getReleaseToAdd chooses an eligible merged version → getTagHead resolves release heads → maintenance mergeRange is checked → getCommits and generateNotes run → updated channel notes are pushed → addChannel and success run.
- Failure: run errors enter callFail, which sends only semanticRelease errors to fail plugins and logs fail-hook errors; the outer catch logs the original error, removes output hooks, and rethrows for CLI exit code 1.
Testing signals
Tests and CI definitions were inspected, not executed; their existence is not a passing result. AVA suites cover core helpers, Git behavior, lifecycle integration, and container-backed publishing scenarios. Inspection sampled test implementations and declarations, so it does not establish exhaustive coverage or live provider compatibility.
- AVA's default file selection includes test/**/*.test.js except integration and E2E; separate scripts run those suites, and c8 instruments lib JavaScript, index.js, and cli.js with all enabled.
- Inspected integration tests create real local Git histories and stub lifecycle hooks with Sinon, replacing env-ci and logging through testdouble to assert plugin context.
- Integration assertions cover channel promotion across fast-forward, merge, and rebase histories; dry-run hook suppression; pull-request skipping; stale clones; and no relevant commits.
- E2E tests invoke the executable against Gitbox and Verdaccio, mock GitHub API requests, and assert package/shrinkwrap versions, registry dist-tags, and local/remote tag heads.
- E2E infrastructure uses Testcontainers with pinned image digests from e2e.compose.yml, mapped service ports, readiness probes, a copied Verdaccio configuration, and teardown of started containers.
- Security regressions include dash-prefixed repository URL rejection, Git upload/receive-pack argument-injection checks, encoded credential masking, and masking in a failing Git command's captured logs.
- The inspected test workflow runs npm test on Ubuntu for Node 22.14.0, 24.10.0, 24, and .nvmrc; jobs also clean-install dependencies and audit signatures, but this brief reports no execution or coverage result.
- The sampled E2E path verifies mocked GitHub responses rather than live GitHub behavior, and the inspected CI jobs use Ubuntu; CONTRIBUTING asks for 100% unit coverage, while the inspected c8 config specifies reports without a threshold.
Risk map
These are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. Sensitive boundaries include executable configuration, plugins receiving environment data, authenticated Git subprocesses, and durable remote publication. The main operational hazards arise from partial publication, dry-run side effects, and coupling between branch history and note metadata.
- Shareable configurations and plugins execute imported modules; plugin context includes cloned environment data, so these extension inputs cross a code-execution and credential-trust boundary.
- Authentication fallback embeds selected Git credentials in repository URLs passed to Git; masking covers raw and encoded environment values, but does not remove credentials from subprocess arguments.
- Redaction only selects matching environment names with trimmed values of at least five characters, excludes GOPRIVATE, and only replaces strings; hideSensitiveValues visits direct properties rather than recursively traversing objects.
- Git subprocesses use argument arrays and remote operations include -- before repository URLs; verify additionally rejects URLs beginning with -, with explicit regression tests for Git option injection.
- Dry-run still resolves authentication and fetches branch/tag/note state before lifecycle publication is skipped; it is not a no-network or no-local-Git-write execution mode.
- Hypothesis: a failing publish or success plugin can leave a partially completed release because tags and notes are already pushed, provider hooks run sequentially, and the inspected failure handler performs logging/notification without rollback. (inferred)
- getTagsNotes ignores JSON parse failures, but does not validate parsed note shape before get-tags assigns channels; downstream last-release and promotion logic assume channel arrays.
- The inspected release workflow independently triggers on pushes and has no declared dependency on test jobs; it grants repository/issue/PR write permissions and runs published semantic-release@25.0.1 rather than the checked-out executable.
Extension points
The lifecycle definition table is the core extension registry. Users extend behavior through package plugins, inline hook functions, and shareable configuration modules. Branch policy and typed API contracts provide separate extension surfaces, with explicit validation and result transformation rules.
- PLUGINS_DEFINITIONS registers verifyConditions, analyzeCommits, verifyRelease, generateNotes, prepare, publish, addChannel, success, and fail, with per-stage required/dryRun/validation/pipeline rules.
- A configured plugin object contributes only exports matching registered lifecycle names; each contributed function receives immutable pluginName metadata and retains list order.
- Plugin configuration accepts names, name/config tuples, and path-bearing objects; API callers can supply inline plugin objects, while individual stages accept functions.
- loadPlugin resolves relative to a shareable configuration when recorded, otherwise from core or cwd, and supports function inputs, default exports, and ESM named exports.
- Stage-specific options override collected plugin hooks; a plain options object without path is merged into existing stage plugin configurations before normalization.
- pipeline exposes settleAll, getNextInput, and transform; definitions use these for aggregated verification errors, accumulated notes, prepare HEAD refresh, and enriched publication results.
- Shareable configuration modules merge in sequence and record plugin origins; branch patterns can interpolate name into properties, while branch definitions provide classification and validation hooks.
- index.d.ts exposes stage-specific contexts, branch/release types, configuration, and the asynchronous API signature; extensions changing runtime shapes should also inspect these public declarations.
Contributor handoff
Begin with the orchestrator and lifecycle definitions, then follow the affected branch, Git, or configuration helper. Tests and CI were inspected, not executed, and this brief is bounded evidence rather than an exhaustive audit. Future implementation work should preserve publishing order, dry-run eligibility, configuration precedence, and credential masking while validating only within the authorized environment.
- Read index.js and lib/definitions/plugins.js together before lifecycle changes: core call order and per-stage dry-run/preprocess/postprocess behavior jointly determine observable execution.
- Use npm and the tracked lockfile; packageManager pins npm 12.0.2, Node engines define runtime compatibility, and .nvmrc selects Node 24 for development.
- For authorized validation, start with the affected AVA file, then use test:unit, test:integration, or test:e2e as appropriate; npm test combines lint scripts with test suites, including container-backed E2E.
- Tests and CI were inspected, not executed: coverage configuration, assertions, and workflow commands establish intended checks only; no passing suite or measured coverage is asserted here.
- Preserve configuration precedence and shareable-config plugin resolution when editing get-config; explicit CLI/API values override discovered configuration and imported defaults.
- Before changing branch policy, inspect normalization, last-release selection, and promotion together; branch order controls ranges and higher-channel eligibility, while Git notes carry channel history.
- Security edits should retain dash-prefixed URL rejection, Git argument separators, and raw/encoded masking; focused regression files include verify, git, hide-sensitive, and integration tests.
- Contract trap: runtime can return only {releases} after channel promotion when analysis produces no new release, but Result declares a release object requiring lastRelease, commits, and nextRelease.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 65 | 98% |
| YAML | 9 | docs/config |
| Markdown | 3 | docs/config |
| JSON | 1 | docs/config |
| TypeScript | 1 | 2% |
Start reading here
Probable source entrypoint: index.js — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (24 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/branches/ | 4 |
lib/definitions/ | 4 |
lib/plugins/ | 4 |
lib/get-commits.js | 1 |
lib/get-config.js | 1 |
lib/get-error.js | 1 |
lib/get-git-auth-url.js | 1 |
lib/get-last-release.js | 1 |
lib/get-logger.js | 1 |
lib/get-next-version.js | 1 |
lib/get-release-to-add.js | 1 |
lib/git.js | 1 |
lib/hide-sensitive.js | 1 |
lib/utils.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
test/ | 39 |
lib/ | 24 |
.github/ | 7 |
media/ | 1 |
bin/ | 1 |
Docs found: CONTRIBUTING.mdREADME.md
Package
package.json — semantic-release v0.0.0-development, 51 declared dependencies.
@semantic-release/commit-analyzer@semantic-release/error@semantic-release/github@semantic-release/npm@semantic-release/release-notes-generator@types/node@types/signaleaggregate-erroravac8clear-modulecosmiconfig
CI & config entrypoints
README.md— Repository readme.github/workflows/release.yml— GitHub Actions workflow.github/workflows/test.yml— GitHub Actions workflow.github/workflows/scorecard.yml— GitHub Actions workflowpackage.json— npm scripts: lint:engines, lint:lockfile, lint:prettier, lint:prettier:fix, lint:publish, semantic-release, test, test:e2e
Structural health signals
Average score 8.7/10 across 64 analyzed source files; 1 hotspot surfaced.
test/integration.test.js— 2041 lines, score 6.2 (Large file, Long function/block)test/e2e.test.js— 743 lines, score 6.7 (Large file, Long function/block)index.d.ts— 724 lines, score 6.7 (Large file, Long function/block)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
e8c2436e57042026-09-21 — build(deps-dev): bump fast-uri from 3.1.4 to 3.1.7 (#4293)a1def89ffdaa2026-09-21 — build(deps): bump js-yaml from 3.15.0 to 3.15.2 (#4298)64b5e0ba5b422026-09-14 — chore(deps): update dependency got to v16 (#4305)afe0ddbbdf462026-09-14 — ci(action): update github/codeql-action action to v4.38.0 (#4304)ddab29d3cf612026-09-14 — chore(deps): update dependency testcontainers to v12.1.0 (#4303)