feat(release): global create_release.py with deterministic OS marking #66
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/release/global-create-release"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что сделано
.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-скрипте).platformarg из SKILL.md (после issue #63).Почему
Раньше
create_release.pyдублировался в каждом репо (voice_assistant, video_uniq) — риск рассинхрона, нет единого стандарта. Платформа в release notes не маркировалась детерминированно: asset filenamevoice-assistant.zipбез ОС, в body нет секции «Системные требования». Глобальная утилита в opencode-config (одна версия, curl-download в каждом репо) + детерминированный стандарт маркировки (name из pyproject + ОС из runner + платформа из release.yml env var) решают обе проблемы. Источник правды — 3 независимых источника, не память агента (агент не может забыть или ошибиться).Watch out
--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.tomlper-file-ignoresдля.opencode/scripts/*расширенS110(try-except-pass для best-effort detail extraction) — не затрагиваетS310/PLR0911(оставлены какnoqaв коде, чтобы не ломать существующиеnoqaвpipeline-status.py).Pending
create_release.py— отдельный issue в voice_assistant.release_notes.py— опционально (см. Watch out про coverage).--cov-fail-underили тесты для скриптов — отдельный issue (pre-existing red CI).Closes #65
Closes #65
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.tomlname, парситRUNNER_OS/RUNNER_ARCH→{os}-{arch}lowercase, формирует{name}-{os}-{arch}.zip, body с «Системные требования» изRELEASE_PLATFORM, idempotent (delete+create), upload через multipart/form-datarelease_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_PLATFORMenv varplatformargPositives
release_notes.py(чистая функция-экстрактор) ↔create_release.py(оркестратор Forgejo API) — чёткое разделение ответственности. Размещение в.opencode/scripts/соответствует существующей структуре (pipeline-status.py,project-status.py)v, multi-version slicing, trailing links, missing CHANGELOG, empty section, missingpyproject.toml, missingRUNNER_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.pyHTTPError,URLError),contextlib.suppress(Exception)для best-effort detail extraction (оправдано для CI-скрипта, отсюдаS110в per-file-ignores),_die()для fatal errors с осмысленными сообщениями,_warn()для non-fatal (upload failure → release без ассета)# noqa: S310дляurlopen(operator-configured URL — тот же паттерн что вpipeline-status.py/project-status.py/spec-status.py),subprocess.check_outputс list args (безshell=True)RELEASE_PLATFORMне задан → body без секции (старый формат)create_release.py↔release_notes.py(paired в одном PR),SKILL.mdобновлён совместно,pyproject.tomlper-file-ignoresрасширениеS110не затрагивает существующиеnoqaвpipeline-status.py. Oracle-скрипты (pipeline-status.py,project-status.py,spec-status.py) не ссылаются наcreate_release/release_notes— нет breakage riskfeat(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 importrelease_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