tenex 1.0.9

Terminal multiplexer for AI coding agents
Documentation
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -euo pipefail -c

BRANCH ?= master
REMOTE ?= origin

.PHONY: help release release-notes
.SILENT: release release-notes

help:
	@printf '%s\n' \
		'Targets:' \
		'  make release        Publish the version in Cargo.toml (tag + crates.io + GitHub release)' \
		'  make release-notes  Print the CHANGELOG notes for the current version' \
		'' \
		'Variables:' \
		'  BRANCH=master REMOTE=origin'

release-notes:
	version="$$(awk -F'\"' '/^version =/ {print $$2; exit}' Cargo.toml)"
	awk -v v="$$version" '\
		$$0 ~ "^## \\[" v "\\]" {p=1; next} \
		p && $$0 ~ "^## \\[" {exit} \
		p {print} \
	' CHANGELOG.md

release:
	if [[ -n "$$(git status --porcelain=v1)" ]]; then
		echo "ERROR: working tree not clean" >&2
		git status --porcelain=v1
		exit 1
	fi

	git checkout "$(BRANCH)"
	git pull --ff-only "$(REMOTE)" "$(BRANCH)"
	git fetch --tags "$(REMOTE)"

	if [[ -n "$$(git status --porcelain=v1)" ]]; then
		echo "ERROR: working tree not clean after pull" >&2
		git status --porcelain=v1
		exit 1
	fi

	current_branch="$$(git branch --show-current)"
	if [[ "$$current_branch" != "$(BRANCH)" ]]; then
		echo "ERROR: expected branch $(BRANCH), got $$current_branch" >&2
		exit 1
	fi

	local_head="$$(git rev-parse HEAD)"
	remote_head="$$(git rev-parse "$(REMOTE)/$(BRANCH)")"
	if [[ "$$local_head" != "$$remote_head" ]]; then
		echo "ERROR: $(BRANCH) is not at $(REMOTE)/$(BRANCH) (run: git push $(REMOTE) $(BRANCH))" >&2
		exit 1
	fi

	version="$$(awk -F'\"' '/^version =/ {print $$2; exit}' Cargo.toml)"
	if [[ -z "$$version" ]]; then
		echo "ERROR: failed to read version from Cargo.toml" >&2
		exit 1
	fi

	tag="v$$version"
	commit="$$(git rev-parse --short HEAD)"

	if ! grep -qE "^## \\[$$version\\]" CHANGELOG.md; then
		echo "ERROR: missing CHANGELOG entry for $$version" >&2
		exit 1
	fi

	echo "Running: cargo publish --dry-run --locked"
	cargo publish --dry-run --locked

	if git rev-parse -q --verify "refs/tags/$$tag" >/dev/null; then
		echo "ERROR: tag $$tag already exists locally" >&2
		exit 1
	fi

	if git ls-remote --exit-code --tags "$(REMOTE)" "refs/tags/$$tag" >/dev/null 2>&1; then
		echo "ERROR: tag $$tag already exists on $(REMOTE)" >&2
		exit 1
	fi

	if ! command -v gh >/dev/null 2>&1; then
		echo "ERROR: gh is required to create the GitHub release" >&2
		exit 1
	fi

	if ! gh auth status -h github.com >/dev/null 2>&1; then
		echo "ERROR: gh is not authenticated (run: gh auth login)" >&2
		exit 1
	fi

	if gh release view "$$tag" >/dev/null 2>&1; then
		echo "ERROR: GitHub release $$tag already exists" >&2
		exit 1
	fi

	if [[ ! -t 0 ]]; then
		echo "ERROR: refusing to publish without an interactive terminal" >&2
		exit 1
	fi

	printf '\n%s\n' "Ready to publish Tenex $$version"
	printf '%s\n' "  Branch: $(BRANCH) ($(REMOTE)/$(BRANCH))"
	printf '%s\n' "  Commit: $$commit"
	printf '%s\n' "  Tag:    $$tag"
	printf '\n%s' "Publish to crates.io and create a GitHub release? [y/N] "
	read -r confirm
	if [[ ! "$$confirm" =~ ^([Yy]|[Yy][Ee][Ss])$$ ]]; then
		echo "Aborted."
		exit 0
	fi

	echo "Publishing to crates.io..."
	cargo publish --locked

	echo "Tagging $$tag..."
	git tag -a "$$tag" -m "$$tag"
	git push "$(REMOTE)" "$$tag"

	notes_file="$$(mktemp)"
	trap 'rm -f "$$notes_file"' EXIT
	awk -v v="$$version" '\
		$$0 ~ "^## \\[" v "\\]" {p=1; next} \
		p && $$0 ~ "^## \\[" {exit} \
		p {print} \
	' CHANGELOG.md > "$$notes_file"

	if [[ ! -s "$$notes_file" ]]; then
		echo "ERROR: failed to extract release notes from CHANGELOG.md for $$version" >&2
		exit 1
	fi

	echo "Creating GitHub release $$tag..."
	gh release create "$$tag" --title "$$tag" --notes-file "$$notes_file"