Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .agents/skills/kitup-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: kitup-release
description: Use when preparing or publishing a kitup release, including release branch preparation, version bumps, manual PR handoff, manual tags, GitHub Actions publishing, and public smoke checks.
---

# kitup Release

Use this only inside `/Users/x/git/samzong/kitup` or the upstream kitup repository.

## Release Contract

Do not publish from a pull request. Do not tag the release branch.

The maintainer-owned flow is:

1. Start from a clean, up-to-date `main`.
2. Run one release prep command:

```bash
make release-patch
make release-minor
make release-major
```

3. Let the command create `release/vX.Y.Z`, update versions, run `make check`, and commit `chore: prepare vX.Y.Z release`.
4. Ask the maintainer to open and merge the release PR manually.
5. After merge, the maintainer tags `main` manually:

```bash
git checkout main
git pull --ff-only
git tag vX.Y.Z
git push origin vX.Y.Z
```

## Version Surfaces

Release prep must keep these in sync:

- `ts/package.json`
- `rust/Cargo.toml`
- `rust/Cargo.lock`
- `examples/rust/Cargo.lock`
- `go-cobra/go.mod`

## Automation

The root `vX.Y.Z` tag triggers `.github/workflows/release.yml`. The workflow runs `make check`, verifies package versions, publishes npm and crates.io packages, creates `go/vX.Y.Z` and `go-cobra/vX.Y.Z`, creates GitHub Release notes, and runs `scripts/smoke-release.sh X.Y.Z`.

If a registry already accepted a version, do not delete and recreate tags without an explicit recovery plan.
20 changes: 12 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
VERSION="$version" node -e 'const { readFileSync } = require("node:fs"); const version = process.env.VERSION; const pkg = JSON.parse(readFileSync("ts/package.json", "utf8")); if (pkg.version !== version) { throw new Error(`ts/package.json version ${pkg.version} != ${version}`); }'
rust_version="$(awk -F '"' '/^version = / { print $2; exit }' rust/Cargo.toml)"
test "$rust_version" = "$version"
go_cobra_core_version="$(awk '/github.com\/samzong\/kitup\/go / { print $2; exit }' go-cobra/go.mod)"
test "$go_cobra_core_version" = "v$version"
- name: Check published versions
id: published
run: |
Expand Down Expand Up @@ -72,16 +74,18 @@ jobs:
cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish Go module tag
- name: Publish Go module tags
run: |
version="${GITHUB_REF_NAME#v}"
go_tag="go/v${version}"
if git rev-parse -q --verify "refs/tags/${go_tag}" >/dev/null; then
test "$(git rev-list -n 1 "${go_tag}")" = "$GITHUB_SHA"
else
git tag "$go_tag" "$GITHUB_SHA"
git push origin "refs/tags/${go_tag}"
fi
for module in go go-cobra; do
go_tag="${module}/v${version}"
if git rev-parse -q --verify "refs/tags/${go_tag}" >/dev/null; then
test "$(git rev-list -n 1 "${go_tag}")" = "$GITHUB_SHA"
else
git tag "$go_tag" "$GITHUB_SHA"
git push origin "refs/tags/${go_tag}"
fi
done
- name: Create GitHub release
run: |
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@ The check must pass locally before claiming parity.

Do not publish packages from a pull request.

Release tags are cut from `main` after `make check` passes. The release workflow publishes:
Use `make release-patch`, `make release-minor`, or `make release-major` from a clean, up-to-date `main` branch to create the release branch and version commit. Open and merge the release PR manually, then tag `main` manually. The release workflow publishes:

- `@kitup/sdk`
- `kitup` on crates.io
- `github.com/samzong/kitup/go` through the `go/vX.Y.Z` tag
- `github.com/samzong/kitup/go-cobra` through the `go-cobra/vX.Y.Z` tag
- GitHub Release notes

See [docs/RELEASE.md](docs/RELEASE.md) for the release flow, first npm release recovery, and public install smoke check.
119 changes: 81 additions & 38 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,90 +1,133 @@
SHELL := /bin/sh
.DEFAULT_GOAL := check

BOLD := \033[1m
CYAN := \033[36m
GREEN := \033[32m
RESET := \033[0m

.DEFAULT_GOAL := help

TS_DIR := ts
GO_DIR := go
GO_COBRA_DIR := go-cobra
RUST_DIR := rust
EXAMPLE_TS_DIR := examples/ts
EXAMPLE_GO_DIR := examples/go
EXAMPLE_RUST_DIR := examples/rust
GO_FILES := $(shell find $(GO_DIR) $(EXAMPLE_GO_DIR) -name '*.go' -type f)
GO_FILES := $(shell find $(GO_DIR) $(GO_COBRA_DIR) $(EXAMPLE_GO_DIR) -name '*.go' -type f)

# ── Quality ──────────────────────────────────────────────────────────────────

.PHONY: check hooks generate generate-check build build-ts test test-ts test-go test-rust examples example-ts example-go example-rust fmt fmt-ts fmt-go fmt-rust clean clean-ts clean-go clean-rust clean-examples
.PHONY: check test test-ts test-go test-go-cobra test-rust fmt fmt-ts fmt-go fmt-rust

check:
check: ## Full parity gate
node scripts/check.mjs

hooks:
git config core.hooksPath .githooks
test: test-ts test-go test-go-cobra test-rust ## Run SDK tests

generate:
node scripts/sync-hosts.mjs
test-ts: ## Run TypeScript tests
pnpm --dir $(TS_DIR) test

generate-check:
node scripts/sync-hosts.mjs --check
test-go: ## Run Go SDK tests
cd $(GO_DIR) && go test ./...

build: generate-check build-ts
test-go-cobra: ## Run Go Cobra adapter tests
cd $(GO_COBRA_DIR) && go test ./...

build-ts: generate-check
pnpm --dir $(TS_DIR) build
test-rust: ## Run Rust SDK tests
cargo test --manifest-path $(RUST_DIR)/Cargo.toml

test: test-ts test-go test-rust
fmt: fmt-ts fmt-go fmt-rust ## Format all SDK code

test-ts:
pnpm --dir $(TS_DIR) test
fmt-ts: ## Format TypeScript code
cd $(TS_DIR) && pnpm exec prettier --write src test ../examples/ts/cli.ts ../scripts/prepare-release.mjs

test-go:
cd $(GO_DIR) && go test ./...
fmt-go: ## Format Go code
gofmt -w $(GO_FILES)

test-rust:
cargo test --manifest-path $(RUST_DIR)/Cargo.toml
fmt-rust: ## Format Rust code
cargo fmt --manifest-path $(RUST_DIR)/Cargo.toml
cargo fmt --manifest-path $(EXAMPLE_RUST_DIR)/Cargo.toml

# ── Generated Data ───────────────────────────────────────────────────────────

.PHONY: generate generate-check

examples: example-ts example-go example-rust
generate: ## Refresh generated host constants
node scripts/sync-hosts.mjs

generate-check: ## Verify generated host constants
node scripts/sync-hosts.mjs --check

# ── Examples ─────────────────────────────────────────────────────────────────

.PHONY: examples example-ts example-go example-rust

example-ts:
examples: example-ts example-go example-rust ## Run all examples

example-ts: ## Run TypeScript example
tmp="$$(mktemp -d)" && mkdir -p "$$tmp/.codex" && HOME="$$tmp" pnpm --dir $(EXAMPLE_TS_DIR) install-skill

example-go:
example-go: ## Run Go example
cd $(EXAMPLE_GO_DIR) && tmp="$$(mktemp -d)" && mkdir -p "$$tmp/.codex" && HOME="$$tmp" go run .

example-rust:
example-rust: ## Run Rust example
cd $(EXAMPLE_RUST_DIR) && tmp="$$(mktemp -d)" && mkdir -p "$$tmp/.codex" && CARGO_HOME="$${CARGO_HOME:-$$HOME/.cargo}" RUSTUP_HOME="$${RUSTUP_HOME:-$$HOME/.rustup}" HOME="$$tmp" cargo run --quiet

fmt: fmt-ts fmt-go fmt-rust
# ── Release ──────────────────────────────────────────────────────────────────

fmt-ts:
cd $(TS_DIR) && pnpm exec prettier --write src test ../examples/ts/cli.ts
.PHONY: release-patch release-minor release-major

fmt-go:
gofmt -w $(GO_FILES)
release-patch: ## Prepare patch release branch and commit
node scripts/prepare-release.mjs patch

fmt-rust:
cargo fmt --manifest-path $(RUST_DIR)/Cargo.toml
cargo fmt --manifest-path $(EXAMPLE_RUST_DIR)/Cargo.toml
release-minor: ## Prepare minor release branch and commit
node scripts/prepare-release.mjs minor

clean: clean-ts clean-go clean-rust clean-examples
release-major: ## Prepare major release branch and commit
node scripts/prepare-release.mjs major

clean-ts:
# ── Maintenance ──────────────────────────────────────────────────────────────

.PHONY: hooks clean clean-ts clean-go clean-rust clean-examples

hooks: ## Install repo git hooks
git config core.hooksPath .githooks

clean: clean-ts clean-go clean-rust clean-examples ## Remove build artifacts

clean-ts: ## Remove TypeScript build artifacts
rm -rf \
$(TS_DIR)/node_modules \
$(TS_DIR)/dist \
$(TS_DIR)/coverage \
$(TS_DIR)/*.tsbuildinfo

clean-go:
clean-go: ## Remove Go build artifacts
rm -f \
$(GO_DIR)/coverage.out
$(GO_DIR)/coverage.out \
$(GO_COBRA_DIR)/coverage.out

clean-rust:
clean-rust: ## Remove Rust build artifacts
rm -rf \
target \
$(RUST_DIR)/target

clean-examples:
clean-examples: ## Remove example build artifacts
rm -rf \
$(EXAMPLE_TS_DIR)/node_modules \
$(EXAMPLE_TS_DIR)/dist \
$(EXAMPLE_TS_DIR)/coverage \
$(EXAMPLE_TS_DIR)/*.tsbuildinfo \
$(EXAMPLE_RUST_DIR)/target \
$(EXAMPLE_GO_DIR)/coverage.out

# ── Help ─────────────────────────────────────────────────────────────────────

.PHONY: help

help: ## Show available targets
@awk 'BEGIN {FS = ":.*## "; printf "$(BOLD)kitup$(RESET) — bundled Agent Skill installer SDK\n"} \
/^# ── / {n = $$0; gsub(/(^# ── | ─+$$)/, "", n); printf "\n$(BOLD)%s$(RESET)\n", n} \
/^[a-zA-Z_-]+:.*## / {printf " $(CYAN)make %-18s$(RESET) %s\n", $$1, $$2} \
END {printf "\n"}' $(MAKEFILE_LIST)
46 changes: 37 additions & 9 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
# Release

`kitup` publishes one version across three SDKs:
`kitup` publishes one version across four package surfaces:

- npm: `@kitup/sdk`
- crates.io: `kitup`
- Go module: `github.com/samzong/kitup/go`
- Go Cobra adapter: `github.com/samzong/kitup/go-cobra`

## Normal Release

1. Update package versions.
2. Run:
Start from an up-to-date `main` branch:

```bash
make check
git checkout main
git pull --ff-only
```

3. Merge to `main`.
4. Push a release tag:
Prepare the release branch and version commit:

```bash
make release-patch
# or
make release-minor
# or
make release-major
```

The release target creates `release/vX.Y.Z`, updates:

- `ts/package.json`
- `rust/Cargo.toml`
- `rust/Cargo.lock`
- `examples/rust/Cargo.lock`
- `go-cobra/go.mod`

It then runs `make check` and commits:

```bash
chore: prepare vX.Y.Z release
```

Open the release PR manually. After it is merged, tag the merge commit on `main` manually:

```bash
git checkout main
git pull --ff-only
git tag vX.Y.Z
git push origin vX.Y.Z
```

The release workflow publishes npm and crates.io packages, creates the `go/vX.Y.Z` tag, creates the GitHub Release, and runs the public install smoke check.
Do not tag the release branch. Do not publish packages by hand during the normal flow.

The release workflow publishes npm and crates.io packages, creates the `go/vX.Y.Z` and `go-cobra/vX.Y.Z` tags, creates the GitHub Release, and runs the public install smoke check.

## First npm Release

Expand All @@ -44,7 +72,7 @@ The release workflow is resumable:

- If npm already has the version, npm publish is skipped.
- If crates.io already has the version, crate publish is skipped.
- If `go/vX.Y.Z` already exists, the workflow verifies that it points at the release commit.
- If `go/vX.Y.Z` or `go-cobra/vX.Y.Z` already exists, the workflow verifies that it points at the release commit.

Do not delete and recreate a release tag after any registry has accepted the version unless the tag points at the wrong commit and the recovery plan is explicit.

Expand All @@ -56,4 +84,4 @@ Run the public install smoke check manually with:
scripts/smoke-release.sh X.Y.Z
```

The smoke check installs from npm, crates.io, and the public Go module, then verifies that each SDK can load the default host spec.
The smoke check installs from npm, crates.io, the public Go module, and the public Go Cobra adapter, then verifies that each SDK can load the default host spec or instantiate its adapter.
Loading
Loading