DEVX-117: refactor: remove project-specific references from devx
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 19s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 46s
Post-merge / badges (push) Successful in 46s
Post-merge / publish (push) Successful in 17s

This commit was merged in pull request #176.
This commit is contained in:
2026-07-06 06:17:52 +00:00
parent 990f2fa612
commit e796b06a91
18 changed files with 59 additions and 60 deletions
+1 -1
View File
@@ -572,7 +572,7 @@ the user should not need to specify which profile to use.
### Available Profiles
**Global** (shared with infra and grm):
**Global** (shared across all projects):
| Profile | Location | Purpose |
|---------|----------|---------|
-1
View File
@@ -185,7 +185,6 @@ def main(translations: tuple[Path, ...], source_dir: str | None) -> None:
# Try common locations
candidates = [
root / "src" / "devx" / "translations.json",
root / "src" / "gitea_runner_manager" / "translations.json",
]
# Also search for any translations.json in src/
for match in root.glob("src/*/translations.json"):
+2 -2
View File
@@ -7,7 +7,7 @@ ordering, then assigned to *max_runners* groups using LPT (Longest
Processing Time first) scheduling.
Each item is a string (e.g. an Ansible ``--limit`` pattern like
``observability`` or ``infra-314-vm``). Optionally, items can be objects
``observability`` or ``customer-1-vm``). Optionally, items can be objects
with ``{"id": "...", "weight": N}`` to provide explicit weights.
The assigned group for *runner_index* is written to ``$GITHUB_ENV`` as
@@ -15,7 +15,7 @@ The assigned group for *runner_index* is written to ``$GITHUB_ENV`` as
Usage::
echo '["observability", "infra-314-vm"]' | \\
echo '["observability", "customer-1-vm"]' | \\
python3 -m devx.ci.distribute_items \\
--runner-index 1 --max-runners 3 \\
--github-env --skip-if-excess
+1 -1
View File
@@ -66,7 +66,7 @@ DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi;
# ── Virtual environment management ────────────────────────────────────────────
#
# These targets provide a single, consistent venv setup across all
# devx-integrated projects (infra, grm, devx). Each project includes
# devx-integrated projects. Each project includes
# devx.mak and aliases its local targets to these.
#
# The venv is a standard .venv directory (no pyenv virtualenv dependency).
+1 -1
View File
@@ -16,7 +16,7 @@ Outputs:
- (default): prints both as ``count=N`` and ``indices=[0,1,...]``
Usage:
python3 -m devx.molecule.discover_runners --owner oblachno-oss --repo grm
python3 -m devx.molecule.discover_runners --owner my-org --repo my-repo
python3 -m devx.molecule.discover_runners --indices
python3 -m devx.molecule.discover_runners --count
"""
+1 -1
View File
@@ -137,7 +137,7 @@ def build_multi_role_pairs(
# --- Molecule weight configuration ---
#
# Weights are loaded from ``[tool.devx.molecule.weights]`` in
# ``pyproject.toml``. Each project (infra, grm, …) contributes its own
# ``pyproject.toml``. Each project contributes its own
# weights calibrated from actual CI execution times.
#
# Two key formats are supported:
+2 -2
View File
@@ -15,9 +15,9 @@ exits early with code 1.
Usage::
# Single-role (grm-style)
# Single-role
python3 -m devx.molecule.molecule_ci_guard pair1 pair2 ...
# Multi-role (infra-style)
# Multi-role
python3 -m devx.molecule.molecule_ci_guard --roles-root ansible/roles pair1 pair2 ...
Environment variables:
+1 -1
View File
@@ -182,7 +182,7 @@ def main(repo: str | None, owner: str | None, branch: str, api_url: str | None)
if not repo:
raise click.ClickException(_("ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME."))
# If DEVX_REPO_NAME contains a slash (e.g. "oblachno/infra"), split into owner/repo.
# If DEVX_REPO_NAME contains a slash (e.g. "my-org/my-repo"), split into owner/repo.
# This prevents 404s when workflows set DEVX_REPO_NAME to the full path.
if "/" in repo and owner is None:
parts = repo.split("/", 1)
+2 -2
View File
@@ -68,8 +68,8 @@ def detect_package_name(repo_root: Path) -> str | None:
Looks for the first subdirectory under ``src/`` that contains
an ``__init__.py`` file with ``__version__``.
Returns the package directory name (e.g., ``devx``,
``gitea_runner_manager``) or ``None`` if no package is found.
Returns the package directory name (e.g., ``devx``) or ``None`` if
no package is found.
"""
src_dir = repo_root / "src"
if not src_dir.is_dir():
+9 -9
View File
@@ -185,7 +185,7 @@ class TestMain:
args = mock_client.ensure_branch_protection.call_args
assert args[0][0] == "develop"
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "oblachno/infra"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "my-org/my-repo"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_parses_owner_repo_from_env(self, mock_client_cls: MagicMock) -> None:
"""DEVX_REPO_NAME with 'owner/repo' format should be split."""
@@ -197,15 +197,15 @@ class TestMain:
assert result.exit_code == 0
# Verify GiteaClient was constructed with parsed owner and repo (positional)
call_args = mock_client_cls.call_args
assert call_args[0][2] == "oblachno" # owner is 3rd positional arg
assert call_args[0][3] == "infra" # repo is 4th positional arg
assert call_args[0][2] == "my-org" # owner is 3rd positional arg
assert call_args[0][3] == "my-repo" # repo is 4th positional arg
@patch.dict(
"os.environ",
{"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "infra", "DEVX_REPO_OWNER": "oblachno"},
{"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "my-repo", "DEVX_REPO_OWNER": "my-org"},
clear=True,
)
@patch("devx.tools.configure_repo.REPO_OWNER", "oblachno")
@patch("devx.tools.configure_repo.REPO_OWNER", "my-org")
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_no_slash_when_owner_set_separately(self, mock_client_cls: MagicMock) -> None:
"""When DEVX_REPO_OWNER is set, DEVX_REPO_NAME should not be split."""
@@ -216,12 +216,12 @@ class TestMain:
result = runner.invoke(main, [])
assert result.exit_code == 0
call_args = mock_client_cls.call_args
assert call_args[0][2] == "oblachno" # owner
assert call_args[0][3] == "infra" # repo
assert call_args[0][2] == "my-org" # owner
assert call_args[0][3] == "my-repo" # repo
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.REPO_NAME", "devx")
@patch("devx.tools.configure_repo.REPO_OWNER", "oblachno-oss")
@patch("devx.tools.configure_repo.REPO_OWNER", "my-org")
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_repo_from_pyproject(self, mock_client_cls: MagicMock) -> None:
"""When no env var is set, repo name should come from pyproject.toml."""
@@ -232,5 +232,5 @@ class TestMain:
result = runner.invoke(main, [])
assert result.exit_code == 0
call_args = mock_client_cls.call_args
assert call_args[0][2] == "oblachno-oss" # owner
assert call_args[0][2] == "my-org" # owner
assert call_args[0][3] == "devx" # repo
+6 -6
View File
@@ -25,14 +25,14 @@ class TestExtractTaskId:
class TestGetRepoName:
@patch.dict("os.environ", {"DEVX_REPO_NAME": "infra"})
@patch.dict("os.environ", {"DEVX_REPO_NAME": "my-repo"})
def test_from_env(self) -> None:
assert get_repo_name() == "infra"
assert get_repo_name() == "my-repo"
@patch("devx.tools.create_pr.REPO_NAME", "devx")
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "oblachno/infra"}, clear=True)
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "my-org/my-repo"}, clear=True)
def test_env_overrides_pyproject(self) -> None:
assert get_repo_name() == "infra"
assert get_repo_name() == "my-repo"
@patch("devx.tools.create_pr.REPO_NAME", "devx")
@patch.dict("os.environ", {}, clear=True)
@@ -40,9 +40,9 @@ class TestGetRepoName:
assert get_repo_name() == "devx"
@patch("devx.tools.create_pr.REPO_NAME", "")
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "oblachno/infra"}, clear=True)
@patch.dict("os.environ", {"GITHUB_REPOSITORY": "my-org/my-repo"}, clear=True)
def test_from_github(self) -> None:
assert get_repo_name() == "infra"
assert get_repo_name() == "my-repo"
@patch("devx.tools.create_pr.REPO_NAME", "")
@patch.dict("os.environ", {}, clear=True)
+1 -1
View File
@@ -592,7 +592,7 @@ class TestLptDistribute:
def test_load_balance_with_varying_weights(self) -> None:
"""LPT should produce better load balance than round-robin."""
items = list(range(7))
# Simulate infra-like weights: 2 heavy, 2 medium, 3 light
# Simulate multi-role-like weights: 2 heavy, 2 medium, 3 light
weights = [10, 10, 7, 7, 3, 3, 3]
groups = _lpt_distribute(items, weights, 3)
loads = [sum(weights[i] for i in g) for g in groups]
+3 -3
View File
@@ -105,7 +105,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/infra",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
@@ -152,7 +152,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/infra",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
@@ -197,7 +197,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/infra",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
+4 -4
View File
@@ -224,7 +224,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/grm",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
@@ -292,7 +292,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/grm",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
@@ -375,7 +375,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/grm",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
@@ -422,7 +422,7 @@ class TestCli:
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
"GITEA_REPOSITORY": "oblachno-oss/grm",
"GITEA_REPOSITORY": "my-org/my-repo",
"PATH": os.environ.get("PATH", ""),
},
clear=True,
+5 -5
View File
@@ -713,7 +713,7 @@ class TestMain:
def test_dry_run_does_not_post(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = ReviewResult()
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm", "--dry-run"], env={"CI_GITEA_TOKEN": "fake"})
result = runner.invoke(main, ["42", "my-org/my-repo", "--dry-run"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "[dry-run]" in result.output
mock_client_class.return_value.create_review.assert_not_called()
@@ -724,7 +724,7 @@ class TestMain:
mock_run.return_value = ReviewResult()
mock_client_class.return_value.create_review.return_value = {"id": 123}
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #123" in result.output
mock_client_class.return_value.create_review.assert_called_once()
@@ -740,7 +740,7 @@ class TestMain:
{"id": 124},
]
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #124" in result.output
assert client.create_review.call_count == 2
@@ -753,12 +753,12 @@ class TestMain:
client = mock_client_class.return_value
client.create_review.side_effect = APIError(500, "Internal server error")
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code != 0
def test_no_token_raises(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": ""})
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": ""})
assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output
+7 -7
View File
@@ -84,7 +84,7 @@ class TestPushToBadgesBranch:
class TestUpdateBadgeUrls:
def test_replaces_branch_url(self) -> None:
content = "[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]"
content = "[![Tests](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/branch/badges/tests.svg)]"
result = push_badges.update_badge_urls(content, "abc123def456")
assert "raw/commit/abc123def456/tests.svg" in result
assert "raw/branch/badges" not in result
@@ -93,7 +93,7 @@ class TestUpdateBadgeUrls:
"""Old commit SHA URLs should be replaced with the new one."""
old_sha = "aabb123456789012345678901234567890123456" # 40 hex chars
new_sha = "ccdd123456789012345678901234567890123456" # 40 hex chars
content = f"[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/{old_sha}/tests.svg)]"
content = f"[![Tests](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/commit/{old_sha}/tests.svg)]"
result = push_badges.update_badge_urls(content, new_sha)
assert f"raw/commit/{new_sha}/tests.svg" in result
assert old_sha not in result
@@ -105,16 +105,16 @@ class TestUpdateBadgeUrls:
def test_multiple_badges(self) -> None:
content = (
"[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/coverage.svg)]\n"
"[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]\n"
"[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/version.svg)]"
"[![Coverage](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/branch/badges/coverage.svg)]\n"
"[![Tests](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/branch/badges/tests.svg)]\n"
"[![Version](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/branch/badges/version.svg)]"
)
result = push_badges.update_badge_urls(content, "abc123def456")
assert result.count("raw/commit/abc123def456/") == 3
assert "raw/branch/badges" not in result
def test_preserves_non_badge_urls(self) -> None:
content = "[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)]"
content = "[![CI](https://git.oblachno.oblachno.fyi/my-org/my-repo/actions/workflows/ci.yml/badge.svg)]"
result = push_badges.update_badge_urls(content, "abc123")
assert result == content
@@ -122,7 +122,7 @@ class TestUpdateBadgeUrls:
class TestUpdateReadmeWithBadgeSha:
def test_updates_readme(self, tmp_path: Path) -> None:
readme = tmp_path / "README.md"
readme.write_text("[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]")
readme.write_text("[![Tests](https://git.oblachno.oblachno.fyi/my-org/my-repo/raw/branch/badges/tests.svg)]")
with patch("subprocess.run"):
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
content = readme.read_text()
+1 -1
View File
@@ -1147,7 +1147,7 @@ class TestMain:
mock_ft: MagicMock,
mock_vtc: MagicMock,
) -> None:
"""Release is skipped when only workflow/infra files changed."""
"""Release is skipped when only workflow/infrastructure files changed."""
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
runner = CliRunner()
result = runner.invoke(main, [])
+12 -12
View File
@@ -166,9 +166,9 @@ class TestCustomPrefix:
f.write(content)
return path
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"})
def test_master_accepts_grm_prefix(self) -> None:
"""Master branch accepts GRM-N: prefix when DEVX_TASK_PREFIX=GRM."""
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_master_accepts_proj_prefix(self) -> None:
"""Master branch accepts PROJ-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
import devx.ci.validate_commit_msg as vcm
@@ -177,7 +177,7 @@ class TestCustomPrefix:
importlib.reload(devx.config)
importlib.reload(vcm)
try:
msg_path = self._write_msg("GRM-66: fix: add scripts/** to infrastructure")
msg_path = self._write_msg("PROJ-66: fix: add scripts/** to infrastructure")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
result = runner.invoke(vcm.main, [msg_path])
@@ -188,8 +188,8 @@ class TestCustomPrefix:
importlib.reload(devx.config)
importlib.reload(vcm)
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"})
def test_master_rejects_devx_prefix_when_grm_configured(self) -> None:
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_master_rejects_devx_prefix_when_proj_configured(self) -> None:
"""Master branch rejects DEVX-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
@@ -204,16 +204,16 @@ class TestCustomPrefix:
runner = CliRunner()
result = runner.invoke(vcm.main, [msg_path])
assert result.exit_code == 1
assert "GRM-N" in result.output
assert "PROJ-N" in result.output
os.unlink(msg_path)
finally:
os.environ.pop("DEVX_TASK_PREFIX", None)
importlib.reload(devx.config)
importlib.reload(vcm)
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"})
def test_feature_branch_rejects_grm_prefix(self) -> None:
"""Feature branch rejects GRM-N: prefix when DEVX_TASK_PREFIX=GRM."""
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_feature_branch_rejects_proj_prefix(self) -> None:
"""Feature branch rejects PROJ-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
import devx.ci.validate_commit_msg as vcm
@@ -222,8 +222,8 @@ class TestCustomPrefix:
importlib.reload(devx.config)
importlib.reload(vcm)
try:
msg_path = self._write_msg("GRM-66: fix: should not have prefix on branch")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="GRM-66-fix"):
msg_path = self._write_msg("PROJ-66: fix: should not have prefix on branch")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="PROJ-66-fix"):
runner = CliRunner()
result = runner.invoke(vcm.main, [msg_path])
assert result.exit_code == 1