--- url: 'https://tagsmith.sadiksaifi.dev/docs.md' description: >- Read the Tagsmith documentation for SemVer Git tag creation, annotated release tags, monorepo targets, CI validation, and JSONC configuration. --- # Tagsmith documentation Tagsmith is an opinionated Git tag and SemVer release-tag manager for single-target repositories and monorepos. It manages release intent through a declarative JSONC config file, resolves SemVer versions, creates annotated Git tags, optionally pushes them, and validates existing tags for CI. Tagsmith deliberately does **not**: * run deployments or execute user-defined release functions * mutate release branches, fetch automatically, checkout, merge, or switch branches * read your project `package.json` to decide release versions * support JavaScript or TypeScript config files * accept SemVer build metadata * expose any non-essential shorthand flags (only `-h` and `-v` exist) Deployment systems should react to Git tags that Tagsmith creates and validates. ## Where to start | You want to… | Go to | | ------------------------------------------------------------ | ------------------------------------------ | | Create your first tag in five commands | [Get started](./getting-started) | | Hand setup to an AI assistant | [Setup with AI](./setup-with-ai) | | Build a mental model of targets, channels, and base versions | [Mental model](./concepts) | | Look up a config field | [Configuration reference](./configuration) | | Understand a CLI flag or output | [Commands](./cli/init) | | Wire it into CI | [GitHub Actions](./ci) | | Decode an error message | [Error catalogue](./errors) | ## Operating principles These principles are normative — every behavior in the rest of these docs flows from them. 1. **One predictable way.** There is exactly one supported way to do every operation. No alternate paths, no compatibility shims. 2. **Fail loudly.** Invalid config, invalid flags, unsafe Git state, malformed managed tags, or ambiguous tags fail before any mutation. 3. **All policy is in user config.** Hidden defaults and implicit recovery are forbidden. If you can't see it in `.tagsmith.jsonc`, Tagsmith isn't doing it. 4. **Conservative Git model.** Tagsmith creates annotated tags only, refuses to auto-fetch or mutate branches, and never overwrites existing tags. 5. **Non-rollback on push failure.** If local tag creation succeeds but push or post-push verification fails, the local tag remains and you handle it. 6. **Same surface for humans and machines.** Every interactive prompt maps to an explicit flag. Machine modes (`--json`, `--github-output`, `init --dry-run` raw) never prompt and never emit warnings to stdout. ## Output and exit-code contract * Exit code `0` on success, `1` on any failure (config, CLI, validation, Git, version, or unsafe state). Tagsmith does not use specialized non-zero exit codes. * Successful `--json` writes pretty-printed JSON (2-space indent, trailing newline, full commit SHAs) to stdout and nothing to stderr. * Failures in machine modes write no stdout and a plain human-readable error to stderr. * Color is forbidden in `--json`, `--github-output`, and `init --dry-run` raw output. ## Schema URL Tagsmith publishes its JSON Schema for editor integration: ``` https://tagsmith.sadiksaifi.dev/schema/v1.json ``` `init` writes a `$schema` line into the template automatically. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/getting-started.md' description: >- Install Tagsmith, create a .tagsmith.jsonc config, preview the next SemVer release tag, create an annotated Git tag, and validate it in CI. --- # Get started Five commands to your first validated release. Run them inside a Git repo with a clean working tree and a remote called `origin`. ## 1. Create the config ```sh npx tagsmith@latest init ``` Writes `.tagsmith.jsonc` at the repo root. The template ships with three example targets (`web`, `api`, `auth`) and a full `alpha → beta → rc → stable` channel ladder. Edit it down to what your repo actually has. A minimal single-target shape: ```jsonc { "$schema": "https://tagsmith.sadiksaifi.dev/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": "stable", "strategy": "stable" }], }, }, } ``` When the config has exactly one target, `--target` is optional on `tag` and `validate`. ::: tip Editor schema The `$schema` line wires JSON Schema completion in any editor that supports it. `init` writes it for you. ::: ## 2. Check configured targets ```sh npx tagsmith@latest targets ``` Validates the config and target paths, then prints each target. It does **not** read tags, remotes, or remote refs. ## 3. Preview the next tag ```sh npx tagsmith@latest tag --channel stable --bump patch --dry-run --json ``` Runs the full [preflight](./preflight) and skips create/push. The `--json` payload has the same shape as a real run with `created: false`, `pushed: false`, `dryRun: true`. See [Output modes](./output). ## 4. Create the annotated tag Pick one. Bare creates the annotated tag locally; `--push` also pushes to the configured `git.remote` and verifies the remote tag is annotated and peels to the same commit. ```sh # Local only. npx tagsmith@latest tag --channel stable --bump patch # Create and push. npx tagsmith@latest tag --channel stable --bump patch --push ``` Run **one** of these — both resolve `--bump patch` against managed tag history, so running the second after the first would create and push a second tag instead of pushing the first. If push or verification fails, the local tag remains and Tagsmith exits non-zero. See [Git safety model](./git-safety). ## 5. Validate in CI ```sh npx tagsmith@latest validate --tag "$GITHUB_REF_NAME" --github-output ``` Runs the [validation pipeline](./preflight#validate-pipeline-in-order) and writes single-line `KEY=VALUE` records to `$GITHUB_OUTPUT` after every check passes. Use the keys as inputs for downstream release jobs. See [GitHub Actions](./ci) for a ready-to-paste workflow. ## Bump types at a glance | Channel strategy | `--bump major` | `--bump minor` | `--bump patch` | `--bump prerelease` | | ---------------- | ----------------------- | ----------------------- | ----------------------- | -------------------------------------------------------- | | `stable` | `2.0.0` | `1.3.0` | `1.2.4` | rejected | | `prerelease` | new `X.Y.Z-.1` line | new `X.Y.Z-.1` line | new `X.Y.Z-.1` line | increments `N` on the highest existing same-channel line | `--version ` overrides bump and supplies the version literally. Channel shape, monotonicity, and `dependsOn` rules still apply. See [Versioning](./versioning). ## What's next * Add prerelease channels and `dependsOn` gates — see [Mental model](./concepts). * Configure a monorepo with multiple targets — see [Configuration](./configuration). * Wire `validate` into CI — see [GitHub Actions](./ci). * Want an AI assistant to set this up for you? — see [Setup with AI](./setup-with-ai). --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/setup-with-ai.md' description: >- Use the generated Tagsmith LLM documentation to let an AI coding agent configure release targets, tag patterns, channels, and CI validation safely. --- # Setup with AI Copy this into your AI coding agent (Claude Code, Cursor, Codex, opencode, etc.) from inside your repository: ```text Follow the LLM-only instructions at https://tagsmith.sadiksaifi.dev/docs/setup-with-ai.md to set up Tagsmith in this repository. ``` ## After setup Validate the generated config: ```sh npx tagsmith@latest targets ``` If anything is wrong, Tagsmith prints the exact field and reason. ## Important AI Agent Guidelines These instructions are for AI coding agents like you: Claude Code, Cursor, Codex, Gemini, opencode, pi-coding-agent, and similar tools. When setting up Tagsmith for a user: 1. Fetch and read `https://tagsmith.sadiksaifi.dev/llms-full.txt` first. Treat it as the source of truth for Tagsmith setup, configuration, commands, CI, and errors. 2. Inspect the user's repository before proposing config: Git remotes, current branch, existing tags, workspace/package layout, release workflows, and whether `.tagsmith.jsonc` already exists. 3. Do not invent target names, paths, channel names, remote names, or base branches. Ask the user when a real release-shape decision is needed. 4. Match the user's package runner instead of blindly using `npx`: * Inspect `package.json#packageManager` first. * If absent, infer from lockfiles and project docs: `bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, `README.md`, `AGENTS.md`, `CLAUDE.md`, or equivalent repo guidance. * Bun → `bunx tagsmith@latest ...`. * pnpm → `pnpx tagsmith@latest ...` or `pnpm dlx tagsmith@latest ...`. * Yarn → `yarn dlx tagsmith@latest ...`. * npm or unknown → `npx tagsmith@latest ...`. * Keep generated README snippets, scripts, CI examples, and user-facing commands consistent with the detected runner. 5. Validate config with ` tagsmith@latest targets --json`. 6. If the user wants to keep an existing `v{version}` namespace with historical lightweight release tags, do not invent a new namespace. Set `tagPattern` to `"v{version}"` and set `initialVersion` to the last pre-adoption base version; Tagsmith will ignore matching tags at or below that boundary and manage newer tags strictly. 7. Before any tag mutation, run a dry-run with `--json` and show the resolved target, channel, version, tag, base version, and commit. 8. Do not create or push a tag until the user explicitly confirms. 9. If adding CI, place `tagsmith validate --tag "$GITHUB_REF_NAME" --github-output` before publish/deploy side effects, using the detected package runner. 10. Never work around a failed Tagsmith preflight check. Fix the repo state, config, or command that caused it. 11. After setup, ask whether the user wants a short README note. If yes, add a package-runner-aware snippet such as: ```md Releases are managed by [Tagsmith](https://tagsmith.sadiksaifi.dev/). Use ` tagsmith@latest` to create and validate release tags. ``` --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/concepts.md' description: >- Learn the Tagsmith mental model for release targets, stable and prerelease channels, SemVer bumps, dependency gates, managed tags, and interactive flows. --- # Mental model This page explains the concepts that show up across every Tagsmith command and config field. Everything else in these docs maps back to here. ## Target A **target** is a releasable unit in the repository. In a single-app repo there is exactly one target (often named `app` or the project name); in a monorepo there are typically several (`web`, `api`, `auth`, ...). Every target has: * a unique **name** matching `^[a-z][a-z0-9-]*$` * a **path** that resolves from the repo root, must exist as a directory, must stay inside the repo's realpath, and must be unique by realpath across all targets * a **channel set** (at least one channel; exactly one must have `strategy: "stable"`) * optional overrides for `tagPattern`, `tagMessage`, and `initialVersion` (defaults inherit from the top-level `defaults` block) Target names and channel names are case-sensitive. Tagsmith never normalizes case. Target and channel names live in separate namespaces — a channel may share a name with a target. ## Channel A **channel** is a release line within a target. Each channel has: * a **name** matching `^[a-z][a-z0-9-]*$`, unique within the target * a **strategy** — either `"stable"` or `"prerelease"` * an optional **`dependsOn`** array of channel names in the same target There is exactly one `"stable"` channel per target. Convention is to call it `stable`, but the name doesn't matter; the strategy controls behavior. `"prerelease"` channels are everything else (`alpha`, `beta`, `rc`, custom names). Channel name semantics: * For `"prerelease"` channels, the channel name is encoded in the SemVer prerelease identifier. `rc` channel produces `1.2.3-rc.1`, `1.2.3-rc.2`, etc. * For the `"stable"` channel, the name is never encoded in the version or the tag. `1.2.3` is just `1.2.3`. ## Strategy * **`"stable"`** — versions are canonical stable SemVer (`X.Y.Z`, no prerelease, no build metadata). Allowed bumps: `major`, `minor`, `patch`. `--bump prerelease` is rejected. * **`"prerelease"`** — versions have a prerelease identifier matching the channel name (`X.Y.Z-.N` with `N` ≥ 1). All four bumps are allowed. ## Base version The **base version** of a release is its stable `X.Y.Z`. For stable channels, the base version equals the version. For prerelease channels, the base version is the `X.Y.Z` part before the `-`. So `1.2.3-rc.1` has base version `1.2.3`, and so does the eventual stable `1.2.3`. Base versions matter for two reasons: 1. **`dependsOn` is evaluated at the same base.** Tagsmith requires the dependency channel's tag at the same base, locally and remotely, peeling to the current `HEAD`. The exact tag depends on the dependency channel's strategy: a `prerelease` dependency means the highest `-.N` (e.g. tagging `1.2.3` on `stable` with `dependsOn: ["rc"]` requires `1.2.3-rc.N` for the highest existing `N`); a `stable` dependency means the canonical `` tag itself. 2. **Stable bumps ignore prerelease lines.** A prerelease at a higher base doesn't influence the next stable bump. Worked example: latest stable `1.2.0`, latest beta `1.4.0-beta.1`, `--channel stable --bump minor` resolves to `1.3.0`. ## Bump Tagsmith resolves the next version one of two ways: * **`--bump major | minor | patch | prerelease`** — incremental bump from existing tag history (or from `initialVersion` when there is no tag history for that target). * **`--version `** — explicit version literal. Tagsmith still enforces channel shape, monotonicity, and `dependsOn`. Exactly one of `--bump` or `--version` is required. ### Stable channels Latest stable for the target is the maximum `X.Y.Z` across all stable tags managed by Tagsmith for that target. * `--bump major` → `.0.0` * `--bump minor` → `..0` * `--bump patch` → `..` * `--bump prerelease` → **rejected** (stable channels cannot bump prerelease) If no stable tag exists, the bump resolves from `initialVersion`. ### Prerelease channels * `--bump major | minor | patch` — start a **new prerelease line** at `X.Y.Z-.1`, where `X.Y.Z` is the bumped base computed from the latest stable (or `initialVersion`). * `--bump prerelease` — continue the **highest existing** same-target / same-channel line by incrementing `N`. Fails with an actionable error if no prerelease tag exists yet for that channel — you must start a line with `--bump major|minor|patch` or `--version` first. Worked example: latest stable `1.2.0` for target `app`, no `rc` tags yet. `--channel rc --bump minor` produces `1.3.0-rc.1`. The next `--channel rc --bump prerelease` produces `1.3.0-rc.2`. A subsequent `--channel rc --bump major` then starts a fresh line at `2.0.0-rc.1`. Each prerelease bump still has to be **strictly greater** than the latest existing same-target / same-channel prerelease. After `1.3.0-rc.2`, `--bump patch` would resolve to `1.2.1-rc.1` (base `1.2.1` from latest stable `1.2.0`), which is less than `1.3.0-rc.2` — Tagsmith rejects it. Use `--bump major` to leap above the existing line, or `--version` to set the line explicitly. ### Explicit `--version` `--version` accepts the same shapes the matching strategy would produce. Tagsmith enforces: * stable: canonical `X.Y.Z`, strictly greater than the latest stable for this target (or strictly greater than `initialVersion` if no stable exists). * prerelease: `X.Y.Z-.N` with `N` ≥ 1, strictly greater than the latest same-target / same-channel prerelease, and a base that is strictly greater than the latest stable if one exists — otherwise strictly greater than `initialVersion`. * `dependsOn` is enforced just like a bumped version. You can skip numbers — `--version 5.0.0` after `1.2.0` is fine — but you can't go backwards. ## `dependsOn` `dependsOn` is a **direct, validation-only gate** between channels in the same target. * Direct only — Tagsmith does not transitively walk dependencies. If `stable` `dependsOn: ["rc"]` and `rc` `dependsOn: ["beta"]`, tagging `stable` checks `rc` at the same base but does not also check `beta`. (Indirect coverage comes from your own promotion discipline.) * Same target only — you cannot depend on a channel in another target. * No self-dependencies, no cycles — both are config-validation errors. * **Does not participate in version resolution.** `dependsOn` is checked **after** the version has been resolved; it cannot influence which version comes next. * Evaluated at the same base. The dependency tag must exist locally **and** remotely, both must peel to the same commit, and that commit must equal the current `HEAD` when creating the tag. During `validate`, both must peel to the validated tag's commit. The exact dependency tag depends on the dependency channel's strategy: * `prerelease` dependency — the highest `-.N`. For `stable` `dependsOn: ["rc"]` at base `1.2.3`, that's the highest `1.2.3-rc.N`. * `stable` dependency — the canonical stable `` tag (no prerelease identifier). A channel that depends on `stable` at base `1.2.3` requires the `1.2.3` tag itself to exist. * If multiple matching prerelease dependency tags exist for the same base, only the **highest** `N` is checked. Stable dependencies have exactly one canonical tag per base. ## Tag and tag message The Git tag name is rendered from `tagPattern`. The annotated tag message is rendered from `tagMessage`. See [Tag patterns](./tag-patterns) for the full grammar. * `tagPattern` supports `{target}` (optional, at most once) and exactly one `{version}`. Allowed literal characters: `[a-z0-9._@-]`. Channel name is **never** rendered into the tag name; it's encoded inside the version for prerelease channels. * `tagMessage` supports `{target}`, `{version}`, `{tag}`. Must be single-line printable text and non-empty after interpolation. The annotated tag message is **data**, not code. Tagsmith never executes it. `validate` does not compare the existing annotated tag message to the rendered message — only the rendered version, tag, and target. ## Managed namespace A tag is **managed** by Tagsmith when it matches the target's effective `tagPattern` literals and its parsed base version is greater than `initialVersion`. If a managed tag's literal parts match but the `{version}` capture or ref state is invalid (e.g. lightweight, has build metadata, wrong prerelease shape, local/remote peel to different commits), it is a **malformed managed tag** and fails the relevant command. Matching tags whose parsed base version is less than or equal to `initialVersion` are **legacy baseline tags**. Tagsmith ignores them while resolving history so repositories can adopt `tagPattern: "v{version}"` without rewriting old lightweight `v*` tags. `validate` intentionally rejects those legacy tags with an adoption-boundary error because they predate Tagsmith management. This means Tagsmith is strict **inside** its managed namespace, ignores tags **outside** it, and treats at/below-boundary matches as pre-adoption history. `init` is the only command that doesn't read tags. `targets` validates config and paths but does not read tags or remotes. `tag` and `validate` scan the managed namespace and fail on anything malformed they encounter. ## Reachability `validate` requires that the validated tag's commit is reachable from `/` according to local Git history. Tagsmith **never fetches automatically**, so CI must check out enough history (typically `fetch-depth: 0`) and fetch tags before invoking `validate`. If reachability cannot be proven, `validate` fails with explicit fetch guidance. ## Interactive vs non-interactive Every interactive decision is also an explicit flag. Tagsmith prompts only when **all** of the following hold: * stdin and stdout are TTY * `CI` is unset or falsy (empty, `0`, or `false`) * no `--help` / `--version` * no `--json` / `--github-output` * not `init --dry-run` (raw mode) * argv parsed cleanly (unknown commands/flags fail before any prompt) See [Interactive flows](./interactive) for the full eligibility rules and the per-command flow. ## Vocabulary cheat sheet | Term | Meaning | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Target** | Named releasable unit with a path and channels. | | **Channel** | Release line within a target; one is `"stable"`, others are `"prerelease"`. | | **Strategy** | `"stable"` or `"prerelease"` — controls allowed bumps and version shape. | | **Base version** | Stable `X.Y.Z` portion. `1.2.3-rc.1` has base `1.2.3`. | | **Bump** | `major`, `minor`, `patch`, or `prerelease`. | | **Managed tag** | Tag matching a target's effective `tagPattern` literals with base version greater than `initialVersion`. | | **Legacy baseline tag** | Matching tag whose parsed base version is less than or equal to `initialVersion`; ignored for history resolution and rejected by `validate` with an adoption-boundary error. | | **Malformed managed tag** | Managed tag whose `{version}` capture or peel state is invalid. | | **dependsOn** | Direct, validation-only gate between channels in the same target. | | **Preflight** | Ordered set of checks run before mutation. See [Preflight](./preflight). | --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/tag-patterns.md' description: >- Configure Tagsmith tagPattern templates for single-target repos and monorepos, including SemVer captures, target names, ambiguity rules, and Git ref safety. --- # Tag patterns `tagPattern` controls the Git tag name Tagsmith renders for a given target and version. The grammar is intentionally strict to keep tag names safe and unambiguous. ## Grammar A `tagPattern` is a literal string that may contain: * exactly one `{version}` placeholder — **required** * at most one `{target}` placeholder — optional * literal characters from `[a-z0-9._@-]` Channel name is **never** rendered into the tag. It is encoded inside the version for prerelease channels (e.g. `1.2.3-rc.1`). ### Rejected forms * More than one `{version}`. * More than one `{target}`. * Any other placeholder (e.g. `{channel}`, `{date}`). `{channel}` is intentionally unsupported. * Slashes (`/`), whitespace, uppercase letters, or any punctuation outside the allowed set. * Patterns whose rendered output starts with `-` or `.`. * Patterns whose rendered output ends with `.` or `.lock`. * Patterns whose rendered output contains `..`. The check is twofold: the **pattern** must use only allowed characters and placeholders, and every **rendered** tag must also be a safe Git tag name. ## Recommended patterns | Repo shape | Pattern | Example tag | | ------------------- | -------------------- | ----------------------------- | | Single-target | `v{version}` | `v1.2.3`, `v1.2.4-rc.1` | | Monorepo | `{target}@{version}` | `api@1.2.3`, `web@1.2.4-rc.1` | | Migration namespace | `managed-v{version}` | `managed-v1.2.3` | The migration pattern is useful when you want a fresh namespace. If you want to keep an existing `v{version}` namespace with historical lightweight tags, prefer setting `initialVersion` to the last pre-adoption release instead; matching tags at or below that base are treated as legacy history without rewriting them. ## Warnings Tagsmith emits a human-mode warning (no exit-code change) when `{version}` touches an alphanumeric character or `_` on either side. The single exception is the recommended `v{version}` pattern, which does not warn. Why: patterns like `release1.2.3` are valid Git tag names but read ambiguously. Patterns like `release-1.2.3` or `release_1.2.3` may also confuse downstream tools that key on numeric prefixes. The warning prompts you to add a clear separator. Add a `.`, `-`, `_`, `@`, or end-of-string boundary around `{version}` to silence it. Warnings are suppressed in `--json`, `--github-output`, and `init --dry-run` raw modes. ## Multi-target ambiguity When a config has multiple targets, every target's effective pattern must be statically unambiguous against the others. Identical patterns are an obvious conflict, but the validator also detects effective overlap. Config validation fails with: ``` targets and have ambiguous effective tagPattern ``` The fix is to: * give each target its own `{target}` placeholder (recommended), or * set distinct per-target `tagPattern` overrides with non-overlapping literal portions. ## Render examples | Pattern | Target | Version | Rendered tag | | -------------------- | ------------------------------------------ | ------------ | ---------------- | | `v{version}` | `app` (single target, `{target}` optional) | `1.2.3` | `v1.2.3` | | `v{version}` | `app` | `1.2.4-rc.1` | `v1.2.4-rc.1` | | `{target}@{version}` | `api` | `1.2.3` | `api@1.2.3` | | `{target}@{version}` | `web` | `1.2.4-rc.2` | `web@1.2.4-rc.2` | | `pkg-auth@{version}` | `auth` | `1.0.0` | `pkg-auth@1.0.0` | ## Tag name as Git ref Tagsmith creates **annotated tags only**. Lightweight tags are rejected as malformed managed tags during preflight. After push, Tagsmith re-reads the remote and verifies the tag is annotated there too. The rendered tag must satisfy Git's own ref naming rules in addition to Tagsmith's literal-character set. The full Git ref restrictions Tagsmith enforces (in addition to the pattern character whitelist): * no leading `.` or `-` * no trailing `.` * no `..` anywhere * no `.lock` suffix If a pattern can render a tag that violates these, validation fails with `targets..tagPattern renders an unsafe Git tag name`. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/versioning.md' description: >- Understand how Tagsmith resolves SemVer major, minor, patch, and prerelease bumps across stable channels, alpha, beta, rc flows, and explicit versions. --- # Versioning and bumps Tagsmith resolves the next version from one of two inputs: * `--bump major | minor | patch | prerelease` — incremental bump from existing tag history (or `initialVersion`). * `--version ` — explicit version literal. Exactly one is required on `tag`. ## SemVer policy All versions are canonical SemVer **without** build metadata and **without** a leading `v`. Valid: ``` 1.2.3 1.2.4-rc.1 1.2.4-pre-prod.1 ``` Invalid: ``` v1.2.3 1.2.3+build.5 1.2.4-rc // prerelease counter required 1.2.4-rc.0 // counter must be ≥ 1 01.2.3 1.02.3 1.2.03 ``` Prerelease counters start at `1`. Hyphenated channel names form a single prerelease identifier (so the `pre-prod` channel produces `1.2.3-pre-prod.1`, not `1.2.3-pre-prod-1`). ## Stable channels The latest stable for a target is the maximum `X.Y.Z` across all managed stable tags for that target. | Bump | Result | | ------------ | --------------------------------------------------------------- | | `major` | `.0.0` | | `minor` | `..0` | | `patch` | `..` | | `prerelease` | **rejected**: `stable channel rejects --bump prerelease` | If no stable tag exists yet for the target, the bump resolves from `initialVersion`. ### Stable ignores prerelease lines Stable bumps look at stable tags only. Prerelease tags at higher bases do not influence the next stable bump. Worked example: * latest `stable` tag: `1.2.0` * latest `beta` tag: `1.4.0-beta.1` * `--channel stable --bump minor` → `1.3.0` The `1.4.0-beta.1` line is ignored. To promote `1.4.0-beta.1` to stable, use `--version 1.4.0` or first promote it through the dependency chain. ## Prerelease channels The latest prerelease for a target+channel is the maximum SemVer across managed prerelease tags whose prerelease identifier is the channel name. The base version is the `X.Y.Z` part. | Bump | Result | | ------------ | ---------------------------------------------------------------------------------- | | `major` | new line at `.0.0-.1` | | `minor` | new line at `..0-.1` | | `patch` | new line at `..-.1` | | `prerelease` | continues the highest existing same-target / same-channel line by incrementing `N` | The bumped base computation reads from the **latest stable tag** for the target (or `initialVersion`), not from the latest prerelease. Each prerelease bump major/minor/patch starts a fresh line at counter `1`. ### `--bump prerelease` requires an existing line `--bump prerelease` fails if there is no managed prerelease tag for the target+channel yet: ``` Cannot bump prerelease for : no existing prerelease tag found. Use --bump major, --bump minor, --bump patch, or --version to start a prerelease line. ``` Start a line with `--bump major|minor|patch` or `--version` first. ### Worked example: a full ladder Starting state: no tags. `initialVersion` is `0.0.0`. Channels: `alpha`, `beta`, `rc`, `stable`. ```sh # Cut alpha.1 at a fresh minor line. tagsmith tag --target app --channel alpha --bump minor # → 0.1.0-alpha.1 # Continue alpha line. tagsmith tag --target app --channel alpha --bump prerelease # → 0.1.0-alpha.2 # Promote to beta. Same base. tagsmith tag --target app --channel beta --version 0.1.0-beta.1 # → 0.1.0-beta.1 # Cut rc against beta. tagsmith tag --target app --channel rc --version 0.1.0-rc.1 # → 0.1.0-rc.1 # Promote to stable. dependsOn rc → highest same-base rc must exist. tagsmith tag --target app --channel stable --bump minor # → 0.1.0 ``` (Each `tag` invocation also requires the channel's `dependsOn` chain to be satisfied at the same base, see [Mental model](./concepts#dependson).) ## Explicit `--version` `--version ` skips bump computation but does not skip validation. Stable channels require the literal to be: * canonical stable SemVer (no prerelease, no build metadata, no leading `v`) * **strictly greater** than the latest stable for the target — or **strictly greater than** `initialVersion` if no stable exists Prerelease channels require the literal to: * match shape `X.Y.Z-.N` with `N` ≥ 1 * be strictly greater than the latest same-target / same-channel prerelease * have a base `X.Y.Z` **strictly greater than** the latest stable if any stable exists, otherwise strictly greater than `initialVersion` You may skip numbers (`--version 5.0.0` after `1.2.0` is fine). You cannot go backwards. ## Monotonicity Versions move forward only. Tagsmith never overwrites or reorders existing tags. If you need to abandon a version, pick the next one. ## How Tagsmith counts "existing tags" A tag is **managed** when it matches the target's effective `tagPattern` literals and its parsed base version is greater than `initialVersion`. Within managed history: * Lightweight tags are malformed and fail preflight. * Tags with build metadata or non-canonical SemVer in the `{version}` capture are malformed. * Tags whose local and remote refs peel to **different** commits are malformed. * Remote tags that cannot be proven annotated (e.g. missing `^{}` peel record) are malformed. Matching tags whose parsed base version is less than or equal to `initialVersion` are **legacy baseline tags**. Tagsmith ignores them while resolving history, so existing lightweight tags such as `v2.9.0` and `v2.10.0` can remain in place when `tagPattern` is `"v{version}"` and `initialVersion` is `"2.10.0"`. Validating one of those legacy tags fails with an adoption-boundary error instead of treating it as a malformed managed tag. Malformed managed tags fail `tag` and `validate` even if they aren't the tag you're trying to create or validate. They never silently get ignored. The fix is to correct them, delete/rename them, or move the adoption boundary if they are historical tags you intentionally do not want Tagsmith to manage. Tags **outside** the managed namespace (anything that does not match the literal parts of the pattern) are ignored. Tagsmith does not look at them. ## `initialVersion` * Canonical stable SemVer. No prerelease, no build metadata, no leading `v`. * Acts as both the **adoption boundary** and the **bump baseline** when no newer managed stable tag exists. * Existing matching tags at or below the boundary are legacy; matching tags above it are managed and must be valid annotated Tagsmith tags. * `--bump` never **creates** `initialVersion`; it increments from it. Worked example: a repository already has lightweight `v2.9.0` and `v2.10.0`. Set `tagPattern: "v{version}"` and `initialVersion: "2.10.0"`; `--channel stable --bump patch` resolves to `2.10.1` / `v2.10.1` without rewriting old tags. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/interactive.md' description: >- Understand when Tagsmith prompts in a TTY, how interactive init, targets, validate, and tag flows work, and how every prompt maps to explicit CLI flags. --- # Interactive flows Tagsmith's contract is **100% non-interactive capability equals 100% interactive capability**. Every interactive decision maps to an explicit flag, and every explicit-flag invocation does the same thing the interactive flow would do. Interactive mode only **fills omissions**; it never reinterprets invalid input. ## Prompt eligibility Tagsmith opens prompts only when **all** of the following hold: * argv parsed cleanly (unknown command/flag/shorthand/attached-value/missing-value/invalid-enum/conflict all fail before any prompt) * `--help` / `-h` not present * `--version` / `-v` not present * `--json` not active * `--github-output` not active * raw output not active (`init --dry-run`) * `process.stdin.isTTY === true` * `process.stdout.isTTY === true` * `CI` environment variable is unset, empty, `0`, or `false` Notes: * **`stderr` being a TTY is not sufficient.** Prompts need both stdin and stdout TTY. * **`CI=true` disables prompts even on pseudo-TTYs.** * **`TERM=dumb` does not disable prompts by itself.** Clack decides how much decoration it can render. * **No `--non-interactive` flag.** TTY/CI detection is sufficient. * **No `--yes` / `-y`.** Tagsmith does not ship a confirmation skip. If you want non-interactive behavior, supply the flags explicitly. ## Bare `tagsmith` In an eligible TTY, bare `tagsmith` opens an **action menu** after confirming Git repo context: ``` init Create a Tagsmith config file. tag Resolve, create, and optionally push a release tag. validate Validate a release tag and emit CI-safe facts. targets List configured release targets. ``` Selecting one runs that command's interactive flow. The menu does not loop — after the chosen command completes, Tagsmith exits. Bare `tagsmith` in non-TTY contexts prints global help and exits 0. Outside a Git repo, even bare `tagsmith` fails with `Git repository not found from `. ### Globals carry into the chosen command `--config ` and `--verbose` may be supplied before bare `tagsmith` and apply to whichever command you pick: ```sh tagsmith --config ./release/tagsmith.jsonc --verbose ``` `--config` and `--verbose` are **never** prompted — they are explicit-only. ## Per-command interactive flows ### `init` * If the destination does not exist: confirm creation, then write. * If it exists and `--force` was **not** given: choose between "overwrite" and the safe-negative option. Default: safe-negative. * If it exists and `--force` **was** given: confirmation is still required before mutation. * `init --dry-run` is raw mode and never prompts, even in a TTY. ### `targets` Non-mutating. Prompts nothing. Renders config warnings via Clack-friendly UI and prints target facts. ### `validate` Read-only. No mutation confirmation. * Prompts for `--tag` if missing (manual entry; no discovery). * If `--target` and `--channel` are missing, offers optional assertions: * "infer target and channel from tag" (default) * "assert target" * "assert target and channel" * In multi-target configs, asserting a channel requires asserting a target first (otherwise the channel list would be ambiguously merged). `--json` and `--github-output` bypass all prompts and preserve existing stream contracts. ### `tag` Input collection order: **target → channel → version intent**. Rationale: channel strategy determines which bump choices are valid. 1. Load and validate config first. 2. **Target**: auto-select if single-target. Otherwise prompt with config-order menu when `--target` missing. 3. **Channel**: auto-select if the target has only one total channel. Otherwise prompt with config-order menu when `--channel` missing. 4. **Version intent**: skip if `--bump` or `--version` was given. Otherwise prompt: * "bump" → menu filtered by strategy (`stable` shows `major|minor|patch`; `prerelease` shows all four). * "explicit version" → enter a SemVer literal with strategy-shaped hints (stable example: `1.2.3`; prerelease example: `1.2.3-rc.1`). 5. **Preflight** — runs full preflight; on failure, stop **before** review with the canonical error. 6. **Review screen** — shows target, channel, strategy, version intent, resolved version, rendered tag, rendered annotated message, full commit SHA, and the **equivalent non-interactive command** (canonical flag order, shell-escaped). 7. **Confirmation:** * Without `--push`: "local create" / "create and push" / "no action". Default: local create. * With `--push`: confirm or cancel. Default: cancel (safe-negative). 8. **Execute** the chosen action. On push or post-push verification failure, the local tag remains. Even when **every** flag is supplied, the review/confirmation still runs in interactive mode. That's intentional — interactive runs always pause before mutation. CI / machine modes / non-TTY runs execute as soon as preflight passes. `tag --dry-run` in interactive mode shows the dry-run facts and exits with no confirmation prompt. ## No auto-pivot Interactive `tag` with a missing config fails with the canonical error and may **suggest** `tagsmith init`. It must not auto-launch `init` for you. Each command stays within its own scope. ## Equivalent command rendering Whenever a review screen shows an equivalent command, it follows strict rules: * canonical binary name `tagsmith` * canonical flag order from the shared CLI contract, not user-supplied order * includes `--config ` if the user supplied one * omits `--verbose` (diagnostic, not workflow intent) * includes the real command flags: `--target`, `--channel`, `--bump`, `--version`, `--push`, `--dry-run`, `--force` where applicable * shell-escapes arguments when needed (especially config paths with spaces) * never includes prompt-only confirmation concepts Example: ```sh tagsmith --config './release config/tagsmith.jsonc' tag --target api --channel rc --bump prerelease --push ``` This is the value of the interactive review: it's a copy-paste-ready non-interactive invocation you can drop into CI or a script. ## Cancellation Cancellation sources: * Ctrl+C during any prompt. * Selecting the safe-negative option in a review (e.g. "no action" or "cancel"). Behavior: * exit code `1` * no mutation * no machine output * no stack trace * concise message: `tagsmith failed: tagsmith cancelled.` ## Confirmation defaults (matrix) | Situation | Default | | ----------------------------------------------------------- | ---------------------- | | `init` overwrites an existing config | safe-negative | | `tag` with `--push` review | safe-negative (cancel) | | `tag` without `--push` review | local create (primary) | | `tag` review with no `--push` preselected, three-way action | local create | ## Output safety * Interactive mode never emits JSON or GitHub output. * `--json` and `--github-output` always disable prompts. * `init --dry-run` raw output always disables prompts. * Interactive warnings use the same strings as non-interactive human mode, rendered through Clack. * `--verbose` may coexist with prompts in TTY human mode. Verbose lines are emitted before or after active prompts, not interleaved. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/configuration.md' description: >- Reference every Tagsmith .tagsmith.jsonc field, including git settings, defaults, targets, channels, tag patterns, tag messages, and SemVer validation rules. --- # Configuration reference Tagsmith reads a JSONC config file. The default path is `/.tagsmith.jsonc`. Use `--config ` 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://tagsmith.sadiksaifi.dev/schema/v1.json ``` Add `"$schema": ""` (optional, recommended) for editor completion. `init` writes it for you. ## Top-level shape ```jsonc { "$schema": "https://tagsmith.sadiksaifi.dev/schema/v1.json", "configVersion": 1, "git": { "remote": "...", "baseBranch": "..." }, "defaults": { "tagPattern": "...", "tagMessage": "...", "initialVersion": "...", }, "targets": { "": { /* 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` ```jsonc "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` ```jsonc "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](./tag-patterns). | | `tagMessage` | See [`tagMessage` grammar](#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 **adoption boundary** and the **bump baseline** when no newer managed stable tag exists. Existing matching tags whose base version is less than or equal to `initialVersion` are treated as legacy history, not managed Tagsmith tags. | ## `targets` Object keyed by target name. At least one entry required. ```jsonc "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](#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 and have ambiguous effective tagPattern `. 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: ```jsonc { "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 `name` does not need to be `stable`. The strategy is what matters. Convention is `stable`; older configs may use other names. * `dependsOn` is direct and validation-only — see [Mental model](./concepts#dependson). It does not influence version resolution. * Targets do not share channels. To gate `api@stable` on `web@stable`, you cannot; `dependsOn` is intra-target only. ## `tagPattern` grammar See [Tag patterns](./tag-patterns) for the full grammar, allowed characters, ambiguity rules, and warnings. ## `tagMessage` grammar ```jsonc "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. * `validate` renders `tagMessage` from 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.42 ``` Invalid: ``` 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.03 ``` Prerelease 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 `: `): * `: malformed JSONC ()` * `: reserved key __proto__ at ` * `: duplicate key at ` * `: : unrecognized keys` * `: : ` Validation errors (returned as `: `, first failure wins): * `git.remote must be a safe configured remote name without whitespace or slash` * `git.baseBranch must be an unqualified branch name` * `defaults.initialVersion must be canonical stable SemVer without build metadata or leading v` * `targets must contain at least one target` * `targets. must match /^[a-z][a-z0-9-]*$/u` * `targets..channels contains duplicate channel ` * `targets..channels must contain exactly one stable channel` * `targets..channels..dependsOn may not depend on self` * `targets..channels..dependsOn references missing channel ` * `targets..channels dependency cycle is invalid` * `targets..tagPattern …` — see [Tag patterns](./tag-patterns) * `targets..tagMessage must be printable single-line text` * `targets..tagMessage must be non-empty after interpolation` * `targets..tagPattern renders an unsafe Git tag name` * `targets and have ambiguous effective tagPattern ` See the [Error catalogue](./errors) for the full set. ## Example: single-target ```jsonc { "$schema": "https://tagsmith.sadiksaifi.dev/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 ```jsonc { "$schema": "https://tagsmith.sadiksaifi.dev/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. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/preflight.md' description: >- Review the Git, config, target, tag, working tree, remote branch, reachability, and validation checks Tagsmith runs before creating or accepting release tags. --- # Preflight checks Preflight is the ordered set of checks Tagsmith runs **before any mutation**. `tag` runs the full pipeline; `tag --dry-run` runs the same pipeline and skips only the create/push step. `validate` runs the validation-specific subset on an already-existing tag. Each check has a canonical error message. Tagsmith stops at the first failure and reports that error verbatim. ## `tag` preflight (in order) 1. **Repo discovery.** `git rev-parse --show-toplevel` from `process.cwd()`. * Outside a repo: `Git repository not found from `. 2. **Config load.** Parse and validate `.tagsmith.jsonc` (or `--config `). Any [config error](./configuration#parse-and-validation-errors) stops here. 3. **Target path validation.** For every configured target: path exists, is a directory, realpath inside the repo, realpath unique across targets. 4. **Working tree clean.** `git status --porcelain --untracked-files=all` must report nothing. * Dirty: `working tree must be clean before tagging`. 5. **Read local tags.** `git for-each-ref refs/tags` to enumerate the managed namespace locally. 6. **Read remote tags.** `git ls-remote --tags ` to enumerate the managed namespace remotely. 7. **Read remote base branch tip.** `git ls-remote refs/heads/`. 8. **Read current HEAD.** `git rev-parse HEAD`. 9. **HEAD equality.** `HEAD` must equal the remote base branch tip. * Mismatch: `HEAD must equal / () before tagging`. 10. **Dry-run resolution.** Resolve the requested target, channel, and version against the managed history. Matching tags with parsed base versions at or below `initialVersion` are legacy baseline tags and are ignored for history resolution. * tag doesn't already exist locally or remotely (no duplicate) * `--bump`/`--version` shape valid for the channel's strategy * prerelease `--bump prerelease` has an existing same-channel line * `dependsOn` checks: for each direct dependency, the dependency channel's tag at the **same base** exists locally **and** remotely, both peel to the same commit, and that commit equals current `HEAD`. For a `prerelease` dependency that's the **highest** `-.N`; for a `stable` dependency it's the canonical `` tag itself. 11. **Malformed managed tag scan.** Any managed tag above the adoption boundary with a broken `{version}` capture, lightweight ref, build metadata, non-canonical SemVer, mismatched peel, or unprovable remote annotation fails the run — even if it isn't the tag you're trying to create. 12. **Channel/strategy assertions.** Stable channels reject `--bump prerelease`. Explicit `--version` must match the channel's expected shape. 13. **Render.** Render `tagPattern` and `tagMessage` against the resolved target/version/tag. After preflight succeeds: * `tag` creates an annotated tag at `HEAD` (`git tag -a`). * `tag --push` then `git push refs/tags/`. * After push, Tagsmith re-reads remote tags and verifies the pushed tag is annotated and peels to the same commit. `tag --dry-run` stops after step 13. ## `tag --dry-run --push` `--dry-run --push` performs the same preflight and does **not** push. The JSON output keeps `pushed: false` and `dryRun: true`. There is no `wouldPush` field — `--dry-run --push --json` is intentionally indistinguishable from `--dry-run --json`. Human output mentions the push intent in a separate sentence. ## `validate` pipeline (in order) 1. **Repo discovery.** Same as `tag` step 1. 2. **Config load.** Same as `tag` step 2. 3. **Target path validation.** Same as `tag` step 3. 4. **Read local tags.** Same as `tag` step 5. 5. **Read remote tags.** Same as `tag` step 6. 6. **Target selection.** If `--target` provided, the tag must match that target's effective pattern; otherwise the tag must match exactly one target's effective pattern. 7. **Pattern match.** Extract the `{version}` capture from the tag name using the target's effective pattern. Failure: `tag does not match target ` (or `… does not match any configured target` / `… matches multiple targets`). 8. **Adoption boundary.** If the captured version has a parsed base version less than or equal to `initialVersion`, validation fails with an adoption-boundary error because the tag predates Tagsmith management. 9. **SemVer parse.** Parse the captured version. Failure: malformed managed tag error. 10. **Strategy classification.** Determine whether the version is stable or prerelease shape. For prerelease, the prerelease identifier must equal the channel name (computed during channel resolution). 11. **Channel resolution.** Compute the channel from the version shape. If `--channel` provided, the asserted channel must equal the computed channel. 12. **Local existence.** The tag must exist locally as an annotated tag. 13. **Remote existence.** The tag must exist remotely; remote annotation must be provable (peeled `^{}` record). 14. **Peel equality.** Local and remote refs must peel to the same commit. 15. **Malformed scan.** Any malformed managed tag above the adoption boundary fails validation, not only the validated tag. 16. **`dependsOn` validation.** For each direct dependency: the dependency channel's tag at the validated tag's base exists locally and remotely, both peel to the same commit, and that commit equals the validated tag's commit. For a `prerelease` dependency, that's the highest same-base prerelease; for a `stable` dependency, the canonical stable tag at that base. 17. **Read remote base branch tip.** Same as `tag` step 7. 18. **Reachability.** The validated tag's commit must be reachable from `/` according to local Git history (`git merge-base --is-ancestor`). * Not reachable / cannot be proven from local history: `cannot prove tag commit is reachable from / with local history. Fetch enough history and retry: git fetch --tags`. After validation succeeds, `validate` emits release facts (see [Output modes](./output)). ## Why preflight before mutation The preflight pipeline is non-negotiable because Tagsmith does not roll back on partial failure. If you create a tag and push it before catching a `dependsOn` violation, the tag is already on the remote. The preflight order is designed so that every failure mode is caught **before** Tagsmith touches Git state. `tag --dry-run` exists to let you exercise the entire pipeline without mutating anything. Use it in CI dry-run jobs, before promotions, or anywhere you want to verify "if I ran this for real, would it succeed?". ## Cancelation and signals When prompts are eligible (TTY, no machine output), Ctrl+C during a prompt cancels the run with exit code 1, no mutation, no machine output, and a concise message (`tagsmith failed: tagsmith cancelled.`). The local tag is not created; the remote is not contacted past the read steps already completed. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/git-safety.md' description: >- See how Tagsmith safely creates annotated Git tags without fetching, checkout, merging, branch mutation, tag overwrites, or rollback on push failure. --- # Git safety model Tagsmith is intentionally conservative. Every Git interaction is either read-only or a single, narrowly scoped write. The model is designed around one rule: **Tagsmith never makes a Git decision the user did not declare in config or on the command line.** ## What Tagsmith reads | Read | Source | Used by | | ---------------------- | ------------------------------------------------ | ---------------------------- | | Repo root | `git rev-parse --show-toplevel` | every command | | Working tree state | `git status --porcelain --untracked-files=all` | `tag` | | Current HEAD | `git rev-parse HEAD` | `tag` | | Remote base branch tip | `git ls-remote refs/heads/` | `tag`, `validate` | | Local tags | `git for-each-ref refs/tags` | `tag`, `validate` | | Remote tags | `git ls-remote --tags ` | `tag`, `validate` | | Reachability | `git merge-base --is-ancestor` | `validate` | | Remote URL | `git remote get-url ` | every command (verification) | Remote reads happen directly via `ls-remote`. Tagsmith does **not** depend on locally fetched tag state for remote tag truth. ## What Tagsmith writes | Write | Source | Used by | | ------------- | ----------------------------------------- | ------------ | | Annotated tag | `git tag -a -m ` | `tag` | | Tag push | `git push refs/tags/` | `tag --push` | Both writes are explicit and singular. Local tag creation is the default for `tag`; push only happens with `--push`. ## What Tagsmith never does * **No `git fetch`.** Tagsmith does not update local refs automatically. If reachability can't be proven from local history, the command fails with explicit fetch guidance. In CI, fetch enough history before invoking Tagsmith. * **No checkout, merge, reset, branch switch, or release-branch mutation.** * **No moving `HEAD`.** Tagsmith reads `HEAD` but never changes it. * **No lightweight tags.** Annotated only. Lightweight managed tags are malformed. * **No tag overwrites.** Existing same-name tags block creation regardless of lightweight vs annotated. * **No automatic rollback on failed push.** See [Non-rollback](#non-rollback). * **No reading your project `package.json`** to decide release versions. * **No interaction with your shell's git environment variables.** Tagsmith strips `GIT_*` env vars before invoking Git, so user-set `GIT_DIR`, `GIT_WORK_TREE`, hook-injected config, etc. don't bleed into Tagsmith's reads. ## Repo discovery Every non-help/non-version command — including `init --dry-run` and `targets` — discovers the repo from `process.cwd()` using `git rev-parse --show-toplevel`. There is no `--cwd` flag. Outside a Git checkout, Tagsmith fails with: ``` Git repository not found from ``` ## Config path resolution The `--config` flag selects a file location, **not** a repo context. * Absolute paths are used as-is. The file may live outside the repo. * Relative paths resolve from the repo root (not from `cwd`). * Default: `/.tagsmith.jsonc`. Target paths inside the config always resolve from the repo root, regardless of where the config file lives. ## HEAD equality Before `tag` creates a tag, the current `HEAD` must equal the **remote-read** tip of `/`. The current local branch name does **not** matter; you can be on a detached `HEAD` or on a feature branch as long as the commit you're on matches the remote base branch tip. Mismatch error: ``` HEAD must equal / () before tagging ``` This is a hard guard. There is no `--force` to bypass it. The intent: a release tag must point at the canonical commit on the release line, not a sibling or a stale snapshot. ## Working tree must be clean `tag` refuses to run on a dirty working tree. Stash, commit, or clean before tagging. Error: ``` working tree must be clean before tagging ``` ## Annotated only Tagsmith creates annotated tags exclusively (`git tag -a`). The annotated message comes from the rendered `tagMessage`. If a managed tag in the namespace is a lightweight tag (either locally or remotely), preflight reports it as a malformed managed tag and refuses to proceed. The same is true if a remote tag's annotation cannot be proven from the `ls-remote` output (missing `^{}` peeled record). ## Local/remote consistency When the same managed tag name exists locally and remotely, both refs must peel to the **same** commit. The annotated tag object SHAs may differ — only the peeled commit SHA matters. If they peel differently, the tag is malformed. ## Push verification After `git push`, Tagsmith re-reads remote tags and verifies: 1. The pushed tag exists on the remote. 2. The remote tag is provably annotated. 3. The remote tag peels to the same commit as the local tag. If any check fails, Tagsmith exits non-zero with one of: ``` local tag exists but was not pushed: push verification failed for : . Local tag remains. push verification failed for : remote tag does not peel to . Local tag remains. ``` ## Non-rollback Tagsmith **does not roll back** the local tag if push or post-push verification fails. The error message always names the tag and the failure mode. Recovery is yours — typically: ```sh git tag -d # delete locally, then re-investigate ``` Or fix the remote-side issue (network, permissions, ref protection) and push the existing local tag manually: ```sh git push refs/tags/ ``` The local tag is preserved on purpose: it represents work already done, and rolling it back automatically would erase that fact. ## Reachability and remote reads `validate` requires that the validated tag's commit is reachable from `/` per local Git history. Tagsmith reads the **remote** base branch tip via `ls-remote` and then asks the local repo whether that tip's history contains the tag's commit (`git merge-base --is-ancestor`). Because Tagsmith never fetches automatically, the local history may not be deep enough to prove reachability — in which case validation fails with: ``` cannot prove tag commit is reachable from / with local history. Fetch enough history and retry: git fetch --tags ``` In CI, set `fetch-depth: 0` on checkout and explicitly fetch tags before `validate`. See [GitHub Actions integration](./ci). ## Why no auto-fetch Automatic fetches change local state on the user's behalf. That conflicts with two design principles: 1. **All policy is visible.** A user who sees `tagsmith tag` should see exactly what Tagsmith did, with no extra `git fetch` running in the background. 2. **Fail loudly.** When local history isn't deep enough, the explicit failure with `git fetch …` guidance teaches the user what's missing. A silent auto-fetch would mask intermittent network issues and confuse CI environments where fetch policy is set deliberately. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/output.md' description: >- Learn Tagsmith output modes for human CLI text, pretty JSON, GitHub Actions outputs, raw init templates, exit codes, color rules, and error behavior. --- # Output modes Tagsmith has four output modes. The mode is determined by command and flags; you don't pick it explicitly except via `--json` and `--github-output`. | Mode | Trigger | Stdout | Stderr | | -------- | --------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- | | `human` | default | Human-readable messages (color if TTY) | Warnings, errors, optional verbose | | `json` | `--json` | Pretty-printed JSON (2-space indent, trailing newline) | Errors only on failure | | `github` | `--github-output` (validate only) | (silent on success) | Errors only on failure; writes facts to `$GITHUB_OUTPUT` | | `raw` | `init --dry-run` | Exact template bytes | (silent) | ## Exit codes * `0` — success * `1` — any failure (config, CLI, validation, Git, version, unsafe state) Tagsmith does not use specialized non-zero exit codes. ## Color and chatter Color is forbidden in `--json`, `--github-output`, and `init --dry-run` raw output. Successful machine-mode runs emit **no** stderr chatter. Warnings (`warning: `) appear only in human mode and never change the exit code. ## Mutual exclusion * `--json` and `--github-output` are mutually exclusive — `--json is incompatible with --github-output`. * `--verbose` is incompatible with `--json` and `--github-output` — `--verbose is incompatible with --json`. * `--verbose` only emits in human mode. ## `tag --json` and `tag --dry-run --json` ```json { "target": "app", "channel": "stable", "strategy": "stable", "version": "1.2.3", "baseVersion": "1.2.3", "tag": "v1.2.3", "tagMessage": "Release 1.2.3", "commit": "0123456789abcdef0123456789abcdef01234567", "created": true, "pushed": false, "dryRun": false } ``` Field reference: | Key | Meaning | | ------------- | --------------------------------------------------------------------------------------------- | | `target` | Selected target name. | | `channel` | Selected channel name. | | `strategy` | `"stable"` or `"prerelease"`. | | `version` | Resolved SemVer (no leading `v`). | | `baseVersion` | Stable `X.Y.Z` portion. Equals `version` for stable; the part before `-` for prerelease. | | `tag` | Rendered Git tag name. | | `tagMessage` | Rendered annotated tag message. | | `commit` | Full 40-character SHA at which the tag was/would be created. | | `created` | `true` if the local annotated tag was created on this run; `false` for `--dry-run`. | | `pushed` | `true` if `--push` was provided **and** the push + verification succeeded. `false` otherwise. | | `dryRun` | `true` for `--dry-run`; `false` for real runs. | `tag --dry-run --push --json` does **not** push; it sets `dryRun: true, created: false, pushed: false`. There is no `wouldPush` field — dry-run with `--push` is intentionally indistinguishable from dry-run without it in the JSON payload. ## `validate --json` ```json { "target": "app", "channel": "stable", "strategy": "stable", "version": "1.2.3", "baseVersion": "1.2.3", "tag": "v1.2.3", "tagMessage": "Release 1.2.3", "commit": "0123456789abcdef0123456789abcdef01234567", "remote": "origin", "baseBranch": "main", "valid": true } ``` `validate --json` always emits `valid: true` on success. On failure, no stdout is written and a plain human-readable error goes to stderr. `target`, `channel`, `strategy`, `version`, `baseVersion`, `tag`, `tagMessage`, `commit` have the same meanings as in `tag --json`. The extra keys are validation-specific: | Key | Meaning | | ------------ | ------------------------------------------- | | `remote` | The remote name from `git.remote`. | | `baseBranch` | The base branch name from `git.baseBranch`. | | `valid` | Always `true` on success. | ## `validate --github-output` Writes single-line `KEY=VALUE` records to the file named by `$GITHUB_OUTPUT`, **appending** after every check passes. On failure, no output is written. ``` target=app channel=stable strategy=stable version=1.2.3 baseVersion=1.2.3 tag=v1.2.3 tagMessage=Release 1.2.3 commit=0123456789abcdef0123456789abcdef01234567 remote=origin baseBranch=main valid=true ``` Constraints: * Keys are identifiers matching `^[A-Za-z_][A-Za-z0-9_]*$`. * Values must be single-line printable text. Control characters and newlines are rejected. * `--github-output` requires `GITHUB_OUTPUT` to be set and non-empty: `validate --github-output requires GITHUB_OUTPUT`. * Writes happen **after** full validation. There is no partial output on failure. Use these as `steps..outputs.` in downstream GitHub Actions steps. See [GitHub Actions integration](./ci) for the canonical workflow shape. ## `targets --json` `targets --json` emits the **raw parsed config object**, not the effective-inherited shape: ```json { "$schema": "https://tagsmith.sadiksaifi.dev/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": [ /* ... */ ] } } } ``` Properties: * Preserves the original key order from the config file. * Excludes JSONC comments and trailing commas. * Includes `$schema` only if the config has it. * Includes target-level overrides only if the config has them. Inherited defaults are **not** materialized. ## `init --dry-run` (raw) Writes the exact template bytes (including the trailing newline) to stdout. No color, no warnings, no chatter. The output is identical to the bytes `init` would otherwise write to disk. `--dry-run` does **not** accept `--json` or `--github-output`. ## Human-mode shapes Human output is guidance — exit codes, stream routing, and machine output shapes are the durable contracts. Example success lines: `tag`: ``` Tagged v1.2.3 (1.2.3) for target app channel stable. Commit: 012345678901 Created: yes Pushed: yes ``` `tag --dry-run`: ``` Resolved v1.2.3 (1.2.3) for target app channel stable. Commit: 0123456789abcdef0123456789abcdef01234567 Dry run: No tag was created. No push would have happened. ``` `validate`: ``` Validated v1.2.3 (1.2.3) for target app channel stable. Commit: 012345678901 Remote: origin Base branch: main Valid: true ``` `targets`: one block per target showing path, channels (with strategy and `dependsOn`), pattern, message, initial version. Multiple targets are separated by a blank line. Config warnings appear on stderr above the targets output. ## Errors in machine modes When a machine-mode run fails, Tagsmith writes: * **stdout**: nothing * **stderr**: `tagsmith failed: ` * exit code: `1` This is the same error shape used in human mode (minus color). No JSON error object is ever emitted; failures are always plain text. See [Error catalogue](./errors). --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/errors.md' description: >- Look up Tagsmith CLI errors for parsing, repo discovery, config validation, Git safety, release planning, tag validation, push verification, and GitHub outputs. --- # Error catalogue Every Tagsmith error is emitted on stderr with the `tagsmith failed: ` prefix and exits non-zero. Machine modes write no stdout on failure. Wording below matches the actual stderr line. Angle-bracketed segments (``, ``, ``, etc.) are runtime substitutions; everything else is fixed text. Where one site emits multiple variants (e.g. malformed managed tags), every variant is listed. ## CLI parsing | Error | Trigger | Remediation | | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `unknown option ` | Any unrecognized long flag (`--cwd` is also rejected) or any short flag other than `-h`/`-v`. | Use a documented flag — see [Commands](./cli/init). | | `unknown command ` | Unrecognized subcommand. | One of `init`, `tag`, `validate`, `targets`. | | `unexpected argument ` | A second command name (or stray positional) on the same line. | Run subcommands one at a time. | | `option requires a value` | The flag's next token is missing or starts with `-`. | Provide a value separated by a space (`--bump patch`). | | `option does not support attached values. Use .` | `--flag=value` form. | Use space-separated values. | | `tag requires --channel` | `tag` invoked without `--channel`. | Add `--channel `. | | `tag requires exactly one of --bump or --version` | Both or neither provided on `tag`. | Provide exactly one. | | `tag requires --target when config has multiple targets` | Multi-target config with no `--target`. | Add `--target `. Single-target configs may omit it. | | `unknown target ` | `--target` doesn't match any configured target. | Run `tagsmith targets` to see configured names. | | `unknown channel for target ` | `--channel` doesn't match any channel for the selected target. | Run `tagsmith targets` to see channels per target. | | `invalid --bump ; expected major, minor, patch, or prerelease` | `--bump` got an invalid enum value. | One of the four documented bumps. | | `validate requires --tag` | `validate` invoked without `--tag`. | Provide `--tag `. | | `validate --github-output requires GITHUB_OUTPUT` | `--github-output` set but env var missing or empty. | Run inside a GitHub Actions step, or set `GITHUB_OUTPUT=/path/to/file`. | | ` is incompatible with ` | Both `--json` and `--github-output` set. | Pick one. | | `--verbose is incompatible with ` | `--verbose` combined with `--json` or `--github-output`. | Drop `--verbose` from machine runs. | | `tagsmith cancelled.` | Interactive cancellation (Ctrl+C, or selecting the safe-negative option in a review). | Re-run when ready. | ## Repo discovery | Error | Trigger | Remediation | | ------------------------------------------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `Git repository not found from ` | Any non-help/non-version command run outside a Git repo. | `cd` into a repo; `git init` if you're starting one. | | `git.remote is not configured in ` | The configured remote name does not exist in the repo's Git config. | Add the remote (`git remote add `) or fix `git.remote` in `.tagsmith.jsonc`. | ## Config parse and validation `` is the resolved config path. `` is the field path inside the JSONC file. Parse errors: * `: malformed JSONC ()` * `: reserved key __proto__ at ` * `: duplicate key at ` * `: .: ` — unknown/extra keys, wrong types, missing required fields. `` is the Zod runtime message for the specific issue (e.g. `Unrecognized key …`, `Expected string, received number`). * `: invalid config` — fallback when no specific Zod issue is available. * `: failed to read config file: ` — wrapper from the filesystem adapter when the config file can't be read. Runtime validation errors (returned as `: `, first failure wins): * `git.remote must be a safe configured remote name without whitespace or slash` * `git.baseBranch must be an unqualified branch name` * ` must be canonical stable SemVer without build metadata or leading v` — `defaults.initialVersion` or any target-level `initialVersion` override. * ` requires exactly one {version}` — tag pattern. * ` may contain {target} at most once` — tag pattern. * ` contains unsupported placeholder` — tag pattern or tag message. * ` tagPattern contains unsafe characters` * ` must be printable single-line text` — tag message has control characters or newlines. * ` {version} touches an alphanumeric or underscore character` — **warning**, not error; human mode only. * `targets must contain at least one target` * `targets. must match /^[a-z][a-z0-9-]*$/u` * `targets..channels..name must match /^[a-z][a-z0-9-]*$/u` * `targets..channels contains duplicate channel ` * `targets..channels must contain exactly one stable channel` * `targets..channels..dependsOn may not depend on self` * `targets..channels..dependsOn references missing channel ` * `targets..channels dependency cycle is invalid` * `targets..tagPattern renders an unsafe Git tag name` * `targets..tagMessage must be non-empty after interpolation` * `targets and have ambiguous effective tagPattern ` ## Filesystem Target paths: * `targets..path must exist` * `targets..path must be a directory` * `targets..path must resolve inside the Git repository` * `targets..path resolves to the same real directory as targets..path` `init` destination: * `destination already exists: ` * `destination parent directory does not exist: ` * `destination parent directory is not a directory: ` * `: ` — wrapper when reading/writing the destination file fails. ## Git state * `working tree must be clean before tagging` * `HEAD must equal / () before tagging` * `failed to read remote tags from ` * `failed to read remote base branch /` * `failed to create annotated local tag ` * `failed to push tag to ` * `cannot prove tag commit is reachable from / with local history.\n\nFetch enough history and retry:\n git fetch --tags` ## Release planning Channel and bump: * `stable channel rejects --bump prerelease` * `Cannot bump prerelease for : no existing prerelease tag found. Use --bump major, --bump minor, --bump patch, or --version to start a prerelease line.` * `failed to resolve bump` — internal fallback when version increment cannot be computed. * `unknown channel ` — channel resolved from a tag does not exist on the target. * `unknown channel for target ` — explicit `--channel` does not exist on the selected target. Explicit `--version`: * ` must be canonical SemVer without build metadata or leading v` * ` must be a stable SemVer for channel ` — stable channel got a prerelease literal. * ` must match channel ` — prerelease channel got a literal that doesn't carry the channel's prerelease identifier. * ` must be greater than initialVersion ` — first stable at or below the adoption boundary. * ` base version must be greater than initialVersion ` — prerelease whose base is at or below the adoption boundary. * `wrong prerelease shape for channel ` — version classification rejects the shape. Duplicates: * `tag already exists locally or remotely` — a managed tag with the rendered name already exists. Both the `--version` path and the bump path emit this; existing same-name tags are blocked regardless of lightweight vs annotated. Malformed managed tags (preflight scans the namespace and fails on any of these, even if the bad tag is not the one being created or validated): * `malformed managed tag : lightweight tag is not allowed` — local lightweight ref. * `malformed managed tag : remote annotation cannot be proven` — remote ref lacks a peeled `^{}` record. * `malformed managed tag : canonical SemVer is invalid` — `{version}` capture is not canonical SemVer. * `malformed managed tag : build metadata is invalid` — capture contains `+build`. * `malformed managed tag : wrong prerelease shape for channel ` — prerelease shape doesn't match the channel's identifier. * `malformed managed tag : local/remote peeled commits differ` — same-name local and remote tags don't peel to the same commit. Legacy adoption boundary: * `tag predates Tagsmith adoption boundary initialVersion and is outside managed history` — the tag matches the pattern, but its parsed base version is less than or equal to `initialVersion`. Tagsmith treats it as pre-adoption history; validate a newer managed tag instead. Dependencies (same wording shared by `tag` and `validate` with different subject): * `channel depends on missing channel ` — config-level reference to a non-existent dependency channel (also caught at config validation; included here when re-checked at release time). * `resolved requires dependency tag for at ` — the dependency tag at that base does not exist. * `dependency tag must exist locally and remotely` * `dependency tag must peel to ` — `` is `HEAD` during `tag`, the validated tag's commit during `validate`. * `dependency tag must peel to validated tag commit ` — `validate` form when the peel differs. ## `validate` target and tag selection * `tag does not match target ` — `--target` asserted but the tag doesn't match that target's pattern. * `tag does not match any configured target` — without `--target`, the tag matches no target. * `tag matches multiple targets` — without `--target`, the tag is ambiguous across multiple targets. * `tag must contain canonical SemVer without build metadata` * `tag : ` — `` is the channel-classification message (e.g. `wrong prerelease shape for channel `). * `--channel does not match inferred channel ` — asserted `--channel` disagrees with the channel parsed from the tag's prerelease identifier. * `tag must exist locally` * `tag must exist remotely` * `tag must exist locally and remotely` * `tag is not a valid managed tag` ## Push and post-push verification * `local tag exists but was not pushed: ` * `push verification failed for : . Local tag remains.` * `push verification failed for : remote tag does not peel to . Local tag remains.` In every push-related failure, Tagsmith **does not roll back the local tag**. Recovery is yours — see [Non-rollback](./git-safety#non-rollback). ## GitHub output formatter * `validate failed: failed to write GitHub output: ` — wrapper when appending to `$GITHUB_OUTPUT` fails. * `GitHub output key must be an identifier.` — internal sanity check on emitted keys. * `GitHub output value for must be single-line printable text.` — control character or newline in a value. ## How to read an error 1. Strip the `tagsmith failed: ` prefix and find the matching line above. Substitute the `` in your head against the runtime values in the message. 2. The error message names the source — config field, Git state, CLI flag, tag name, remote, or path. 3. Apply the remediation. There is no `--force` to bypass safety guards (working tree clean, HEAD equality, dependency gates, push verification). If you need to override a check, fix the underlying state first. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/cli/init.md' description: >- Reference the tagsmith init command for creating, overwriting, previewing, and customizing the .tagsmith.jsonc release configuration template. --- # `tagsmith init` Creates a Tagsmith config file at the repo root (or wherever `--config` points). ## Synopsis ```sh tagsmith init tagsmith init --force tagsmith init --dry-run tagsmith init --dry-run --force ``` ## Flags | Flag | Type | Description | | ----------- | ------- | ------------------------------------------------------------------------------------------------- | | `--force` | boolean | Overwrite an existing config. Without this, `init` refuses to clobber a file that already exists. | | `--dry-run` | boolean | Print the exact template bytes to stdout. Writes nothing. Skips destination/overwrite checks. | `init` does not accept `--json` or `--github-output`. `--dry-run` puts output in raw mode (template bytes only, no color, no chatter). ## Behavior * Requires a Git repository. Resolves the destination as `/.tagsmith.jsonc` unless `--config ` overrides. * Refuses to overwrite an existing file unless `--force` is supplied. * Fails if the destination parent directory does not exist. * `--dry-run` performs no I/O on the destination and is compatible with `--force` (the `--force` is a no-op in dry-run). ## What the template contains The template ships with three example targets (`web`, `api`, `auth`) and a full `alpha → beta → rc → stable` channel ladder. These are **illustrative**. Edit the file before running `tag`, `validate`, or `targets`, since they validate every configured target path and fail when a path doesn't exist. The template is canonical: it is generated from `src/core/init/init-template.ts` and writes the same bytes for every user, with a trailing newline. ```jsonc { "$schema": "https://tagsmith.sadiksaifi.dev/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": [ /* alpha → beta → rc → stable */ ], }, "api": { "path": "apps/api", "channels": [ /* alpha → beta → rc → stable */ ], }, "auth": { "path": "packages/auth", "channels": [ /* alpha → beta → rc → stable */ ], }, }, } ``` ## Interactive flow In an eligible TTY, `init` shows a review/confirmation before writing: * If the destination does not exist, you confirm creation. * If it exists and you didn't pass `--force`, you choose between "overwrite" and the safe-negative option (default: safe-negative). * If it exists and you did pass `--force`, the overwrite is explicit but you still confirm before mutation. `init --dry-run` is raw mode and never prompts, even in a TTY. ## Output * **Human (success):** `Created Tagsmith config at ` * **`--dry-run` (raw):** the exact template bytes to stdout, no extra lines. * **Failure:** `tagsmith failed: ` on stderr, exit 1. ## Why no `init --json` `init` is a write operation that produces a deterministic byte-identical template; there is no useful JSON shape to expose. `--dry-run` gives you the exact bytes for inspection. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/cli/tag.md' description: >- Reference the tagsmith tag command for resolving SemVer versions, creating annotated Git tags, dry-running releases, pushing tags, and using interactive flows. --- # `tagsmith tag` Resolves a release version, creates an annotated Git tag at `HEAD`, and optionally pushes it. ## Synopsis ```sh tagsmith tag --channel --bump tagsmith tag --channel --version tagsmith tag --target --channel --bump tagsmith tag --target --channel --bump --push tagsmith tag --target --channel --bump --dry-run --json ``` ## Flags | Flag | Required | Description | | -------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `--target ` | required when config has multiple targets; optional with single target | Selects the target. | | `--channel ` | **required** | Selects the channel. Must exist in the target's channel set. | | `--bump ` | one of `--bump`/`--version` required | `major`, `minor`, `patch`, or `prerelease`. Stable channels reject `prerelease`. | | `--version ` | one of `--bump`/`--version` required | Explicit canonical SemVer. Must match the channel's shape. | | `--dry-run` | optional | Runs full [preflight](../preflight) and skips create/push. | | `--push` | optional | After local create, pushes to `git.remote` and verifies. | | `--json` | optional | Machine output. See [Output modes](../output). | `--bump` and `--version` are mutually exclusive. `--github-output` is not accepted on `tag`. `--verbose` is human-only. ## Behavior In order: 1. Discover repo, load config, validate target paths. 2. Run [preflight](../preflight): working tree clean, read local/remote tags, read remote base branch tip, read HEAD, HEAD equals remote tip. 3. Resolve version against managed tag history. See [Versioning](../versioning). 4. Validate `dependsOn` for the resolved version's base. 5. Render `tagPattern` and `tagMessage`. 6. Create the annotated tag at `HEAD` (skipped on `--dry-run`). 7. If `--push`: push to `git.remote`, then re-read remote tags to verify annotation and peel (skipped on `--dry-run`). ## Resolving version | Channel strategy | `--bump major` | `--bump minor` | `--bump patch` | `--bump prerelease` | | ---------------- | --------------------------------------- | --------------------------------------- | --------------------------------------- | ---------------------------------------------- | | `stable` | next major from latest stable | next minor from latest stable | next patch from latest stable | rejected | | `prerelease` | new line at `-.1` | new line at `-.1` | new line at `-.1` | continues highest same-channel line: `N → N+1` | If no stable tag exists yet, bumps resolve from `initialVersion`. `--bump prerelease` fails if no same-channel prerelease exists; start a line with `--bump major|minor|patch` or `--version`. See [Versioning](../versioning) for the full rules and worked examples. ## Single-target auto-selection When the config has exactly one target, `--target` is optional. With multiple targets and no `--target`, Tagsmith fails with `tag requires --target when config has multiple targets`. ## Interactive flow In an eligible TTY (no `--json`, no CI, etc.) Tagsmith fills omissions only: 1. Resolves Git repo, loads config, validates target paths. 2. Selects target — automatic if single-target; prompts if `--target` is missing and multiple targets exist. 3. Selects channel — automatic if single channel; prompts if `--channel` is missing and multiple channels exist. 4. Prompts for version intent if neither `--bump` nor `--version` was given: * "bump" → choose `major | minor | patch | prerelease` (filtered by strategy; `stable` channels show only the first three). * "explicit version" → enter a SemVer literal with strategy-shaped hints. 5. Runs full preflight. 6. Shows a **review screen** with target, channel, strategy, version intent, resolved version, rendered tag, rendered annotated message, full commit SHA, and the equivalent non-interactive command. 7. Asks for confirmation: * **Without `--push`:** "local create" / "create and push" / "no action". Default: local create. * **With `--push`:** confirm or cancel. Default: cancel (safe-negative). 8. Executes the chosen action. Cancellation (Ctrl+C or selecting the safe-negative) exits 1 with `tagsmith failed: tagsmith cancelled.` and no mutation. Even when **all** flags are supplied, the review/confirmation still runs in interactive mode. That's intentional. In non-interactive mode (CI, machine output, non-TTY), Tagsmith executes without confirmation as long as preflight passes. ## Output `tag --json` and `tag --dry-run --json` share the same 11-key shape — see [Output modes](../output). Human-mode success after create: ``` Tagged v1.2.3 (1.2.3) for target app channel stable. Commit: 012345678901 Created: yes Pushed: yes ``` Human-mode dry-run: ``` Resolved v1.2.3 (1.2.3) for target app channel stable. Commit: 0123456789abcdef0123456789abcdef01234567 Dry run: No tag was created. Because --push was provided, Tagsmith would have pushed the tag. ``` ## Errors The most common `tag` failures: * `working tree must be clean before tagging` * `HEAD must equal / () before tagging` * `Cannot bump prerelease for : no existing prerelease tag found. …` * `stable channel rejects --bump prerelease` * `local tag exists but was not pushed: ` * `push verification failed for : remote tag does not peel to . Local tag remains.` See [Error catalogue](../errors) for the full list. Tagsmith does **not** roll back the local tag on push failure. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/cli/validate.md' description: >- Reference the tagsmith validate command for checking managed Git tags, SemVer channel shape, reachability, dependsOn gates, JSON output, and GitHub outputs. --- # `tagsmith validate` Strictly validates an existing managed tag and emits release facts. Primary use: CI verification before a release side effect runs. ## Synopsis ```sh tagsmith validate --tag tagsmith validate --tag --target tagsmith validate --tag --channel tagsmith validate --tag --json tagsmith validate --tag --github-output ``` ## Flags | Flag | Required | Description | | ------------------ | ------------ | ------------------------------------------------------------------------------------------------------------ | | `--tag ` | **required** | The Git tag to validate. | | `--target ` | optional | Asserts the tag matches this target's pattern. Without it, the tag must match exactly one configured target. | | `--channel ` | optional | Asserts the inferred/parsed channel equals this name. | | `--json` | optional | Machine output. | | `--github-output` | optional | Append release facts to `$GITHUB_OUTPUT` after full validation succeeds. | `--json` and `--github-output` are mutually exclusive. ## Behavior `validate` runs the full validation pipeline (18 steps) — see [Preflight checks](../preflight#validate-pipeline-in-order). It is read-only (no Git writes) and never prompts during machine modes. In short: 1. Load config, validate target paths. 2. Read local and remote managed tags. 3. Identify the target by pattern match (and verify against `--target` if asserted). 4. Reject tags at or below the `initialVersion` adoption boundary as legacy; parse newer `{version}` captures, classify strategy, and resolve channel. 5. Assert the channel matches `--channel` if asserted. 6. Check tag exists locally and remotely, is annotated on both sides, and both refs peel to the same commit. 7. Scan the managed namespace above the adoption boundary for malformed tags — any malformed tag fails validation. 8. Validate `dependsOn` for the validated tag's base. 9. Read remote base branch tip; verify reachability of the tag's commit from there. ## Interactive flow In an eligible TTY: * If `--tag` is missing, prompt for the tag string manually. There is no tag discovery — interactive `validate` only validates an explicit tag. * If `--target` and `--channel` are missing, offer to add optional assertions: * "infer target and channel from tag" (default) * "assert target" * "assert target and channel" * In multi-target configs, asserting a channel requires asserting a target first. `--json` and `--github-output` disable all prompts. ## Output `validate --json` emits 11 keys including `remote`, `baseBranch`, and `valid: true` — see [Output modes](../output#validate-json). `validate --github-output` writes single-line `KEY=VALUE` records to `$GITHUB_OUTPUT` only after every check passes. Failures emit no partial output. See [Output modes](../output#validate-github-output). Human-mode success: ``` Validated v1.2.3 (1.2.3) for target app channel stable. Commit: 012345678901 Remote: origin Base branch: main Valid: true ``` ## Reachability and CI fetch `validate` requires the tag's commit to be reachable from `/` via local Git history. Because Tagsmith never fetches automatically, CI must: * check out with enough history (`fetch-depth: 0` in actions/checkout) and * fetch tags + remote branches explicitly before invoking `validate`. If history is insufficient: ``` cannot prove tag commit is reachable from / with local history. Fetch enough history and retry: git fetch --tags ``` See [GitHub Actions integration](../ci) for a working `.github/workflows/publish.yml`. ## `tagMessage` is not compared `validate` renders `tagMessage` from the **current** config and includes it in output. It does **not** read or compare the existing annotated tag's actual message. If you've changed `tagMessage` since the tag was created, `validate` reports the current rendering, not the historical one. ## Common errors * `tag does not match any configured target` — the tag name doesn't match any target's effective pattern. * `tag matches multiple targets` — without `--target`, the tag is ambiguous. * `tag does not match target ` — `--target` asserted but the pattern doesn't match. * `validate --github-output requires GITHUB_OUTPUT` — env var missing. * malformed managed tag errors when the namespace has broken tags (see [Errors](../errors)). * reachability error when local history is shallow (see above). --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/cli/targets.md' description: >- Reference the tagsmith targets command for listing configured release targets, validating target paths, and emitting machine-readable JSON target metadata. --- # `tagsmith targets` Lists configured release targets. Validates config and target paths; does not inspect Git tags, remotes, or remote refs. ## Synopsis ```sh tagsmith targets tagsmith targets --json ``` ## Flags | Flag | Description | | -------- | -------------------------------------- | | `--json` | Emit the parsed config object as JSON. | `--github-output` and `--verbose` (with machine output) are not accepted. ## Behavior 1. Discover repo. 2. Load and validate config. 3. Validate every configured target path (exists, is a directory, inside repo realpath, unique by realpath). 4. Print results. Config warnings (e.g. tag pattern warnings) appear on stderr in human mode. Machine mode suppresses warnings. ## Output ### Human mode One block per target in config order, separated by a blank line: ``` web path: apps/web channels: alpha (prerelease), beta (prerelease, dependsOn: alpha), rc (prerelease, dependsOn: beta), stable (stable, dependsOn: rc) tagPattern: {target}@{version} tagMessage: Release {target} {version} initialVersion: 0.0.0 api path: apps/api channels: alpha (prerelease), beta (prerelease, dependsOn: alpha), rc (prerelease, dependsOn: beta), stable (stable, dependsOn: rc) tagPattern: {target}@{version} tagMessage: Release {target} {version} initialVersion: 0.0.0 ``` ### `--json` mode Writes the **raw parsed config object** with original key order preserved. Comments and trailing commas are excluded. `$schema` is included only if present in the source file. Target-level overrides are included only if present; inherited defaults are **not** materialized. ```json { "$schema": "https://tagsmith.sadiksaifi.dev/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": [ /* ... */ ] }, "api": { "path": "apps/api", "channels": [ /* ... */ ] } } } ``` ## When to use it * After editing `.tagsmith.jsonc` to confirm Tagsmith accepts the new shape. * In CI pre-checks that don't need to read remote refs. * To feed the parsed config into other tooling (`targets --json | jq ...`). * During setup with AI, as the sanity check after the agent writes the config. `targets` is the cheapest sanity check — no remote reads, no tag scans. --- --- url: 'https://tagsmith.sadiksaifi.dev/docs/ci.md' description: >- Use Tagsmith in GitHub Actions to validate annotated SemVer release tags, export release facts to $GITHUB_OUTPUT, and gate publish or deploy jobs safely. --- # GitHub Actions integration Tagsmith validates release tags and exports release facts. It does **not** publish packages, deploy applications, upload artifacts, create cloud releases, or decide what your release process should do after validation. The pattern below uses Tagsmith as the gate; downstream jobs read the validated facts and run your actual release. ## Why use Tagsmith in CI `validate --github-output` writes single-line `KEY=VALUE` records to `$GITHUB_OUTPUT` only **after** every validation check passes. Downstream jobs read these via `needs..outputs.` and run with the confidence that: * the tag is annotated locally and remotely and the refs peel to the same commit * the tag matches exactly one configured target's pattern (or the asserted target) * the parsed `{version}` capture is canonical SemVer in the expected channel shape * the channel's direct `dependsOn` chain is satisfied at the same base * the tag's commit is reachable from `/` * the managed namespace has no malformed tags lurking nearby Invalid tags fail before anything else runs. ## Canonical publish workflow ```yaml name: Publish on: push: tags: # Match your .tagsmith.jsonc tagPattern. # - "v*" for v1.2.3 # - "*@*" for app@1.2.3 monorepo tags - "*" permissions: contents: read concurrency: group: publish-${{ github.ref }} cancel-in-progress: false jobs: validate: name: Validate release tag runs-on: ubuntu-24.04 timeout-minutes: 10 outputs: target: ${{ steps.tagsmith.outputs.target }} channel: ${{ steps.tagsmith.outputs.channel }} strategy: ${{ steps.tagsmith.outputs.strategy }} version: ${{ steps.tagsmith.outputs.version }} base-version: ${{ steps.tagsmith.outputs.baseVersion }} tag: ${{ steps.tagsmith.outputs.tag }} tag-message: ${{ steps.tagsmith.outputs.tagMessage }} commit: ${{ steps.tagsmith.outputs.commit }} steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Fetch tags and remote branches run: git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*' - name: Validate release tag id: tagsmith run: npx tagsmith@latest validate --tag "$GITHUB_REF_NAME" --github-output release: name: Release needs: validate runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read # Add only the permissions your release steps need, for example: # id-token: write # OIDC / trusted publishing # packages: write # GitHub Packages # deployments: write # GitHub Deployments steps: - name: Checkout validated commit uses: actions/checkout@v6 with: ref: ${{ needs.validate.outputs.commit }} fetch-depth: 0 - name: Build, publish, deploy, or upload env: RELEASE_TARGET: ${{ needs.validate.outputs.target }} RELEASE_CHANNEL: ${{ needs.validate.outputs.channel }} RELEASE_STRATEGY: ${{ needs.validate.outputs.strategy }} RELEASE_VERSION: ${{ needs.validate.outputs.version }} RELEASE_BASE_VERSION: ${{ needs.validate.outputs.base-version }} RELEASE_TAG: ${{ needs.validate.outputs.tag }} RELEASE_COMMIT: ${{ needs.validate.outputs.commit }} run: | echo "Release $RELEASE_TAG validated at $RELEASE_COMMIT" echo "Target: $RELEASE_TARGET" echo "Channel: $RELEASE_CHANNEL ($RELEASE_STRATEGY)" echo "Version: $RELEASE_VERSION" # Put your real publish/deploy steps here. ``` ## Why `fetch-depth: 0` and an explicit fetch Tagsmith never fetches automatically. `validate` requires the tag's commit to be reachable from `/` via local Git history. CI needs: * Full history — `fetch-depth: 0` on `actions/checkout`. * Remote branches available locally — the explicit `git fetch ... +refs/heads/*:refs/remotes/origin/*` line. * Tags available locally — same fetch line with `--tags`. Skipping any of these can leave the runner with a shallow checkout and trigger: ``` cannot prove tag commit is reachable from / with local history. Fetch enough history and retry: git fetch --tags ``` ## Running from `$GITHUB_WORKSPACE` Tagsmith discovers the repo from the current working directory. In GitHub Actions, that's `$GITHUB_WORKSPACE` by default, which is where `actions/checkout` lands. If you `cd` somewhere else first, add `cd "$GITHUB_WORKSPACE"` before invoking Tagsmith. ## Reading outputs in downstream jobs The `validate` job exposes these outputs (verbatim keys, all strings): * `target` * `channel` * `strategy` — `prerelease` or `stable` * `version` — canonical SemVer, no leading `v` * `baseVersion` — stable `X.Y.Z` portion; equals `version` for stable channels * `tag` — full rendered Git tag name * `tagMessage` — rendered annotated message from current config * `commit` — full 40-character SHA In downstream steps: ```yaml - name: Use validated facts env: RELEASE_VERSION: ${{ needs.validate.outputs.version }} run: echo "$RELEASE_VERSION" ``` Within the `validate` job itself, later steps can read the same facts as `steps.tagsmith.outputs.`. ## Single-target shortcuts When the config has exactly one target with an unambiguous pattern, `validate` can infer the target from the tag and you can omit `--target`. With multiple targets, supply `--target` to scope pattern matching to one configured target. ## What `validate` does **not** do * Does not contact any package registry. * Does not deploy or upload anything. * Does not write build artifacts. * Does not compare the existing annotated tag's actual message to the rendered `tagMessage`. * Does not roll back on any failure — failures are read-only events. ## Failure handling `validate` exits non-zero with a single error line on stderr. The downstream `release` job is gated by `needs: validate`, so a failed validation skips release work entirely. Inspect the failed step's logs to read the error and use the [Error catalogue](./errors) to map it to a remediation. ## Local CI rehearsal You can rehearse the CI path locally: ```sh # Pretend you're in CI. GITHUB_OUTPUT=/tmp/tagsmith.out CI=true \ npx tagsmith@latest validate --tag v1.2.3 --github-output cat /tmp/tagsmith.out ``` This produces the exact bytes that would be appended to `$GITHUB_OUTPUT` in a real workflow.