feat(release): global create_release.py with deterministic OS marking #66

Merged
slaid098 merged 7 commits from feat/release/global-create-release into main 2026-08-12 19:14:06 +03:00
Owner

Что сделано

  • .opencode/scripts/release_notes.py — self-contained экстрактор секции CHANGELOG (stdlib only: re, pathlib, sys). Функция extract_changelog_section(path, tag) с 3 fallback'ами: секция не найдена → весь CHANGELOG без шапки + warning; CHANGELOG отсутствует → ""; пустая секция (header без bullets) → "". Regex покрывает - и — (em-dash). Срезает шапку # Changelog/boilerplate и self-referential links [v0.1.0]: https://....
  • .opencode/scripts/create_release.py — глобальная утилита релиза (stdlib only: contextlib, json, mimetypes, os, re, subprocess, sys, tomllib, urllib). Читает name из pyproject.toml (через tomllib), парсит RUNNER_OS/RUNNER_ARCH → {os}-{arch} lowercase, формирует asset filename {name}-{os}-{arch}.zip, формирует body с секцией «Системные требования» из RELEASE_PLATFORM, идемпотентно (delete existing release для тега → create new), загружает asset через multipart/form-data. Fallback'ы: pyproject.toml без name → error; RUNNER_OS не задан → error; CHANGELOG.md отсутствует → body ""; upload fail → лог без падения (релиз без ассета лучше нет релиза).
  • skills/release/SKILL.md — раздел «Платформа в релизе (детерминированный стандарт)»: источник правды (pyproject name + runner env + RELEASE_PLATFORM), формат asset filename, формат body, правила поднятия min ОС (обновить RELEASE_PLATFORM + запись в CHANGELOG ### Изменено), multi-platform (отдельный job на ОС), anti-patterns (**Платформы:** заголовок, Python в требованиях, платформа в CHANGELOG). Шаг 7 переписан на create_release.py через curl-download из opencode-config (оба файла: release_notes.py + create_release.py).
  • pyproject.toml — S110 добавлен в per-file-ignores для .opencode/scripts/* (try-except-pass для best-effort detail extraction в CI-скрипте).
  • Удалено устаревшее упоминание platform arg из SKILL.md (после issue #63).

Почему

Раньше create_release.py дублировался в каждом репо (voice_assistant, video_uniq) — риск рассинхрона, нет единого стандарта. Платформа в release notes не маркировалась детерминированно: asset filename voice-assistant.zip без ОС, в body нет секции «Системные требования». Глобальная утилита в opencode-config (одна версия, curl-download в каждом репо) + детерминированный стандарт маркировки (name из pyproject + ОС из runner + платформа из release.yml env var) решают обе проблемы. Источник правды — 3 независимых источника, не память агента (агент не может забыть или ошибиться).

Watch out

  • CI coverage красный (pre-existing): --cov-fail-under=80 по src/+.opencode/scripts/, текущий coverage 75.64% на main (без моих файлов) → 75.76% с моими. Порог недостижим для текущего набора тестов — CI был красным ДО этого PR. Мои файлы (0% coverage, ~280 строк) дают Δ +0.12%, не блокирующий регресс. Спека issue #65 явно говорит «release_notes.py тесты — опционально». Нужен отдельный issue на понижение порога или тесты.
  • create_release.py импортирует release_notes.py через sys.path.insert(0, dirname(__file__)) — оба файла должны лежать рядом. В CI пример в SKILL.md скачивает оба файла в scripts/.
  • GITHUB_TOKEN (не FORGEJO_TOKEN) — Forgejo/act runner инжектит токен под этим именем для совместимости с GitHub Actions. GITHUB_REPOSITORY (owner/repo) auto-set; при локальном запуске деривится из git remote origin.url.
  • S310 (URL open audit) — false positive для CI-скрипта, подавлен через # noqa: S310 на 4 вызовах urllib (как в существующем pipeline-status.py).
  • pyproject.toml per-file-ignores для .opencode/scripts/* расширен S110 (try-except-pass для best-effort detail extraction) — не затрагивает S310/PLR0911 (оставлены как noqa в коде, чтобы не ломать существующие noqa в pipeline-status.py).

Pending

  • Миграция voice_assistant на глобальный create_release.py — отдельный issue в voice_assistant.
  • Миграция video_uniq — отдельный issue в video_uniq (будущее).
  • PATCH существующего релиза v0.1.0 в voice_assistant — после merge миграции.
  • Multi-platform support (Linux/macOS jobs) — стандарт описан в SKILL.md, реализация в каждом репо отдельно.
  • Тесты для release_notes.py — опционально (см. Watch out про coverage).
  • Понижение --cov-fail-under или тесты для скриптов — отдельный issue (pre-existing red CI).

Closes #65

Closes #65

## Что сделано - `.opencode/scripts/release_notes.py` — self-contained экстрактор секции CHANGELOG (stdlib only: re, pathlib, sys). Функция `extract_changelog_section(path, tag)` с 3 fallback'ами: секция не найдена → весь CHANGELOG без шапки + warning; CHANGELOG отсутствует → `""`; пустая секция (header без bullets) → `""`. Regex покрывает `-` и `—` (em-dash). Срезает шапку `# Changelog`/boilerplate и self-referential links `[v0.1.0]: https://...`. - `.opencode/scripts/create_release.py` — глобальная утилита релиза (stdlib only: contextlib, json, mimetypes, os, re, subprocess, sys, tomllib, urllib). Читает `name` из `pyproject.toml` (через `tomllib`), парсит `RUNNER_OS`/`RUNNER_ARCH` → `{os}-{arch}` lowercase, формирует asset filename `{name}-{os}-{arch}.zip`, формирует body с секцией «Системные требования» из `RELEASE_PLATFORM`, идемпотентно (delete existing release для тега → create new), загружает asset через multipart/form-data. Fallback'ы: `pyproject.toml` без `name` → error; `RUNNER_OS` не задан → error; `CHANGELOG.md` отсутствует → body `""`; upload fail → лог без падения (релиз без ассета лучше нет релиза). - `skills/release/SKILL.md` — раздел «Платформа в релизе (детерминированный стандарт)»: источник правды (pyproject name + runner env + RELEASE_PLATFORM), формат asset filename, формат body, правила поднятия min ОС (обновить `RELEASE_PLATFORM` + запись в CHANGELOG `### Изменено`), multi-platform (отдельный job на ОС), anti-patterns (`**Платформы:**` заголовок, Python в требованиях, платформа в CHANGELOG). Шаг 7 переписан на `create_release.py` через curl-download из opencode-config (оба файла: `release_notes.py` + `create_release.py`). - `pyproject.toml` — `S110` добавлен в `per-file-ignores` для `.opencode/scripts/*` (try-except-pass для best-effort detail extraction в CI-скрипте). - Удалено устаревшее упоминание `platform` arg из SKILL.md (после issue #63). ## Почему Раньше `create_release.py` дублировался в каждом репо (voice_assistant, video_uniq) — риск рассинхрона, нет единого стандарта. Платформа в release notes не маркировалась детерминированно: asset filename `voice-assistant.zip` без ОС, в body нет секции «Системные требования». Глобальная утилита в opencode-config (одна версия, curl-download в каждом репо) + детерминированный стандарт маркировки (name из pyproject + ОС из runner + платформа из release.yml env var) решают обе проблемы. Источник правды — 3 независимых источника, не память агента (агент не может забыть или ошибиться). ## Watch out - **CI coverage красный (pre-existing)**: `--cov-fail-under=80` по `src/`+`.opencode/scripts/`, текущий coverage 75.64% на main (без моих файлов) → 75.76% с моими. Порог недостижим для текущего набора тестов — CI был красным ДО этого PR. Мои файлы (0% coverage, ~280 строк) дают Δ +0.12%, не блокирующий регресс. Спека issue #65 явно говорит «`release_notes.py` тесты — опционально». Нужен отдельный issue на понижение порога или тесты. - `create_release.py` импортирует `release_notes.py` через `sys.path.insert(0, dirname(__file__))` — оба файла должны лежать рядом. В CI пример в SKILL.md скачивает оба файла в `scripts/`. - `GITHUB_TOKEN` (не `FORGEJO_TOKEN`) — Forgejo/act runner инжектит токен под этим именем для совместимости с GitHub Actions. `GITHUB_REPOSITORY` (`owner/repo`) auto-set; при локальном запуске деривится из `git remote origin.url`. - `S310` (URL open audit) — false positive для CI-скрипта, подавлен через `# noqa: S310` на 4 вызовах urllib (как в существующем `pipeline-status.py`). - `pyproject.toml` `per-file-ignores` для `.opencode/scripts/*` расширен `S110` (try-except-pass для best-effort detail extraction) — не затрагивает `S310`/`PLR0911` (оставлены как `noqa` в коде, чтобы не ломать существующие `noqa` в `pipeline-status.py`). ## Pending - Миграция voice_assistant на глобальный `create_release.py` — отдельный issue в voice_assistant. - Миграция video_uniq — отдельный issue в video_uniq (будущее). - PATCH существующего релиза v0.1.0 в voice_assistant — после merge миграции. - Multi-platform support (Linux/macOS jobs) — стандарт описан в SKILL.md, реализация в каждом репо отдельно. - Тесты для `release_notes.py` — опционально (см. Watch out про coverage). - Понижение `--cov-fail-under` или тесты для скриптов — отдельный issue (pre-existing red CI). Closes #65 Closes #65
style(scripts): ruff noqa S310 and reduce returns in release_notes
Some checks failed
CI (always) / bootstrap (pull_request) Successful in 4s
CI / bootstrap (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 26s
CI / complexity (pull_request) Successful in 27s
CI / typecheck (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Failing after 1m40s
bf98d563a6
test(scripts): add tests for release_notes and create_release
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 11s
CI / bootstrap (pull_request) Successful in 14s
CI / lint (pull_request) Successful in 29s
CI / typecheck (pull_request) Successful in 29s
CI / complexity (pull_request) Successful in 30s
CI / test (3.13) (pull_request) Successful in 1m42s
b90a714f13
Author
Owner

Code Review Summary

PR полностью реализует контракт issue #65: глобальная утилита create_release.py + экстрактор release_notes.py (оба self-contained, stdlib only) + обновлённый раздел в skills/release/SKILL.md с детерминированным стандартом маркировки платформы. Качество высокое, 47 тестов покрывают все инварианты и граничные случаи из спеки.

Соответствие контракту issue #65

  • ✅ create_release.py — self-contained (re, pathlib, tomllib, json, os, urllib, sys, mimetypes, contextlib, subprocess), читает pyproject.toml name, парсит RUNNER_OS/RUNNER_ARCH → {os}-{arch} lowercase, формирует {name}-{os}-{arch}.zip, body с «Системные требования» из RELEASE_PLATFORM, idempotent (delete+create), upload через multipart/form-data
  • ✅ release_notes.py — self-contained (re, sys, pathlib), extract_changelog_section(path, tag), 3 fallback'а: секция не найдена → весь CHANGELOG (sans boilerplate) + warning; CHANGELOG отсутствует → ""; пустая секция → "". Regex покрывает - и — (em-dash)
  • ✅ SKILL.md — раздел «Платформа в релизе (детерминированный стандарт)»: 3 источника правды (pyproject name + runner env + RELEASE_PLATFORM), формат asset filename, формат body, правила поднятия min ОС (обновить RELEASE_PLATFORM + CHANGELOG ### Изменено), multi-platform (отдельный job на ОС), anti-patterns (**Платформы:** заголовок, Python в требованиях, платформа в CHANGELOG)
  • ✅ Пример release.yml с curl-download обоих файлов + RELEASE_PLATFORM env var
  • ✅ Удалено устаревшее упоминание platform arg
  • ✅ Все 9 критериев приёмки выполнены

Positives

  • Архитектура: release_notes.py (чистая функция-экстрактор) ↔ create_release.py (оркестратор Forgejo API) — чёткое разделение ответственности. Размещение в .opencode/scripts/ соответствует существующей структуре (pipeline-status.py, project-status.py)
  • Тесты: 34 + 13 = 47 тестов покрывают все инварианты и граничные случаи: em-dash vs hyphen, tag с/без v, multi-version slicing, trailing links, missing CHANGELOG, empty section, missing pyproject.toml, missing RUNNER_OS, default arch, idempotent delete, upload failure non-fatal, no release id, missing tag env. Шаблон importlib.util.spec_from_file_location соответствует test_pipeline_status.py/test_project_status.py
  • Error handling: специффичные exception types (HTTPError, URLError), contextlib.suppress(Exception) для best-effort detail extraction (оправдано для CI-скрипта, отсюда S110 в per-file-ignores), _die() для fatal errors с осмысленными сообщениями, _warn() для non-fatal (upload failure → release без ассета)
  • Security: нет secrets в коде, URLs из env vars, # noqa: S310 для urlopen (operator-configured URL — тот же паттерн что в pipeline-status.py/project-status.py/spec-status.py), subprocess.check_output с list args (без shell=True)
  • Idempotency: delete existing release для тега → create new (Forgejo не авто-clean'ит orphan releases при пересоздании тега → HTTP 409 без этого)
  • Backward compat: RELEASE_PLATFORM не задан → body без секции (старый формат)
  • Cross-file impact: create_release.py ↔ release_notes.py (paired в одном PR), SKILL.md обновлён совместно, pyproject.toml per-file-ignores расширение S110 не затрагивает существующие noqa в pipeline-status.py. Oracle-скрипты (pipeline-status.py, project-status.py, spec-status.py) не ссылаются на create_release/release_notes — нет breakage risk
  • PR hygiene: title feat(release): ... (conventional commits), body с 4 heading'ами заполнен осмысленно, ## Watch out честно описывает pre-existing red CI coverage (Δ +0.12%, не блокирующий), ## Pending перечисляет миграции downstream репо

Suggestions (info, not blocking)

  • create_release.py:310 [style] Файл 310 строк — незначительно превышает рекомендацию code-standards skill (200-300 строк). Для CLI-скрипта с entry point это оправдано (main + 12 helper functions, каждая < 50 строк), но при будущем расширении (multi-platform, checksums) стоит рассмотреть декомпозицию (например, вынести Forgejo API helpers в отдельный модуль).

  • create_release.py:154 [style] _api возвращает object — работает с isinstance(release, dict) проверкой в main(), но dict | None или Any точнее выразили бы контракт. Не блокирующее — текущий код type-safe через runtime проверки.

  • create_release.py:69 [style] sys.path.insert(0, ...) для sibling import release_notes.py — оправдано для curl-downloaded скрипта (оба файла в scripts/), но стоит помнить что при in-tree запуске из .opencode/scripts/ это модифицирует sys.path глобально. Для тестов это решено через sys.path.insert(0, str(SCRIPTS_DIR)) в test_create_release.py:29.

  • release_notes.py:90 [edge] Fallback «секция не найдена → весь CHANGELOG» возвращает контент включая ## [Unreleased] секцию — это может быть нежелательно для release body (пользователь увидит unreleased notes в релизе конкретной версии). Тест test_extract_section_not_found_falls_back_to_whole явно проверяет assert "Unreleased" in out or "0.1.0" in out — то есть поведение задокументировано. Если в будущем это станет проблемой — стоит рассмотреть fallback на последнюю версию вместо всего CHANGELOG. Не блокирующее — контракт issue #65 явно говорит «весь CHANGELOG + warning».

Verdict: APPROVE

## Code Review Summary PR полностью реализует контракт issue #65: глобальная утилита `create_release.py` + экстрактор `release_notes.py` (оба self-contained, stdlib only) + обновлённый раздел в `skills/release/SKILL.md` с детерминированным стандартом маркировки платформы. Качество высокое, 47 тестов покрывают все инварианты и граничные случаи из спеки. ### Соответствие контракту issue #65 - ✅ `create_release.py` — self-contained (re, pathlib, tomllib, json, os, urllib, sys, mimetypes, contextlib, subprocess), читает `pyproject.toml` name, парсит `RUNNER_OS`/`RUNNER_ARCH` → `{os}-{arch}` lowercase, формирует `{name}-{os}-{arch}.zip`, body с «Системные требования» из `RELEASE_PLATFORM`, idempotent (delete+create), upload через multipart/form-data - ✅ `release_notes.py` — self-contained (re, sys, pathlib), `extract_changelog_section(path, tag)`, 3 fallback'а: секция не найдена → весь CHANGELOG (sans boilerplate) + warning; CHANGELOG отсутствует → `""`; пустая секция → `""`. Regex покрывает `-` и `—` (em-dash) - ✅ `SKILL.md` — раздел «Платформа в релизе (детерминированный стандарт)»: 3 источника правды (pyproject name + runner env + RELEASE_PLATFORM), формат asset filename, формат body, правила поднятия min ОС (обновить `RELEASE_PLATFORM` + CHANGELOG `### Изменено`), multi-platform (отдельный job на ОС), anti-patterns (`**Платформы:**` заголовок, Python в требованиях, платформа в CHANGELOG) - ✅ Пример release.yml с curl-download обоих файлов + `RELEASE_PLATFORM` env var - ✅ Удалено устаревшее упоминание `platform` arg - ✅ Все 9 критериев приёмки выполнены ### Positives - **Архитектура**: `release_notes.py` (чистая функция-экстрактор) ↔ `create_release.py` (оркестратор Forgejo API) — чёткое разделение ответственности. Размещение в `.opencode/scripts/` соответствует существующей структуре (`pipeline-status.py`, `project-status.py`) - **Тесты**: 34 + 13 = 47 тестов покрывают все инварианты и граничные случаи: em-dash vs hyphen, tag с/без `v`, multi-version slicing, trailing links, missing CHANGELOG, empty section, missing `pyproject.toml`, missing `RUNNER_OS`, default arch, idempotent delete, upload failure non-fatal, no release id, missing tag env. Шаблон `importlib.util.spec_from_file_location` соответствует `test_pipeline_status.py`/`test_project_status.py` - **Error handling**: специффичные exception types (`HTTPError`, `URLError`), `contextlib.suppress(Exception)` для best-effort detail extraction (оправдано для CI-скрипта, отсюда `S110` в per-file-ignores), `_die()` для fatal errors с осмысленными сообщениями, `_warn()` для non-fatal (upload failure → release без ассета) - **Security**: нет secrets в коде, URLs из env vars, `# noqa: S310` для `urlopen` (operator-configured URL — тот же паттерн что в `pipeline-status.py`/`project-status.py`/`spec-status.py`), `subprocess.check_output` с list args (без `shell=True`) - **Idempotency**: delete existing release для тега → create new (Forgejo не авто-clean'ит orphan releases при пересоздании тега → HTTP 409 без этого) - **Backward compat**: `RELEASE_PLATFORM` не задан → body без секции (старый формат) - **Cross-file impact**: `create_release.py` ↔ `release_notes.py` (paired в одном PR), `SKILL.md` обновлён совместно, `pyproject.toml` `per-file-ignores` расширение `S110` не затрагивает существующие `noqa` в `pipeline-status.py`. Oracle-скрипты (`pipeline-status.py`, `project-status.py`, `spec-status.py`) не ссылаются на `create_release`/`release_notes` — нет breakage risk - **PR hygiene**: title `feat(release): ...` (conventional commits), body с 4 heading'ами заполнен осмысленно, `## Watch out` честно описывает pre-existing red CI coverage (Δ +0.12%, не блокирующий), `## Pending` перечисляет миграции downstream репо ### Suggestions (info, not blocking) - **create_release.py:310** [style] Файл 310 строк — незначительно превышает рекомендацию code-standards skill (200-300 строк). Для CLI-скрипта с entry point это оправдано (main + 12 helper functions, каждая < 50 строк), но при будущем расширении (multi-platform, checksums) стоит рассмотреть декомпозицию (например, вынести Forgejo API helpers в отдельный модуль). - **create_release.py:154** [style] `_api` возвращает `object` — работает с `isinstance(release, dict)` проверкой в `main()`, но `dict | None` или `Any` точнее выразили бы контракт. Не блокирующее — текущий код type-safe через runtime проверки. - **create_release.py:69** [style] `sys.path.insert(0, ...)` для sibling import `release_notes.py` — оправдано для curl-downloaded скрипта (оба файла в `scripts/`), но стоит помнить что при in-tree запуске из `.opencode/scripts/` это модифицирует `sys.path` глобально. Для тестов это решено через `sys.path.insert(0, str(SCRIPTS_DIR))` в `test_create_release.py:29`. - **release_notes.py:90** [edge] Fallback «секция не найдена → весь CHANGELOG» возвращает контент включая `## [Unreleased]` секцию — это может быть нежелательно для release body (пользователь увидит unreleased notes в релизе конкретной версии). Тест `test_extract_section_not_found_falls_back_to_whole` явно проверяет `assert "Unreleased" in out or "0.1.0" in out` — то есть поведение задокументировано. Если в будущем это станет проблемой — стоит рассмотреть fallback на последнюю версию вместо всего CHANGELOG. Не блокирующее — контракт issue #65 явно говорит «весь CHANGELOG + warning». ### Verdict: APPROVE
slaid098 deleted branch feat/release/global-create-release 2026-08-12 19:14:06 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config!66
No description provided.