From 480b1e86c6bb9d8c9c4061750d4e4e8a31ffec5b Mon Sep 17 00:00:00 2001 From: Kasperczyk Date: Fri, 17 Jul 2026 22:52:14 +0200 Subject: [PATCH 1/2] ci: automate versioning and publishing with Release Please and commitlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What and why Replaces the manual "cut a tag to publish" flow with automated, Conventional-Commit-driven versioning. Merges no longer require hand-picking a version: Release Please computes the next version and changelog from commit types, and merging its release PR tags the commit and publishes to nuget.org. ## How it works merge feature/fix PRs -> Release Please opens/updates a "release PR" (version bump + CHANGELOG) merge the release PR -> Release Please creates tag vX.Y.Z + GitHub Release -> `publish` job (same run) packs and pushes to nuget.org Bump level comes from the commit type: `fix:` → patch, `feat:` → minor, `feat!:` / `BREAKING CHANGE:` → major. Publishing happens only on a deliberate release, not on every merge. ## Changes - **`.github/workflows/release.yml`**: reworked from a tag-triggered publish to `push: main` → `release-please` job + `publish` job (`if: release_created`). `publish` keeps the reviewed `release` environment and OIDC trusted publishing. It's a downstream job in the same run, so the default `GITHUB_TOKEN` is enough — no PAT needed. - **`release-please-config.json` / `.release-please-manifest.json`** (new): `simple` release type, `include-component-in-tag: false` so tags stay `vX.Y.Z` (matching the existing `v1.0.0`), and `extra-files` bumps `` in the csproj. - **`src/RuleCraft/RuleCraft.csproj`**: `x-release-please-version` marker on `` so it is bumped automatically. - **`.github/workflows/commitlint.yml` + `commitlint.config.js`** (new): enforces Conventional Commits on every PR to main (CI-side via wagoid action — no local Node/husky footprint). Prose commit bodies are exempt from the 100-char wrap rule. - **`CHANGELOG.md`**: reformatted to the Release Please layout so future auto-generated entries stay consistent. ## Required repo settings (must be enabled for this to work) - **Settings → Actions → General → Workflow permissions**: "Read and write permissions" **and** "Allow GitHub Actions to create and approve pull requests" — otherwise Release Please cannot open the release PR. - **`release` environment** with required reviewers (gates the NuGet push). - Secret **`NUGET_USER`** present (unchanged). ## Notes - Normal PRs still run the Build (`ci.yml`) workflow. The one exception is the Release-Please-authored release PR, which the default `GITHUB_TOKEN` does not trigger CI on; it only touches version + changelog, and `ci.yml` + the publish job's own `dotnet test` both run once it lands on main. - First release after this lands will be **1.0.1** (the commits since `v1.0.0` are fixes). --- .github/workflows/commitlint.yml | 24 ++++++++++++++ .github/workflows/release.yml | 56 ++++++++++++++++++++++++-------- .release-please-manifest.json | 3 ++ CHANGELOG.md | 42 ++++++++---------------- commitlint.config.js | 10 ++++++ release-please-config.json | 14 ++++++++ src/RuleCraft/RuleCraft.csproj | 2 +- 7 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/commitlint.yml create mode 100644 .release-please-manifest.json create mode 100644 commitlint.config.js create mode 100644 release-please-config.json diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..7cb87cf --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,24 @@ +name: Commit lint + +# Every commit in a PR to main must be a Conventional Commit, because Release Please reads those +# messages to compute the next version and the changelog. Enforced in CI (not just a local hook) so +# it holds regardless of a contributor's toolchain and cannot be skipped with --no-verify. +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + # Full history so the action can see every commit in the PR range, not just the tip. + fetch-depth: 0 + + - uses: wagoid/commitlint-github-action@v6 + with: + configFile: commitlint.config.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5adcc2b..f981677 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,50 @@ name: Release -# Publishing to nuget.org cannot be undone — a version can be unlisted, never replaced. -# Hence: a tag, a reviewed environment, and a test run before anything is pushed. +# Release Please drives versioning from Conventional Commits. On each push to main it opens/updates +# a "release PR" that bumps the version and CHANGELOG from the commits since the last release; the +# level of the bump comes from the commit types (fix: -> patch, feat: -> minor, ! / BREAKING CHANGE +# -> major). Merging that PR is the release: Release Please then creates the tag + GitHub release, +# and the `publish` job below packs and pushes to nuget.org. +# +# publish is a *downstream job in the same run* as the release creation (gated on release_created), +# NOT a separate workflow triggered by the new tag. That matters: a tag created with the default +# GITHUB_TOKEN would not trigger a tag-listening workflow, so wiring publish as `needs` is what keeps +# it firing without a PAT. (Trade-off of the default token: the release PR it opens does not trigger +# the Build workflow's pull_request runs. Switch to a PAT only if you want CI on the release PR.) on: push: - tags: ['v*'] + branches: [main] + +# Publishing to nuget.org cannot be undone — a version can be unlisted, never replaced — so releases +# are serialized and a publish under way is never cancelled. +concurrency: + group: release + cancel-in-progress: false permissions: contents: read jobs: + release-please: + runs-on: ubuntu-latest + permissions: + contents: write # create the release tag and GitHub release + pull-requests: write # open and update the release PR + outputs: + release_created: ${{ steps.rp.outputs.release_created }} + tag_name: ${{ steps.rp.outputs.tag_name }} + version: ${{ steps.rp.outputs.version }} + steps: + # Reads release-please-config.json + .release-please-manifest.json from the repo root. + - uses: googleapis/release-please-action@v4 + id: rp + publish: + needs: release-please + # Only the run that actually cut a release (the one where the release PR was merged) publishes. + if: needs.release-please.outputs.release_created == 'true' runs-on: ubuntu-latest - # Add required reviewers to the `release` environment in repo settings, so cutting a tag - # proposes a publish rather than performing one. + # Add required reviewers to the `release` environment in repo settings to gate the push. environment: release permissions: contents: read @@ -22,7 +53,10 @@ jobs: id-token: write steps: + # Build the exact commit Release Please tagged. - uses: actions/checkout@v7 + with: + ref: ${{ needs.release-please.outputs.tag_name }} - name: Set up .NET uses: actions/setup-dotnet@v6 @@ -30,22 +64,18 @@ jobs: dotnet-version: 8.0.x global-json-file: global.json - - name: Version from the tag - # is hard-coded to 1.0.0 in the csproj. Without this, the second tag would - # push 1.0.0 again and nuget.org would reject it as a duplicate. v1.2.3 -> 1.2.3. - run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" - - name: Test run: dotnet test RuleCraft.slnx --configuration Release - name: Pack - # ContinuousIntegrationBuild makes the build deterministic and gives SourceLink the - # normalized paths it needs for symbols to resolve to the source of this commit. + # Release Please already bumped in the csproj, but pass it explicitly so the package + # version cannot drift from the tag. ContinuousIntegrationBuild makes the build deterministic + # and gives SourceLink the normalized paths it needs for symbols to resolve to this commit. run: > dotnet pack src/RuleCraft/RuleCraft.csproj --configuration Release --output artifacts - -p:Version=${{ env.VERSION }} + -p:Version=${{ needs.release-please.outputs.version }} -p:ContinuousIntegrationBuild=true # Trusted publishing: no long-lived secret, just a token good for one key for one hour. diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..37fcefa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f52662a..b47919d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,19 @@ # Changelog -All notable changes to RuleCraft are recorded here. The format follows -[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to follow -[Semantic Versioning](https://semver.org/spec/v2.0.0.html) — for a library, the public surface -that governs SemVer is the API *and* the rule-persistence format on disk. +Maintained automatically by [Release Please](https://github.com/googleapis/release-please) from +[Conventional Commit](https://www.conventionalcommits.org/) messages — new entries are prepended +above on each release; do not edit by hand. -## [Unreleased] +## [1.0.0](https://github.com/mkasperczyk90/RuleCraft/releases/tag/v1.0.0) (2026-07-17) -## [1.0.0] — unreleased -First public release. The engine, its three rule kinds and the persistence format are considered -stable from this version. +### Features -### Added -- **Runtime rule engine** for one contract/context pair: describe a rule in natural language, an - LLM implements it, it is verified and human-approved, then hot-loaded into the running process - with no redeploy. -- **Three rule kinds**, competing purely by priority: interpreted **JSON-DSL** rules (a real - sandbox), **compiled C#** rules (Roslyn, in-memory, into a collectible `AssemblyLoadContext`), - and **static** host-code rules. -- **Security gate for compiled rules**: a minimal whitelisted reference set plus a semantic-model - analyzer, resolving most-specific-first (member → type → namespace). -- **Approval workflow**: rules are parked as `PendingApproval` and nothing runs until approved; - `AutoApprove` defaults to off. Reject, disable, enable and quarantine transitions included. -- **Durable, crash-safe store**: source is the single source of truth, written atomically - alongside audit metadata (spec, status, priority, SHA-256, contract/context fingerprint, model - id, approver, full validation report). -- **DI integration** in-box: `services.AddRuleCraft(…)`. -- **Vendor-neutral generation** through `Microsoft.Extensions.AI.IChatClient` — no model id is - hard-coded. -- Ships as a single assembly, `RuleCraft.dll`, with XML docs and a symbol package. - -[Unreleased]: https://github.com/mkasperczyk90/RuleCraft/compare/v1.0.0...HEAD -[1.0.0]: https://github.com/mkasperczyk90/RuleCraft/releases/tag/v1.0.0 +* Runtime rule engine for one contract/context pair: describe a rule in natural language, an LLM implements it, it is verified and human-approved, then hot-loaded into the running process with no redeploy. +* Three rule kinds, competing purely by priority: interpreted JSON-DSL rules (a real sandbox), compiled C# rules (Roslyn, in-memory, into a collectible `AssemblyLoadContext`), and static host-code rules. +* Security gate for compiled rules: a minimal whitelisted reference set plus a semantic-model analyzer, resolving most-specific-first (member → type → namespace). +* Approval workflow: rules are parked as `PendingApproval` and nothing runs until approved; `AutoApprove` defaults to off. Reject, disable, enable and quarantine transitions included. +* Durable, crash-safe store: source is the single source of truth, written atomically alongside audit metadata (spec, status, priority, SHA-256, contract/context fingerprint, model id, approver, full validation report). +* DI integration in-box: `services.AddRuleCraft(…)`. +* Vendor-neutral generation through `Microsoft.Extensions.AI.IChatClient` — no model id is hard-coded. +* Ships as a single assembly, `RuleCraft.dll`, with XML docs and a symbol package. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..314f367 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,10 @@ +// Conventional Commits, enforced in CI by .github/workflows/commitlint.yml. The commit types drive +// Release Please: fix -> patch, feat -> minor, `!` / BREAKING CHANGE -> major. +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + // Commit bodies here carry design rationale in prose; the 100-char wrap rule fights that. The + // subject (header-max-length) is still capped by config-conventional. + 'body-max-line-length': [0, 'always'], + }, +}; diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..e93bd5c --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "simple", + "package-name": "RuleCraft", + "changelog-path": "CHANGELOG.md", + "include-component-in-tag": false, + "extra-files": [ + "src/RuleCraft/RuleCraft.csproj" + ] + } + } +} diff --git a/src/RuleCraft/RuleCraft.csproj b/src/RuleCraft/RuleCraft.csproj index d7fdfac..c1d2e20 100644 --- a/src/RuleCraft/RuleCraft.csproj +++ b/src/RuleCraft/RuleCraft.csproj @@ -7,7 +7,7 @@ RuleCraft - 1.0.0 + 1.0.0 mkasperczyk90@gmail.com Runtime rule engine whose implementations are generated by an LLM, verified, human-approved and hot-loaded into the running application. rules;rule-engine;llm;ai;roslyn;codegen;business-rules;hot-reload From 1e37fdc281200d46f099eaecad698f09867aeb58 Mon Sep 17 00:00:00 2001 From: Kasperczyk Date: Fri, 17 Jul 2026 22:54:13 +0200 Subject: [PATCH 2/2] fix: change .js to .mjs --- .github/workflows/commitlint.yml | 2 +- commitlint.config.js => commitlint.config.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename commitlint.config.js => commitlint.config.mjs (96%) diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 7cb87cf..991c2a9 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -21,4 +21,4 @@ jobs: - uses: wagoid/commitlint-github-action@v6 with: - configFile: commitlint.config.js + configFile: commitlint.config.mjs diff --git a/commitlint.config.js b/commitlint.config.mjs similarity index 96% rename from commitlint.config.js rename to commitlint.config.mjs index 314f367..efd2db5 100644 --- a/commitlint.config.js +++ b/commitlint.config.mjs @@ -1,6 +1,6 @@ // Conventional Commits, enforced in CI by .github/workflows/commitlint.yml. The commit types drive // Release Please: fix -> patch, feat -> minor, `!` / BREAKING CHANGE -> major. -module.exports = { +export default { extends: ['@commitlint/config-conventional'], rules: { // Commit bodies here carry design rationale in prose; the 100-char wrap rule fights that. The