diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94043ca..2ce2bdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,33 @@ name: Release +# Releases are triggered on a fixed schedule, not on every merge. +# This separates "code merged to main" from "version published" — merging +# is still cheap and frequent, releasing is a deliberate, reviewable event. +# +# Triggers: +# - schedule: nightly at 04:00 UTC. semantic-release inspects all commits +# since the previous tag and publishes if there's at least one +# releasable commit (feat, fix, perf). Otherwise the run skips silently. +# - workflow_dispatch: manual trigger from the Actions tab when a +# release should happen ahead of schedule. +# +# When a scheduled run skips publishing: +# semantic-release evaluates the ENTIRE history since the last tag, not +# just commits made since the previous run. A scheduled release only +# skips if NO releasable commits (feat/fix/perf) exist between the last +# tag and HEAD. Adding a chore/docs/etc. commit on top of an already- +# present feat does NOT suppress the release — the existing feat will +# still trigger it. on: - push: - branches: [main] + schedule: + - cron: '0 4 * * *' # daily at 04:00 UTC workflow_dispatch: + inputs: + dry_run: + description: "Dry run — analyse commits but don't publish" + required: false + default: false + type: boolean permissions: contents: write # create tags + GitHub Releases @@ -20,6 +44,9 @@ jobs: release: name: semantic-release runs-on: ubuntu-24.04 + # workflow_dispatch can be triggered against any ref via the Actions UI. + # Releases must only ever come from main — refuse to run on anything else. + if: github.ref == 'refs/heads/main' outputs: new-release-published: ${{ steps.semantic.outputs.new_release_published }} new-release-version: ${{ steps.semantic.outputs.new_release_version }} @@ -38,6 +65,7 @@ jobs: id: semantic uses: cycjimmy/semantic-release-action@v6 with: + dry_run: ${{ inputs.dry_run || false }} extra_plugins: | @semantic-release/changelog@6 @semantic-release/git@10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3ad00..1c9ff7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,15 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/) and uses [Conventional Commits](https://www.conventionalcommits.org/) via [semantic-release](https://github.com/semantic-release/semantic-release). -## 1.0.0 (2026-05-10) +## Pre-1.0 development -* ci: make Python lint/test job branch-tolerant for dependabot PRs (#30) ([6ee8e16](https://github.com/strausmann/label-printer-hub/commit/6ee8e16)), closes [#30](https://github.com/strausmann/label-printer-hub/issues/30) [2-#9](https://github.com/2-/issues/9) [#2](https://github.com/strausmann/label-printer-hub/issues/2) [#3](https://github.com/strausmann/label-printer-hub/issues/3) [#4](https://github.com/strausmann/label-printer-hub/issues/4) [#6](https://github.com/strausmann/label-printer-hub/issues/6) [#7](https://github.com/strausmann/label-printer-hub/issues/7) [#8](https://github.com/strausmann/label-printer-hub/issues/8) [#9](https://github.com/strausmann/label-printer-hub/issues/9) -* ci(deps): bump the actions-all group across 1 directory with 14 updates (#9) ([a88a027](https://github.com/strausmann/label-printer-hub/commit/a88a027)), closes [#9](https://github.com/strausmann/label-printer-hub/issues/9) -* chore: initial repository skeleton ([0319e7e](https://github.com/strausmann/label-printer-hub/commit/0319e7e)) -* chore(deps-dev): bump @commitlint/cli from 19.8.1 to 21.0.0 (#3) ([2642b26](https://github.com/strausmann/label-printer-hub/commit/2642b26)), closes [#3](https://github.com/strausmann/label-printer-hub/issues/3) -* chore(deps-dev): bump @commitlint/config-conventional (#4) ([44be5f9](https://github.com/strausmann/label-printer-hub/commit/44be5f9)), closes [#4](https://github.com/strausmann/label-printer-hub/issues/4) -* chore(deps-dev): bump @semantic-release/exec from 6.0.3 to 7.1.0 (#2) ([a8097b0](https://github.com/strausmann/label-printer-hub/commit/a8097b0)), closes [#2](https://github.com/strausmann/label-printer-hub/issues/2) -* chore(deps-dev): bump @semantic-release/github from 11.0.6 to 12.0.8 (#8) ([098e766](https://github.com/strausmann/label-printer-hub/commit/098e766)), closes [#8](https://github.com/strausmann/label-printer-hub/issues/8) -* chore(deps-dev): bump conventional-changelog-conventionalcommits (#7) ([9f5370f](https://github.com/strausmann/label-printer-hub/commit/9f5370f)), closes [#7](https://github.com/strausmann/label-printer-hub/issues/7) -* chore(deps-dev): bump semantic-release from 24.2.9 to 25.0.3 (#6) ([306010f](https://github.com/strausmann/label-printer-hub/commit/306010f)), closes [#6](https://github.com/strausmann/label-printer-hub/issues/6) -* feat(status): add Brother 32-byte status block parser (#29) ([ced0ff8](https://github.com/strausmann/label-printer-hub/commit/ced0ff8)), closes [#29](https://github.com/strausmann/label-printer-hub/issues/29) [#11](https://github.com/strausmann/label-printer-hub/issues/11) [#19](https://github.com/strausmann/label-printer-hub/issues/19) [#29](https://github.com/strausmann/label-printer-hub/issues/29) -* docs: refactor — ADRs in docs/decisions/, policies in docs/policies/, slim CLAUDE.md ([51c2cf1](https://github.com/strausmann/label-printer-hub/commit/51c2cf1)), closes [#1](https://github.com/strausmann/label-printer-hub/issues/1) -* docs(decisions): ADR 0006 — PT vs QL ESC i S behaviour from Phase-0 hardware test ([0d12c63](https://github.com/strausmann/label-printer-hub/commit/0d12c63)), closes [#12](https://github.com/strausmann/label-printer-hub/issues/12) [#11](https://github.com/strausmann/label-printer-hub/issues/11) -* docs(decisions): ADR 0012 — layout management; clarify integration push/pull capabilities ([effcbdf](https://github.com/strausmann/label-printer-hub/commit/effcbdf)), closes [#17](https://github.com/strausmann/label-printer-hub/issues/17) [#19](https://github.com/strausmann/label-printer-hub/issues/19) -* docs(decisions): ADR 0013 + cart UI spec + AI-review workflow ([c72dc85](https://github.com/strausmann/label-printer-hub/commit/c72dc85)), closes [#26](https://github.com/strausmann/label-printer-hub/issues/26) [#27](https://github.com/strausmann/label-printer-hub/issues/27) [#28](https://github.com/strausmann/label-printer-hub/issues/28) -* docs(docker): document image tag scheme (latest, 1.0.0, 1.0, 1) ([1f72396](https://github.com/strausmann/label-printer-hub/commit/1f72396)) -* docs(examples): sample compose files for standalone/Traefik/Pangolin/Caddy ([a2f6f3d](https://github.com/strausmann/label-printer-hub/commit/a2f6f3d)) +The project is in active **pre-1.0 development**. The public API (REST endpoints, plugin protocol, configuration shape) may change between minor versions until `1.0.0` ships. Releases on the `0.x.y` track are explicitly *not* covered by SemVer's stable-API contract. + +When `1.0.0` ships, that release will mark the first stable, supported version. The expected breaking-change cadence will follow normal SemVer rules from there on. + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e048e6b..cc6330b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,10 +182,18 @@ The CI workflow runs the same. PRs that fail any check won't be reviewed until g ## Releases -Releases are **fully automated** via semantic-release on push to `main`: -- Conventional commit messages → version bump + changelog → tag → GitHub Release → Docker images on GHCR + Docker Hub. +Releases are **scheduled, not on every merge.** Merging to `main` does NOT publish a release — it only bundles changes for the next scheduled release window. -Maintainers do not manually tag releases. +| Trigger | When | +|---|---| +| `cron: '0 4 * * *'` | Nightly at 04:00 UTC. semantic-release analyses commits since the last tag and publishes if there's anything releasable, otherwise skips silently | +| `workflow_dispatch` | Manual button in the Actions tab. Optional `dry-run` input lets a maintainer preview what would be released without publishing | + +Behind the scenes, semantic-release reads the Conventional Commit messages on `main` since the previous tag, decides the next semver version, generates the `CHANGELOG.md` entry, creates the Git tag and GitHub Release, and triggers the Docker image push to GHCR and Docker Hub. + +**A scheduled run only skips publishing when there are no releasable commits since the last tag.** semantic-release evaluates the entire history since the previous release, not just "today's" commits. If a `feat`, `fix`, or `perf` commit is already on `main` from an earlier merge, the next scheduled run **will** publish — adding a `chore` or `docs` commit on top doesn't suppress that. To genuinely skip a release window, the only options are: rely on there being no releasable commits, or postpone merges that would bump the version. + +Maintainers do not manually tag releases. If a hotfix needs to ship before the next scheduled window, use the **Run workflow** button on the Release workflow in the Actions tab. The dry-run input lets a maintainer preview which version would be published before committing. ## Trademarks diff --git a/docs/decisions/0008-conventional-commits-and-semantic-release.md b/docs/decisions/0008-conventional-commits-and-semantic-release.md index b83807f..f1b1687 100644 --- a/docs/decisions/0008-conventional-commits-and-semantic-release.md +++ b/docs/decisions/0008-conventional-commits-and-semantic-release.md @@ -34,15 +34,21 @@ We adopt **[Conventional Commits 1.0.0](https://www.conventionalcommits.org/)** Allowed scopes: `printer-models`, `queue`, `status`, `api`, `ui`, `webhook`, `docker`, `ci`, `examples`, `docs`, `integration`, `pwa`, `security`, `release`. Validated by `commitlint` on PR titles. -**Release trigger:** push to `main`. Maintainers do not manually tag releases. +**Release trigger:** **scheduled (nightly 04:00 UTC) + manual via `workflow_dispatch`**. Merges to `main` do NOT publish releases — they only accumulate changes for the next release window. This separation means merging stays cheap and frequent, while releasing remains a deliberate event the maintainer can review or postpone. + +Maintainers do not manually tag releases. To trigger an unscheduled release (e.g. for a security hotfix), use the **Run workflow** button on the Release workflow. **Release pipeline:** -1. Push to `main` -2. `release.yml` workflow runs `semantic-release` -3. semantic-release inspects commits since last tag → decides version bump -4. Updates `CHANGELOG.md`, creates Git tag, creates GitHub Release +1. Schedule fires at 04:00 UTC (or maintainer hits "Run workflow") +2. `release.yml` workflow runs `semantic-release` against `main` HEAD +3. semantic-release inspects commits since last tag → decides version bump (or skips if no releasable commits exist) +4. If a release is due: updates `CHANGELOG.md`, creates Git tag, creates GitHub Release 5. Release-published event triggers `docker-publish.yml` → builds and pushes both backend and frontend images per ADR 0007 +**When a scheduled run skips publishing:** semantic-release evaluates *all* commits between the last tag and HEAD, not just commits made since the previous run. A scheduled release only skips when there are no `feat`, `fix`, or `perf` commits in that range. If a previous merge already added a `feat` commit, the next scheduled run will publish — adding `chore` or `docs` commits afterwards does not suppress the release. To intentionally postpone a release window, the only mechanism is to defer merges that would bump the version. + +**Branch guard:** the release job runs only when `github.ref == 'refs/heads/main'`. `workflow_dispatch` can be triggered against any branch from the Actions UI; the guard ensures a release can never be published from a non-main ref. + ## Options considered ### Option A — Conventional Commits + semantic-release (chosen)