UNPKG

package-versioner

Version:

A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.

468 lines (348 loc) 17.9 kB
# Versioning Strategies and Concepts `package-versioner` offers flexible ways to determine the next version for your project based on its history and your configuration. ## How the Next Version is Calculated There are two primary methods the tool uses to decide the version bump (e.g., patch, minor, major), configured via the `versionStrategy` option in `version.config.json`: ### 1. Conventional Commits (`versionStrategy: "conventional"`) This is the default strategy. `package-versioner` analyzes Git commit messages since the last Git tag that follows semver patterns. It uses the [conventional-commits](https://www.conventionalcommits.org/) specification to determine the bump: - **Patch Bump (e.g., 1.2.3 -> 1.2.4):** Triggered by `fix:` commit types. - **Minor Bump (e.g., 1.2.3 -> 1.3.0):** Triggered by `feat:` commit types. - **Major Bump (e.g., 1.2.3 -> 2.0.0):** Triggered by commits with `BREAKING CHANGE:` in the footer or `feat!:`, `fix!:` etc. in the header. The specific preset used for analysis (e.g., "angular", "conventional") can be set using the `preset` option in `version.config.json`. **Format:** `<type>(<scope>): <subject>` `<scope>` is optional. **Example Commit Types:** - `feat:` (new feature for the user) - `fix:` (bug fix for the user) - `docs:` (changes to the documentation) - `style:` (formatting, missing semi-colons, etc; no production code change) - `refactor:` (refactoring production code, e.g. renaming a variable) - `test:` (adding missing tests, refactoring tests; no production code change) - `chore:` (updating build tasks etc; no production code change) **References:** - [https://www.conventionalcommits.org/](https://www.conventionalcommits.org/) - [https://github.com/conventional-changelog/conventional-changelog](https://github.com/conventional-changelog/conventional-changelog) ### 2. Branch Pattern (`versionStrategy: "branchPattern"`) This strategy uses the name of the current Git branch (or the most recently merged branch matching a pattern, if applicable) to determine the version bump. You define patterns in the `branchPattern` array in `version.config.json`. Each pattern is a string like `"prefix:bumptype"`. **Example `version.config.json`:** ```json { "versionStrategy": "branchPattern", "branchPattern": [ "feature:minor", "hotfix:patch", "fix:patch", "release:major" ], "baseBranch": "main" } ``` **How it works:** 1. The tool checks the current branch name. 2. It might also look for the most recently merged branch into `baseBranch` that matches any pattern in `branchPattern`. 3. It compares the relevant branch name (current or last merged) against the prefixes in `branchPattern`. 4. If a match is found (e.g., current branch is `feature/add-login`), it applies the corresponding bump type (`minor` in this case). This allows you to enforce version bumps based on your branching workflow (e.g., all branches starting with `feature/` result in a minor bump). ## Package Type Support `package-versioner` supports both JavaScript/TypeScript projects using `package.json` and Rust projects using `Cargo.toml`: ### JavaScript/TypeScript Projects For JavaScript/TypeScript projects, the tool looks for and updates the `version` field in `package.json` files according to the versioning strategies described above. ### Rust Projects For Rust projects, the tool looks for and updates the `package.version` field in `Cargo.toml` files using the same versioning strategies. ### Mixed Projects with Both Manifests When both `package.json` and `Cargo.toml` exist in the same directory, `package-versioner` will: 1. Update both manifest files independently with the same calculated version 2. First check `package.json` for the current version (when no tags exist) 3. Fall back to checking `Cargo.toml` only if `package.json` doesn't exist or doesn't have a version This allows you to maintain consistent versioning across JavaScript and Rust components in the same package. ## Version Source Selection `package-versioner` uses a smart version source selection strategy to determine the base version for calculating the next version: 1. First, it checks for Git tags: - In normal mode: Uses the latest reachable tag, falling back to unreachable tags if needed - In strict mode (`--strict-reachable`): Only uses reachable tags 2. Then, it checks manifest files (package.json, Cargo.toml): - Reads version from package.json if it exists - Falls back to Cargo.toml if package.json doesn't exist or has no version 3. Finally, it compares the versions: - If both Git tag and manifest versions exist, it uses the newer version - If the versions are equal, it prefers the Git tag for better history tracking - If only one source has a version, it uses that - If no version is found, it uses the default initial version (0.1.0) This strategy ensures that: - Version numbers never go backwards - Git history is respected when possible - Manifest files are considered as valid version sources - The tool always has a valid base version to work from For example: ``` Scenario 1: - Git tag: v1.0.0 - package.json: 1.1.0 Result: Uses 1.1.0 as base (package.json is newer) Scenario 2: - Git tag: v1.0.0 - package.json: 1.0.0 Result: Uses v1.0.0 as base (versions equal, prefer Git) Scenario 3: - Git tag: unreachable v2.0.0 - package.json: 1.0.0 Result: Uses 2.0.0 as base in normal mode (unreachable tag is newer) Uses 1.0.0 as base in strict mode (unreachable tag ignored) ``` ## Package Targeting in Monorepos When working with monorepos, you can control which packages are processed for versioning using the `packages` configuration option. This provides flexible targeting with support for various pattern types. ### Targeting Patterns #### Exact Package Names Target specific packages by their exact names: ```json { "packages": ["@mycompany/core", "@mycompany/utils", "standalone-package"] } ``` #### Scope Wildcards Target all packages within a specific scope: ```json { "packages": ["@mycompany/*"] } ``` This will match all packages whose names start with `@mycompany/`. #### Global Wildcard Target all packages in the workspace: ```json { "packages": ["*"] } ``` #### Mixed Patterns Target different types of packages using a combination of patterns: ```json { "packages": ["@mycompany/*", "@utils/logger", "legacy-package"] } ``` ### Skip Patterns The `skip` configuration option allows you to exclude specific packages from versioning using the same pattern matching capabilities as package targeting. #### Pattern Types 1. **Exact Package Names** ```json { "skip": ["@internal/docs", "test-utils"] } ``` 2. **Scope Wildcards** ```json { "skip": ["@internal/*"] } ``` This will skip all packages whose names start with `@internal/`. 3. **Path Patterns** ```json { "skip": ["packages/**/test-*", "examples/**/*"] } ``` This will skip packages matching the specified path patterns. 4. **Mixed Patterns** ```json { "skip": ["@internal/*", "test-*", "packages/examples/**/*"] } ``` #### Skip Pattern Priority Skip patterns take precedence over include patterns. If a package matches both a pattern in `packages` and a pattern in `skip`, it will be excluded from versioning. Example: ```json { "packages": ["@company/*"], "skip": ["@company/internal-*"] } ``` In this case, all packages under the `@company` scope will be versioned except those starting with `@company/internal-`. ### Behaviour - **When `packages` is specified**: Only packages matching those patterns will be processed for versioning - **When `packages` is empty or not specified**: All workspace packages will be processed - **Error handling**: If no packages match the specified patterns, a warning is displayed ### Excluding Packages Use the `skip` option to exclude specific packages from processing: ```json { "packages": ["@mycompany/*"], "skip": ["@mycompany/deprecated-package"] } ``` This configuration will process all packages in the `@mycompany` scope except for `@mycompany/deprecated-package`. **Note**: Your workspace configuration (pnpm-workspace.yaml, package.json workspaces, etc.) determines which packages are available in your workspace, but the `packages` option directly controls which ones get versioned. ## Tag Templates and Configuration `package-versioner` provides flexible configuration for how Git tags are formatted, allowing you to customize the tag structure for both single package repositories and monorepos. ### Tag Template Configuration You can customize how tags are formatted using the following configuration options in `version.config.json`: ```json { "versionPrefix": "v", "tagTemplate": "${prefix}${version}", "packageSpecificTags": false } ``` - **versionPrefix**: The prefix used for all version numbers in tags (default: `"v"`) - **tagTemplate**: The template for Git tags (default: `"${prefix}${version}"`) - **packageSpecificTags**: Whether to enable package-specific tagging behaviour (default: `false`) ### Available Template Variables The tag template supports the following variables: - `${prefix}`: Replaced with the value of `versionPrefix` - `${version}`: Replaced with the calculated version number - `${packageName}`: Replaced with the package name (only populated when `packageSpecificTags` is `true`) ### How Package-Specific Tagging Works The `packageSpecificTags` option controls whether the `${packageName}` variable is populated in your template: - **When `packageSpecificTags` is `false`**: The `${packageName}` variable is empty, so use templates like `${prefix}${version}` - **When `packageSpecificTags` is `true`**: The `${packageName}` variable contains the actual package name ### Examples #### Global Versioning (Default) ```json { "versionPrefix": "v", "tagTemplate": "${prefix}${version}", "packageSpecificTags": false } ``` This produces tags like `v1.2.3` for all packages. #### Package-Specific Versioning ```json { "versionPrefix": "v", "tagTemplate": "${packageName}@${prefix}${version}", "packageSpecificTags": true } ``` This produces tags like `@scope/package-name@v1.2.3` for each package. #### Custom Tag Format Examples ```json { "versionPrefix": "", "tagTemplate": "release-${version}", "packageSpecificTags": false } ``` This would produce tags like `release-1.2.3` instead of `v1.2.3`. ```json { "versionPrefix": "v", "tagTemplate": "${packageName}-${prefix}${version}", "packageSpecificTags": true } ``` This would produce package tags like `@scope/package-name-v1.2.3` instead of `@scope/package-name@v1.2.3`. ### Behaviour in Different Modes - **Synced Mode with Single Package**: When `packageSpecificTags` is `true`, the package name is used even though all packages are versioned together - **Synced Mode with Multiple Packages**: Package names are not used regardless of the `packageSpecificTags` setting - **Async Mode**: Each package gets its own tag when `packageSpecificTags` is enabled ## Troubleshooting Template Configuration `package-versioner` provides helpful warnings when template configurations don't match your project setup. Here are common issues and their solutions: ### Template Contains ${packageName} but No Package Name Available If you see this warning, it means your template includes `${packageName}` but the tool cannot determine a package name for the current context. **Example Warning:** ``` Warning: Your tagTemplate contains ${packageName} but no package name is available. This will result in an empty package name in the tag (e.g., "@v1.0.0" instead of "my-package@v1.0.0"). To fix this: • If using sync mode: Set "packageSpecificTags": true in your config to enable package names in tags • If you want global tags: Remove ${packageName} from your tagTemplate (e.g., use "${prefix}${version}") • If using single/async mode: Ensure your package.json has a valid "name" field ``` **Solutions:** 1. **For Synced Mode with Package Names**: Enable package-specific tags ```json { "sync": true, "packageSpecificTags": true, "tagTemplate": "${packageName}@${prefix}${version}" } ``` 2. **For Global Tags**: Remove `${packageName}` from your template ```json { "tagTemplate": "${prefix}${version}", "packageSpecificTags": false } ``` 3. **For Single/Async Mode**: Ensure your `package.json` has a valid `name` field ```json { "name": "my-package", "version": "1.0.0" } ``` ### Common Template Patterns Here are some common template patterns and when to use them: | Pattern | Use Case | Example Output | |---------|----------|----------------| | `"${prefix}${version}"` | Global versioning, all packages get same tag | `v1.2.3` | | `"${packageName}@${prefix}${version}"` | Package-specific versioning | `@scope/package@v1.2.3` | | `"release-${version}"` | Custom release format | `release-1.2.3` | | `"${packageName}-${version}"` | Simple package versioning | `@scope/package-1.2.3` | ### Commit Message Templates The same principles apply to `commitMessage` templates. If your commit message template includes `${packageName}`, ensure that package names are available in your current mode: ```json { "commitMessage": "chore: release ${packageName}@${version}", "packageSpecificTags": true } ``` For global commit messages, use templates without `${packageName}`: ```json { "commitMessage": "chore: release ${version}" } ``` ## Monorepo Versioning Modes While primarily used for single packages now, `package-versioner` retains options for monorepo workflows, controlled mainly by the `sync` flag in `version.config.json`. ### Sync Mode (`sync: true`) This is the default if the `sync` flag is present and true. - **Behaviour:** The tool calculates **one** version bump based on the overall history (or branch pattern). This single new version is applied to **all** packages within the repository (or just the root `package.json` if not a structured monorepo). A single Git tag is created. - **Tag Behaviour:** - In **multi-package monorepos**: Creates global tags like `v1.2.3` regardless of `packageSpecificTags` setting - In **single-package repositories**: Respects the `packageSpecificTags` setting - can create either `v1.2.3` or `package-name@v1.2.3` - **Use Case:** Suitable for monorepos where all packages are tightly coupled and released together with the same version number. Also the effective mode for single-package repositories. ### Async Mode (`sync: false`) *(Note: This mode relies heavily on monorepo tooling and structure, like `pnpm workspaces` and correctly configured package dependencies.)* - **Behaviour (Default - No `-t` flag):** The tool analyzes commits to determine which specific packages within the monorepo have changed since the last relevant commit/tag. - It calculates an appropriate version bump **independently for each changed package** based on the commits affecting that package. - Only the `package.json` files of the changed packages are updated. - A **single commit** is created grouping all the version bumps, using the commit message template. **No Git tags are created** in this mode. - **Use Case:** Suitable for monorepos where packages are versioned independently, but a single commit represents the batch of updates for traceability. - **Behaviour (Targeted - With `-t` flag):** When using the `-t, --target <targets>` flag: - Only the specified packages (respecting the `skip` list) are considered for versioning. - It calculates an appropriate version bump **independently for each targeted package** based on its commit history. - The `package.json` file of each successfully updated targeted package is modified. - An **individual Git tag** (e.g., `packageName@1.2.3`) is created **for each successfully updated package** immediately after its version is bumped. - Finally, a **single commit** is created including all the updated `package.json` files, using a summary commit message (e.g., `chore(release): pkg-a, pkg-b 1.2.3 [skip-ci]`). - **Important:** Only package-specific tags are created. The global tag (e.g., `v1.2.3`) is **not** automatically generated in this mode. If your release process (like GitHub Releases) depends on a global tag, you'll need to create it manually in your CI/CD script *after* `package-versioner` completes. - **Use Case:** Releasing specific packages independently while still tagging each released package individually. ## Prerelease Handling `package-versioner` provides flexible handling for prerelease versions, allowing both creation of prereleases and promotion to stable releases. ### Creating Prereleases Use the `--prerelease` flag with an identifier to create a prerelease version: ```bash # Create a beta prerelease npx package-versioner --bump minor --prerelease beta # Result: 1.0.0 -> 1.1.0-beta.0 ``` You can also set a default prerelease identifier in your `version.config.json`: ```json { "prereleaseIdentifier": "beta" } ``` ### Promoting Prereleases to Stable Releases When using standard bump types (`major`, `minor`, `patch`) with the `--bump` flag on a prerelease version, `package-versioner` will automatically clean the prerelease identifier: ```bash # Starting from version 1.0.0-beta.1 npx package-versioner --bump major # Result: 1.0.0-beta.1 -> 2.0.0 (not 2.0.0-beta.0) ``` This intuitive behaviour means you don't need to use an empty prerelease identifier (`--prerelease ""`) to promote a prerelease to a stable version. Simply specify the standard bump type and the tool will automatically produce a clean version number. This applies to all standard bump types: - `--bump major`: 1.0.0-beta.1 -> 2.0.0 - `--bump minor`: 1.0.0-beta.1 -> 1.1.0 - `--bump patch`: 1.0.0-beta.1 -> 1.0.1