diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..96fec33
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,18 @@
+
+
+## What & why
+
+
+## Checklist
+
+- [ ] PR title is a Conventional Commit (`feat:`, `fix:`, `docs:`, …)
+- [ ] Tests added/updated for new behavior
+- [ ] New/changed public APIs are documented
diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml
new file mode 100644
index 0000000..5f7b4bc
--- /dev/null
+++ b/.github/workflows/pr-title-lint.yml
@@ -0,0 +1,36 @@
+name: pr-title-lint
+
+# Enforce that every PR title is a valid Conventional Commit. PRs are squash-merged, so the PR
+# title becomes the single commit on main that release-please reads to compute the next version
+# and changelog — keeping it valid here is what keeps releases working.
+#
+# pull_request_target (not pull_request) so the check also runs on PRs from forks. This is safe:
+# the job only reads the PR title via the API and never checks out or runs the PR's code.
+on:
+ pull_request_target:
+ types: [opened, edited, reopened, synchronize]
+
+permissions:
+ pull-requests: write # post/update a comment explaining a failing title
+
+jobs:
+ validate:
+ name: Validate PR title (Conventional Commits)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: amannn/action-semantic-pull-request@v6
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ # Allowed types — mirrors the Conventional Commits spec and what release-please understands.
+ types: |
+ feat
+ fix
+ perf
+ refactor
+ docs
+ test
+ build
+ ci
+ chore
+ revert
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f9b6512..873b50a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,28 +1,53 @@
name: release
+# Trunk-based releases via release-please (Conventional Commits):
+# 1. Every push to main updates a "release PR" that bumps the version + CHANGELOG.
+# 2. Merging that release PR creates the GitHub Release + tag, which flips
+# release_created=true and runs the publish job below in the same workflow run.
+# Because publish is a downstream job (not a separate workflow keyed off the tag),
+# the default GITHUB_TOKEN is sufficient — no PAT required.
on:
push:
- tags: [ 'v*' ]
- workflow_dispatch:
- inputs:
- version:
- description: 'Version to publish (e.g. 1.2.3)'
- required: true
+ branches: [ main ]
-permissions:
- contents: write # create the GitHub Release
- packages: write # push to GitHub Packages
+# Serialize so two quick merges can't race the release PR or a publish.
+concurrency:
+ group: release-${{ github.ref }}
+ cancel-in-progress: false
jobs:
- release:
+ release-please:
+ name: Release Please
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # create the GitHub Release + tag
+ pull-requests: write # open/update the release PR
+ outputs:
+ release_created: ${{ steps.release.outputs.release_created }}
+ tag_name: ${{ steps.release.outputs.tag_name }}
+ version: ${{ steps.release.outputs.version }}
+ steps:
+ - uses: googleapis/release-please-action@v4
+ id: release
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ config-file: release-please-config.json
+ manifest-file: .release-please-manifest.json
+
+ publish:
name: Pack & publish (OverTone + DI)
+ needs: release-please
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
+ permissions:
+ contents: write # attach package assets to the GitHub Release
+ packages: write # push to GitHub Packages
steps:
- name: Checkout
uses: actions/checkout@v6
with:
- fetch-depth: 0
+ fetch-depth: 0 # full history -> deterministic Source Link
- name: Setup .NET (8 + 10)
uses: actions/setup-dotnet@v5
@@ -31,50 +56,55 @@ jobs:
8.0.x
10.0.x
- - name: Determine version
- id: version
- shell: bash
- run: |
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- VERSION="${{ github.event.inputs.version }}"
- else
- VERSION="${GITHUB_REF_NAME#v}"
- fi
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- echo "Publishing OverTone $VERSION"
+ - name: Restore
+ run: dotnet restore
+
+ # Build with the release version so the embedded assembly version matches the package.
+ - name: Build
+ run: >
+ dotnet build --configuration Release --no-restore
+ -p:Version=${{ needs.release-please.outputs.version }}
+
+ # Gate the publish on a green test run — a bad NuGet.org push can't be unpublished.
+ - name: Test (net8.0 + net10.0)
+ run: dotnet test --configuration Release --no-build --verbosity normal
# Packs every IsPackable project: OverTone and OverTone.Extensions.DependencyInjection.
- name: Pack
run: >
- dotnet pack --configuration Release
- -p:Version=${{ steps.version.outputs.version }}
+ dotnet pack --configuration Release --no-build
+ -p:Version=${{ needs.release-please.outputs.version }}
--output ${{ runner.temp }}/nupkg
- name: Push to NuGet.org
- if: ${{ env.NUGET_API_KEY != '' }}
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
- run: >
- dotnet nuget push "${{ runner.temp }}/nupkg/*.nupkg"
- --api-key "$NUGET_API_KEY"
- --source https://api.nuget.org/v3/index.json
- --skip-duplicate
+ run: |
+ if [ -z "$NUGET_API_KEY" ]; then
+ echo "NUGET_API_KEY secret not set — skipping NuGet.org push."
+ exit 0
+ fi
+ dotnet nuget push "${{ runner.temp }}"/nupkg/*.nupkg \
+ --api-key "$NUGET_API_KEY" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
- name: Push to GitHub Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: >
- dotnet nuget push "${{ runner.temp }}/nupkg/*.nupkg"
+ dotnet nuget push "${{ runner.temp }}"/nupkg/*.nupkg
--api-key "$GH_TOKEN"
--source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
--skip-duplicate
--no-symbols
- - name: Create GitHub Release
- if: startsWith(github.ref, 'refs/tags/')
- uses: softprops/action-gh-release@v3
- with:
- generate_release_notes: true
- files: |
- ${{ runner.temp }}/nupkg/*.nupkg
- ${{ runner.temp }}/nupkg/*.snupkg
+ # Attach the .nupkg/.snupkg to the release release-please just created.
+ - name: Attach packages to the GitHub Release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: >
+ gh release upload "${{ needs.release-please.outputs.tag_name }}"
+ "${{ runner.temp }}"/nupkg/*.nupkg
+ "${{ runner.temp }}"/nupkg/*.snupkg
+ --clobber
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..5fdd883
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "1.1.0"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e212b..926459e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
This file covers both published packages — **OverTone** and
**OverTone.Extensions.DependencyInjection** — which are versioned together.
-## [Unreleased]
+## [1.1.0] - 2026-06-06
### Added
- **One-call theming from an image**: `Palette.GetThemeAsync` / `GetThemePairAsync` and the matching
@@ -47,5 +47,5 @@ This file covers both published packages — **OverTone** and
- Multi-targeting for **.NET 8.0** and **.NET 10.0**.
- Source Link, deterministic builds, a package icon, and symbol packages (`.snupkg`).
-[Unreleased]: https://github.com/ChocoStout/OverTone/compare/v1.0.0...HEAD
+[1.1.0]: https://github.com/ChocoStout/OverTone/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/ChocoStout/OverTone/releases/tag/v1.0.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 788df29..1ce2226 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,14 +33,43 @@ cross-platform and builds on Windows, macOS, and Linux.
- Add tests for new behavior.
- NuGet versions are centralized in `Directory.Packages.props` (Central Package Management) —
add or change versions there, not in individual `.csproj` files.
-- Versions follow [Semantic Versioning](https://semver.org/). Update `CHANGELOG.md` and bump
- `VersionPrefix` in `Directory.Build.props`.
+- Versions follow [Semantic Versioning](https://semver.org/), derived automatically from commit
+ messages — **don't** hand-edit `CHANGELOG.md` or `VersionPrefix`; release-please owns both. See
+ [Commit messages](#commit-messages) and [Releasing](#releasing-maintainers).
+
+## Commit messages
+
+OverTone uses [Conventional Commits](https://www.conventionalcommits.org/). The release automation
+([release-please](https://github.com/googleapis/release-please)) reads them to pick the next version
+and to write `CHANGELOG.md`, so the prefix matters:
+
+| Prefix | Example | Effect |
+|---|---|---|
+| `feat:` | `feat: add Tailwind v4 exporter` | minor bump |
+| `fix:` | `fix: clamp K-Means seeds to image bounds` | patch bump |
+| `feat!:` / `fix!:`, or a `BREAKING CHANGE:` footer | `feat!: drop the net6.0 target` | major bump |
+| `docs:` `test:` `refactor:` `perf:` `build:` `ci:` `chore:` | `chore: tidy usings` | no release |
+
+Only `feat`, `fix`, and breaking changes cut a release; other types just record history. A commit whose
+subject doesn't start with one of these types is ignored by the changelog.
+
+PRs are **squash-merged**, so the **pull-request title becomes the commit on `main`** and must itself be
+a valid Conventional Commit. The `pr-title-lint` workflow enforces this on every PR.
## Releasing (maintainers)
-Pushing a `v*` tag (for example `v1.1.0`) triggers `.github/workflows/release.yml`, which packs both
-packages with the tag's version and publishes them to NuGet.org and GitHub Packages. The NuGet.org
-push requires a `NUGET_API_KEY` repository secret.
+Releases are automated with [release-please](https://github.com/googleapis/release-please) — there are
+no manual tags or version bumps:
+
+1. Merging Conventional Commits to `main` makes release-please open (and keep updating) a **release PR**
+ titled `chore(main): release X.Y.Z` that bumps `VersionPrefix` in `Directory.Build.props` and
+ updates `CHANGELOG.md`.
+2. **Merge that release PR** when you're ready to ship. release-please creates the GitHub Release and the
+ `vX.Y.Z` tag, which runs the `publish` job: it builds, tests, packs both packages, and pushes them to
+ NuGet.org and GitHub Packages (attaching the `.nupkg`/`.snupkg` to the release).
+
+The NuGet.org push requires a `NUGET_API_KEY` repository secret (GitHub Packages uses the built-in
+token). Configuration lives in `release-please-config.json` and `.release-please-manifest.json`.
## License
diff --git a/Directory.Build.props b/Directory.Build.props
index 8df10e4..b064df2 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,9 +8,11 @@
https://github.com/ChocoStout/OverTone
git
-
- 1.1.0
+
+ 1.1.0
enable
diff --git a/README.md b/README.md
index 54f69c7..4f46903 100644
--- a/README.md
+++ b/README.md
@@ -497,9 +497,11 @@ dotnet run --project OverTone.Web
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-extractor`
3. Add your extractor implementing `IColorPaletteExtractor`
-4. Open a pull request
+4. Open a pull request with a [Conventional Commits](https://www.conventionalcommits.org/) title
+ (e.g. `feat: add my extractor`) — it drives the version bump and changelog
All contributions welcome — new algorithms, bug fixes, performance improvements, docs.
+See [CONTRIBUTING.md](CONTRIBUTING.md) for commit conventions and the release process.
---
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..42dbf43
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+ "include-component-in-tag": false,
+ "last-release-sha": "a216562fbbf39a840d6d76b8962e2f750c9bc897",
+ "packages": {
+ ".": {
+ "release-type": "simple",
+ "package-name": "OverTone",
+ "changelog-path": "CHANGELOG.md",
+ "extra-files": [
+ { "type": "generic", "path": "Directory.Build.props" }
+ ]
+ }
+ }
+}