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" } + ] + } + } +}