Configuration reference
Tagsmith reads a JSONC config file. The default path is <repo-root>/.tagsmith.jsonc. Use --config <path> to override. Relative paths resolve from the repo root, not the current directory. Absolute paths are used as-is, even when the file is outside the repo.
File format
- Plain UTF-8 JSON with comments and trailing commas.
- Comments: line (
//) and block (/* */). - Unknown keys, duplicate object keys, and the reserved key
__proto__are rejected at parse time with a field path. ~is not expanded by Tagsmith.
Schema URL
https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.jsonAdd "$schema": "<url>" (optional, recommended) for editor completion. init writes it for you.
Top-level shape
{
"$schema": "https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.json",
"configVersion": 1,
"git": { "remote": "...", "baseBranch": "..." },
"defaults": {
"tagPattern": "...",
"tagMessage": "...",
"initialVersion": "...",
},
"targets": {
"<targetName>": {
/* target config */
},
},
}| Field | Required | Type | Description |
|---|---|---|---|
$schema | optional | string | JSON Schema URL. Whatever value you set is preserved in targets --json. |
configVersion | required | 1 | Must be the literal 1. No other version is supported. |
git | required | object | Repository-wide Git policy. No per-target Git config. |
defaults | required | object | Values inherited by every target unless overridden. |
targets | required | object | At least one named target. Keys are target names. |
git
"git": {
"remote": "origin",
"baseBranch": "main"
}| Field | Required | Rule |
|---|---|---|
remote | required | Non-empty, no whitespace, no slash. Must be the name of a configured Git remote (typically origin). URLs are rejected. |
baseBranch | required | Unqualified branch name. main, release/1.x are valid (the slash is part of an unqualified branch name). origin/main, refs/heads/main, HEAD are rejected. |
There is no per-target Git config. Every target uses the same remote and baseBranch.
defaults
"defaults": {
"tagPattern": "{target}@{version}",
"tagMessage": "Release {target} {version}",
"initialVersion": "0.0.0"
}All three fields are required. Targets inherit them unless they override.
| Field | Rule |
|---|---|
tagPattern | See Tag patterns. |
tagMessage | See tagMessage grammar. |
initialVersion | Canonical stable SemVer: X.Y.Z. No leading v, no prerelease, no build metadata, no leading zeros, no whitespace. Acts as both the declared minimum managed baseline and the bump baseline when no stable tag exists yet for a target. Existing managed tags equal to initialVersion are allowed; tags below it are malformed. |
targets
Object keyed by target name. At least one entry required.
"targets": {
"web": {
"path": "apps/web",
"channels": [
{ "name": "alpha", "strategy": "prerelease" },
{ "name": "beta", "strategy": "prerelease", "dependsOn": ["alpha"] },
{ "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] },
{ "name": "stable","strategy": "stable", "dependsOn": ["rc"] }
]
}
}Target keys
Target names must match ^[a-z][a-z0-9-]*$. Case-sensitive. Names live in their own namespace; a channel may share a name with a target without conflict.
Per-target fields
| Field | Required | Rule |
|---|---|---|
path | required | Relative paths resolve from the repo root; absolute paths used as-is. The realpath must exist as a directory, stay inside the repo realpath, and be unique across all targets. Nested target paths are allowed if they resolve to different directories. init may emit example paths that don't exist; you must edit them before any config-required command passes. |
channels | required | Array, at least one entry. Exactly one channel must have strategy: "stable". See Channels. |
tagPattern | optional | Overrides defaults.tagPattern. Same grammar. |
tagMessage | optional | Overrides defaults.tagMessage. Same grammar. |
initialVersion | optional | Overrides defaults.initialVersion. Same rules. |
Multi-target pattern ambiguity
When effective patterns of two targets could match the same Git tag, config validation fails with targets <A> and <B> have ambiguous effective tagPattern <pattern>. The classic mistake is using defaults.tagPattern: "v{version}" with multiple targets — both would match v1.2.3. Use {target}@{version} for monorepos, or set distinct per-target overrides.
Channels
Each channel entry:
{ "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] }| Field | Required | Rule |
|---|---|---|
name | required | Matches ^[a-z][a-z0-9-]*$. Unique within the target. |
strategy | required | "prerelease" or "stable". Exactly one channel per target has "stable". |
dependsOn | optional | Array of channel names in the same target. No self-references. No cycles. |
Channel rules
- A target may have only the stable channel (
channels: [{ "name": "stable", "strategy": "stable" }]). - The stable channel's
namedoes not need to bestable. The strategy is what matters. Convention isstable; older configs may use other names. dependsOnis direct and validation-only — see Mental model. It does not influence version resolution.- Targets do not share channels. To gate
api@stableonweb@stable, you cannot;dependsOnis intra-target only.
tagPattern grammar
See Tag patterns for the full grammar, allowed characters, ambiguity rules, and warnings.
tagMessage grammar
"tagMessage": "Release {target} {version}"- Placeholders:
{target},{version},{tag}(all optional, any number of each). - Must be printable single-line text. Control characters and newlines are rejected.
- The final rendered message must be non-empty.
- Tagsmith never executes
tagMessage; it is annotation message data only. validaterenderstagMessagefrom current config; it does not read or compare the existing annotated tag's actual message.
SemVer policy
Pure SemVer everywhere a version appears (initialVersion, --version, parsed managed tags). Tagsmith does not normalize versions.
Valid:
1.2.3
1.2.4-rc.1
1.2.4-pre-prod.1
1.0.0-alpha.42Invalid:
v1.2.3 // leading v
1.2.3+build.5 // build metadata
1.2.4-rc // missing prerelease counter
1.2.4-rc.0 // prerelease counter must be ≥ 1
01.2.3 // leading zero
1.02.3
1.2.03Prerelease counters start at 1. Hyphenated channel names render as a single prerelease identifier (e.g. pre-prod channel produces 1.2.3-pre-prod.1).
Parse and validation errors
Parse errors (returned as <filePath>: <message>):
<filePath>: malformed JSONC (<ParseErrorCode>)<filePath>: reserved key __proto__ at <jsonPath><filePath>: duplicate key <name> at <jsonPath><filePath>: <fieldPath>: unrecognized keys<filePath>: <fieldPath>: <zod message>
Validation errors (returned as <filePath>: <message>, first failure wins):
git.remote must be a safe configured remote name without whitespace or slashgit.baseBranch must be an unqualified branch namedefaults.initialVersion must be canonical stable SemVer without build metadata or leading vtargets must contain at least one targettargets.<name> must match /^[a-z][a-z0-9-]*$/utargets.<name>.channels contains duplicate channel <name>targets.<name>.channels must contain exactly one stable channeltargets.<name>.channels.<name>.dependsOn may not depend on selftargets.<name>.channels.<name>.dependsOn references missing channel <name>targets.<name>.channels dependency cycle is invalidtargets.<name>.tagPattern …— see Tag patternstargets.<name>.tagMessage must be printable single-line texttargets.<name>.tagMessage must be non-empty after interpolationtargets.<name>.tagPattern renders an unsafe Git tag nametargets <A> and <B> have ambiguous effective tagPattern <pattern>
See the Error catalogue for the full set.
Example: single-target
{
"$schema": "https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.json",
"configVersion": 1,
"git": { "remote": "origin", "baseBranch": "main" },
"defaults": {
"tagPattern": "v{version}",
"tagMessage": "Release {version}",
"initialVersion": "0.0.0",
},
"targets": {
"app": {
"path": ".",
"channels": [
{ "name": "rc", "strategy": "prerelease" },
{ "name": "stable", "strategy": "stable", "dependsOn": ["rc"] },
],
},
},
}Example: monorepo with full ladder
{
"$schema": "https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.json",
"configVersion": 1,
"git": { "remote": "origin", "baseBranch": "main" },
"defaults": {
"tagPattern": "{target}@{version}",
"tagMessage": "Release {target} {version}",
"initialVersion": "0.0.0",
},
"targets": {
"web": {
"path": "apps/web",
"channels": [
{ "name": "alpha", "strategy": "prerelease" },
{ "name": "beta", "strategy": "prerelease", "dependsOn": ["alpha"] },
{ "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] },
{ "name": "stable", "strategy": "stable", "dependsOn": ["rc"] },
],
},
"api": {
"path": "apps/api",
"channels": [
{ "name": "alpha", "strategy": "prerelease" },
{ "name": "beta", "strategy": "prerelease", "dependsOn": ["alpha"] },
{ "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] },
{ "name": "stable", "strategy": "stable", "dependsOn": ["rc"] },
],
},
"auth": {
"path": "packages/auth",
"tagPattern": "pkg-auth@{version}",
"tagMessage": "Release auth package {version}",
"initialVersion": "1.0.0",
"channels": [
{ "name": "rc", "strategy": "prerelease" },
{ "name": "stable", "strategy": "stable", "dependsOn": ["rc"] },
],
},
},
}The auth target overrides all three defaults — different pattern (so pkg-auth@1.0.0 instead of auth@1.0.0), different message, different baseline. Useful when migrating from a legacy tag namespace.