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)
- Repo discovery.
git rev-parse --show-toplevelfromprocess.cwd().- Outside a repo:
Git repository not found from <cwd>.
- Outside a repo:
- Config load. Parse and validate
.tagsmith.jsonc(or--config <path>). Any config error stops here. - Target path validation. For every configured target: path exists, is a directory, realpath inside the repo, realpath unique across targets.
- Working tree clean.
git status --porcelain --untracked-files=allmust report nothing.- Dirty:
working tree must be clean before tagging.
- Dirty:
- Read local tags.
git for-each-ref refs/tagsto enumerate the managed namespace locally. - Read remote tags.
git ls-remote --tags <remote>to enumerate the managed namespace remotely. - Read remote base branch tip.
git ls-remote <remote> refs/heads/<baseBranch>. - Read current HEAD.
git rev-parse HEAD. - HEAD equality.
HEADmust equal the remote base branch tip.- Mismatch:
HEAD must equal <remote>/<baseBranch> (<sha>) before tagging.
- Mismatch:
- Dry-run resolution. Resolve the requested target, channel, and version against the managed history:
- tag doesn't already exist locally or remotely (no duplicate)
--bump/--versionshape valid for the channel's strategy- prerelease
--bump prereleasehas an existing same-channel line dependsOnchecks: 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 currentHEAD. For aprereleasedependency that's the highest<base>-<channel>.N; for astabledependency it's the canonical<base>tag itself.
- Malformed managed tag scan. Any managed tag 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. - Channel/strategy assertions. Stable channels reject
--bump prerelease. Explicit--versionmust match the channel's expected shape. - Render. Render
tagPatternandtagMessageagainst the resolved target/version/tag.
After preflight succeeds:
tagcreates an annotated tag atHEAD(git tag -a).tag --pushthengit push <remote> refs/tags/<tag>.- 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)
- Repo discovery. Same as
tagstep 1. - Config load. Same as
tagstep 2. - Target path validation. Same as
tagstep 3. - Read local tags. Same as
tagstep 5. - Read remote tags. Same as
tagstep 6. - Target selection. If
--targetprovided, the tag must match that target's effective pattern; otherwise the tag must match exactly one target's effective pattern. - Pattern match. Extract the
{version}capture from the tag name using the target's effective pattern. Failure:tag <name> does not match target <target>(or… does not match any configured target/… matches multiple targets). - SemVer parse. Parse the captured version. Failure: malformed managed tag error.
- Strategy classification. Determine whether the version is stable or prerelease shape. For prerelease, the prerelease identifier must equal the channel name (computed during step 10).
- Channel resolution. Compute the channel from the version shape. If
--channelprovided, the asserted channel must equal the computed channel. - Local existence. The tag must exist locally as an annotated tag.
- Remote existence. The tag must exist remotely; remote annotation must be provable (peeled
^{}record). - Peel equality. Local and remote refs must peel to the same commit.
- Malformed scan. Any malformed managed tag in the namespace fails validation, not only the validated tag.
dependsOnvalidation. 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 aprereleasedependency, that's the highest same-base prerelease; for astabledependency, the canonical stable tag at that base.- Read remote base branch tip. Same as
tagstep 7. - Reachability. The validated tag's commit must be reachable from
<remote>/<baseBranch>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 <remote>/<baseBranch> with local history. Fetch enough history and retry: git fetch <remote> <baseBranch> --tags.
- Not reachable / cannot be proven from local history:
After validation succeeds, validate emits release facts (see Output modes).
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.