Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ PR_MERGE_METHOD ?= squash
PR_AUTO ?= 0
PR_DELETE_BRANCH ?= 0
PR_CHECKS_STRICT ?= 0
PR_RELEASE_ON_MERGE ?= 1
PR_INCLUDE_ROOT ?= 1

Q := @
ifdef VERBOSE
Expand Down Expand Up @@ -177,6 +179,8 @@ help: ## Show simple workspace verbs
$(Q)echo " PR_TITLE='title' PR_BODY='body' PR_MERGE_METHOD=squash|merge|rebase"
$(Q)echo " PR_AUTO=0|1 PR_DELETE_BRANCH=0|1"
$(Q)echo " PR_CHECKS_STRICT=0|1 checks action strict failure toggle"
$(Q)echo " PR_RELEASE_ON_MERGE=0|1 merge action: dispatch release workflow"
$(Q)echo " PR_INCLUDE_ROOT=0|1 include root repo in workspace PR automation"
$(Q)echo " DEPS_REPORT=0 Skip dependency report after upgrade/typings"
$(Q)echo ""
$(Q)echo "Examples:"
Expand Down Expand Up @@ -471,7 +475,24 @@ pr: ## Manage pull requests for selected projects
--make-arg "PR_AUTO=$(PR_AUTO)" \
--make-arg "PR_DELETE_BRANCH=$(PR_DELETE_BRANCH)" \
--make-arg "PR_CHECKS_STRICT=$(PR_CHECKS_STRICT)" \
--make-arg "PR_RELEASE_ON_MERGE=$(PR_RELEASE_ON_MERGE)" \
$(SELECTED_PROJECTS)
$(Q)if [ "$(PR_INCLUDE_ROOT)" = "1" ]; then \
python scripts/github/pr_manager.py \
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Bare python invocation bypasses workspace venv. The pr target lacks $(ENFORCE_WORKSPACE_VENV) and this direct script call doesn't use $(POETRY_ENV) like the $(ORCHESTRATOR) does. This could run pr_manager.py under system Python, missing workspace dependencies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Makefile, line 481:

<comment>Bare `python` invocation bypasses workspace venv. The `pr` target lacks `$(ENFORCE_WORKSPACE_VENV)` and this direct script call doesn't use `$(POETRY_ENV)` like the `$(ORCHESTRATOR)` does. This could run `pr_manager.py` under system Python, missing workspace dependencies.</comment>

<file context>
@@ -471,7 +475,24 @@ pr: ## Manage pull requests for selected projects
+		--make-arg "PR_RELEASE_ON_MERGE=$(PR_RELEASE_ON_MERGE)" \
 		$(SELECTED_PROJECTS)
+	$(Q)if [ "$(PR_INCLUDE_ROOT)" = "1" ]; then \
+		python scripts/github/pr_manager.py \
+			--repo-root "$(CURDIR)" \
+			--action "$(PR_ACTION)" \
</file context>
Suggested change
python scripts/github/pr_manager.py \
$(POETRY_ENV) python scripts/github/pr_manager.py \
Fix with Cubic

--repo-root "$(CURDIR)" \
--action "$(PR_ACTION)" \
--base "$(PR_BASE)" \
$(if $(PR_HEAD),--head "$(PR_HEAD)",) \
$(if $(PR_NUMBER),--number "$(PR_NUMBER)",) \
$(if $(PR_TITLE),--title "$(PR_TITLE)",) \
$(if $(PR_BODY),--body "$(PR_BODY)",) \
--draft "$(PR_DRAFT)" \
--merge-method "$(PR_MERGE_METHOD)" \
--auto "$(PR_AUTO)" \
--delete-branch "$(PR_DELETE_BRANCH)" \
--checks-strict "$(PR_CHECKS_STRICT)" \
--release-on-merge "$(PR_RELEASE_ON_MERGE)"; \
fi

security: ## Run all security checks in all projects
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
Expand Down
5 changes: 4 additions & 1 deletion base.mk
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ PR_MERGE_METHOD ?= squash
PR_AUTO ?= 0
PR_DELETE_BRANCH ?= 0
PR_CHECKS_STRICT ?= 0
PR_RELEASE_ON_MERGE ?= 1

PYTEST_REPORT_ARGS := -ra --durations=25 --durations-min=0.001 --tb=short
PYTEST_DIAG_ARGS := -rA --durations=0 --tb=long --showlocals
Expand Down Expand Up @@ -190,6 +191,7 @@ help: ## Show commands
$(Q)echo " PR_TITLE='title' PR_BODY='body' PR_DRAFT=0|1"
$(Q)echo " PR_MERGE_METHOD=squash|merge|rebase PR_AUTO=0|1 PR_DELETE_BRANCH=0|1"
$(Q)echo " PR_CHECKS_STRICT=0|1 (checks: fail command only when strict=1)"
$(Q)echo " PR_RELEASE_ON_MERGE=0|1 (merge: dispatch release workflow when branch maps to semver)"

setup: ## Complete setup
$(Q)if [ "$(CORE_STACK)" = "go" ]; then \
Expand Down Expand Up @@ -516,7 +518,8 @@ pr: ## Manage pull requests for this repository
--merge-method "$(PR_MERGE_METHOD)" \
--auto "$(PR_AUTO)" \
--delete-branch "$(PR_DELETE_BRANCH)" \
--checks-strict "$(PR_CHECKS_STRICT)"
--checks-strict "$(PR_CHECKS_STRICT)" \
--release-on-merge "$(PR_RELEASE_ON_MERGE)"

clean: ## Clean artifacts
$(Q)if [ "$(CORE_STACK)" = "go" ]; then \
Expand Down
43 changes: 43 additions & 0 deletions scripts/github/pr_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import argparse
import json
import re
import subprocess
from pathlib import Path

Expand Down Expand Up @@ -79,6 +80,41 @@ def _selector(pr_number: str, head: str) -> str:
return pr_number if pr_number else head


def _release_tag_from_head(head: str) -> str | None:
version = head.removesuffix("-dev")
if re.fullmatch(r"\d+\.\d+\.\d+", version):
return f"v{version}"
match = re.fullmatch(r"release/(?P<version>\d+\.\d+\.\d+)", head)
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inconsistent -dev suffix handling: the release/ branch path matches against the original head (with -dev still present), so release/X.Y.Z-dev branches will never produce a tag. Apply removesuffix consistently or match the stripped version variable in the second regex as well.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/github/pr_manager.py, line 87:

<comment>Inconsistent `-dev` suffix handling: the `release/` branch path matches against the original `head` (with `-dev` still present), so `release/X.Y.Z-dev` branches will never produce a tag. Apply `removesuffix` consistently or match the stripped `version` variable in the second regex as well.</comment>

<file context>
@@ -79,6 +80,41 @@ def _selector(pr_number: str, head: str) -> str:
+    version = head.removesuffix("-dev")
+    if re.fullmatch(r"\d+\.\d+\.\d+", version):
+        return f"v{version}"
+    match = re.fullmatch(r"release/(?P<version>\d+\.\d+\.\d+)", head)
+    if match:
+        return f"v{match.group('version')}"
</file context>
Suggested change
match = re.fullmatch(r"release/(?P<version>\d+\.\d+\.\d+)", head)
match = re.fullmatch(r"release/(?P<version>\d+\.\d+\.\d+)", version)
Fix with Cubic

if match:
return f"v{match.group('version')}"
return None


def _is_workspace_release_repo(repo_root: Path) -> bool:
return (repo_root / ".github" / "workflows" / "release.yml").exists()


def _trigger_release_if_needed(repo_root: Path, head: str) -> None:
if not _is_workspace_release_repo(repo_root):
return
tag = _release_tag_from_head(head)
if tag is None:
return

if _run_stream(["gh", "release", "view", tag], repo_root) == 0:
print(f"status=release-exists tag={tag}")
return

run_code = _run_stream(
["gh", "workflow", "run", "release.yml", "-f", f"tag={tag}"],
repo_root,
)
if run_code == 0:
print(f"status=release-dispatched tag={tag}")
else:
print(f"status=release-dispatch-failed tag={tag} exit={run_code}")


def _create_pr(
repo_root: Path,
base: str,
Expand Down Expand Up @@ -118,9 +154,11 @@ def _create_pr(
def _merge_pr(
repo_root: Path,
selector: str,
head: str,
method: str,
auto: int,
delete_branch: int,
release_on_merge: int,
) -> int:
command = ["gh", "pr", "merge", selector]
merge_flag = {
Expand All @@ -136,6 +174,8 @@ def _merge_pr(
exit_code = _run_stream(command, repo_root)
if exit_code == 0:
print("status=merged")
if release_on_merge == 1:
_trigger_release_if_needed(repo_root, head)
return exit_code


Expand All @@ -157,6 +197,7 @@ def _parse_args() -> argparse.Namespace:
_ = parser.add_argument("--auto", type=int, default=0)
_ = parser.add_argument("--delete-branch", type=int, default=0)
_ = parser.add_argument("--checks-strict", type=int, default=0)
_ = parser.add_argument("--release-on-merge", type=int, default=1)
return parser.parse_args()


Expand Down Expand Up @@ -189,9 +230,11 @@ def main() -> int:
return _merge_pr(
repo_root,
selector,
head,
args.merge_method,
args.auto,
args.delete_branch,
args.release_on_merge,
)

if args.action == "close":
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/scripts/github/pr_manager_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,45 @@ def _fake_run_stream(_command: list[str], _cwd: Path) -> int:
)

assert mod.main() == 8


def test_release_tag_from_head_patterns() -> None:
mod = _load_module("pr_manager_release_tag", "scripts/github/pr_manager.py")
assert mod._release_tag_from_head("0.11.0-dev") == "v0.11.0"
assert mod._release_tag_from_head("release/0.12.3") == "v0.12.3"
assert mod._release_tag_from_head("feature/x") is None


def test_merge_triggers_release_dispatch_when_workspace_repo(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
mod = _load_module("pr_manager_merge_release", "scripts/github/pr_manager.py")
workflows = tmp_path / ".github" / "workflows"
_ = workflows.mkdir(parents=True)
_ = (workflows / "release.yml").write_text("name: release\n", encoding="utf-8")

calls: list[list[str]] = []

def _fake_run_stream(command: list[str], _cwd: Path) -> int:
calls.append(command)
if command[:3] == ["gh", "release", "view"]:
return 1
return 0

monkeypatch.setattr(mod, "_run_stream", _fake_run_stream)

exit_code = mod._merge_pr(
repo_root=tmp_path,
selector="123",
head="0.11.0-dev",
method="squash",
auto=1,
delete_branch=0,
release_on_merge=1,
)

assert exit_code == 0
assert calls[0] == ["gh", "pr", "merge", "123", "--squash", "--auto"]
assert calls[1] == ["gh", "release", "view", "v0.11.0"]
assert calls[2] == ["gh", "workflow", "run", "release.yml", "-f", "tag=v0.11.0"]
assert "status=release-dispatched tag=v0.11.0" in capsys.readouterr().out
Loading