{"id":51433077,"url":"https://github.com/carllee1983/tagsmith","last_synced_at":"2026-07-23T11:00:36.692Z","repository":{"id":363948007,"uuid":"1265092260","full_name":"CarlLee1983/Tagsmith","owner":"CarlLee1983","description":"Define git tag specs, view tags, and generate the next git tag safely — avoiding ordering or format anomalies (SemVer / CalVer / build).","archived":false,"fork":false,"pushed_at":"2026-07-23T06:17:09.000Z","size":478,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-23T07:04:55.802Z","etag":null,"topics":["calver","cli","git","git-tags","release","semver","typescript","versioning"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CarlLee1983.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-10T13:07:08.000Z","updated_at":"2026-07-23T06:13:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/CarlLee1983/Tagsmith","commit_stats":null,"previous_names":["carllee1983/tagsmith"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/CarlLee1983/Tagsmith","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CarlLee1983%2FTagsmith","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CarlLee1983%2FTagsmith/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CarlLee1983%2FTagsmith/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CarlLee1983%2FTagsmith/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CarlLee1983","download_url":"https://codeload.github.com/CarlLee1983/Tagsmith/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CarlLee1983%2FTagsmith/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35800203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-23T02:00:06.683Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["calver","cli","git","git-tags","release","semver","typescript","versioning"],"created_at":"2026-07-05T05:03:15.896Z","updated_at":"2026-07-23T11:00:36.685Z","avatar_url":"https://github.com/CarlLee1983.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tagsmith\n\n[![npm version](https://img.shields.io/npm/v/@carllee1983/tagsmith.svg)](https://www.npmjs.com/package/@carllee1983/tagsmith)\n\nDefine a git-tag convention, inspect the tags already in a repository, and safely\ngenerate the next one. Tagsmith prevents format drift, duplicate versions, and\nout-of-order releases before a tag is created or pushed.\n\nSupports **SemVer**, **CalVer**, and monotonically increasing **build numbers**.\nTag formats are configurable, including `v{version}` and `release/{version}`.\n\n\u003e [繁體中文文件](docs/README.zh-TW.md) · [Documentation index](docs/README.md)\n\n## Why Tagsmith?\n\n- **Standardize** — keep the project's tag convention in `.tagsmith.json`.\n- **Inspect** — list or audit tags in semantic order and identify format,\n  parsing, duplicate-version, orphan, and tag-line assignment problems.\n- **Create safely** — validate the format, parseability, strict version ordering,\n  and tag uniqueness before creating a tag.\n- **Start with zero configuration** — infer a SemVer-style pattern from existing\n  tags when no configuration file exists.\n- **Support multiple release lines** — give independent version models and tag\n  patterns to application, release, or other tag lines.\n- **Plan monorepo releases** — inspect every line's committed changes, candidate\n  tag, and blockers before choosing which existing create commands to run.\n- **Release safely with a team** — fetch remote tags before a push and use the\n  included GitHub Action to enforce the same checks in CI.\n- **Check release readiness** — optionally require the correct branch, a clean\n  worktree, an annotated or signed tag, and a `HEAD` target before creation.\n- **Guard merges locally** — optionally restrict which branches may merge into\n  protected branches through managed git hooks.\n\n## Install\n\n```bash\n# Install globally (the command is still named `tagsmith`)\nnpm install -g @carllee1983/tagsmith\n\n# Or run without installing\nnpx @carllee1983/tagsmith \u003ccommand\u003e\n\n# Add to a project for CI or hooks\nnpm install -D @carllee1983/tagsmith\n# Then run: npx tagsmith \u003ccommand\u003e\n```\n\nRequirements: Node.js \u003e=18 and git.\n\n## Quick start\n\n### Zero configuration\n\nIn a repository that already has SemVer-like tags such as `v0.1.0`:\n\n```bash\ntagsmith list                    # Inspect existing tags\ntagsmith next                    # Preview the next patch tag\ntagsmith next --level minor      # For example: v0.1.0 → v0.2.0\ntagsmith create --push           # Create and push the tag\n```\n\nWithout `.tagsmith.json`, Tagsmith uses SemVer and `v{version}` by default and\ninfers common existing formats such as `{version}`. For a team repository, run\n`tagsmith init` and commit the generated configuration to make the convention\nexplicit.\n\n### Configure a project\n\n```bash\n# 1. Define the convention in the current repository (interactive and optional)\ntagsmith init\n\n# Need help? Use the interactive walkthrough\ntagsmith guide\n\n# 2. Inspect tags and anomalies\ntagsmith list\n\n# 3. Preview a tag without changing the repository\ntagsmith next --level minor\n\n# 4. Create and push an annotated tag\ntagsmith create --level minor -m \"Release 1.2.0\" --push\n```\n\n## Configuration\n\n`tagsmith init` creates `.tagsmith.json` in the repository root. A configuration\ncan define one or more independent tag lines:\n\n```json tagsmith-config\n{\n  \"tags\": [\n    {\n      \"name\": \"app\",\n      \"pattern\": \"v{version}\",\n      \"model\": { \"type\": \"semver\", \"allowPrerelease\": true },\n      \"initialVersion\": \"0.1.0\",\n      \"push\": false\n    },\n    {\n      \"name\": \"release\",\n      \"pattern\": \"release/{version}\",\n      \"model\": { \"type\": \"calver\", \"format\": \"YYYY.MM.MICRO\" },\n      \"initialVersion\": \"2026.06.0\",\n      \"push\": true\n    }\n  ],\n  \"default\": \"app\"\n}\n```\n\nEach line has its own pattern and version sequence. Select a non-default line\nwith `--tag \u003cname\u003e`.\n\n| Field | Required | Meaning |\n| --- | :---: | --- |\n| `name` | Yes | Unique line name, used by `--tag` |\n| `pattern` | Yes | Tag format containing `{version}` |\n| `model` | Yes | Version-model object |\n| `initialVersion` | Yes | Starting point when no valid tag exists |\n| `push` | No | Whether `create` pushes by default (`false`) |\n| `workspace` | No | Repository-relative monorepo path used by `--require-changes` |\n| `artifact` | No | `{ \"type\": \"package-json\" }` version source for this line |\n| `default` | No | Default line; defaults to the first item in `tags` |\n\nThe legacy single-line format remains supported. It is normalized internally as\na line named `default`, so existing users do not need to migrate.\n\n```json tagsmith-config\n{\n  \"pattern\": \"v{version}\",\n  \"model\": { \"type\": \"semver\", \"allowPrerelease\": true },\n  \"initialVersion\": \"0.1.0\",\n  \"push\": false\n}\n```\n\nAdd `\"$schema\": \"./node_modules/@carllee1983/tagsmith/schema.json\"` to enable\neditor completion and validation.\n\n### Monorepo workspaces\n\nA tag line can be scoped to a package path. The existing `--tag` selector then\nkeeps each workspace's tag sequence independent, while `--require-changes`\nprevents a release when that package has not changed since its latest tag.\nWhen using `--from-commits`, Tagsmith likewise considers only commits that\ntouch the selected workspace.\n\n```json tagsmith-config\n{\n  \"tags\": [\n    {\n      \"name\": \"api\",\n      \"workspace\": \"packages/api\",\n      \"pattern\": \"api/v{version}\",\n      \"model\": { \"type\": \"semver\" },\n      \"initialVersion\": \"0.1.0\"\n    }\n  ],\n  \"default\": \"api\"\n}\n```\n\n```bash\ntagsmith next --tag api --require-changes\ntagsmith create --tag api --require-changes --push\ntagsmith plan --all --from-commits\n```\n\n`workspace` must stay inside the repository and is evaluated against committed\nchanges only. It is optional, so existing single-repository configurations are\nunchanged. `plan --all` reads every configured line: it scopes lines with a\n`workspace` to that package and evaluates an unscoped line against the entire\nrepository.\n\n### Artifact version consistency\n\nAdd `artifact: { \"type\": \"package-json\" }` to a line to make its package\nmanifest a version source. Tagsmith reads `workspace/package.json`, or the\nrepository-root `package.json` for an unscoped line. The `version` must parse\nunder the line's version model and exactly match the tag version without its\ntag-pattern prefix or suffix.\n\n```json tagsmith-config\n{\n  \"tags\": [{\n    \"name\": \"api\",\n    \"workspace\": \"packages/api\",\n    \"pattern\": \"api/v{version}\",\n    \"model\": { \"type\": \"semver\" },\n    \"initialVersion\": \"0.1.0\",\n    \"artifact\": { \"type\": \"package-json\" }\n  }],\n  \"releasePolicy\": { \"requireArtifactVersion\": true }\n}\n```\n\n`audit` reads the manifest from each historical tag, so an in-progress local\nversion bump cannot invalidate a previous release. With the policy flag,\n`create --enforce-policy` reads the selected target commit and blocks a missing,\nmalformed, invalid, or mismatched manifest. Tagsmith never edits manifests,\nlockfiles, or artifacts.\n\n### Release readiness\n\nAn optional top-level `releasePolicy` records local, pre-create guardrails. It\nis disabled by default: existing `create` behaviour changes only when you add\nthe policy **and** pass `--enforce-policy`.\n\n```json tagsmith-config\n{\n  \"pattern\": \"v{version}\",\n  \"model\": { \"type\": \"semver\" },\n  \"initialVersion\": \"0.1.0\",\n  \"releasePolicy\": {\n    \"allowedBranches\": [\"main\", \"release/*\"],\n    \"requireCleanWorktree\": true,\n    \"requireAnnotatedTag\": true,\n    \"requireHeadTag\": true,\n    \"signature\": \"required\",\n    \"requireArtifactVersion\": true\n  }\n}\n```\n\n`allowedBranches` supports `*` and `?` globs. A clean worktree includes no\nstaged, unstaged, or untracked files. `requireAnnotatedTag` needs `--message`;\n`signature: \"required\"` needs `--sign --message \"…\"` and a Git signing key.\n`requireHeadTag` applies to the candidate being created, not old tags; use\n`--target \u003cref\u003e` only when an intentional non-`HEAD` target is needed.\n`requireArtifactVersion` requires the selected line to configure a supported\nartifact and makes its version match the candidate tag at the candidate target.\n\nUse `tagsmith audit` to inspect the configured branch/worktree rules. It makes\nno remote request unless `--fetch` is explicit. `tagsmith create --enforce-policy`\nruns the same checks with the concrete tag candidate before it changes Git state.\n\n### Version models\n\n| Model | Example | Model setting | Increment |\n| --- | --- | --- | --- |\n| `semver` | `1.2.3`, `1.2.3-rc.1` | `allowPrerelease` (default `true`) | `major`, `minor`, `patch`, `prerelease` |\n| `calver` | `2026.06.0` | `format`: `YYYY`, `YY`, `MM`, `DD`, `MICRO` | Rolls by date; increments `MICRO` on the same date |\n| `build` | `0042` | `padding` (default `0`) | Increments by one |\n\nFor CalVer, numeric token widths are strict: `YYYY` has four digits, `YY`,\n`MM`, and `DD` have two, and `MICRO` has no leading zero. This prevents\nnon-canonical tags from representing the same version.\n\n## Commands\n\n| Command | Purpose |\n| --- | --- |\n| `tagsmith init` | Interactively create a tag specification (optional) |\n| `tagsmith guide` | Walk through init → list → next → create |\n| `tagsmith list` / `ls` | List tags in semantic order and report anomalies |\n| `tagsmith audit` | Audit tag history plus configured release readiness |\n| `tagsmith plan --all` | Plan read-only releases across every configured tag line |\n| `tagsmith check [tags...]` | Validate supplied tags, or all repository tags with no arguments |\n| `tagsmith next` | Compute the next valid tag without creating it |\n| `tagsmith create` | Create the next or an explicit tag after validation |\n| `tagsmith hooks install` | Install merge-policy hooks |\n| `tagsmith hooks uninstall` | Remove only Tagsmith-managed merge-policy hooks |\n\nUseful options:\n\n```bash\ntagsmith list --all                    # Show every configured line and orphan tags\ntagsmith audit --json                  # Machine-readable complete tag audit\ntagsmith audit --fetch --remote origin # Include freshly fetched remote tags\ntagsmith plan --all --json             # Read every line's release decision\ntagsmith plan --all --from-commits     # Use Conventional Commits for SemVer lines\ntagsmith check --strict --json         # Audit tag shape and duplicate versions\ntagsmith next --fetch --remote origin  # Include tags fetched from a remote\ntagsmith next --from-commits           # Recommend a SemVer bump from commits\ntagsmith next --tag api --require-changes\ntagsmith create --level minor --push   # Create and push a minor release\ntagsmith create --set-version 1.0.5 --allow-out-of-order\ntagsmith create --level major --dry-run\ntagsmith create --enforce-policy -m \"Release 1.2.0\"\n```\n\n`next` and `create` accept `--level major|minor|patch|prerelease|auto`.\n`--from-commits` is an alternative to `--level` for SemVer lines: a breaking\nchange recommends `major`, `feat` recommends `minor`, and `fix` or `perf`\nrecommends `patch`. For a workspace-scoped line, the recommendation considers\nonly commits that touch that workspace. `create` also accepts `--message`, `--set-version`,\n`--push`, `--dry-run`, and `--allow-out-of-order`.\n\n### Commit policy\n\nWhen `commitPolicy` is absent, those default Conventional Commit rules remain\nunchanged. To use a team-specific taxonomy, declare ordered rules. The first\nmatching rule wins; a rule can filter exact `type`, `scope`, and `breaking`\nstatus, then set one `release` level or `ignore: true`. Unmatched commits are\nignored when a custom policy is configured.\n\n```json\n{\n  \"commitPolicy\": {\n    \"rules\": [\n      { \"name\": \"breaking\", \"breaking\": true, \"release\": \"major\" },\n      { \"name\": \"product work\", \"type\": \"product\", \"release\": \"minor\" },\n      { \"name\": \"website docs\", \"type\": \"docs\", \"scope\": \"website\", \"release\": \"patch\" },\n      { \"type\": \"docs\", \"ignore\": true }\n    ]\n  }\n}\n```\n\n`next --from-commits`, `create --from-commits`, and `plan --all --from-commits`\nall include the contributing commit IDs, summaries, and matching rule in their\nrecommendation evidence.\n\n### Planning monorepo releases\n\n`tagsmith plan --all` is a read-only multi-line decision. Every configured\nline is reported in configuration order as `ready`, `skipped`, or `blocked`.\nA ready line supplies a candidate that matches `tagsmith next --tag \u003cname\u003e`;\nunchanged workspaces are skipped, not errors. The plan never creates or pushes\ntags.\n\n| Flag | Meaning |\n| --- | --- |\n| `--all` | Required acknowledgement that every configured line will be inspected |\n| `--json` | Output the versioned plan envelope for CI or scripts |\n| `--fetch` / `--remote \u003cname\u003e` | Fetch tags before planning (default remote: `origin`) |\n| `--from-commits` | Derive SemVer bumps from Conventional Commits; non-SemVer lines are blocked explicitly |\n| `--require-changes` | Require every line to define `workspace`; unchanged lines remain `skipped` |\n\n```jsonc\n// tagsmith plan --all --from-commits --json\n{\n  \"schemaVersion\": 1,\n  \"command\": \"plan\",\n  \"ok\": true,\n  \"data\": {\n    \"defaultLine\": \"api\",\n    \"hasReleases\": true,\n    \"lines\": [\n      {\n        \"line\": \"api\",\n        \"workspace\": \"packages/api\",\n        \"status\": \"ready\",\n        \"changed\": true,\n        \"bump\": \"minor\",\n        \"candidate\": { \"tag\": \"api/v1.3.0\", \"version\": \"1.3.0\", \"fromVersion\": \"1.2.0\", \"fresh\": false },\n        \"recommendation\": { \"level\": \"minor\", \"reasons\": [{ \"id\": \"…\", \"level\": \"minor\", \"rule\": \"default.feat\", \"summary\": \"feat(api): add search\" }] },\n        \"commits\": [{ \"id\": \"…\", \"summary\": \"feat(api): add search\" }],\n        \"blockers\": [],\n        \"anomalies\": []\n      },\n      { \"line\": \"web\", \"status\": \"skipped\", \"changed\": false, \"bump\": null, \"candidate\": null }\n    ]\n  },\n  \"diagnostics\": []\n}\n```\n\n`blocked` lines make the command exit non-zero and include stable diagnostic\ncodes such as `ambiguous-assignment`, `workspace-required`, or\n`from-commits-unsupported`; the other lines are still available in the result.\n\n### Auditing assignment safety\n\n`tagsmith audit` is a read-only repository check. It reports malformed and\nduplicate versions, tags that no configured line owns, and tags whose pattern\nmatches more than one line. An ambiguous tag is not silently assigned according\nto configuration order: `next` and `create` refuse to use a line affected by\nthat history until it is resolved.\n\nFor a line with a package-json artifact, audit also checks the exact manifest\nstored at every conforming historical tag. It reports stable errors for a\nmissing or malformed manifest, missing/invalid version, or version mismatch.\n\nEvery `--json` result from `list`, `check`, `next`, `audit`, and `plan` uses the same\nversioned envelope. Read command-specific fields from `data`, and make\nautomation decisions from stable diagnostic `code` values rather than parsing\nhuman-readable messages:\n\n```json\n{\n  \"schemaVersion\": 1,\n  \"command\": \"next\",\n  \"ok\": true,\n  \"data\": { \"tag\": \"v1.3.0\" },\n  \"diagnostics\": []\n}\n```\n\nThe published [JSON output schema](json-output.schema.json) describes the common\nenvelope. Existing scripts that previously read `.tag` should read `.data.tag`.\n\nWhen `releasePolicy` is configured, audit also reports `PASS`, `WARN`, or\n`FAIL` checks for the current branch and worktree. Candidate-only checks\n(annotation, signature, and target) are marked not applicable until `create\n--enforce-policy` supplies the tag it intends to make. In JSON, use the stable\n`release-*` diagnostic codes rather than parsing messages.\n\n## Remote safety and CI\n\n`create --push` fetches tags from `origin` before choosing a version, so a\nstale local clone does not reuse a version already published by a teammate.\nUse `--fetch` for the same protection on a preview, and `--remote \u003cname\u003e` to\nselect another remote. A push can still be rejected if another release wins the\nrace after the fetch; re-fetch and choose a new version in that case. A\n`--dry-run` remains local unless `--fetch` is supplied explicitly.\n\n`check --strict \u003ctag\u003e` also checks a proposed version against the repository's\nexisting tag history. `check` reports `matches` for every candidate; a\n`line: null` plus `ambiguous-assignment` means more than one configured pattern\nmatched, so there is no safe unique owner.\n\nThe repository is a reusable GitHub Action. It builds Tagsmith from the checked\nout action, fetches tags by default, runs `audit --json`, then exposes the\nread-only `plan --all --json` result:\n\n```yaml\nname: Validate tags\non: [push, pull_request]\n\njobs:\n  tags:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - id: tagsmith\n        uses: CarlLee1983/Tagsmith@main # Pin a release tag or commit SHA in production.\n        with:\n          plan-from-commits: \"true\"\n          plan-tag: api\n      - if: steps.tagsmith.outputs.has-releases == 'true'\n        run: echo '${{ steps.tagsmith.outputs.plan }}' | jq .\n```\n\nSet `with: { working-directory: packages/api, fetch-tags: \"false\" }` when the\nconfiguration lives below the repository root or tags are already available.\nThe Action's `plan` output is the full JSON envelope; `has-releases` is `true`\nwhen any line is ready, and `next-tag` is the ready candidate for `plan-tag` or\nthe config default line (otherwise an empty string). It does not create tags.\n\n## Merge policy\n\nTagsmith can also act as a local git-workflow guardrail. A `mergePolicy` limits\nthe source branches that may merge into protected branches; it is enforced by\nmanaged `prepare-commit-msg` and `post-merge` hooks. It does not configure PR\nrules or remote/server-side policy.\n\n```jsonc\n{\n  \"pattern\": \"v{version}\",\n  \"model\": { \"type\": \"semver\" },\n  \"initialVersion\": \"0.1.0\",\n  \"mergePolicy\": {\n    \"protectedBranches\": {\n      \"develop\": { \"allow\": [\"main\"] },\n      \"main\": { \"deny\": [\"develop\", \"testing\", \"feature/*\"] }\n    },\n    \"onUnknownSource\": \"block\"\n  }\n}\n```\n\n- `mergePolicy` is optional and disabled by default.\n- A protected branch must specify exactly one of `allow` or `deny`.\n- Patterns support `*` (including `/`) and `?`.\n- `onUnknownSource` defaults to `block`; choose `allow` only when that is an\n  intentional policy decision.\n\nInstall the hooks after adding the policy:\n\n```bash\nnpm install -D @carllee1983/tagsmith\nnpx tagsmith hooks install\n```\n\nIf a target hook contains unmanaged content, installation stops without writing\nanything; use `--force` only when replacing that hook is intended. For an\nemergency one-off bypass, set `TAGSMITH_SKIP=1` or `HUSKY=0` for the `git merge`\ncommand.\n\n## Documentation\n\n- [Documentation index](docs/README.md)\n- [Contributing guide](CONTRIBUTING.md) · [繁體中文](docs/CONTRIBUTING.zh-TW.md)\n- [Husky pre-push setup](docs/husky-pre-push.md) · [繁體中文](docs/husky-pre-push.zh-TW.md)\n- [Changelog](CHANGELOG.md)\n- [Development roadmap (繁體中文)](ROADMAP.zh-TW.md)\n\n## Development\n\n```bash\nnpm install\nnpm test\nnpm run typecheck\nnpm run build\nnpm run dev -- \u003ccommand\u003e\n```\n\n## License\n\n[MIT](LICENSE) © 2026 carl\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcarllee1983%2Ftagsmith","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcarllee1983%2Ftagsmith","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcarllee1983%2Ftagsmith/lists"}