# 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. There are two supported release workflows: - **Direct release** — run `stamp version` and `stamp publish` locally or in CI on the base branch. - **Release PR** — run `stamp release-pr` in CI to open a pull request that contains the version bumps. Merging the PR triggers the actual publish step. ## Installation Via [Mise](https://mise.jdx.dev/): ```toml # mise.toml [tools] "go:git.thokra.dev/thokra/stamp/cmd/stamp" = "latest" ``` Or with `go install`: ```sh go install git.thokra.dev/thokra/stamp/cmd/stamp@latest ``` Or build from source: ```sh git clone https://git.thokra.dev/thokra/stamp cd stamp go build -o bin/stamp ./cmd/stamp ``` ## Quick Start ### 1. Create `.stamp/stamp.toml` ```toml [[projects]] name = "my-app" path = "." version = "0.1.0" [projects.publish] tags = true releases = true artifacts = ["dist/my-app-*"] ``` Because `stamp.toml` lives inside `.stamp/`, git will track the directory without needing a `.gitignore` file in it. 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/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 release-pr` Creates or updates a **release pull request** — a PR that contains all the version bumps and changelog updates that would be applied by `stamp version`. Merging it acts as the explicit release trigger. The command: 1. Reads all pending stamp files. If there are none, it exits cleanly with no side effects. 2. Fetches `origin` and resets a well-known branch (default: `stamp/release`) from the tip of the base branch, so the branch is always a clean, fast-forwardable head. 3. Applies all version bumps and changelog updates to that branch (identical logic to `stamp version --no-commit`), then commits and force-pushes it. 4. Opens a new PR — or updates the existing one — targeting the base branch. The PR is identified across runs by a hidden marker in the body, so only one stamp release PR exists at a time. Once the PR is merged, run `stamp publish` to tag and release. | Flag | Description | |------|-------------| | `--branch` | Release branch name (default: `stamp/release`) | | `--base` | Base branch the PR targets (default: from `stamp.toml`, fallback: `main`) | | `--repo` | Repository slug `owner/repo` (defaults to `STAMP_REPO` or `GITHUB_REPOSITORY`) | | `--snapshot ` | Pre-release identifier (e.g. `alpha`, `rc`) | | `--dry-run` / `-n` | Print what would be done without pushing or opening a PR | ### `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 preview` Manages **pre-release mode** for a project. While a project is in pre-release mode, every `stamp version` (and `stamp release-pr`) run produces pre-release versions (e.g. `1.3.0-beta.0`, `1.3.0-beta.1`) instead of normal ones. Exiting preview mode causes the next run to produce a regular release. ```sh # Enter pre-release mode — all future version bumps will be pre-releases stamp preview enter my-app beta # Leave pre-release mode — the next version bump will be a normal release stamp preview exit my-app ``` ## Stamp File Format Stamp files live in `.stamp/` alongside `stamp.toml`, 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/stamp.toml`) ```toml # Global settings (all optional) [config] base_branch = "main" # Base branch for PR change detection (default: main) [[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) pre_tag = "" # Set by `stamp preview enter`; leave blank for normal releases [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`, `comment`, and `release-pr` | | `GITHUB_TOKEN` | GitHub token for releases and PR comments — automatically provided by the GitHub Actions runner; no manual setup needed | | `GITEA_TOKEN` | Gitea access token — **must be created manually** (Gitea Actions does not inject one automatically); create a token in your Gitea account settings and store it as a repository secret | | `GITEA_BASE_URL` | Gitea instance URL (e.g. `https://gitea.example.com`) — also enables Gitea mode | ## CI Integration ### Workflow: PR check (`stamp comment`) Posts a comment on every pull request showing which projects will be bumped and to what version. Warns if no stamp file was added. **GitHub Actions** ```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 git.thokra.dev/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** ```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 git.thokra.dev/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 }} ``` --- ### Workflow: Release PR (`stamp release-pr` + `stamp publish`) This is the recommended automation workflow. Whenever a PR with stamp files is merged to the base branch, CI opens (or updates) a release PR. Merging the release PR triggers the actual publish. ``` feature branch ──► merge to main ──► stamp release-pr ──► release PR │ merge to main │ stamp publish ──► tags + releases ``` **Step 1 — Open or update the release PR on every push to `main`** *GitHub Actions* ```yaml # .github/workflows/stamp-release-pr.yml name: release PR on: push: branches: [main] jobs: release-pr: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # A full clone is needed so stamp can fetch and reset the release branch. fetch-depth: 0 - name: Configure git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Install stamp run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Create or update release PR run: stamp release-pr --base=main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} STAMP_REPO: ${{ github.repository }} ``` *Gitea Actions* ```yaml # .gitea/workflows/stamp-release-pr.yml name: release PR on: push: branches: [main] jobs: release-pr: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure git run: | git config user.name "stamp-bot" git config user.email "stamp-bot@users.noreply.gitea.com" - name: Install stamp run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Create or update release PR run: stamp release-pr --base=main env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_BASE_URL: ${{ gitea.server_url }} STAMP_REPO: ${{ gitea.repository }} ``` **Step 2 — Publish after the release PR is merged** *GitHub Actions* ```yaml # .github/workflows/stamp-publish.yml name: publish on: push: branches: [main] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Install stamp run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Publish releases run: stamp publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} STAMP_REPO: ${{ github.repository }} ``` *Gitea Actions* ```yaml # .gitea/workflows/stamp-publish.yml name: publish on: push: branches: [main] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure git run: | git config user.name "stamp-bot" git config user.email "stamp-bot@users.noreply.gitea.com" - name: Install stamp run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Publish releases run: stamp publish env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_BASE_URL: ${{ gitea.server_url }} STAMP_REPO: ${{ gitea.repository }} ``` > **Tip:** Both jobs run on every push to `main`. `stamp release-pr` is a no-op once there are no more pending stamp files (i.e. after the release PR has been merged and `stamp publish` has consumed them). `stamp publish` is a no-op if there are no new tags to create. The two jobs are safe to run in parallel or in sequence. --- ### Workflow: Direct release (no PR) If you prefer to version and publish in a single step without a release PR, run both commands directly on the base branch: ```yaml # .github/workflows/stamp-release.yml name: release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Install stamp run: go install git.thokra.dev/thokra/stamp/cmd/stamp@latest - name: Version and publish run: | stamp version git push stamp publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} STAMP_REPO: ${{ github.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, branch management) │ ├── gitea/ # Gitea API client (releases, PR comments, release PRs) │ ├── github/ # GitHub API client (releases, PR comments, release PRs) │ ├── publish/ # Orchestrates tagging and release publishing │ ├── releasepr/ # Orchestrates release PR creation and updates │ └── semver/ # Semantic version bumping logic ├── examples/ │ └── stamp.toml # Annotated configuration example └── docs/ └── workflow.md # Detailed workflow guide ``` ## License MIT