diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..af2274fd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + release: + name: Semantic Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Run semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release diff --git a/.releaserc.json b/.releaserc.json index 4aa52407..1fd7ffac 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,27 +1,34 @@ { - "branches": ["main"], - "repositoryUrl": "https://github.com/steamicc/micropython-steami-lib.git", - "debug": false, - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - [ - "@semantic-release/changelog", - { - "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file. See\n[Conventional Commits](https://conventionalcommits.org) for commit guidelines." - } - ], - [ - "@semantic-release/github", - { - "assets": "pack/*.tgz" - } - ], - [ - "@semantic-release/git", - { - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - } - ] - ] -} \ No newline at end of file + "branches": ["main"], + "repositoryUrl": "https://github.com/steamicc/micropython-steami-lib.git", + "tagFormat": "v${version}", + "plugins": [ + ["@semantic-release/commit-analyzer", { + "releaseRules": [ + {"type": "feat", "release": "minor"}, + {"type": "fix", "release": "patch"}, + {"type": "perf", "release": "patch"}, + {"type": "refactor", "release": "patch"}, + {"type": "tooling", "release": "patch"}, + {"type": "docs", "release": false}, + {"type": "style", "release": false}, + {"type": "test", "release": false}, + {"type": "ci", "release": false}, + {"type": "build", "release": false}, + {"type": "chore", "release": false} + ] + }], + "@semantic-release/release-notes-generator", + ["@semantic-release/changelog", { + "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines." + }], + ["@semantic-release/exec", { + "prepareCmd": "sed -i 's/^version = \".*\"/version = \"${nextRelease.version}\"/' pyproject.toml" + }], + ["@semantic-release/git", { + "assets": ["CHANGELOG.md", "pyproject.toml"], + "message": "chore(release): v${nextRelease.version}. [skip ci]\n\n${nextRelease.notes}" + }], + "@semantic-release/github" + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9220485a..53d74824 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,6 +103,25 @@ All pull requests must pass these checks: | Mock tests | `tests.yml` | Runs mock driver tests | | Example validation | `tests.yml` | Validates example files syntax and imports | +## Releasing + +Releases are handled automatically by [semantic-release](https://semantic-release.gitbook.io/) when commits are pushed to `main`. The version is determined from commit messages: + +- `fix:` → patch bump (v1.0.0 → v1.0.1) +- `feat:` → minor bump (v1.0.0 → v1.1.0) +- `BREAKING CHANGE:` in commit body → major bump (v1.0.0 → v2.0.0) +- `docs:`, `style:`, `test:`, `ci:`, `chore:` → no release + +semantic-release automatically updates `pyproject.toml`, generates `CHANGELOG.md`, creates a git tag, and publishes a GitHub release. + +To force a specific version manually (override): + +```bash +make bump # patch: v1.0.0 → v1.0.1 +make bump PART=minor # minor: v1.0.1 → v1.1.0 +make bump PART=major # major: v1.1.0 → v2.0.0 +``` + ## Notes * Keep implementations simple and readable diff --git a/Makefile b/Makefile index 824279b6..9f06d607 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,48 @@ repl: ## Open MicroPython REPL on the board mount: ## Mount lib/ on the board for live testing mpremote connect $(PORT) mount lib/ +# --- Release --- + +PART ?= patch + +.PHONY: bump +bump: ## Create a version tag (PART=patch|minor|major, default: patch) + @echo "Note: releases are normally handled by semantic-release in CI." + @echo "Use 'make bump' only to force a specific version.\n" + @if [ "$$(git symbolic-ref --short HEAD)" != "main" ]; then \ + echo "Error: bump must be run on the main branch."; exit 1; \ + fi + @if [ -n "$$(git status --porcelain)" ]; then \ + echo "Error: working tree is not clean. Commit or stash changes first."; exit 1; \ + fi + @set -e; \ + LAST=$$(git tag --sort=-v:refname | head -1); \ + if [ -z "$$LAST" ]; then \ + NEXT="v1.0.0"; \ + else \ + if ! echo "$$LAST" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$$'; then \ + echo "Error: latest tag '$$LAST' is not in supported format v..."; exit 1; \ + fi; \ + MAJOR=$$(echo "$$LAST" | sed 's/^v//' | cut -d. -f1); \ + MINOR=$$(echo "$$LAST" | sed 's/^v//' | cut -d. -f2); \ + PATCH=$$(echo "$$LAST" | sed 's/^v//' | cut -d. -f3); \ + case "$(PART)" in \ + major) MAJOR=$$((MAJOR + 1)); MINOR=0; PATCH=0 ;; \ + minor) MINOR=$$((MINOR + 1)); PATCH=0 ;; \ + patch) PATCH=$$((PATCH + 1)) ;; \ + *) echo "Error: PART must be patch, minor or major."; exit 1 ;; \ + esac; \ + NEXT="v$$MAJOR.$$MINOR.$$PATCH"; \ + fi; \ + echo "$$LAST → $$NEXT"; \ + VERSION=$${NEXT#v}; \ + python3 -c "import re, pathlib; p=pathlib.Path('pyproject.toml'); p.write_text(re.sub(r'^version = \".*\"', 'version = \"$$VERSION\"', p.read_text(), count=1, flags=re.MULTILINE))"; \ + git add pyproject.toml; \ + git commit -m "chore: Bump version to $$NEXT."; \ + git tag -a "$$NEXT" -m "Release $$NEXT"; \ + git push origin main "$$NEXT"; \ + echo "Tag $$NEXT pushed to origin." + # --- Utilities --- .PHONY: clean diff --git a/package-lock.json b/package-lock.json index 5cc56e05..9a748406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "@commitlint/cz-commitlint": "^20.5.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.0", "commitizen": "^4.2.4", + "conventional-changelog-conventionalcommits": "^9.3.0", "cross-env": "^7.0.3", "git-precommit-checks": "^3.1.0", "husky": "^9.1.7", @@ -21,6 +23,9 @@ "prettier": "^3.3.3", "semantic-release": "^24.1.2", "validate-branch-name": "^1.3.1" + }, + "engines": { + "node": ">=20.17" } }, "node_modules/@babel/code-frame": { @@ -675,6 +680,191 @@ "node": ">=14.17" } }, + "node_modules/@semantic-release/exec": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz", + "integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "execa": "^9.0.0", + "lodash-es": "^4.17.21", + "parse-json": "^8.0.0" + }, + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=24.1.0" + } + }, + "node_modules/@semantic-release/exec/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/exec/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/exec/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@semantic-release/exec/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@semantic-release/exec/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@semantic-release/git": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", diff --git a/package.json b/package.json index 57884940..bf7b5f89 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "@commitlint/cz-commitlint": "^20.5.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.0", "commitizen": "^4.2.4", + "conventional-changelog-conventionalcommits": "^9.3.0", "cross-env": "^7.0.3", "git-precommit-checks": "^3.1.0", "husky": "^9.1.7",