From fb347eaa5456af50649d885adfb9eaa991b19ce2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Mar 2026 20:55:31 +0100 Subject: [PATCH] Stamp --- README.md | 309 +++++++++++++++++++++++++++ cmd/stamp/cmd_add.go | 4 +- cmd/stamp/cmd_comment.go | 10 +- cmd/stamp/cmd_publish.go | 4 +- cmd/stamp/cmd_status.go | 18 +- cmd/stamp/cmd_version.go | 10 +- docs/workflow.md | 8 +- go.mod | 2 +- internal/changelog/changelog_test.go | 2 +- internal/changeset/changeset_test.go | 2 +- internal/config/config_test.go | 12 +- internal/publish/publish.go | 8 +- internal/semver/semver.go | 2 +- internal/semver/semver_test.go | 4 +- 14 files changed, 352 insertions(+), 43 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fa1d4a --- /dev/null +++ b/README.md @@ -0,0 +1,309 @@ +# stamp + +A language-agnostic, changesets-style versioning and changelog tool. Works with any project type and supports monorepos. + +## Overview + +`stamp` is inspired by the [Changesets](https://github.com/changesets/changesets) workflow, but without the Node.js dependency. It works by having contributors add small **stamp files** (`.stamp/*.md`) alongside their code changes. When it's time to release, `stamp` consumes those files, bumps versions, updates changelogs, creates git tags, and publishes releases to GitHub or Gitea. + +## Installation + +Via [Mise](https://mise.jdx.dev/): + +```toml +# mise.toml +[tools] +"go:github.com/thokra/stamp/cmd/stamp" = "latest" +``` + +Or with `go install`: + +```sh +go install github.com/thokra/stamp/cmd/stamp@latest +``` + +Or build from source: + +```sh +git clone https://github.com/thokra/stamp +cd stamp +go build -o bin/stamp ./cmd/stamp +``` + +## Quick Start + +### 1. Create `stamp.toml` at the root of your repository + +```toml +[[projects]] +name = "my-app" +path = "." +version = "0.1.0" + + [projects.publish] + tags = true + releases = true + artifacts = ["dist/my-app-*"] +``` + +See [`examples/stamp.toml`](examples/stamp.toml) for a fully annotated example. + +### 2. Add a stamp file when making a change + +```sh +# Interactive +stamp add + +# Non-interactive (great for scripts and AI agents) +stamp add --project=my-app --bump=minor --message="Added new feature" +``` + +This creates a `.stamp/.md` file. Commit it alongside your code. + +### 3. Check pending stamps + +```sh +stamp status +``` + +``` +📦 Pending stamps: 2 + + PROJECT CURRENT NEXT BUMP STAMPS + ------- ------- ---- ---- ------ + my-app 1.2.3 1.3.0 minor 2 +``` + +### 4. Release + +```sh +# Bump versions and update changelogs +stamp version + +# Create git tags, GitHub/Gitea releases, and upload artifacts +STAMP_REPO=owner/repo stamp publish +``` + +## Commands + +### `stamp add` + +Creates a new stamp file describing a change. + +| Flag | Description | +|------|-------------| +| `--project` | Project name to include (repeatable) | +| `--bump` | Bump type: `major`, `minor`, `patch`, `premajor`, `preminor`, `prepatch`, `prerelease` | +| `--message` | Description of the change | +| `--no-interactive` | Disable interactive prompts (requires `--project`, `--bump`, `--message`) | +| `--slug` | Custom filename slug (default: auto-generated, e.g. `brave-river`) | + +### `stamp status` + +Shows all pending stamp files and the projected next version for each project. + +| Flag | Description | +|------|-------------| +| `--json` | Output as JSON | + +### `stamp version` + +Consumes all pending stamp files, bumps versions in `stamp.toml`, and prepends a new entry to each project's `CHANGELOG.md`. Then stages and commits the changes via git. + +| Flag | Description | +|------|-------------| +| `--snapshot ` | Create a pre-release with the given identifier (e.g. `alpha`, `rc`) | +| `--no-commit` | Apply changes without creating a git commit | + +### `stamp publish` + +Creates git tags and publishes releases (with optional artifact uploads) to GitHub or Gitea. + +| Flag | Description | +|------|-------------| +| `--dry-run` / `-n` | Print what would happen without executing | +| `--project` | Only publish a specific project | + +Requires `STAMP_REPO=owner/repo` to be set. Detects GitHub vs Gitea automatically via the `GITEA_BASE_URL` environment variable. + +Tag format: `@v` (e.g. `my-app@v1.3.0`). + +### `stamp comment` + +Posts or updates a PR comment summarising pending stamps. Useful in CI to remind contributors to add stamp files or to show reviewers what versions will change. + +- **No stamps found** → warns the author with instructions on how to add one. +- **Stamps found** → shows a table of affected projects and their next versions. + +| Flag | Description | +|------|-------------| +| `--pr` | Pull request number (required) | +| `--repo` | Repository slug `owner/repo` (defaults to `STAMP_REPO` or `GITHUB_REPOSITORY`) | + +## Stamp File Format + +Stamp files live in `.stamp/` and use Markdown with YAML or TOML frontmatter. + +### YAML (default) + +```markdown +--- +bumps: + my-app: minor + my-lib: patch +--- + +Short description used as the changelog entry. + +- Optional bullet points for more detail. +``` + +### TOML + +```markdown ++++ +[bumps] +my-app = "minor" ++++ + +Short description. +``` + +### Bump types + +| Type | Description | +|------|-------------| +| `major` | Breaking change — `1.2.3` → `2.0.0` | +| `minor` | New feature — `1.2.3` → `1.3.0` | +| `patch` | Bug fix — `1.2.3` → `1.2.4` | +| `premajor` | Pre-release major — `1.2.3` → `2.0.0-alpha.0` | +| `preminor` | Pre-release minor — `1.2.3` → `1.3.0-alpha.0` | +| `prepatch` | Pre-release patch — `1.2.3` → `1.2.4-alpha.0` | +| `prerelease` | Increment pre-release — `1.2.4-rc.0` → `1.2.4-rc.1` | + +## Configuration Reference (`stamp.toml`) + +```toml +# Global settings (all optional) +[config] +base_branch = "main" # Base branch for PR change detection (default: main) +changeset_dir = ".stamp" # Directory for stamp files (default: .stamp) + +[[projects]] +name = "my-app" +path = "apps/my-app" # Path relative to repo root +version = "1.2.3" # Current version — updated by `stamp version` +changelog = "CHANGELOG.md" # Relative to path (default: CHANGELOG.md) + + [projects.publish] + tags = true + releases = true + artifacts = [ + "apps/my-app/dist/my-app-linux-amd64", + "apps/my-app/dist/my-app-darwin-arm64", + ] +``` + +## Environment Variables + +| Variable | Purpose | +|----------|---------| +| `STAMP_REPO` | Repository slug `owner/repo` — required for `publish` and `comment` | +| `GITHUB_TOKEN` | GitHub personal access token for releases and PR comments | +| `GITEA_TOKEN` | Gitea access token | +| `GITEA_BASE_URL` | Gitea instance URL (e.g. `https://gitea.example.com`) — also enables Gitea mode | + +## CI Integration + +### GitHub Actions — PR check + +```yaml +# .github/workflows/stamp-check.yml +name: stamp check + +on: + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install stamp + run: go install github.com/thokra/stamp/cmd/stamp@latest + + - name: Comment on PR + run: stamp comment --pr=${{ github.event.pull_request.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + STAMP_REPO: ${{ github.repository }} +``` + +### Gitea Actions — PR check + +```yaml +# .gitea/workflows/stamp-check.yml +name: stamp check + +on: + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install stamp + run: go install github.com/thokra/stamp/cmd/stamp@latest + + - name: Comment on PR + run: stamp comment --pr=${{ gitea.event.pull_request.number }} + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_BASE_URL: ${{ gitea.server_url }} + STAMP_REPO: ${{ gitea.repository }} +``` + +## Development + +This project uses [Mise](https://mise.jdx.dev/) to manage the Go toolchain and common tasks. + +```sh +# Build +mise run build + +# Run tests +mise run test + +# Lint +mise run lint + +# Install locally +mise run install +``` + +## Project Structure + +``` +stamp/ +├── cmd/stamp/ # CLI entry point and command definitions +├── internal/ +│ ├── changeset/ # Stamp file parsing, writing, and slug generation +│ ├── changelog/ # CHANGELOG.md generation and appending +│ ├── config/ # stamp.toml loading, validation, and saving +│ ├── git/ # Git operations (tag, commit, push) +│ ├── gitea/ # Gitea API client (releases, PR comments) +│ ├── github/ # GitHub API client (releases, PR comments) +│ ├── publish/ # Orchestrates tagging and release publishing +│ └── semver/ # Semantic version bumping logic +├── examples/ +│ └── stamp.toml # Annotated configuration example +└── docs/ + └── workflow.md # Detailed workflow guide +``` + +## License + +MIT \ No newline at end of file diff --git a/cmd/stamp/cmd_add.go b/cmd/stamp/cmd_add.go index ed8f083..9a53e62 100644 --- a/cmd/stamp/cmd_add.go +++ b/cmd/stamp/cmd_add.go @@ -7,8 +7,8 @@ import ( "github.com/urfave/cli/v3" - "github.com/thokra/stamp/internal/changeset" - "github.com/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/config" ) func addCmd() *cli.Command { diff --git a/cmd/stamp/cmd_comment.go b/cmd/stamp/cmd_comment.go index 10e39e5..11b630f 100644 --- a/cmd/stamp/cmd_comment.go +++ b/cmd/stamp/cmd_comment.go @@ -8,11 +8,11 @@ import ( "github.com/urfave/cli/v3" - "github.com/thokra/stamp/internal/changeset" - "github.com/thokra/stamp/internal/config" - "github.com/thokra/stamp/internal/semver" - ghClient "github.com/thokra/stamp/internal/github" - giteaClient "github.com/thokra/stamp/internal/gitea" + "git.thokra.dev/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/config" + giteaClient "git.thokra.dev/thokra/stamp/internal/gitea" + ghClient "git.thokra.dev/thokra/stamp/internal/github" + "git.thokra.dev/thokra/stamp/internal/semver" ) func commentCmd() *cli.Command { diff --git a/cmd/stamp/cmd_publish.go b/cmd/stamp/cmd_publish.go index ffe4eb4..451b740 100644 --- a/cmd/stamp/cmd_publish.go +++ b/cmd/stamp/cmd_publish.go @@ -7,8 +7,8 @@ import ( "github.com/urfave/cli/v3" - "github.com/thokra/stamp/internal/config" - "github.com/thokra/stamp/internal/publish" + "git.thokra.dev/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/publish" ) func publishCmd() *cli.Command { diff --git a/cmd/stamp/cmd_status.go b/cmd/stamp/cmd_status.go index b92a21b..4014c6d 100644 --- a/cmd/stamp/cmd_status.go +++ b/cmd/stamp/cmd_status.go @@ -10,9 +10,9 @@ import ( "github.com/urfave/cli/v3" - "github.com/thokra/stamp/internal/changeset" - "github.com/thokra/stamp/internal/config" - "github.com/thokra/stamp/internal/semver" + "git.thokra.dev/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/semver" ) // statusResult is the machine-readable form of stamp status. @@ -22,12 +22,12 @@ type statusResult struct { } type projectStatus struct { - Name string `json:"name"` - Current string `json:"current"` - Next string `json:"next"` - Bump string `json:"bump"` - Changesets int `json:"changesets"` - NoBump bool `json:"no_bump"` + Name string `json:"name"` + Current string `json:"current"` + Next string `json:"next"` + Bump string `json:"bump"` + Changesets int `json:"changesets"` + NoBump bool `json:"no_bump"` } func statusCmd() *cli.Command { diff --git a/cmd/stamp/cmd_version.go b/cmd/stamp/cmd_version.go index 20d1a28..7f5638a 100644 --- a/cmd/stamp/cmd_version.go +++ b/cmd/stamp/cmd_version.go @@ -9,11 +9,11 @@ import ( "github.com/urfave/cli/v3" - "github.com/thokra/stamp/internal/changeset" - "github.com/thokra/stamp/internal/changelog" - "github.com/thokra/stamp/internal/config" - "github.com/thokra/stamp/internal/git" - "github.com/thokra/stamp/internal/semver" + "git.thokra.dev/thokra/stamp/internal/changelog" + "git.thokra.dev/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/git" + "git.thokra.dev/thokra/stamp/internal/semver" ) func versionCmd() *cli.Command { diff --git a/docs/workflow.md b/docs/workflow.md index 8773f36..9615418 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -23,13 +23,13 @@ Via [Mise](https://mise.jdx.dev/): ```toml # mise.toml [tools] -"go:github.com/thokra/stamp/cmd/stamp" = "latest" +"go:git.thokra.dev/thokra/stamp/cmd/stamp" = "latest" ``` Or build from source: ```sh -go install github.com/thokra/stamp/cmd/stamp@latest +go install git.thokra.dev/thokra/stamp/cmd/stamp@latest ``` ### 2. Create stamp.toml @@ -138,7 +138,7 @@ jobs: - uses: actions/checkout@v4 - name: Install stamp - run: go install github.com/thokra/stamp/cmd/stamp@latest + run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Comment on PR run: stamp comment --pr=${{ github.event.pull_request.number }} @@ -163,7 +163,7 @@ jobs: - uses: actions/checkout@v4 - name: Install stamp - run: go install github.com/thokra/stamp/cmd/stamp@latest + run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Comment on PR run: stamp comment --pr=${{ gitea.event.pull_request.number }} diff --git a/go.mod b/go.mod index 3aee18b..e69b107 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/thokra/stamp +module git.thokra.dev/thokra/stamp go 1.26.1 diff --git a/internal/changelog/changelog_test.go b/internal/changelog/changelog_test.go index 404c60c..fcceef2 100644 --- a/internal/changelog/changelog_test.go +++ b/internal/changelog/changelog_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/thokra/stamp/internal/changelog" + "git.thokra.dev/thokra/stamp/internal/changelog" ) var testDate = time.Date(2026, 3, 8, 0, 0, 0, 0, time.UTC) diff --git a/internal/changeset/changeset_test.go b/internal/changeset/changeset_test.go index c1f23f9..13fa632 100644 --- a/internal/changeset/changeset_test.go +++ b/internal/changeset/changeset_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/changeset" ) func TestParseYAMLFrontmatter(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3574d72..560c037 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/config" ) const validTOML = ` @@ -26,7 +26,7 @@ version = "0.1.0" func TestLoad_Valid(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(validTOML), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(validTOML), 0o644); err != nil { t.Fatal(err) } @@ -49,7 +49,7 @@ func TestLoad_Valid(t *testing.T) { func TestLoad_Defaults(t *testing.T) { dir := t.TempDir() minimal := "[[projects]]\nname = \"app\"\npath = \".\"\nversion = \"1.0.0\"\n" - if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(minimal), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(minimal), 0o644); err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestLoad_MissingFile(t *testing.T) { func TestLoad_NoProjects(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte("[config]\n"), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte("[config]\n"), 0o644); err != nil { t.Fatal(err) } _, err := config.Load(dir) @@ -97,7 +97,7 @@ name = "app" path = "other" version = "2.0.0" ` - if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(dup), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(dup), 0o644); err != nil { t.Fatal(err) } _, err := config.Load(dir) @@ -108,7 +108,7 @@ version = "2.0.0" func TestFindProject(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(validTOML), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, "stamp.toml"), []byte(validTOML), 0o644); err != nil { t.Fatal(err) } cfg, _ := config.Load(dir) diff --git a/internal/publish/publish.go b/internal/publish/publish.go index 0e4a4c5..87c2982 100644 --- a/internal/publish/publish.go +++ b/internal/publish/publish.go @@ -6,10 +6,10 @@ import ( "os" "path/filepath" - "github.com/thokra/stamp/internal/config" - "github.com/thokra/stamp/internal/git" - ghClient "github.com/thokra/stamp/internal/github" - giteaClient "github.com/thokra/stamp/internal/gitea" + "git.thokra.dev/thokra/stamp/internal/config" + "git.thokra.dev/thokra/stamp/internal/git" + giteaClient "git.thokra.dev/thokra/stamp/internal/gitea" + ghClient "git.thokra.dev/thokra/stamp/internal/github" ) // Options controls publish behaviour. diff --git a/internal/semver/semver.go b/internal/semver/semver.go index 966c4f3..9c7c15a 100644 --- a/internal/semver/semver.go +++ b/internal/semver/semver.go @@ -6,7 +6,7 @@ import ( goSemver "github.com/Masterminds/semver/v3" - "github.com/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/changeset" ) // Bump computes the next version given the current version string and a bump type. diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go index 572edc6..5ee8bab 100644 --- a/internal/semver/semver_test.go +++ b/internal/semver/semver_test.go @@ -3,8 +3,8 @@ package semver_test import ( "testing" - "github.com/thokra/stamp/internal/changeset" - "github.com/thokra/stamp/internal/semver" + "git.thokra.dev/thokra/stamp/internal/changeset" + "git.thokra.dev/thokra/stamp/internal/semver" ) func TestBump_Major(t *testing.T) {