{"id":50297543,"url":"https://github.com/alejandrohdezma/sbt-changesets","last_synced_at":"2026-05-28T10:01:02.133Z","repository":{"id":349538482,"uuid":"1202699515","full_name":"alejandrohdezma/sbt-changesets","owner":"alejandrohdezma","description":"Changeset-based versioning for Scala multi-module builds (sbt plugin + GitHub Action)","archived":false,"fork":false,"pushed_at":"2026-05-18T09:08:56.000Z","size":131,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-18T11:13:12.113Z","etag":null,"topics":["changesets","scala","scala-library"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alejandrohdezma.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"ko_fi":"alejandrohdezma"}},"created_at":"2026-04-06T09:56:19.000Z","updated_at":"2026-05-18T09:08:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/alejandrohdezma/sbt-changesets","commit_stats":null,"previous_names":["alejandrohdezma/sbt-changesets"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/alejandrohdezma/sbt-changesets","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fsbt-changesets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fsbt-changesets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fsbt-changesets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fsbt-changesets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alejandrohdezma","download_url":"https://codeload.github.com/alejandrohdezma/sbt-changesets/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fsbt-changesets/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33603475,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["changesets","scala","scala-library"],"created_at":"2026-05-28T10:01:01.139Z","updated_at":"2026-05-28T10:01:02.112Z","avatar_url":"https://github.com/alejandrohdezma.png","language":"Scala","funding_links":["https://ko-fi.com/alejandrohdezma"],"categories":[],"sub_categories":[],"readme":"Changeset-based versioning for Scala multi-module builds (sbt plugin + GitHub Action)\n\n`sbt-changesets` provides a better (and easier) way to manage libraries. Specially useful for company-internal monorepos, bringing changeset-based versioning to Scala multi-module builds. Developers describe their changes in small markdown files, and the plugin takes care of version bumps, dependency cascade, and changelog generation.\n\n\u003e `sbt-changesets` is an SBT-opinionated take on [changesets](https://github.com/changesets/changesets).\n\n**Looking for the GitHub Action?** Jump to [GitHub Actions](#github-actions).\n\n## Installation\n\nAdd the plugin to your `project/plugins.sbt`:\n\n```sbt\naddSbtPlugin(\"com.alejandrohdezma\" % \"sbt-changesets\" % \"0.6.0\")\n```\n\nThis plugin depends on [sbt-modules](https://github.com/alejandrohdezma/sbt-modules), which is pulled in automatically. It expects modules to be defined using `module` instead of `project` in your `build.sbt`, with source code living under `modules/\u003cmodule-name\u003e/`. See the [sbt-modules documentation](https://github.com/alejandrohdezma/sbt-modules) for details.\n\n## How it works\n\nEach module in your build has a `VERSION` file and a `CHANGELOG.md`. Instead of bumping versions manually, developers create small `.changeset/*.md` files describing their changes. When it's time to release, the plugin reads those files, bumps versions (cascading through the dependency graph), updates changelogs, and cleans up.\n\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003eAll available commands\u003c/b\u003e\u003c/summary\u003e\u003cbr/\u003e\n\n| Command | Description |\n|---|---|\n| `changesetAdd \u003cbump\u003e \u003cdescription\u003e` | Create a changeset for changed modules |\n| `changesetVersion` | Apply version bumps with cascade through dependency graph |\n| `changesetMatrix \u003cvalidate\\|release\u003e` | Output the stage-appropriate work matrix as JSON |\n| `changesetConfig` | Output module dependency graph as JSON |\n\n\u003c/details\u003e\n\n### 1. Creating a changeset\n\nAfter making changes, run:\n\n```\nsbt\u003e changesetAdd minor add-retry-logic\n```\n\nThis detects which modules you changed (via `git diff`) and creates a file like `.changeset/add-retry-logic.md`:\n\n```markdown\n---\n\"my-module\": minor\n\"other-module\": minor\n---\n\nTODO: Describe your changes here\n```\n\nThe first argument is the bump type (`patch`, `minor`, or `major`) and the rest becomes the filename. Edit the file to replace the placeholder with a meaningful description — this will end up in the changelog.\n\n### 2. Validating changesets (CI)\n\nOn pull requests, run `changesetMatrix validate` to ensure every modified module has at least one changeset entry and emit `target/changeset/matrix.json` — a JSON array of `{module, scala-version, version, coordinate}` rows (one per Scala version in the module's `crossScalaVersions`, including affected dependents) that you can feed into a CI matrix as `matrix.include`. It fails if any module is missing coverage or if a description still contains the placeholder text.\n\n```json\n[\n  { \"module\": \"module-a\", \"scala-version\": \"2.13.18\", \"version\": \"1.2.3-abc-SNAPSHOT\", \"coordinate\": \"\\\"com.example\\\" %% \\\"module-a\\\" % \\\"1.2.3-abc-SNAPSHOT\\\"\" },\n  { \"module\": \"module-a\", \"scala-version\": \"3.3.7\",   \"version\": \"1.2.3-abc-SNAPSHOT\", \"coordinate\": \"\\\"com.example\\\" %% \\\"module-a\\\" % \\\"1.2.3-abc-SNAPSHOT\\\"\" },\n  { \"module\": \"module-b\", \"scala-version\": \"3.3.7\",   \"version\": \"2.0.0-abc-SNAPSHOT\", \"coordinate\": \"\\\"com.example\\\" %% \\\"module-b\\\" % \\\"2.0.0-abc-SNAPSHOT\\\"\" }\n]\n```\n\nIf you need the matrix without requiring changeset entries (e.g. for snapshot publishing or local development), set the `CHANGESET_SKIP_VALIDATION` environment variable to `true`. The command will skip validation and still output the matrix.\n\nA module counts as affected when it transitively depends on a changed module through one of `changesetAffectedScopes` (default `Seq(\"compile\")`). A module that depends on it only in **test** scope (e.g. `dependsOn(other % Test)`) is left out — not rebuilt, not version-bumped. To treat such test-scope dependents as affected too, add the scope (`ThisBuild / changesetAffectedScopes += \"test\"`); use `Seq(\"*\")` to match every scope.\n\n### 3. Publishing snapshots (CI)\n\nOn feature branches, the rows from `changesetMatrix validate` feed a CI matrix that publishes each `(module, scala-version)` snapshot on its own runner via `sbt \"++\u003cscala-version\u003e \u003cmodule\u003e/publish\"`. The version is the default `\u003cbase\u003e-\u003csuffix\u003e-SNAPSHOT` from the module's `VERSION` file (suffix from `SNAPSHOT_SUFFIX` env / sys-prop, else a memoised JVM timestamp); each row carries the resolved Maven `coordinate` so a follow-up job can post a PR comment listing them.\n\n### 4. Applying version bumps (CI)\n\nWhen changesets are merged to main branch, run `changesetVersion`. This:\n\n1. Parses all `.changeset/*.md` files.\n2. Cascades bumps through the dependency graph following [early-semver](https://www.scala-lang.org/blog/2021/02/16/preventing-version-conflicts-with-versionscheme.html) rules.\n3. Updates each module's `VERSION` file.\n4. Prepends new entries to each module's `CHANGELOG.md`.\n5. Removes processed changeset files.\n6. Writes `target/changeset/version-summary.json` with old/new versions.\n\nModules that are only bumped through cascade get auto-generated descriptions listing which dependencies changed.\n\n### 5. Publishing releases (CI)\n\nAfter version bumps are committed, `changesetMatrix` writes `target/changeset/matrix.json` — a JSON array of `{module, scala-version}` rows for every (module, Scala version) whose `VERSION` file just changed — which feeds a `publish` matrix that publishes each pair on its own runner in parallel via `sbt \"++\u003cscala-version\u003e \u003cmodule\u003e/publish\"`. A downstream `release-tag` job then runs once per distinct module, calls `extractLatestChangelog \u003cmodule\u003e`, and uses the result as the GitHub release notes.\n\nThe composite [GitHub Action](#github-actions) bundles this flow into `detect` mode, which (when the workflow runs on a Version Packages PR merge) emits both `matrix` (for `publish`) and `release-modules` (for `release-tag`) alongside its PR-side outputs.\n\n## GitHub Actions\n\nThis repository also provides a composite GitHub Action that orchestrates the full CI workflow. Reference it as `alejandrohdezma/sbt-changesets@v0.6.0` and choose a mode depending on the context.\n\n### `detect` mode\n\nValidates that all changed modules have changeset entries and emits exactly two outputs:\n\n- `stage`: a single dispatch classification — `validate` (PR with affected modules), `apply-changesets` (push-to-main with pending changesets), `release` (push-to-main with VERSION bumps to publish), or empty (nothing to do). Use as the single gating condition for every downstream job.\n- `matrix`: the work matrix for this run; shape depends on `stage`.\n  - `stage == 'validate'`: array of `{module, scala-version, version, coordinate}` rows. Each row carries the publish-matrix dimensions plus the snapshot Maven coordinate. Plug into `matrix.include` for the validate job; pass to `snapshot-comment` mode.\n  - `stage == 'release'`: array of `{module, scala-version, version, changelog}` rows. Each row carries the publish-matrix dimensions plus the release-tag note body. Plug into `matrix.include` for the publish job; pass to `release-tag` mode.\n  - Otherwise: empty array. Hard-fails the action if the release-stage payload exceeds 400 KB UTF-8 (≈ 800 KB UTF-16, well under GitHub's 1 MB per-job output cap) — if you hit it, reduce changelog verbosity or split the release.\n\nRun on every event. The consumer gates each downstream job on `stage` and feeds `matrix` directly into all matrix-style consumers and post-processing modes.\n\n```yaml\n# .github/workflows/ci.yaml\non:\n  pull_request:\n  push:\n    branches: [main]\n\njobs:\n  detect:\n    runs-on: ubuntu-latest\n    env:\n      SNAPSHOT_SUFFIX: ${{ github.run_id }}-${{ github.run_attempt }}\n    outputs:\n      matrix: ${{ steps.changesets.outputs.matrix }}\n      stage: ${{ steps.changesets.outputs.stage }}\n    steps:\n      - uses: actions/checkout@@v4\n        with: { fetch-depth: 0 }\n\n      - id: changesets\n        uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: detect\n          error-help-url: https://your-repo/docs/versioning  # shown on validation failure\n\n  validate:\n    needs: detect\n    if: needs.detect.outputs.stage == 'validate'\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include: ${{ fromJson(needs.detect.outputs.matrix) }}\n    env:\n      SNAPSHOT_SUFFIX: ${{ github.run_id }}-${{ github.run_attempt }}\n    steps:\n      - uses: actions/checkout@@v4\n\n      - run: sbt \"++${{ matrix.scala-version }} ${{ matrix.module }}/test\"\n\n      - run: sbt \"++${{ matrix.scala-version }} ${{ matrix.module }}/publish\"\n\n  snapshot-comment:\n    needs: [detect, validate]\n    if: needs.detect.outputs.stage == 'validate'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: snapshot-comment\n          matrix: ${{ needs.detect.outputs.matrix }}\n```\n\n`SNAPSHOT_SUFFIX` (e.g. `${{ github.run_id }}-${{ github.run_attempt }}`) is set on both `detect` and `validate` so the coordinates resolved up-front in `detect` match the artifacts published by the `validate` matrix. Because every matrix cell in a single workflow run shares the same `SNAPSHOT_SUFFIX`, the per-Scala-version publishes that make up one module produce consistent versions. The `coordinate` field carried in each matrix row is rendered from each module's sbt `organization` setting, so per-module org overrides (e.g. `com.permutive.metrics`) are respected without any consumer-side hardcoding. Snapshot publishes are intended for private monorepos only — exposing publishing credentials on PRs in public repositories is a security risk.\n\n#### Customising the `coordinate` per module\n\nThe default coordinate is `\"org\" %% \"name\" % \"version\"` for Scala modules and `\"org\" % \"name\" % \"version\"` for Java modules (`crossPaths := false`). Override the `changesetCoordinate` sbt setting on individual projects when you want the snapshot-comment to render something different — for example, testkit modules that consumers always import in the `test` configuration:\n\n```scala\nlazy val `my-testkit` = module.settings(changesetCoordinate ~= { _ + \" % \\\"test\\\"\" })\n```\n\nThe `~=` transform composes with the default rendering, so you don't have to rebuild the whole coordinate string from `organization` / `name` / `version`. The override applies to every row for that module in the validate-stage matrix.\n\n### `snapshot-comment` mode\n\nPosts (or edits) a PR comment listing snapshot coordinates produced by a matrix snapshot publish. Consumes the validate-stage `matrix` from `detect` and dedupes by module before rendering the markdown blocks.\n\n```yaml\n  snapshot-comment:\n    needs: [detect, validate]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: snapshot-comment\n          matrix: ${{ needs.detect.outputs.matrix }}\n```\n\n### `apply-changesets` mode\n\nBumps `VERSION` files via `changesetVersion` and upserts the **Version Packages** pull request on the `changeset-release/main` branch. Run on push to main when `detect.changesets-count != 0` (i.e. a developer's PR carrying changeset files just merged).\n\nPass `extra-command` to chain additional sbt commands after `changesetVersion` in the same sbt invocation — useful for regenerating doc files (e.g. `mdoc`) so they're committed as part of the same version-PR commit.\n\n### `release-tag` mode\n\nLoops over the release-stage `matrix` (as produced by `detect`) and creates one GitHub release per distinct module — `module@version` as the tag and title, `changelog` as the notes body. Duplicate rows from cross-built modules become no-ops on the second `gh release view`. No sbt, no checkout — pure `gh` API calls. Reruns are idempotent.\n\n```yaml\n  release-tag:\n    needs: [detect, publish]\n    if: needs.detect.outputs.stage == 'release'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: release-tag\n          matrix: ${{ needs.detect.outputs.matrix }}\n```\n\nOptionally pass `target` to override the branch the releases point at (defaults to `main`).\n\n### Putting it together: release workflow\n\nThe push-to-main pipeline is dispatched by `detect.outputs.stage`: when there are pending changeset files `stage` is `apply-changesets` and the version-bump job runs; once that PR is merged `stage` becomes `release`, `publish` fans out one runner per `(module, Scala version)`, and `release-tag` creates one GitHub release per module.\n\n```yaml\n# .github/workflows/ci.yaml (continued)\njobs:\n  # ...detect, validate, snapshot-comment from above...\n\n  apply-changesets:\n    needs: detect\n    if: needs.detect.outputs.stage == 'apply-changesets'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@@v4\n        with: { fetch-depth: 0 }\n\n      - uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: apply-changesets\n          # Optional: regenerate docs as part of the same version-PR commit.\n          extra-command: mdoc\n\n  publish:\n    needs: detect\n    if: needs.detect.outputs.stage == 'release'\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 16\n      matrix:\n        include: ${{ fromJson(needs.detect.outputs.matrix) }}\n    env:\n      RELEASE: \"true\"\n    steps:\n      - uses: actions/checkout@@v4\n\n      - run: sbt \"++${{ matrix.scala-version }} ${{ matrix.module }}/publish\"\n\n  release-tag:\n    needs: [detect, publish]\n    if: needs.detect.outputs.stage == 'release'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: alejandrohdezma/sbt-changesets@v0.6.0\n        with:\n          mode: release-tag\n          matrix: ${{ needs.detect.outputs.matrix }}\n```\n\n`release-tag` `needs: publish` so a publish failure on any matrix cell (e.g. one Scala version fails to compile) blocks all GitHub release creation — preventing half-published modules from getting tagged. Re-running after a fix proceeds cleanly because the action's loop skips tags that already exist; new tags get created as expected.\n\nThe same `matrix` output feeds both `publish` (as `matrix.include`, ignoring the extra `version` and `changelog` fields) and `release-tag` (as a per-row loop, deduping by module). One source of truth for \"what work needs to happen this run\".\n\n### Inputs\n\n| Input | Required | Default | Description |\n|---|---|---|---|\n| `mode` | yes | — | `detect`, `apply-changesets`, `snapshot-comment`, or `release-tag` |\n| `github-token` | no | `github.token` | GitHub token for API operations |\n| `error-help-url` | no | — | URL shown on changeset validation failure |\n| `skip-validation` | no | `false` | Skip changeset validation in `detect` mode while still computing affected modules |\n| `extra-command` | no | — | sbt command(s) chained after `changesetVersion` in `apply-changesets` mode (e.g. `documentation/mdoc`) |\n| `matrix` | no | — | JSON array produced by `detect`'s `matrix` output. Consumed by `snapshot-comment` (validate-stage rows with `coordinate`) and `release-tag` (release-stage rows with `version` + `changelog`) |\n| `pr-number` | no | `github.event.pull_request.number` | PR number to comment on in `snapshot-comment` mode |\n| `target` | no | `main` | Branch / commit SHA to target for the GitHub releases created by `release-tag` mode |\n\n### Outputs\n\n| Output | Modes | Description |\n|---|---|---|\n| `matrix` | `detect` | Stage-dependent work matrix. Validate stage: `{module, scala-version, version, coordinate}` rows. Release stage: `{module, scala-version, version, changelog}` rows. Empty otherwise |\n| `stage` | `detect` | Dispatch classification: `validate`, `apply-changesets`, `release`, or empty. Gate every downstream job on this |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falejandrohdezma%2Fsbt-changesets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falejandrohdezma%2Fsbt-changesets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falejandrohdezma%2Fsbt-changesets/lists"}