enablement-build-monorepo-version
v2.0.9
Published
This detects changes in the children packages of a monorepo.
Readme
enablement-build-monorepo-version
Detects which packages in a monorepo have changed by hashing their source folders, computes the next semantic version for each changed package, and emits Azure DevOps pipeline variables. Dependency propagation is recursive — changing a leaf package automatically marks all transitive dependents as changed too.
Usage
npx enablement-build-monorepo-version@latest [flags]How it works
- Hash — Each subfolder under the configured
childrendirectories is hashed (source files only;node_modules,dist, etc. are excluded). - Compare — Hashes are diffed against saved state from
{configDir}/manifest.jsonwhen present (cicd[packageName].hash), falling back to{configDir}/hash.jsonfor legacy repos. - Propagate — Any package whose hash changed is marked
CHANGED. All transitive dependents (read from{configDir}/manifest.jsondependencieswhen available, or from--dependencieswhen explicitly provided) are also markedCHANGEDvia a breadth-first traversal. - Version — The next semver is determined from the current version found in each package's version manifest (
package.json,version.json, orpyproject.toml) using conventional-commit rules. - Output — Azure DevOps
##vso[task.setvariable]lines are written to stdout for consumption by downstream pipeline steps.
CLI flags
| Flag | Default | Description |
| ----------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --hash | off | Compute and save current hashes to the hash state file. Must be run before --changed / --version to establish a baseline. |
| --changed | off | Print the list of changed packages and emit a changed pipeline variable. |
| --version | off | Emit a pipeline variable per changed package containing its next version. |
| --save | off | Write the bumped version back into each changed package's version manifests (package.json, version.json, and/or pyproject.toml). Requires --version. |
| --tag | off | Create a git tag (<short-name>/<version>) for each changed package, where short-name is the npm package name with its scope prefix removed (e.g. @scope/lib-foo → lib-foo/1.2.3). |
| --retag | off | Convert existing tags that use the old folder-name format to the new short-package-name format. Non-destructive: old tags are left in place. |
| --init | off | Inject release:* scripts into the root package.json of the target project. Adds scripts that are missing; strips --children from any that already exist. Combine with --try to preview changes. |
| --try | off | Dry-run mode. Prints what each operation would do without writing any files, creating tags, or modifying any manifests. Combine with any other flags to preview their effect. |
| --debug | off | Verbose logging of hashes, comparisons, and version resolution. |
| --children <dirs> | auto-detect | Comma- or space-separated list of top-level directories to scan (e.g. components,saas). When omitted, directories are read from pnpm-workspace.yaml or package.json workspaces automatically. Falls back to packages if neither file is found. |
| --platform <name> | auto-detect | Force the CI output format: ado (Azure DevOps) or github (GitHub Actions). Auto-detected from the presence of a .github/ directory. |
| --prefixPath <path> | ./ | Root path prepended to all file lookups. Useful when running from a different working directory. |
| --hashFile <path> | auto-detect | Path (relative to prefixPath) where hash state is stored between runs. Defaults to .github/manifest.json or .cicd/manifest.json when those files exist (hashes stored inside cicd[packageName].hash); falls back to .github/hash.json or .cicd/hash.json for legacy standalone hash files. |
| --dependencies <path> | dependencies.json | Path to the NX-style dependency graph used for transitive change propagation. By default, manifest dependencies are used when {configDir}/manifest.json exists; passing this flag explicitly overrides that and forces file-based dependency input. |
| --hashExcludeFolders <list> | see below | Comma-separated folder names to skip when hashing. |
| --hashExcludeFiles <list> | see below | Comma-separated file names to skip when hashing. |
| --hashFiles <list> | — | Comma-separated individual file paths to include in the hash state (outside of children folders). |
Default excluded folders: node_modules, coverage, dist, bin, obj, __pycache__, .vs, .nx, .vscode, .idea, .git, .github, .azuredevops, .release
Default excluded files: .npmrc, CHANGELOG.md, README.md
Supported version manifests
Each package directory is scanned for a version manifest in this priority order:
| File | Format | Version field |
| ---------------- | ----------------------------------- | -------------------------------------------------------- |
| package.json | JSON | "version": "1.2.3" |
| version.json | JSON (same shape as package.json) | "version": "1.2.3" |
| pyproject.toml | TOML | version = "1.2.3" under [project] or [tool.poetry] |
The first file found is used to determine the current version. When --save is enabled, all existing manifests in this set are updated. If none exists the package is treated as version 0.0.0.
Version bump rules
Versions follow semver and are bumped based on the current version in the package's version manifest:
| Trigger | Bump | Example |
| ------------------------------------------------ | --------------- | ----------------- |
| fix: prefix in any recent commit message | Patch (0.0.x) | 1.2.3 → 1.2.4 |
| feat: prefix in any recent commit message | Minor (0.x.0) | 1.2.3 → 1.3.0 |
| BREAKING anywhere in any recent commit message | Major (x.0.0) | 1.2.3 → 2.0.0 |
Currently the version bump always applies a patch increment — conventional-commit scanning via
git logis stubbed. The bump logic is insrc/version.mjs.
Pipeline variable output
The output format is auto-detected from the presence of a .github/ directory at the root. It can also be forced with --platform ado or --platform github.
Azure DevOps (default when no .github/ directory)
Variables are written to stdout using the ##vso task command:
##vso[task.setvariable variable=<safeName>;isoutput=true;]<version>
##vso[task.setvariable variable=changed;isoutput=true]["pkg-a","pkg-b"]
##vso[task.setvariable variable=components;isoutput=true]trueGitHub Actions (when .github/ directory is present)
Variables are written to the $GITHUB_OUTPUT file (falls back to stdout if the env var is not set):
safeName=<version>
changed=["pkg-a","pkg-b"]
components=trueConsume them in a subsequent step with ${{ steps.<step-id>.outputs.<name> }}.
Variable reference
| Variable | Value |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| <safeName> (per changed package) | Next version for changed packages; previous version for unchanged ones. safeName is the folder name with -, _, and . removed. |
| changed | JSON array of changed package folder names. |
| <folderName> | true for each top-level folder containing at least one changed package. |
Dependency Source
By default, transitive dependency propagation uses dependencies from {configDir}/manifest.json when that manifest exists.
To force file-based dependency input, pass --dependencies <path>. The file must be an NX project graph export. The relevant section is graph.dependencies, where each key is a package name and its value is an array of { source, target } edges:
{
"graph": {
"dependencies": {
"@scope/pkg-a": [
{
"source": "@scope/pkg-a",
"target": "@scope/shared-lib",
"type": "static"
}
]
}
}
}Generate it from an NX workspace with:
nx graph --file=dependencies.jsonIf the file is missing or the --dependencies path doesn't exist, change propagation is skipped and only directly-changed packages are reported (a warning is printed).
Typical pipeline usage
# Step 1 — establish baseline hashes (run once, commit the result)
# Workspace directories are auto-detected from pnpm-workspace.yaml / package.json workspaces
- script: npx enablement-build-monorepo-version --hash
# Step 2 — detect changes and emit version variables for downstream steps
- script: npx enablement-build-monorepo-version --changed --version
name: versions
# Step 3 — consume a variable from step 2
- script: echo "$(versions.mypackagename)"To bump and persist the new versions back to the package version manifests (package.json, version.json, and pyproject.toml when present):
npx enablement-build-monorepo-version --changed --version --saveWhen --changed --version --save detects any changed package, the root package.json version is also bumped by a patch increment.
To override the auto-detected directories, pass --children explicitly:
npx enablement-build-monorepo-version --changed --version --children components,saasBootstrapping a new repo
Run --init once from the root of any monorepo to inject the standard release:* scripts into its package.json:
npx enablement-build-monorepo-version --initPreview what would change without writing anything:
npx enablement-build-monorepo-version --init --tryThe following scripts are injected (skipped if already present; --children stripped if already there):
| Script | Command |
| ------------------ | ----------------------------------- |
| release:try | dry-run of the full release flow |
| release:changed | list changed packages |
| release:version | bump and save versions |
| release:finalize | write hashes |
| release:retag | migrate tags to package-name format |
Development
src/
├── index.mjs CLI entry point and main() orchestration
├── lib.mjs Core logic (exported for testing)
├── version.mjs Semver bump logic
└── folder-hash.mjs Recursive folder hashing
tests/
├── lib.test.mjs Unit tests for lib.mjs
├── dependencies.json Sample NX dependency graph fixture
├── package.json Sample package.json fixture
├── version.json Sample version.json fixture
└── pyproject.toml Sample pyproject.toml fixtureRun the test suite:
pnpm testWatch mode:
pnpm test:watch