Introducing Sampo

My new open-source side project to automate changelogs, versioning, and publishing... even for monorepos across multiple package registries.

31 October 2025 — Goulven CLEC'H

  1. Tl;dr
  2. Genesis & motivations
  3. Sampo in action
    1. Versioning
    2. Changesets & changelog
    3. Releases & publishing
    4. Philosophy
    5. Alternatives
  4. Some technical details
    1. Monorepo architecture
    2. Multi-ecosystem support
    3. Error handling
    4. Testing GitHub Actions
  5. What’s next?

Tl;dr

Sampo is a new open-source tool suite that automates changelogs, versioning, and publishing for your projects. This blog entry explores the project’s motivations, philosophy, technical details, and future plans. If you don’t care about the details, you can check out the repository right away! (and leave a star if you like it!)

The project is still in its initial development versions (0.x.x), so expect some rough edges. However, its core features are already here, and breaking changes should be minimal going forward. Feedback and contributions are welcome!

Genesis & motivations

October 2024, after a trip in Finland, I meet up with ...Erika in Tornio/Haparanda, before crossing Sweden together. During this journey in Ikea homeland, the idea for Bruits comes to life. Since neither of us had the opportunity to use Rust in our jobs, this collective serves as an excuse to work on open-source projects together, and to practice this language that had attracted us for a long time. Me (on the left) taking a selfie with Erika (on the right) in a swedish street. Erika and myself in Sweden, October 2024.

The first project didn’t survive to this day. WEBlsp was meant to be a modern language server for the web, with our own language services HTMLlsrs and CSSlsrs, powered by Rust and WASM.

However, by bringing together Rust crates and npm packages in a single monorepo, WEBlsp revealed a gap in multi-ecosystem version and publication management tools. Erika and I, being used to the comfort of changesets for our JavaScript/TypeScript projects, going back to a manual workflow was unthinkable.

While Sampo’s repo was initialized in November 2024, it would take me several months before having the time and motivation to seriously dedicate myself to it. But faced with the lack of satisfying alternatives, whether in the Rust or Elixir ecosystem (which I use at work), the project was reopened in September 2025.

Sampo in action

Sampo is a CLI tool (for now only available on Cargo, but soon on your favorite OS package manager), a Github Action, and a GitHub App that work together to streamline your release process.

Using Sampo follows a simple three-step workflow: add changesets to describe what changed, prepare a release to bump versions and update changelogs, and finally publish packages to their registries. Let’s explore each of these steps in detail, plus a brief explanation on how versions are managed. In a longboat on green, swirling seas at dusk, the bearded hero Väinämöinen—hair streaming—raises a sword against the eagle-winged Louhi perched on the gunwale, while his crew brace long poles around the Sampo to keep it aboard; crossbows and gear lie scattered on the deck beneath a star-streaked sky. Gallen-Kallela A (1896). The Defense of the Sampo.
In the Kalevala, the Sampo is a mythical artifact that brings good fortune to its holder, definitely lost at sea after a battle between the hero Väinämöinen and the sorceress Louhi.

Sampo enforces Semantic Versioning (SemVer) with the familiar MAJOR.MINOR.PATCH format. Each changeset specifies a bump level: patch for bug fixes, minor for new features, and major for breaking changes.

This allows Sampo to automatically calculate the new versions, but also informs your users about the nature of the changes. For example, moving from 1.2.3 to 1.2.4 is safe, but upgrading to 2.0.0 requires reviewing the breaking changes.

We also follow SemVer §9 conventions for pre-release versions (1.0.0-alpha, 2.1.0-beta.2, etc.). While in pre-release mode, numeric suffixes increment naturally (alphaalpha.1alpha.2). If a higher bump is needed, the base version advances and the suffix resets (1.8.0-alpha.2 + major → 2.0.0-alpha).

Using the CLI, sampo add will guide you through creating a changeset. A simple markdown file that describes what changed, which packages are affected, and the bump level required. Here’s the generated result:

---
cargo/example: minor
npm/web-app: patch
---
A helpful description of the change, to be read by your users.

These files live in .sampo/changesets/ until you’re ready to release. Sampo consumes them to generate human-readable changelogs at the root of each affected package. It enriches entries with commit hash links and author acknowledgments, including special messages for first-time contributors.

# Example
## 0.2.0 — 2024-06-20
### Minor changes
- [abcdefg](link/to/commit) A helpful description of the changes. — Thanks @user!
## 0.1.1 — 2024-05-12
### Patch changes
- [hijklmn](link/to/commit) A brief description of the fix. — Thanks @user2 for their first contribution!
... previous entries ...

Since we appends new entries at the top of the first ## section, any intro content or custom main header is preserved. You can also manually edit the previously released entries, and Sampo will keep them intact. Screenshot of a GitHub pull-request comment from “sampo-s-bot (bot)” in light theme, showing a warning titled “No changeset detected.” The message explains that no new .sampo/changesets/*.md files were found and lists steps: run sampo add, follow the prompts to pick affected packages and describe the change, then commit the generated file to the PR so it's detected automatically. Our GitHub App can remind you to create changesets directly in GitHub Pull Requests. When a changeset has been added, it even comments with a summary of the changes.

Running sampo release will prepare your packages for release, by processing all pending changesets, bumping package versions, updating changelogs, and automatically handling workspace dependencies.1 As long as the release is not finalized, you can continue to add changesets and re-run the sampo release command. Sampo will update package versions and pending changelogs accordingly.1 — Sampo automatically detects packages within the same repository that depend on each other. By default, dependent packages are patched when a workspace dependency is updated, but you can configure this behaviour with fixed/linked options.

Once you’re ready, sampo publish publishes updated packages to their registries (crates.io for Rust, npm for JavaScript/TypeScript, etc) and creates git tags for the new versions. You need to be authenticated to each registry beforehand, or provide tokens via environment variables.

Both steps can be automated in CI pipelines by using our GitHub Action, which can also create GitHub Releases and open Discussions for each published version.

The action runs in auto mode by default: when you have pending changesets, it prepares or updates a « release » Pull Request; when that PR is merged, it publishes your packages. For pre-release workflows, it even prepares a stabilize PR that exits pre-release mode and lines up the stable release.

Sampo is designed to be a helpful, reliable, and flexible tool that users can trust to manage changelogs, versioning, and publishing. This means:

Also, as an open-source project, we adopted the Contributor Covenant code of conduct. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. In a smoky forest smithy, the blacksmith Ilmarinen and a helper strain on a long wooden bellows, peering into a blazing furnace; workers in the background haul logs, with axes and tools scattered in the firelit foreground. Gallen-Kallela A (1893). The Forging of the Sampo.
Sampo is named after this mythical artifact, both to honor the importance Finland took in my recent life, and because I find the symbolism quite fitting to the chaotic nature of software development.

Sampo is deeply inspired by Changesets and Lerna, from which we borrow the changeset format and monorepo release workflows. But our project goes beyond the JavaScript/TypeScript ecosystem, as it is made with Rust, and designed to support multiple mixed ecosystems. Other npm-limited tools include Rush, Ship.js, Release It!, and beachball.

Google’s Release Please is ecosystem-agnostic, but lacks publishing capabilities, and is not monorepo-focused. Also, it uses Conventional Commits messages to infer changes instead of explicit changesets, which confuses the technical git history (useful for developers) with the user-facing API changelog (made for users). Other commit-based tools include semantic-release and auto.

Knope is an ecosystem-agnostic tool inspired by Changesets, but lacks publishing capabilities, and is more config-heavy. But we are thankful for their open-source changeset parser that we reused in Sampo!

To my knowledge, no other tool automates versioning, changelogs, and publishing, with explicit changesets, and multi-ecosystem support.

Some technical details

Sampo is the first Rust project I led, and it’s a great learning experience. Unlike WEBlsp, led by Erika, it has less lifetime and borrowing complexities, so I could focus on designing a clean architecture, and learning idiomatic Rust.

During development, I particularly appreciated Clippy, by far the most helpful linter I’ve used, with impressive suggestions and error messages. I’ve also been impressed by the quality of ChatGPT’s Codex and Claude Code work with Rust, far better than with other languages I’ve tried.22 — Another proof (if needed) that strong static typing not only helps humans, but also editor toolings and AI copilots.

The main pain points were the compilation times (especially in the Github Action), and the general poor documentation of some crates.

Sampo uses Cargo workspaces to organize four crates around a shared core.

sampo-core implements the domain logic—release planning, changelog generation, configuration parsing, version math (semver), and registry communication (reqwest/tokio). The PackageAdapter enum abstracts ecosystem-specific operations (more on this later).

Three consumer crates depend on this core: sampo (the CLI using clap and dialoguer), sampo-github-action (CI/CD orchestrator calling Git and GitHub API), and sampo-github-bot (an axum web service). Path dependencies (path = "../sampo-core") enable instant recompilation during development, and Sampo ensures consistent core versions.

This architecture limits code duplication, while keeping deployment isolated: the CLI ships via cargo install, the action as pre-built binaries, and the bot as a Docker container (hosted on Fly.io).

Sampo’s multi-ecosystem architecture centers on the PackageAdapter enum, which abstracts ecosystem-specific operations behind a unified interface. Each adapter (Cargo, npm, Hex) implements the same set of operations (workspace discovery, manifest parsing, registry queries, and publishing) but tailored to its ecosystem’s conventions.33 — We use an enum over a trait for a closed set of known adapters with static dispatch, exhaustive pattern matching, zero-cost abstraction via Copy, and trivial iteration through all(). The trade-off is some verbosity in match statements, but this can be eliminated later with enum-iterators or strum.

This comes with challenges, for example handling multiple package managers in the JavaScript ecosystem (npm, pnpm, yarn, bun), or dealing with executable formats like mix.exs for Elixir packages (requiring parsing with tree-sitter-elixir). But it also helps contributors add support for their favorite ecosystems, without dealing with existing implementations.

Packages are uniquely identified using « canonical identifiers » in the format <ecosystem>/<name> (e.g., cargo/sampo-core, npm/web-app). The PackageSpecifier type parses user references—either explicit (npm/utils) or plain (utils)—and resolves them against the workspace. When a plain name appears in multiple ecosystems, Sampo detects the ambiguity and prompts for clarification.

This system enables cross-ecosystem features like fixed and linked dependency groups (e.g., synchronizing a Rust library with its JavaScript bindings), or simply writing a single changeset for multiple packages across ecosystems.

Sampo uses typed error enums with thiserror in a layered architecture: sampo-core defines SampoError with domain-specific variants, while user-facing crates wrap this with boundary-specific types (ActionError for the GitHub Action). Errors propagate through Result<T> type aliases using the ? operator, avoiding dynamic error boxing, and enabling precise error matching at each layer.

At the presentation boundary, main() catches Result from run() and displays errors via eprintln!() before returning exit codes. Interactive CLI prompts validate input in-place, showing warnings (yellow ⚠️) for recoverable issues, and converting third-party errors into SampoError variants.

I tried to provide clear, actionable error messages without overwhelming users with stack traces. But I have to admit this is still a work in progress!

Testing sampo-github-action has been one of the most challenging aspects of the project, as it relies heavily on runtime environment variables and external API interactions. The action creates branches, opens pull requests, publishes packages, and tags releases—operations that only work properly within GitHub’s infrastructure.

We implemented minimal integration tests that spawn the binary in temporary repositories, and we run the action with use-local-build: true to compile and test the current codebase in production workflows. However, this leaves critical gaps, and still requires manual testing in production environments.

I need to try act in the future to simulate GitHub Actions locally, but I’ve heard mixed reviews about its complexity/reliability ratio… Stay tuned! Scrooge McDuck steers a massive golden Sampo spilling coins, as Donald dangles from a green one-eyed monster; Huey, Dewey, and Louie cling on while a towering bearded, helmeted figure parts storm clouds and hurls lightning over a starry sky. Don Rosa (1999). “The Quest for Kalevala”.
Like a horn of plenty, the Sampo is supposed to bring abundance and prosperity to its holder. I hope our tooling suite can achieve this for developers 😉

What’s next?

I’m happy that Sampo has reached a usable state. We’re already dogfooding it to publish Sampo itself, using it to release our static site generator Maudit (another Bruits project), and I’ve integrated it into my workflow at work. I hope to see more users adopt the tool, and gather valuable feedback that will help consolidate its reliability and uncover edge cases we haven’t encountered yet.

The core features are already here, and breaking changes should be minimal going forward. My focus now shifts to expanding ecosystem support with additional adapters, enriching configuration options to handle more advanced use cases, and publishing the CLI binaries to various OS package registries to make adoption easier.

If you’re looking for a Rust open-source project to contribute to, we would be happy to welcome you aboard! Check out the contributing guide and the code of conduct to get started. Bugs and ongoing RFCs live in issues, and new features and improvements are discussed in discussions.

If the project looks interesting to you, don’t hesitate to star the repository to show your support!