Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ff8870b1 | ||
|
|
4c1ecbf4fa | ||
|
|
93a1cb9945 |
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.24.1] - 2026-06-28
|
||||
|
||||
### Refactor
|
||||
|
||||
- Add find_task_by_identifier, config fallbacks for tools
|
||||
|
||||
## [0.24.0] - 2026-06-27
|
||||
|
||||
### Features
|
||||
|
||||
@@ -16,12 +16,12 @@ quality badges.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why devx?
|
||||
|
||||
|
||||
+6
-6
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||
|
||||
__version__ = "0.24.0"
|
||||
__version__ = "0.24.1"
|
||||
|
||||
@@ -407,6 +407,25 @@ class VikunjaClient:
|
||||
r = self._request("GET", f"/projects/{project_id}/tasks", params=params)
|
||||
return r.json()
|
||||
|
||||
def find_task_by_identifier(self, project_id: int, identifier: str, per_page: int = 50) -> dict[str, Any] | None:
|
||||
"""Find a task by its identifier (e.g. ``DEVX-42``) in a project.
|
||||
|
||||
Paginates through all tasks in the project. Returns the task dict
|
||||
or None if not found.
|
||||
"""
|
||||
page = 1
|
||||
while True:
|
||||
tasks = self.list_project_tasks(project_id, page=page, per_page=per_page)
|
||||
if not tasks:
|
||||
break
|
||||
for t in tasks:
|
||||
if t.get("identifier") == identifier:
|
||||
return t
|
||||
if len(tasks) < per_page:
|
||||
break
|
||||
page += 1
|
||||
return None
|
||||
|
||||
def create_task(self, project_id: int, title: str, description: str = "") -> dict[str, Any]:
|
||||
"""Create a task in a project and return the created task dict.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import os
|
||||
import click
|
||||
import requests
|
||||
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||
|
||||
DEFAULT_MAX_RUNNERS = 3
|
||||
|
||||
@@ -151,9 +151,9 @@ def main(
|
||||
token = os.environ.get("CI_GITEA_TOKEN", "")
|
||||
|
||||
if owner is None:
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
|
||||
if repo is None:
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "devx")
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
|
||||
|
||||
count = get_runner_count(GITEA_API_URL, token, owner, repo)
|
||||
indices = generate_indices(count)
|
||||
|
||||
@@ -36,6 +36,7 @@ import time
|
||||
|
||||
import click
|
||||
|
||||
from devx.config import REPO_NAME, REPO_OWNER
|
||||
from devx.i18n import _
|
||||
from devx.molecule.molecule_ci_guard import (
|
||||
poll_for_other_failures,
|
||||
@@ -53,10 +54,10 @@ def cli(pytest_args: tuple[str, ...]) -> None:
|
||||
run_id = int(os.environ.get("RUN_ID", "0"))
|
||||
job_name = os.environ.get("JOB_NAME", "integration-tests")
|
||||
current_index = int(os.environ.get("MATRIX_INDEX", "0"))
|
||||
repository = os.environ.get("GITEA_REPOSITORY", "oblachno-oss/devx")
|
||||
repository = os.environ.get("GITEA_REPOSITORY", "")
|
||||
owner, _sep, repo = repository.partition("/")
|
||||
if not owner or not repo:
|
||||
owner, repo = "oblachno-oss", "devx"
|
||||
owner, repo = REPO_OWNER, REPO_NAME
|
||||
|
||||
if not all([gitea_url, token, run_id]):
|
||||
click.echo(_("GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
|
||||
|
||||
@@ -28,7 +28,7 @@ from pathlib import Path
|
||||
import click
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.config import GITEA_API_URL, REPO_OWNER
|
||||
from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login
|
||||
from devx.i18n import _
|
||||
|
||||
@@ -165,7 +165,7 @@ def _default_gitea_registry_url() -> str:
|
||||
base = base[: -len("/api/v1")]
|
||||
elif base.endswith("/api"):
|
||||
base = base[: -len("/api")]
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
|
||||
return f"{base}/api/packages/{owner}/pypi"
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import click
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
|
||||
from devx.api_clients import GiteaClient
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
@@ -230,8 +230,8 @@ def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
|
||||
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
|
||||
|
||||
if repo is None:
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
|
||||
repo_name = os.environ.get("DEVX_REPO_NAME", "devx")
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
|
||||
repo_name = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
|
||||
else:
|
||||
owner, repo_name = repo.split("/")
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import os
|
||||
import click
|
||||
import requests
|
||||
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||
|
||||
DEFAULT_MAX_RUNNERS = 3
|
||||
|
||||
@@ -145,9 +145,9 @@ def main(
|
||||
token = os.environ.get("CI_GITEA_TOKEN", "")
|
||||
|
||||
if owner is None:
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
|
||||
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
|
||||
if repo is None:
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "devx")
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
|
||||
|
||||
count = get_runner_count(GITEA_API_URL, token, owner, repo)
|
||||
indices = generate_indices(count)
|
||||
|
||||
@@ -43,6 +43,7 @@ from pathlib import Path
|
||||
import click
|
||||
import requests
|
||||
|
||||
from devx.config import REPO_NAME, REPO_OWNER
|
||||
from devx.i18n import _
|
||||
|
||||
POLL_INTERVAL = 10
|
||||
@@ -167,10 +168,10 @@ def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None:
|
||||
run_id = int(os.environ.get("RUN_ID", "0"))
|
||||
job_name = os.environ.get("JOB_NAME", "molecule-tests")
|
||||
current_index = int(os.environ.get("MATRIX_INDEX", "0"))
|
||||
repository = os.environ.get("GITEA_REPOSITORY", "oblachno-oss/devx")
|
||||
repository = os.environ.get("GITEA_REPOSITORY", "")
|
||||
owner, _sep, repo = repository.partition("/")
|
||||
if not owner or not repo:
|
||||
owner, repo = "oblachno-oss", "devx"
|
||||
owner, repo = REPO_OWNER, REPO_NAME
|
||||
|
||||
if not all([gitea_url, token, run_id]):
|
||||
click.echo(_("GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
|
||||
|
||||
@@ -13,7 +13,6 @@ Usage::
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
@@ -27,8 +26,7 @@ def cli() -> None:
|
||||
"""Validate devx configuration in pyproject.toml."""
|
||||
path = Path("pyproject.toml")
|
||||
if not path.exists():
|
||||
click.echo(_("pyproject.toml not found in current directory."))
|
||||
sys.exit(1)
|
||||
raise click.ClickException(_("pyproject.toml not found in current directory."))
|
||||
|
||||
with open(path, "rb") as f: # noqa: PTH123
|
||||
data = tomllib.load(f)
|
||||
@@ -65,7 +63,7 @@ def cli() -> None:
|
||||
if errors:
|
||||
for err in errors:
|
||||
click.echo(f"ERROR: {err}", err=True)
|
||||
sys.exit(1)
|
||||
raise click.ClickException(_("Configuration validation failed."))
|
||||
|
||||
click.echo(_("Configuration OK: [tool.devx] present, devx versions consistent."))
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ Usage::
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from devx.config import _load_pyproject_devx
|
||||
from devx.i18n import _
|
||||
|
||||
@@ -203,49 +203,37 @@ def _find_missing_tests(
|
||||
return missing
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=_("Check that changed files have corresponding tests"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--staged-only",
|
||||
action="store_true",
|
||||
help=_("Only check staged files (for pre-commit)"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warn-only",
|
||||
action="store_true",
|
||||
help=_("Print warnings but always exit 0"),
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
@click.command()
|
||||
@click.option("--staged-only", is_flag=True, help=_("Only check staged files (for pre-commit)"))
|
||||
@click.option("--warn-only", is_flag=True, help=_("Print warnings but always exit 0"))
|
||||
def cli(staged_only: bool, warn_only: bool) -> None:
|
||||
"""Check that changed files have corresponding tests."""
|
||||
repo_root = Path.cwd()
|
||||
rules, skip_patterns, test_indicators, skip_extensions = _load_rules()
|
||||
|
||||
files = _changed_files(args.staged_only, repo_root)
|
||||
files = _changed_files(staged_only, repo_root)
|
||||
if not files:
|
||||
print(_("[check_test_coverage] No changed files to check."))
|
||||
return 0
|
||||
click.echo(_("[check_test_coverage] No changed files to check."))
|
||||
return
|
||||
|
||||
missing = _find_missing_tests(files, repo_root, rules, skip_patterns, test_indicators, skip_extensions)
|
||||
if not missing:
|
||||
print(f"[check_test_coverage] All {len(files)} changed file(s) have tests.")
|
||||
return 0
|
||||
click.echo(f"[check_test_coverage] All {len(files)} changed file(s) have tests.")
|
||||
return
|
||||
|
||||
print("[check_test_coverage] FAILED: missing tests for changed files:\n", file=sys.stderr)
|
||||
click.echo("[check_test_coverage] FAILED: missing tests for changed files:\n", err=True)
|
||||
for f, reason in missing.items():
|
||||
print(f" {f}", file=sys.stderr)
|
||||
print(f" -> {reason}", file=sys.stderr)
|
||||
click.echo(f" {f}", err=True)
|
||||
click.echo(f" -> {reason}", err=True)
|
||||
|
||||
print(
|
||||
"\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
file=sys.stderr,
|
||||
click.echo(
|
||||
_("\n[check_test_coverage] Fix: add the missing test file(s) before committing."),
|
||||
err=True,
|
||||
)
|
||||
|
||||
if args.warn_only:
|
||||
return 0
|
||||
return 1
|
||||
if not warn_only:
|
||||
raise click.ClickException(_("Missing tests for changed files."))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
cli() # pragma: no cover
|
||||
|
||||
@@ -40,7 +40,7 @@ from typing import Any
|
||||
import click
|
||||
import requests
|
||||
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.config import GITEA_API_URL, REPO_OWNER
|
||||
from devx.i18n import _
|
||||
|
||||
|
||||
@@ -151,8 +151,8 @@ def select_for_deletion(
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--owner",
|
||||
required=True,
|
||||
help="Package owner (user or org).",
|
||||
default=None,
|
||||
help="Package owner (user or org, default: from [tool.devx] repo_owner).",
|
||||
)
|
||||
@click.option(
|
||||
"--name",
|
||||
@@ -180,7 +180,7 @@ def select_for_deletion(
|
||||
help="Gitea API URL (defaults to DEVX_GITEA_API_URL or built-in default).",
|
||||
)
|
||||
def main(
|
||||
owner: str,
|
||||
owner: str | None,
|
||||
names: tuple[str, ...],
|
||||
keep: int,
|
||||
dry_run: bool,
|
||||
@@ -190,6 +190,10 @@ def main(
|
||||
token = os.environ.get("CI_GITEA_TOKEN", "")
|
||||
if not token:
|
||||
raise click.ClickException(_("CI_GITEA_TOKEN environment variable required"))
|
||||
if not owner:
|
||||
owner = REPO_OWNER
|
||||
if not owner:
|
||||
raise click.ClickException(_("Package owner not specified. Use --owner or set [tool.devx] repo_owner."))
|
||||
base_url = api_url or GITEA_API_URL
|
||||
|
||||
total_deleted = 0
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Any, cast
|
||||
import click
|
||||
|
||||
from devx.api_clients import GiteaClient
|
||||
from devx.config import GITEA_API_URL, REPO_OWNER
|
||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
@@ -151,7 +151,7 @@ def main(repo: str | None, owner: str | None, branch: str, api_url: str | None)
|
||||
token = os.environ.get("CI_GITEA_TOKEN", "")
|
||||
|
||||
if repo is None:
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "")
|
||||
repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
|
||||
if not repo:
|
||||
raise click.ClickException(_("ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME."))
|
||||
|
||||
|
||||
+10
-18
@@ -76,24 +76,16 @@ def get_vikunja_task_title(task_id: str) -> str:
|
||||
if not token:
|
||||
raise click.ClickException(_("VIKUNJA_TOKEN is not set. Required to derive PR title."))
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
page = 1
|
||||
while True:
|
||||
tasks = client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE)
|
||||
if not tasks:
|
||||
break
|
||||
matches = [t for t in tasks if t.get("identifier") == task_id]
|
||||
if matches:
|
||||
return str(matches[0].get("title", ""))
|
||||
if len(tasks) < DEFAULT_PER_PAGE:
|
||||
break
|
||||
page += 1
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Could not find Vikunja task {task_id} in project {project_id}.",
|
||||
task_id=task_id,
|
||||
project_id=VIKUNJA_PROJECT_ID,
|
||||
),
|
||||
)
|
||||
task = client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE)
|
||||
if not task:
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Could not find Vikunja task {task_id} in project {project_id}.",
|
||||
task_id=task_id,
|
||||
project_id=VIKUNJA_PROJECT_ID,
|
||||
),
|
||||
)
|
||||
return str(task.get("title", ""))
|
||||
|
||||
|
||||
def find_existing_pr(client: GiteaClient, branch: str) -> dict | None:
|
||||
|
||||
@@ -109,7 +109,7 @@ def _generate(prefix: str) -> str:
|
||||
@click.option(
|
||||
"--prefix",
|
||||
default=TASK_PREFIX,
|
||||
help="Task ID prefix for commit preprocessor (default: DEVX_TASK_PREFIX env var or 'DEVX').",
|
||||
help="Task ID prefix for commit preprocessor (default: from [tool.devx] task_prefix in pyproject.toml).",
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
|
||||
@@ -56,7 +56,8 @@ def _download_binary() -> None:
|
||||
TARGET_PATH.chmod(0o755)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@click.command()
|
||||
def cli() -> None:
|
||||
"""Install checkmake if not already present."""
|
||||
if shutil.which("checkmake") is not None:
|
||||
return
|
||||
@@ -66,4 +67,4 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main() # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
|
||||
@@ -59,17 +59,7 @@ def task_exists(task_id: str) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
page = 1
|
||||
while True:
|
||||
tasks = client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE)
|
||||
if not tasks:
|
||||
break
|
||||
if any(t.get("identifier") == task_id for t in tasks):
|
||||
return True
|
||||
if len(tasks) < DEFAULT_PER_PAGE:
|
||||
break
|
||||
page += 1
|
||||
return False
|
||||
return client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE) is not None
|
||||
|
||||
|
||||
def validate(branch: str) -> None:
|
||||
|
||||
@@ -463,14 +463,6 @@
|
||||
"ru": "Bumping version: {current} -> v{new_version}",
|
||||
"zh": "Bumping version: {current} -> v{new_version}"
|
||||
},
|
||||
"Check that changed files have corresponding tests": {
|
||||
"bg": "Check that changed files have corresponding tests",
|
||||
"de": "Check that changed files have corresponding tests",
|
||||
"en": "Check that changed files have corresponding tests",
|
||||
"pl": "Check that changed files have corresponding tests",
|
||||
"ru": "Check that changed files have corresponding tests",
|
||||
"zh": "Check that changed files have corresponding tests"
|
||||
},
|
||||
"Checking CLI command documentation...": {
|
||||
"bg": "Checking CLI command documentation...",
|
||||
"de": "Checking CLI command documentation...",
|
||||
@@ -2190,5 +2182,37 @@
|
||||
"pl": "Waiting for CI checks to complete (timeout: {timeout}s)...",
|
||||
"ru": "Waiting for CI checks to complete (timeout: {timeout}s)...",
|
||||
"zh": "Waiting for CI checks to complete (timeout: {timeout}s)..."
|
||||
},
|
||||
"\n[check_test_coverage] Fix: add the missing test file(s) before committing.": {
|
||||
"en": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
"bg": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
"de": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
"pl": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
"ru": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
|
||||
"zh": "\n[check_test_coverage] Fix: add the missing test file(s) before committing."
|
||||
},
|
||||
"Package owner not specified. Use --owner or set [tool.devx] repo_owner.": {
|
||||
"en": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
|
||||
"bg": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
|
||||
"de": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
|
||||
"pl": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
|
||||
"ru": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
|
||||
"zh": "Package owner not specified. Use --owner or set [tool.devx] repo_owner."
|
||||
},
|
||||
"Configuration validation failed.": {
|
||||
"en": "Configuration validation failed.",
|
||||
"bg": "Configuration validation failed.",
|
||||
"de": "Configuration validation failed.",
|
||||
"pl": "Configuration validation failed.",
|
||||
"ru": "Configuration validation failed.",
|
||||
"zh": "Configuration validation failed."
|
||||
},
|
||||
"Missing tests for changed files.": {
|
||||
"en": "Missing tests for changed files.",
|
||||
"bg": "Missing tests for changed files.",
|
||||
"de": "Missing tests for changed files.",
|
||||
"pl": "Missing tests for changed files.",
|
||||
"ru": "Missing tests for changed files.",
|
||||
"zh": "Missing tests for changed files."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,6 +781,42 @@ class TestVikunjaClient:
|
||||
call_kwargs = client._session.request.call_args.kwargs
|
||||
assert call_kwargs["json"]["description"] == ""
|
||||
|
||||
def test_find_task_by_identifier_found(self) -> None:
|
||||
client = VikunjaClient("https://work.example.com", "tok")
|
||||
client._session.request = MagicMock(
|
||||
return_value=_mock_response([{"identifier": "DEVX-1"}, {"identifier": "DEVX-42", "title": "Found"}])
|
||||
)
|
||||
result = client.find_task_by_identifier(6, "DEVX-42", per_page=50)
|
||||
assert result is not None
|
||||
assert result["title"] == "Found"
|
||||
|
||||
def test_find_task_by_identifier_not_found(self) -> None:
|
||||
client = VikunjaClient("https://work.example.com", "tok")
|
||||
client._session.request = MagicMock(
|
||||
return_value=_mock_response([{"identifier": "DEVX-1"}, {"identifier": "DEVX-2"}])
|
||||
)
|
||||
result = client.find_task_by_identifier(6, "DEVX-99", per_page=50)
|
||||
assert result is None
|
||||
|
||||
def test_find_task_by_identifier_empty_project(self) -> None:
|
||||
client = VikunjaClient("https://work.example.com", "tok")
|
||||
client._session.request = MagicMock(return_value=_mock_response([]))
|
||||
result = client.find_task_by_identifier(6, "DEVX-1", per_page=50)
|
||||
assert result is None
|
||||
|
||||
def test_find_task_by_identifier_paginates(self) -> None:
|
||||
client = VikunjaClient("https://work.example.com", "tok")
|
||||
full_page = [{"identifier": f"DEVX-{i}"} for i in range(50)]
|
||||
client._session.request = MagicMock(
|
||||
side_effect=[
|
||||
_mock_response(full_page),
|
||||
_mock_response([{"identifier": "DEVX-50", "title": "Found on page 2"}]),
|
||||
]
|
||||
)
|
||||
result = client.find_task_by_identifier(6, "DEVX-50", per_page=50)
|
||||
assert result is not None
|
||||
assert result["title"] == "Found on page 2"
|
||||
|
||||
|
||||
class TestIsRetryable:
|
||||
def test_connection_error_is_retryable(self) -> None:
|
||||
|
||||
@@ -705,4 +705,33 @@ class TestCLICleanImages:
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "FAILED" in result.output
|
||||
assert "failed" in result.output.lower()
|
||||
|
||||
@patch("devx.tools.clean_images.REPO_OWNER", "oblachno-oss")
|
||||
def test_owner_from_config(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = []
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--name", "ci-base", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "oblachno-oss/ci-base" in result.output
|
||||
|
||||
@patch("devx.tools.clean_images.REPO_OWNER", "")
|
||||
def test_no_owner_raises(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--name", "ci-base"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "owner" in result.output.lower()
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.tools.check_test_coverage import (
|
||||
BUILTIN_RULES,
|
||||
DEFAULT_SKIP_EXTENSIONS,
|
||||
@@ -13,7 +15,7 @@ from devx.tools.check_test_coverage import (
|
||||
_load_rules,
|
||||
_resolve_test_path,
|
||||
_should_skip_file,
|
||||
main,
|
||||
cli,
|
||||
)
|
||||
|
||||
|
||||
@@ -201,7 +203,10 @@ class TestMain:
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 0
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert "No changed files" in result.output
|
||||
|
||||
def test_all_have_tests(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "scripts" / "tests").mkdir(parents=True)
|
||||
@@ -214,7 +219,10 @@ class TestMain:
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 0
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert "have tests" in result.output
|
||||
|
||||
def test_missing_test_returns_1(self, tmp_path: Path) -> None:
|
||||
with (
|
||||
@@ -225,7 +233,9 @@ class TestMain:
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 1
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, [])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_warn_only_returns_0(self, tmp_path: Path) -> None:
|
||||
with (
|
||||
@@ -236,4 +246,6 @@ class TestMain:
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main(["--warn-only"]) == 0
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--warn-only"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
@@ -145,6 +145,7 @@ class TestMain:
|
||||
assert "CI_GITEA_TOKEN" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.tools.configure_repo.REPO_NAME", "")
|
||||
def test_main_no_repo(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
@@ -197,3 +198,19 @@ class TestMain:
|
||||
call_args = mock_client_cls.call_args
|
||||
assert call_args[0][2] == "oblachno" # owner
|
||||
assert call_args[0][3] == "infra" # 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.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."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
runner = CliRunner()
|
||||
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][3] == "devx" # repo
|
||||
|
||||
@@ -56,7 +56,7 @@ class TestGetVikunjaTaskTitle:
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_found(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = [{"identifier": "DEVX-42", "title": "Add feature"}]
|
||||
mock_client.find_task_by_identifier.return_value = {"identifier": "DEVX-42", "title": "Add feature"}
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert get_vikunja_task_title("DEVX-42") == "Add feature"
|
||||
|
||||
@@ -69,20 +69,7 @@ class TestGetVikunjaTaskTitle:
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_not_found(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = []
|
||||
mock_client_cls.return_value = mock_client
|
||||
with pytest.raises(click.ClickException, match="Could not find"):
|
||||
get_vikunja_task_title("DEVX-42")
|
||||
|
||||
@patch("devx.tools.create_pr.VikunjaClient")
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_pagination_not_found(self, mock_client_cls: MagicMock) -> None:
|
||||
from devx.config import DEFAULT_PER_PAGE
|
||||
|
||||
mock_client = MagicMock()
|
||||
page1 = [{"identifier": f"OTHER-{i}"} for i in range(DEFAULT_PER_PAGE)]
|
||||
page2 = [{"identifier": "OTHER-99"}]
|
||||
mock_client.list_project_tasks.side_effect = [page1, page2]
|
||||
mock_client.find_task_by_identifier.return_value = None
|
||||
mock_client_cls.return_value = mock_client
|
||||
with pytest.raises(click.ClickException, match="Could not find"):
|
||||
get_vikunja_task_title("DEVX-42")
|
||||
|
||||
@@ -63,13 +63,19 @@ class TestDownloadBinary:
|
||||
|
||||
class TestMain:
|
||||
def test_already_installed(self) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
with patch("shutil.which", return_value="/usr/bin/checkmake"):
|
||||
install_checkmake.main()
|
||||
runner = CliRunner()
|
||||
runner.invoke(install_checkmake.cli, [])
|
||||
|
||||
def test_install_with_go(self) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
with patch("shutil.which", side_effect=[None, "/usr/bin/go"]):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
install_checkmake.main()
|
||||
runner = CliRunner()
|
||||
runner.invoke(install_checkmake.cli, [])
|
||||
mock_run.assert_called_once_with(
|
||||
[
|
||||
"/usr/bin/go",
|
||||
@@ -80,6 +86,8 @@ class TestMain:
|
||||
)
|
||||
|
||||
def test_download_when_no_go(self, tmp_path: Path) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
target = tmp_path / "checkmake"
|
||||
|
||||
def _write_file(url: str, path: str) -> tuple[str, None]:
|
||||
@@ -90,5 +98,6 @@ class TestMain:
|
||||
with patch("shutil.which", side_effect=[None, None]):
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
with patch("urllib.request.urlretrieve", side_effect=_write_file) as mock_retrieve:
|
||||
install_checkmake.main()
|
||||
runner = CliRunner()
|
||||
runner.invoke(install_checkmake.cli, [])
|
||||
mock_retrieve.assert_called_once()
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestTaskExists:
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_found(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = [{"identifier": "DEVX-42"}]
|
||||
mock_client.find_task_by_identifier.return_value = {"identifier": "DEVX-42"}
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert task_exists("DEVX-42") is True
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestTaskExists:
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_not_found(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = [{"identifier": "DEVX-99"}]
|
||||
mock_client.find_task_by_identifier.return_value = None
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert task_exists("DEVX-42") is False
|
||||
|
||||
@@ -59,37 +59,6 @@ class TestTaskExists:
|
||||
def test_no_token(self) -> None:
|
||||
assert task_exists("DEVX-42") is False
|
||||
|
||||
@patch("devx.tools.pre_push_check.VikunjaClient")
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_pagination(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
# First page: full page (50 items, none matching), second page: match
|
||||
page1 = [{"identifier": f"OTHER-{i}"} for i in range(50)]
|
||||
page2 = [{"identifier": "DEVX-42"}]
|
||||
mock_client.list_project_tasks.side_effect = [page1, page2]
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert task_exists("DEVX-42") is True
|
||||
|
||||
@patch("devx.tools.pre_push_check.VikunjaClient")
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_empty_project(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = []
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert task_exists("DEVX-42") is False
|
||||
|
||||
@patch("devx.tools.pre_push_check.VikunjaClient")
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_pagination_not_found(self, mock_client_cls: MagicMock) -> None:
|
||||
from devx.config import DEFAULT_PER_PAGE
|
||||
|
||||
mock_client = MagicMock()
|
||||
page1 = [{"identifier": f"OTHER-{i}"} for i in range(DEFAULT_PER_PAGE)]
|
||||
page2 = [{"identifier": "OTHER-99"}]
|
||||
mock_client.list_project_tasks.side_effect = [page1, page2]
|
||||
mock_client_cls.return_value = mock_client
|
||||
assert task_exists("DEVX-42") is False
|
||||
|
||||
|
||||
class TestValidate:
|
||||
def test_master_branch_skips(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user