Skip to content

ci(release): auto-tag on milestone close — dry-run default, RELEASE-gated (Track 4)#1102

Draft
100yenadmin wants to merge 2 commits into
mainfrom
worktree-agent-a21109ca4c1ab79d5
Draft

ci(release): auto-tag on milestone close — dry-run default, RELEASE-gated (Track 4)#1102
100yenadmin wants to merge 2 commits into
mainfrom
worktree-agent-a21109ca4c1ab79d5

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

Track 4 — Versioning Phase-2: auto-tag on milestone close

The standing repo automation behind the owner's "complete a milestone → tag/release". Built conservatively as a DRAFT, dry-run-by-default, never auto-enabled on prod without validation.

What's here (4 new files, fully additive — no existing file touched)

File Role
.github/workflows/release-on-milestone-close.yml trigger → gate → (dry-run print | real tag+release)
qa/release_gate_check.py the unit-tested, READ-ONLY gate decision
qa/test_release_gate_check.py 15 tests + a workflow-YAML structural lint
docs/roadmap/release-automation.md marker convention + dry-run→live promotion

The gate design — solving "GitHub milestones don't support labels"

GitHub milestones can't carry labels, so the "ready-for-release" opt-in is an explicit [release-ready] marker in the milestone title or description (case-insensitive, literal brackets so prose can't trip it). The full gate (all must hold for a GO):

  1. [release-ready] marker present — else NO-GO (a development milestone closing must NOT auto-tag).
  2. Clean vX.Y.Z[-rcN] title → tagSprint 12, v1.0, v1.0.5 (final) are refused.
  3. Tag does not already exist — never clobber/re-cut.
  4. Version consistency — milestone base X.Y.Z must equal VERSION and servers/engine/__version__.py.
  5. STATUS: RELEASEgenerate_release_notes.py / the per-gate verdict reports all 11 RRI gates PASSED. DEVELOPMENT (any gate SKIPPED/FAILED/MISSING/UNKNOWN) ⇒ NO-GO. A -rcN pre-release may opt in to ship on DEVELOPMENT via allow_prerelease_dev; a GA always requires RELEASE.

The decision lives in qa/release_gate_check.py (extracted from YAML) so it's unit-tested rather than ad-hoc shell. It reuses generate_release_notes' DEVELOPMENT/RELEASE logic so there is ONE definition of the flag.

Dry-run safety (why merging this is safe)

  • dry_run defaults to true. A real milestone: closed event has no input → dry-run, unless the owner sets repo var RELEASE_AUTOMATION_LIVE=true.
  • Dry-run runs the full resolution + notes and prints exactly what it would tag/release — creates nothing.
  • Only an explicit dispatch dry_run=false or the live repo-var reaches git tag + gh release create. The real step re-confirms the tag is free immediately before cutting (defense-in-depth).
  • Minimal permissions: contents: write. No gameplay/heavy job; reads the scores ledger READ-ONLY; never touches Eva or any gateway.

The RELEASE status comes from a real per-gate RRI artifact (--verdict-json/--rri-json). None is committed, so a bare run falls back to ledger-inference, which can never certify RELEASE (honesty guard) — a GO requires real release evidence, not merely a closed milestone.

Validation

  • qa/test_release_gate_check.py: 15 passed (the four load-bearing cases + edges).
  • Combined with the Phase-1 deps (test_generate_release_notes.py, test_version_consistency.py): 30 passed.
  • actionlint on the new workflow: clean (exit 0).
  • scripts/license_check.py: passed. scores.db/ledger: untouched.

Promoting from dry-run to live (needs owner sign-off)

  1. One successful dry-run on a real closed [release-ready] milestone (title == bumped VERSION, all-PASS RRI artifact wired in).
  2. Go live either per-cut (gh workflow run "release-on-milestone-close.yml" -f milestone=vX.Y.Z -f dry_run=false) or standing (gh variable set RELEASE_AUTOMATION_LIVE --body true).
  3. Verify the tag + gh release (pre-release iff -rc) and the generated notes body.

Kept DRAFT per the conservative scope — flipping to real mode needs owner sign-off + one successful dry-run.

…ated (Track 4)

Versioning Phase-2: the automation behind 'complete a milestone → tag/release'.

- .github/workflows/release-on-milestone-close.yml: fires on milestone:closed +
  workflow_dispatch (milestone + dry_run inputs). DRY-RUN DEFAULT TRUE; real tag/
  release only on explicit dry_run=false OR repo-var RELEASE_AUTOMATION_LIVE=true.
  Minimal permissions: contents:write. No gameplay/heavy job; never touches Eva/gateway.
- qa/release_gate_check.py: the unit-tested, READ-ONLY gate decision — [release-ready]
  marker (solves no-milestone-labels) + clean vX.Y.Z title→tag + tag-not-exists +
  VERSION/__version__ consistency + STATUS:RELEASE (all 11 RRI gates PASSED). Reuses
  generate_release_notes' DEVELOPMENT/RELEASE logic. Refuses GA on DEVELOPMENT; pre-
  release may opt in via --allow-prerelease-dev.
- qa/test_release_gate_check.py: 15 tests (RELEASE+marker⇒go; DEVELOPMENT⇒no-go;
  missing-marker⇒no-go; tag-exists⇒no-go; bad-title; version-mismatch; pre-release-dev
  opt-in; GA-never-on-dev; READ-ONLY-on-db; + a pyyaml-free workflow YAML structural lint).
- docs/roadmap/release-automation.md: marker convention + dry-run→live promotion steps.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b134832d-183e-44d7-8721-8788868a20f1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread qa/release_gate_check.py
version_file = None
try:
version_file = (repo_root / "VERSION").read_text(encoding="utf-8").strip()
except OSError:
Comment thread qa/release_gate_check.py
m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
if m:
engine_version = m.group(1).strip()
except OSError:
import sys
from pathlib import Path

import pytest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant