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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
!src/
!tests/
!typings/
!libs/
!.vscode/
!.claude/

Expand Down Expand Up @@ -93,6 +94,7 @@
!src/**
!tests/**
!typings/**
!libs/**
!.vscode/**
!.claude/**

Expand Down
55 changes: 52 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ VERSION ?=
TAG ?=
BUMP ?=
CREATE_BRANCHES ?= 1
PR_ACTION ?= status
PR_BASE ?= main
PR_HEAD ?=
PR_NUMBER ?=
PR_TITLE ?=
PR_BODY ?=
PR_DRAFT ?= 0
PR_MERGE_METHOD ?= squash
PR_AUTO ?= 0
PR_DELETE_BRANCH ?= 0
PR_CHECKS_STRICT ?= 0

Q := @
ifdef VERBOSE
Expand Down Expand Up @@ -121,7 +132,7 @@ if [ -n "$$residual_venvs" ]; then \
fi
endef

.PHONY: help setup upgrade build check security format docs test validate typings clean release release-ci
.PHONY: help setup upgrade build check security format docs test validate typings clean release release-ci pr

help: ## Show simple workspace verbs
$(Q)echo "FLEXT Workspace"
Expand All @@ -141,6 +152,7 @@ help: ## Show simple workspace verbs
$(Q)echo " validate Run validate gates (FIX=1 auto-fix, VALIDATE_SCOPE=workspace for repo-level)"
$(Q)echo " release Interactive workspace release orchestration"
$(Q)echo " release-ci Non-interactive release run for CI/tag workflows"
$(Q)echo " pr Manage PRs for selected projects"
$(Q)echo " typings Stub supply-chain + typing report (PROJECT/PROJECTS to scope)"
$(Q)echo " clean Clean all projects"
$(Q)echo ""
Expand All @@ -158,8 +170,13 @@ help: ## Show simple workspace verbs
$(Q)echo " INTERACTIVE=1|0 Release prompt mode"
$(Q)echo " DRY_RUN=1 Print plan, do not tag/push"
$(Q)echo " PUSH=1 Push release commit/tag"
$(Q)echo " VERSION=0.10.0 TAG=v0.10.0 BUMP=patch Release controls"
$(Q)echo " VERSION=<semver> TAG=v<semver> BUMP=patch Release controls"
$(Q)echo " CREATE_BRANCHES=1|0 Create release branches in workspace + projects"
$(Q)echo " PR_ACTION=status|create|view|checks|merge|close"
$(Q)echo " PR_BASE=main PR_HEAD=<branch> PR_NUMBER=<id> PR_DRAFT=0|1"
$(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 " DEPS_REPORT=0 Skip dependency report after upgrade/typings"
$(Q)echo ""
$(Q)echo "Examples:"
Expand All @@ -171,7 +188,9 @@ help: ## Show simple workspace verbs
$(Q)echo " make test PROJECT=flext-api PYTEST_ARGS=\"-k unit\" FAIL_FAST=1"
$(Q)echo " make validate VALIDATE_SCOPE=workspace"
$(Q)echo " make release BUMP=minor"
$(Q)echo " make release-ci VERSION=0.10.0 TAG=v0.10.0 RELEASE_PHASE=all"
$(Q)echo " make release-ci VERSION=0.11.0 TAG=v0.11.0 RELEASE_PHASE=all"
$(Q)echo " make pr PROJECT=flext-core PR_ACTION=status"
$(Q)echo " make pr PROJECT=flext-core PR_ACTION=create PR_TITLE='release: 0.11.0-dev'"
$(Q)echo " NOTE: External projects (not in .gitmodules) require manual clone."

setup: ## Install all projects into workspace .venv
Expand All @@ -186,6 +205,7 @@ setup: ## Install all projects into workspace .venv
$(Q)$(ENFORCE_WORKSPACE_VENV)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
$(Q)echo "Enforcing Python 3.13 version guards..."; python3.13 scripts/maintenance/enforce_python_version.py || exit 1
$(Q)$(AUTO_ADJUST_SELECTED_PROJECTS)
$(Q)echo "Modernizing pyproject.toml files..."; \
$(POETRY_ENV) python scripts/dependencies/modernize_pyproject.py --skip-check 2>&1 | grep -E "^Phase|Total:|✓|No semantic" || true; \
Expand Down Expand Up @@ -284,6 +304,7 @@ upgrade: ## Upgrade Python dependencies to latest via Poetry
$(Q)$(ENFORCE_WORKSPACE_VENV)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
$(Q)echo "Enforcing Python 3.13 version guards..."; python3.13 scripts/maintenance/enforce_python_version.py || exit 1
$(Q)echo "Modernizing pyproject.toml files..."; \
$(POETRY_ENV) python scripts/dependencies/modernize_pyproject.py --skip-check 2>&1 | grep -E "^Phase|Total:|✓|No semantic" || true; \
echo ""
Expand Down Expand Up @@ -400,31 +421,58 @@ build: ## Build/package all selected projects
$(Q)$(ORCHESTRATOR) --verb build $(if $(filter 1,$(FAIL_FAST)),--fail-fast) $(SELECTED_PROJECTS)

release: ## Interactive workspace release orchestration
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENFORCE_WORKSPACE_VENV)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
$(Q)python scripts/release/run.py \
--root "$(CURDIR)" \
--phase "$(RELEASE_PHASE)" \
--interactive "$(INTERACTIVE)" \
--create-branches "$(CREATE_BRANCHES)" \
--projects $(SELECTED_PROJECTS) \
$(if $(DRY_RUN),--dry-run "$(DRY_RUN)",) \
$(if $(PUSH),--push "$(PUSH)",) \
$(if $(VERSION),--version "$(VERSION)",) \
$(if $(TAG),--tag "$(TAG)",) \
$(if $(BUMP),--bump "$(BUMP)",)

release-ci: ## Non-interactive release run for CI/tag workflows
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENFORCE_WORKSPACE_VENV)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
$(Q)python scripts/release/run.py \
--root "$(CURDIR)" \
--phase "$(RELEASE_PHASE)" \
--interactive 0 \
--create-branches 0 \
--projects $(SELECTED_PROJECTS) \
$(if $(DRY_RUN),--dry-run "$(DRY_RUN)",) \
$(if $(PUSH),--push "$(PUSH)",) \
$(if $(VERSION),--version "$(VERSION)",) \
$(if $(TAG),--tag "$(TAG)",) \
$(if $(BUMP),--bump "$(BUMP)",)

pr: ## Manage pull requests for selected projects
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
Comment on lines +457 to +460
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: Missing $(ENFORCE_WORKSPACE_VENV) guard in the pr target. Every other target that invokes $(ORCHESTRATOR) (which depends on the workspace venv) includes this guard. Without it, running make pr before make setup will produce a confusing failure instead of the standard venv-not-found error message.

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

<comment>Missing `$(ENFORCE_WORKSPACE_VENV)` guard in the `pr` target. Every other target that invokes `$(ORCHESTRATOR)` (which depends on the workspace venv) includes this guard. Without it, running `make pr` before `make setup` will produce a confusing failure instead of the standard venv-not-found error message.</comment>

<file context>
@@ -435,6 +452,24 @@ release-ci: ## Non-interactive release run for CI/tag workflows
 		$(if $(TAG),--tag "$(TAG)",) \
 		$(if $(BUMP),--bump "$(BUMP)",)
 
+pr: ## Manage pull requests for selected projects
+	$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
+	$(Q)$(ENSURE_SELECTED_PROJECTS)
</file context>
Suggested change
pr: ## Manage pull requests for selected projects
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
pr: ## Manage pull requests for selected projects
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENFORCE_WORKSPACE_VENV)
$(Q)$(ENSURE_SELECTED_PROJECTS)
$(Q)$(ENSURE_PROJECTS_EXIST)
Fix with Cubic

$(Q)$(ORCHESTRATOR) --verb pr \
$(if $(filter 1,$(FAIL_FAST)),--fail-fast) \
--make-arg "PR_ACTION=$(PR_ACTION)" \
--make-arg "PR_BASE=$(PR_BASE)" \
$(if $(PR_HEAD),--make-arg "PR_HEAD=$(PR_HEAD)",) \
$(if $(PR_NUMBER),--make-arg "PR_NUMBER=$(PR_NUMBER)",) \
$(if $(PR_TITLE),--make-arg "PR_TITLE=$(PR_TITLE)",) \
$(if $(PR_BODY),--make-arg "PR_BODY=$(PR_BODY)",) \
--make-arg "PR_DRAFT=$(PR_DRAFT)" \
--make-arg "PR_MERGE_METHOD=$(PR_MERGE_METHOD)" \
--make-arg "PR_AUTO=$(PR_AUTO)" \
--make-arg "PR_DELETE_BRANCH=$(PR_DELETE_BRANCH)" \
--make-arg "PR_CHECKS_STRICT=$(PR_CHECKS_STRICT)" \
$(SELECTED_PROJECTS)

security: ## Run all security checks in all projects
$(Q)$(ENSURE_NO_PROJECT_CONFLICT)
$(Q)$(ENFORCE_WORKSPACE_VENV)
Expand Down Expand Up @@ -470,6 +518,7 @@ ifeq ($(VALIDATE_SCOPE),workspace)
$(Q)$(AUTO_ADJUST_SELECTED_PROJECTS)
$(Q)mkdir -p .reports
$(Q)echo "Running workspace validation (inventory + strict anti-drift gates)..."
$(Q)python3.13 scripts/maintenance/enforce_python_version.py --check || exit 1
$(Q)$(WORKSPACE_VENV)/bin/python scripts/core/generate_scripts_inventory.py --root .
$(Q)$(WORKSPACE_VENV)/bin/python scripts/core/check_base_mk_sync.py
$(Q)$(WORKSPACE_VENV)/bin/python scripts/github/lint_workflows.py --root . --report .reports/workflows/actionlint.json
Expand Down
38 changes: 36 additions & 2 deletions base.mk
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ CHECK_GATES ?=
VALIDATE_GATES ?=
DOCS_PHASE ?= all
AUTO_ADJUST ?= 1
PR_ACTION ?= status
PR_BASE ?= main
PR_HEAD ?=
PR_NUMBER ?=
PR_TITLE ?=
PR_BODY ?=
PR_DRAFT ?= 0
PR_MERGE_METHOD ?= squash
PR_AUTO ?= 0
PR_DELETE_BRANCH ?= 0
PR_CHECKS_STRICT ?= 0

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 @@ -89,8 +100,8 @@ $(LINT_CACHE_DIR):
$(Q)mkdir -p $(LINT_CACHE_DIR)

# === SIMPLE VERB SURFACE ===
.PHONY: help setup build check security format docs docs-base docs-sync-scripts test validate clean _preflight
STANDARD_VERBS := setup build check security format docs test validate clean
.PHONY: help setup build check security format docs docs-base docs-sync-scripts test validate clean pr _preflight
STANDARD_VERBS := setup build check security format docs test validate clean pr
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: pr in STANDARD_VERBS causes every make pr invocation to run _preflight, which auto-formats markdown/Go files and enforces venv existence. This means make pr PR_ACTION=status may unexpectedly modify files and will fail without a venv, even though the pr target only needs system python3. Consider removing pr from STANDARD_VERBS and listing it only in .PHONY.

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

<comment>`pr` in `STANDARD_VERBS` causes every `make pr` invocation to run `_preflight`, which auto-formats markdown/Go files and enforces venv existence. This means `make pr PR_ACTION=status` may unexpectedly modify files and will fail without a venv, even though the `pr` target only needs system `python3`. Consider removing `pr` from `STANDARD_VERBS` and listing it only in `.PHONY`.</comment>

<file context>
@@ -89,8 +99,8 @@ $(LINT_CACHE_DIR):
-.PHONY: help setup build check security format docs docs-base docs-sync-scripts test validate clean _preflight
-STANDARD_VERBS := setup build check security format docs test validate clean
+.PHONY: help setup build check security format docs docs-base docs-sync-scripts test validate clean pr _preflight
+STANDARD_VERBS := setup build check security format docs test validate clean pr
 $(STANDARD_VERBS): _preflight
 
</file context>
Suggested change
STANDARD_VERBS := setup build check security format docs test validate clean pr
STANDARD_VERBS := setup build check security format docs test validate clean
Fix with Cubic

$(STANDARD_VERBS): _preflight

define ENFORCE_WORKSPACE_VENV
Expand Down Expand Up @@ -170,7 +181,15 @@ help: ## Show commands
$(Q)echo " docs Build docs"
$(Q)echo " test Run pytest only"
$(Q)echo " validate Run validate gates only (use FIX=1 to auto-fix first)"
$(Q)echo " pr Manage this repository PR (default: status)"
$(Q)echo " clean Clean build/test/type artifacts"
$(Q)echo ""
$(Q)echo "PR variables:"
$(Q)echo " PR_ACTION=status|create|view|checks|merge|close"
$(Q)echo " PR_BASE=main PR_HEAD=<branch> PR_NUMBER=<id>"
$(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)"

setup: ## Complete setup
$(Q)if [ "$(CORE_STACK)" = "go" ]; then \
Expand Down Expand Up @@ -484,6 +503,21 @@ validate: ## Run validate gates (VALIDATE_GATES=complexity,docstring to select,
$(POETRY) run interrogate $(SRC_DIR) --fail-under=$(DOCSTRING_MIN) --ignore-init-method --ignore-magic -q; \
fi

pr: ## Manage pull requests for this repository
$(Q)python3 "$(WORKSPACE_ROOT)/scripts/github/pr_manager.py" \
--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)",) \
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: Shell quoting breakage for PR_TITLE and PR_BODY: Make expands the variable before the shell sees it, so values containing double quotes, backticks, or $() will break the command or cause unintended shell interpretation. Consider passing these values via environment variables instead of command-line arguments, which avoids shell quoting entirely.

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

<comment>Shell quoting breakage for `PR_TITLE` and `PR_BODY`: Make expands the variable before the shell sees it, so values containing double quotes, backticks, or `$()` will break the command or cause unintended shell interpretation. Consider passing these values via environment variables instead of command-line arguments, which avoids shell quoting entirely.</comment>

<file context>
@@ -484,6 +501,20 @@ validate: ## Run validate gates (VALIDATE_GATES=complexity,docstring to select,
+		--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)" \
</file context>
Fix with Cubic

$(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)"

clean: ## Clean artifacts
$(Q)if [ "$(CORE_STACK)" = "go" ]; then \
rm -f coverage.out coverage.html; \
Expand Down
10 changes: 10 additions & 0 deletions libs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from __future__ import annotations

__all__ = [
"discovery",
"git",
"paths",
"reporting",
"selection",
"subprocess",
]
44 changes: 44 additions & 0 deletions libs/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class ProjectInfo:
path: Path
name: str
kind: str


def _is_git_project(path: Path) -> bool:
return (path / ".git").exists()


def _submodule_names(workspace_root: Path) -> set[str]:
gitmodules = workspace_root / ".gitmodules"
if not gitmodules.exists():
return set()
try:
content = gitmodules.read_text(encoding="utf-8")
except OSError:
return set()
return set(re.findall(r"^\s*path\s*=\s*(.+?)\s*$", content, re.MULTILINE))


def discover_projects(workspace_root: Path) -> list[ProjectInfo]:
projects: list[ProjectInfo] = []
submodules = _submodule_names(workspace_root)
for entry in sorted(workspace_root.iterdir(), key=lambda value: value.name):
if not entry.is_dir() or entry.name == "cmd" or entry.name.startswith("."):
continue
if not _is_git_project(entry):
continue
if not (entry / "Makefile").exists():
continue
if not (entry / "pyproject.toml").exists() and not (entry / "go.mod").exists():
continue
kind = "submodule" if entry.name in submodules else "external"
projects.append(ProjectInfo(path=entry, name=entry.name, kind=kind))
return projects
14 changes: 14 additions & 0 deletions libs/git.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from __future__ import annotations

from pathlib import Path

from libs.subprocess import run_capture


def current_branch(repo_root: Path) -> str:
return run_capture(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root)


def tag_exists(repo_root: Path, tag: str) -> bool:
value = run_capture(["git", "tag", "-l", tag], cwd=repo_root)
return value.strip() == tag
11 changes: 11 additions & 0 deletions libs/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

from pathlib import Path


def workspace_root(path: str | Path = ".") -> Path:
return Path(path).resolve()


def repo_root_from_script(script_file: str | Path) -> Path:
return Path(script_file).resolve().parents[1]
Loading
Loading