From b30c77a6c7516a42d99755355feac8eed20ae61e Mon Sep 17 00:00:00 2001 From: k0te1ch Date: Fri, 5 Jun 2026 02:28:30 +0300 Subject: [PATCH] ci: migrate release automation to release-please --- .github/workflows/release-please.yml | 21 ++++++++ .github/workflows/release.yml | 52 ------------------- .release-please-manifest.json | 3 ++ README.md | 42 +++++++--------- cliff.toml | 68 ------------------------- docs/WORKFLOW.md | 75 +++++++++++++++------------- pyproject.toml | 7 +-- release-please-config.json | 13 +++++ 8 files changed, 99 insertions(+), 182 deletions(-) create mode 100644 .github/workflows/release-please.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json delete mode 100644 cliff.toml create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..02f9e21 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +name: release-please + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + # GITHUB_TOKEN не триггерит другие workflow при создании тега. + # Если нужно, чтобы тег запускал publish-пайплайн — замени на PAT/App-токен. + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 9b50324..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Generate release notes (current tag only) - id: notes - uses: orhun/git-cliff-action@v4 - with: - config: cliff.toml - args: --latest --strip header - env: - OUTPUT: RELEASE_NOTES.md - - - name: Regenerate full CHANGELOG.md - uses: orhun/git-cliff-action@v4 - with: - config: cliff.toml - args: --output CHANGELOG.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - body_path: RELEASE_NOTES.md - draft: false - prerelease: ${{ contains(github.ref_name, '-') }} - - - name: Commit updated CHANGELOG.md back to main - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - if ! git diff --quiet -- CHANGELOG.md; then - git checkout main - git pull --ff-only origin main - git add CHANGELOG.md - git commit -m "docs(changelog): update for ${GITHUB_REF_NAME}" - git push origin main - fi diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..2be9c43 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.2.0" +} diff --git a/README.md b/README.md index 84f1de1..9f09af1 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ A ready-to-use Python project template with: - `pytest` testing support - `poetry` dependency management - `ruff` static analysis -- `commitizen` — Conventional Commits validation (pre-commit) and version bump -- `git-cliff` — automated `CHANGELOG.md` + GitHub Release notes +- `commitizen` — Conventional Commits validation (pre-commit) +- `release-please` — automated version bump, `CHANGELOG.md`, tags and GitHub Releases via CI ## Quick start @@ -54,27 +54,20 @@ Commit messages must follow [Conventional Commits](https://www.conventionalcommi (`feat:`, `fix:`, `docs:`, `chore:`, `feat(scope)!: …` for breaking, etc.). The commitizen `commit-msg` hook rejects non-conforming messages. -To cut a release: +Releases are automated by [release-please](https://github.com/googleapis/release-please) +in CI — you never bump versions, write `CHANGELOG.md`, or create tags by hand: -```bash -poetry run cz bump # bumps version in pyproject.toml, commits, creates tag -git push --follow-tags # pushing the tag triggers .github/workflows/release.yml -``` +1. Land well-formed Conventional Commits on `main` (the commit types decide the bump: + `fix:` → patch, `feat:` → minor, `!` / `BREAKING CHANGE` → major). +2. `release-please` opens and maintains a **release PR** with the version bump in + `pyproject.toml` + the changelog entry. +3. Merge that PR — release-please creates the `vX.Y.Z` tag and the GitHub Release. -The release workflow generates Release Notes for the new tag with `git-cliff --latest`, -publishes a GitHub Release, regenerates the full `CHANGELOG.md`, and commits it back to `main` — -so you do not need `git-cliff` installed locally. +The current released version is seeded in `.release-please-manifest.json`. -Before the first release, set `owner`/`repo` in `cliff.toml` so PR and author links resolve. - -To preview the changelog locally (optional), install `git-cliff` (`scoop install git-cliff` -on Windows, `cargo install git-cliff`, or download from -[git-cliff releases](https://github.com/orhun/git-cliff/releases)) and run: - -```bash -git-cliff --unreleased # preview pending changes -git-cliff -o CHANGELOG.md # rewrite the full changelog -``` +> Enable **Settings → Actions → General → Workflow permissions → "Read and write +> permissions"** and **"Allow GitHub Actions to create and approve pull requests"**, +> otherwise the action cannot open the release PR. ## Logging @@ -86,9 +79,10 @@ Application logs are written to `logs/app.log` and the `logs/` directory is igno - `app/config.py` — configuration model - `app/logger.py` — logger setup - `tests/` — test suite -- `.github/workflows/python-app.yml` — GitHub Actions CI -- `.github/workflows/release.yml` — Release workflow (triggered by `v*` tags) +- `.github/workflows/python-app.yml` — GitHub Actions CI (lint + tests on PRs) +- `.github/workflows/release-please.yml` — release automation (runs on push to `main`) +- `release-please-config.json` — release-please configuration (release-type, changelog) +- `.release-please-manifest.json` — current released version, owned by release-please - `.pre-commit-config.yaml` — pre-commit configuration -- `cliff.toml` — git-cliff (changelog generator) configuration -- `CHANGELOG.md` — auto-generated changelog +- `CHANGELOG.md` — auto-generated changelog (owned by release-please) - `.env.example` — example environment settings diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index 998c383..0000000 --- a/cliff.toml +++ /dev/null @@ -1,68 +0,0 @@ -# git-cliff configuration -# Docs: https://git-cliff.org/docs/configuration - -[changelog] -header = """ -# Changelog - -All notable changes to this project will be documented in this file. -The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -""" -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [Unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ - {{ commit.message | upper_first }}\ - {% if commit.remote.username %} by @{{ commit.remote.username }}{% endif %}\ - {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{% endif %} - {% endfor %} -{% endfor %}\n -""" -footer = """ - -""" -trim = true - -[git] -conventional_commits = true -filter_unconventional = true -split_commits = false -protect_breaking_commits = true -filter_commits = false -tag_pattern = "v[0-9].*" -topo_order = false -sort_commits = "oldest" - -commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix", group = "Bug Fixes" }, - { message = "^perf", group = "Performance" }, - { message = "^refactor", group = "Refactor" }, - { message = "^docs", group = "Documentation" }, - { message = "^test", group = "Tests" }, - { message = "^build", group = "Build System" }, - { message = "^ci", group = "CI" }, - { message = "^style", group = "Styling" }, - { message = "^chore\\(release\\):", skip = true }, - { message = "^chore|^bump", group = "Miscellaneous" }, - { body = ".*security", group = "Security" }, - { message = "^revert", group = "Revert" }, -] - -commit_preprocessors = [ - # Auto-link issue references like `#123` to GitHub - # { pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = "([#${2}](https://github.com///issues/${2}))" }, -] - -[remote.github] -# Fill these in (or set GITHUB_REPO env var in CI) for PR/author links -owner = "" -repo = "" diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 3471127..a9a2d58 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -23,10 +23,10 @@ flowchart TD F --> G[Pull Request на GitHub] G --> H[CI: python-app.yml
pre-commit + pytest] H -- зелёный + approve --> I[Squash/merge в main] - I --> J[poetry run cz bump
+ git push --follow-tags] - J --> K[Tag v* запушен] - K --> L[CI: release.yml
git-cliff] - L --> M[GitHub Release
+ CHANGELOG.md commit в main] + I --> J[CI: release-please.yml] + J --> K[release-please открывает/
обновляет release PR
с bump + CHANGELOG] + K --> L[Merge release PR] + L --> M[Tag vX.Y.Z
+ GitHub Release] classDef bad fill:#fee,stroke:#c33; class X bad; @@ -82,16 +82,16 @@ feat(api): switch to httpx BREAKING CHANGE: requests dependency removed; clients must use httpx. ``` -`cz bump` смотрит на эти типы и решает, как менять версию: +`release-please` смотрит на эти типы и решает, как менять версию: - `BREAKING CHANGE` → **major** (1.2.3 → 2.0.0) - `feat:` → **minor** (1.2.3 → 1.3.0) - `fix:` / `perf:` → **patch** (1.2.3 → 1.2.4) -- остальное → версия не меняется (нужно `--increment` руками) +- остальное → версия не меняется -> Пока в `pyproject.toml` стоит `major_version_zero = true` (версия `0.x.y`), -> breaking change поднимет minor, а не major — это нормально для проекта, -> ещё не выпустившего `1.0.0`. +> Пока в проекте версия `0.x.y`, опция `bump-minor-pre-major` в +> `release-please-config.json` держит breaking change на уровне minor, а не +> major — это нормально для проекта, ещё не выпустившего `1.0.0`. --- @@ -151,7 +151,7 @@ gitGraph commit id: "feat(cache): in-memory" checkout main merge feature/cache tag: "PR #15" - commit id: "bump: 0.1.0 -> 0.2.0" tag: "v0.2.0" + commit id: "chore: release 0.2.0" tag: "v0.2.0" ``` - Одна долгоживущая ветка: `main`. @@ -311,34 +311,39 @@ flowchart LR --- -## 7. Релиз — что происходит после `cz bump` +## 7. Релиз — что делает release-please + +Никаких ручных `cz bump` и тегов. Версия, `CHANGELOG.md`, тег и GitHub Release — +всё автоматизировано через [release-please](https://github.com/googleapis/release-please). +Твоя задача — просто мержить корректные Conventional Commits в `main`. ```mermaid sequenceDiagram autonumber participant Dev as Разработчик - participant Local as Local repo - participant GH as GitHub - participant CI as release.yml + participant GH as GitHub (main) + participant CI as release-please.yml + participant PR as Release PR participant Rel as GitHub Releases - Dev->>Local: poetry run cz bump - Local->>Local: читает коммиты с прошлого тега - Local->>Local: определяет bump: patch/minor/major - Local->>Local: правит pyproject.toml
создаёт коммит "bump: ..."
создаёт тег vX.Y.Z - Dev->>GH: git push --follow-tags - GH->>CI: триггер по тегу v* - CI->>CI: checkout с fetch-depth: 0 - CI->>CI: git-cliff --latest
→ RELEASE_NOTES.md - CI->>CI: git-cliff --output CHANGELOG.md
(полный) - CI->>Rel: создание GitHub Release
с RELEASE_NOTES.md - CI->>GH: commit обновлённого
CHANGELOG.md в main + Dev->>GH: merge feature-ветки в main + GH->>CI: триггер по push в main + CI->>CI: читает Conventional Commits
с прошлого релиза + CI->>CI: определяет bump: patch/minor/major + CI->>PR: открывает/обновляет release PR
(bump pyproject.toml + CHANGELOG.md) + Note over Dev,PR: release PR копится, пока
не решишь выпустить релиз + Dev->>PR: merge release PR + PR->>Rel: создание тега vX.Y.Z
+ GitHub Release ``` Что увидит пользователь: -- На вкладке **Releases** — новый релиз с группированным списком изменений и ссылками на PR/авторов. -- В корне репо — обновлённый [CHANGELOG.md](../CHANGELOG.md) с полной историей. -- Тег `vX.Y.Z` в `git tag`. +- Пока есть невыпущенные изменения — открытый **release PR** с предпросмотром bump и changelog. +- После merge release PR: тег `vX.Y.Z`, GitHub Release и обновлённый [CHANGELOG.md](../CHANGELOG.md). +- Текущая выпущенная версия хранится в [.release-please-manifest.json](../.release-please-manifest.json). + +> Для работы action включи в **Settings → Actions → General → Workflow +> permissions**: «Read and write permissions» и «Allow GitHub Actions to create +> and approve pull requests». --- @@ -367,9 +372,8 @@ gitGraph git switch -c hotfix/1.2.1 v1.2.0 # ... фикс ... git commit -m "fix(api): null pointer in /users" -poetry run cz bump --increment PATCH -git push --follow-tags origin hotfix/1.2.1 -# дальше PR hotfix/1.2.1 -> main +git push origin hotfix/1.2.1 +# дальше PR hotfix/1.2.1 -> main; release-please сам поднимет patch и выпустит релиз ``` Это редкий сценарий, и если он у тебя случается часто — пора смотреть в @@ -382,10 +386,11 @@ git push --follow-tags origin hotfix/1.2.1 | Файл | Что делает | |-------------------------------------------|------------------------------------------------------------------| | [.pre-commit-config.yaml](../.pre-commit-config.yaml) | Запускает ruff и commitizen на каждом `git commit` | -| [pyproject.toml](../pyproject.toml) (`[tool.commitizen]`) | Конфиг bump и формата тегов | -| [cliff.toml](../cliff.toml) | Шаблон changelog и правила группировки коммитов | +| [pyproject.toml](../pyproject.toml) (`[tool.commitizen]`) | Валидация Conventional Commits (commit-msg hook) | +| [release-please-config.json](../release-please-config.json) | Конфиг release-please: release-type, changelog, правила bump | +| [.release-please-manifest.json](../.release-please-manifest.json) | Текущая выпущенная версия (ведёт release-please) | | [.github/workflows/python-app.yml](../.github/workflows/python-app.yml) | CI на каждый PR: lint + tests | -| [.github/workflows/release.yml](../.github/workflows/release.yml) | CI на тег `v*`: changelog + GitHub Release | +| [.github/workflows/release-please.yml](../.github/workflows/release-please.yml) | На push в `main`: release PR → тег + GitHub Release | | [CHANGELOG.md](../CHANGELOG.md) | Авто-сгенерированная история. Руками **не редактировать**. | --- @@ -395,5 +400,5 @@ git push --follow-tags origin hotfix/1.2.1 1. Commit-сообщения пишем по Conventional Commits — без этого коммит не пройдёт. 2. Каждая задача — отдельная короткая ветка `feature/...`, через PR в `main`. 3. На `main` всегда зелёный CI, всегда deployable. -4. Релиз: `poetry run cz bump && git push --follow-tags` — всё остальное делает CI. +4. Релиз: ничего не бампим руками — release-please копит release PR; мерж PR = новый тег и Release. 5. CHANGELOG.md и Release Notes никто руками не пишет — они генерируются из истории коммитов. diff --git a/pyproject.toml b/pyproject.toml index 947b344..421adbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,13 @@ exclude = ["logs", "build", "dist", ".venv"] [tool.pytest.ini_options] addopts = "--cov=app --cov=main --cov-report=term-missing" +# commitizen is kept only for Conventional Commits validation via the +# commit-msg pre-commit hook. Versioning, tagging and CHANGELOG.md are owned by +# release-please in CI (.github/workflows/release-please.yml) — do not run +# `cz bump` locally, it would conflict with the release PR. [tool.commitizen] name = "cz_conventional_commits" version_provider = "poetry" tag_format = "v$version" major_version_zero = true -# Changelog is generated by git-cliff in CI (.github/workflows/release.yml) -# on tag push — keep this disabled to avoid double-writes. update_changelog_on_bump = false -bump_message = "bump: version $current_version -> $new_version" diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..ad4119c --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "python", + "changelog-path": "CHANGELOG.md", + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "draft": false, + "prerelease": false + } + } +}