raise-cli 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,338 @@
1
+ """Release CLI commands — release management, quality gates, and release workflow.
2
+
3
+ Provides commands for:
4
+ - Listing releases from the memory graph
5
+ - Running pre-publish quality checks
6
+ - Orchestrating full releases (bump, changelog, commit, tag, push)
7
+
8
+ The check and publish commands were absorbed from the `publish` group (RAISE-247/S5).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import subprocess # nosec B404 - subprocess required for git operations in CLI tool
15
+ from datetime import date
16
+ from pathlib import Path
17
+ from typing import Annotated
18
+
19
+ import typer
20
+ from rich.console import Console
21
+ from rich.table import Table
22
+
23
+ from raise_cli.cli.error_handler import cli_error
24
+ from raise_cli.graph.backends import get_active_backend
25
+ from raise_cli.publish.check import CheckResult, run_checks
26
+ from raise_cli.publish.version import (
27
+ BumpType,
28
+ bump_version,
29
+ is_pep440,
30
+ sync_version_files,
31
+ )
32
+
33
+ release_app = typer.Typer(help="Release management commands")
34
+ console = Console()
35
+
36
+ # Graph path relative to project root
37
+ GRAPH_REL_PATH = Path(".raise") / "rai" / "memory" / "index.json"
38
+
39
+
40
+ # =============================================================================
41
+ # Helpers (moved from publish.py)
42
+ # =============================================================================
43
+
44
+
45
+ def _find_project_paths(project: Path) -> tuple[Path, Path, Path]:
46
+ """Find pyproject.toml, __init__.py, and CHANGELOG.md paths."""
47
+ pyproject_path = project / "pyproject.toml"
48
+ changelog_path = project / "CHANGELOG.md"
49
+ init_path = project / "src" / "raise_cli" / "__init__.py"
50
+ return pyproject_path, init_path, changelog_path
51
+
52
+
53
+ def _read_current_version(pyproject_path: Path) -> str:
54
+ """Read current version from pyproject.toml."""
55
+ if not pyproject_path.exists():
56
+ console.print("[red]pyproject.toml not found[/red]")
57
+ raise typer.Exit(1)
58
+ content = pyproject_path.read_text(encoding="utf-8")
59
+ match = re.search(r'version\s*=\s*"([^"]*)"', content)
60
+ if not match:
61
+ console.print("[red]Could not find version in pyproject.toml[/red]")
62
+ raise typer.Exit(1)
63
+ return match.group(1)
64
+
65
+
66
+ def _display_results(results: list[CheckResult]) -> bool:
67
+ """Display check results with Rich formatting."""
68
+ console.print()
69
+ console.print("[bold]Pre-publish Quality Check[/bold]")
70
+ console.print("─" * 40)
71
+
72
+ passed_count = 0
73
+ for r in results:
74
+ icon = "[green]✓[/green]" if r.passed else "[red]✗[/red]"
75
+ console.print(f" {icon} {r.gate}: {r.message}")
76
+ if r.passed:
77
+ passed_count += 1
78
+
79
+ total = len(results)
80
+ console.print()
81
+ if passed_count == total:
82
+ console.print(f"[green]All {total} checks passed[/green]")
83
+ else:
84
+ console.print(
85
+ f"[red]{passed_count}/{total} checks passed, "
86
+ f"{total - passed_count} FAILED[/red]"
87
+ )
88
+
89
+ return passed_count == total
90
+
91
+
92
+ # =============================================================================
93
+ # Commands
94
+ # =============================================================================
95
+
96
+
97
+ @release_app.command("list")
98
+ def list_releases(
99
+ project: Annotated[
100
+ Path,
101
+ typer.Option("--project", "-p", help="Project root path"),
102
+ ] = Path("."),
103
+ ) -> None:
104
+ """List releases from the memory graph.
105
+
106
+ Shows all release nodes with their status, target date, and associated epics.
107
+
108
+ Examples:
109
+ $ rai release list
110
+ $ rai release list --project /path/to/project
111
+ """
112
+ graph_path = project / GRAPH_REL_PATH
113
+ if not graph_path.exists():
114
+ cli_error(
115
+ f"Memory index not found: {graph_path}",
116
+ hint="Run 'rai memory build' first to create the index",
117
+ exit_code=4,
118
+ )
119
+
120
+ try:
121
+ graph = get_active_backend(graph_path).load()
122
+ except Exception as e:
123
+ cli_error(f"Error loading memory index: {e}")
124
+
125
+ # Find all release nodes
126
+ releases = [n for n in graph.iter_concepts() if n.type == "release"]
127
+
128
+ if not releases:
129
+ console.print("\nNo release nodes found in graph.")
130
+ return
131
+
132
+ # Find epics linked to each release via part_of edges
133
+ release_epics: dict[str, list[str]] = {}
134
+ for node in graph.iter_concepts():
135
+ if node.type == "epic":
136
+ neighbors = graph.get_neighbors(node.id, depth=1, edge_types=["part_of"])
137
+ for neighbor in neighbors:
138
+ if neighbor.type == "release":
139
+ release_epics.setdefault(neighbor.id, []).append(
140
+ node.id.replace("epic-", "").upper()
141
+ )
142
+
143
+ # Build table
144
+ table = Table(title="Releases")
145
+ table.add_column("ID", style="cyan")
146
+ table.add_column("Name")
147
+ table.add_column("Status", style="yellow")
148
+ table.add_column("Target", style="green")
149
+ table.add_column("Epics", style="dim")
150
+
151
+ for rel in sorted(releases, key=lambda r: r.metadata.get("target", "")):
152
+ release_id = rel.metadata.get("release_id", rel.id)
153
+ name = rel.metadata.get("name", "")
154
+ status = rel.metadata.get("status", "")
155
+ target = rel.metadata.get("target", "")
156
+ epics = ", ".join(sorted(release_epics.get(rel.id, [])))
157
+
158
+ table.add_row(release_id, name, status, target, epics)
159
+
160
+ console.print()
161
+ console.print(table)
162
+
163
+
164
+ @release_app.command("check")
165
+ def check_command(
166
+ project: Annotated[
167
+ Path,
168
+ typer.Option("--project", "-p", help="Project root path"),
169
+ ] = Path("."),
170
+ ) -> None:
171
+ """Run all quality gates before publishing.
172
+
173
+ Runs 9 quality checks: tests (with coverage diagnostic), types, lint,
174
+ security, build, package validation, changelog, PEP 440 version, and
175
+ version sync. Coverage is reported but not a blocking gate.
176
+
177
+ Exits with code 0 if all pass, 1 if any fail.
178
+
179
+ Examples:
180
+ $ rai release check
181
+ $ rai release check --project /path/to/project
182
+ """
183
+ pyproject_path, init_path, changelog_path = _find_project_paths(project)
184
+
185
+ results = run_checks(
186
+ project_root=project,
187
+ pyproject_path=pyproject_path,
188
+ init_path=init_path,
189
+ changelog_path=changelog_path,
190
+ )
191
+
192
+ all_passed = _display_results(results)
193
+ if not all_passed:
194
+ raise typer.Exit(1)
195
+
196
+
197
+ @release_app.command("publish")
198
+ def publish_command(
199
+ bump: Annotated[
200
+ BumpType | None,
201
+ typer.Option("--bump", "-b", help="Version bump type"),
202
+ ] = None,
203
+ version: Annotated[
204
+ str | None,
205
+ typer.Option("--version", "-v", help="Explicit version (overrides --bump)"),
206
+ ] = None,
207
+ dry_run: Annotated[
208
+ bool,
209
+ typer.Option("--dry-run", help="Show what would happen without executing"),
210
+ ] = False,
211
+ skip_check: Annotated[
212
+ bool,
213
+ typer.Option("--skip-check", help="Skip quality gates (dangerous)"),
214
+ ] = False,
215
+ project: Annotated[
216
+ Path,
217
+ typer.Option("--project", "-p", help="Project root path"),
218
+ ] = Path("."),
219
+ ) -> None:
220
+ """Orchestrate a full release: check, bump, changelog, commit, tag, push.
221
+
222
+ Either --bump or --version is required.
223
+
224
+ Examples:
225
+ rai release publish --bump alpha
226
+ rai release publish --bump minor --dry-run
227
+ rai release publish --version 2.1.0
228
+ """
229
+ if bump is None and version is None:
230
+ console.print("[red]Either --bump or --version is required[/red]")
231
+ raise typer.Exit(1)
232
+
233
+ pyproject_path, init_path, changelog_path = _find_project_paths(project)
234
+
235
+ # Run checks unless skipped
236
+ if not skip_check:
237
+ results = run_checks(
238
+ project_root=project,
239
+ pyproject_path=pyproject_path,
240
+ init_path=init_path,
241
+ changelog_path=changelog_path,
242
+ )
243
+ all_passed = _display_results(results)
244
+ if not all_passed:
245
+ console.print(
246
+ "\n[red]Quality gates failed. Fix issues or use --skip-check.[/red]"
247
+ )
248
+ raise typer.Exit(1)
249
+ console.print()
250
+
251
+ # Determine new version
252
+ current = _read_current_version(pyproject_path)
253
+
254
+ if version:
255
+ if not is_pep440(version):
256
+ console.print(f"[red]'{version}' is not valid PEP 440[/red]")
257
+ raise typer.Exit(1)
258
+ new_version = version
259
+ else:
260
+ assert bump is not None # nosec B101 - validated at line above
261
+ new_version = bump_version(current, bump)
262
+
263
+ today = date.today().isoformat()
264
+
265
+ # Display plan
266
+ console.print("[bold]Release Plan[/bold]")
267
+ console.print(f" Current version: {current}")
268
+ console.print(f" New version: {new_version}")
269
+ console.print(f" Date: {today}")
270
+ console.print()
271
+ console.print(" Steps:")
272
+ console.print(f" 1. Update pyproject.toml: {current} → {new_version}")
273
+ console.print(f" 2. Update __init__.py: {current} → {new_version}")
274
+ console.print(
275
+ f" 3. Update CHANGELOG.md: [Unreleased] → [{new_version}] - {today}"
276
+ )
277
+ console.print(f" 4. Commit: release: v{new_version}")
278
+ console.print(f" 5. Tag: v{new_version}")
279
+ console.print(" 6. Push commit + tag → triggers GitHub Actions release")
280
+
281
+ if dry_run:
282
+ console.print("\n[yellow]Dry run — no changes made[/yellow]")
283
+ return
284
+
285
+ # Confirm
286
+ console.print()
287
+ if not typer.confirm("Proceed?"):
288
+ console.print("[yellow]Aborted[/yellow]")
289
+ raise typer.Exit(0)
290
+
291
+ # Execute
292
+ # 1-2: Bump version files
293
+ sync_version_files(new_version, pyproject_path=pyproject_path, init_path=init_path)
294
+ console.print("[green]✓ Version bumped[/green]")
295
+
296
+ # 3: Update changelog
297
+ if changelog_path.exists():
298
+ from raise_cli.publish.changelog import promote_unreleased
299
+
300
+ content = changelog_path.read_text(encoding="utf-8")
301
+ try:
302
+ content = promote_unreleased(content, new_version, today)
303
+ changelog_path.write_text(content, encoding="utf-8")
304
+ console.print("[green]✓ Changelog updated[/green]")
305
+ except ValueError:
306
+ console.print("[yellow]⚠ No unreleased entries to promote[/yellow]")
307
+
308
+ # 4: Commit
309
+ subprocess.run( # nosec B603,B607 - controlled git commands, no untrusted input
310
+ ["git", "add", str(pyproject_path), str(init_path), str(changelog_path)],
311
+ cwd=project,
312
+ check=True,
313
+ )
314
+ subprocess.run( # nosec B603,B607 - controlled git commands, no untrusted input
315
+ ["git", "commit", "-m", f"release: v{new_version}"],
316
+ cwd=project,
317
+ check=True,
318
+ )
319
+ console.print(f"[green]✓ Committed: release: v{new_version}[/green]")
320
+
321
+ # 5: Tag
322
+ subprocess.run( # nosec B603,B607 - controlled git commands, no untrusted input
323
+ ["git", "tag", f"v{new_version}"],
324
+ cwd=project,
325
+ check=True,
326
+ )
327
+ console.print(f"[green]✓ Tagged: v{new_version}[/green]")
328
+
329
+ # 6: Push (with confirmation already given)
330
+ subprocess.run( # nosec B603,B607 - controlled git commands, no untrusted input
331
+ ["git", "push", "--follow-tags"],
332
+ cwd=project,
333
+ check=True,
334
+ )
335
+ console.print("[green]✓ Pushed to origin[/green]")
336
+
337
+ console.print(f"\n[bold green]Release v{new_version} published.[/bold green]")
338
+ console.print("GitHub Actions will handle PyPI upload.")