From 34742bab4078cfe18c75a65e38eb5fa0fbd3e584 Mon Sep 17 00:00:00 2001 From: emil Date: Mon, 6 Jul 2026 06:06:13 +0000 Subject: [PATCH] GRM-136: refactor: rename PyPI package from gitea-runner-manager to grm --- .devin/agents/ci-investigator/AGENT.md | 2 +- .env.example | 2 +- .gitea/workflows/ci.yml | 4 +- .gitea/workflows/post-merge.yml | 2 +- .pre-commit-config.yaml | 4 +- AGENTS.md | 22 +-- Makefile | 4 +- README.md | 6 +- docs/tech/architecture.md | 6 +- docs/tech/ci-cd-workflow.md | 6 +- docs/tech/contributing.md | 6 +- docs/tech/decision-log.md | 10 +- docs/tech/development-setup.md | 4 +- docs/tech/testing-strategy.md | 4 +- docs/user/cli-commands.md | 2 +- docs/user/faq.md | 2 +- docs/user/installation.md | 4 +- hooks/pre-push | 2 +- pyproject.toml | 16 +- src/{gitea_runner_manager => grm}/__init__.py | 0 src/{gitea_runner_manager => grm}/cli.py | 0 .../exceptions.py | 0 src/{gitea_runner_manager => grm}/executor.py | 0 .../gitea_client.py | 0 src/{gitea_runner_manager => grm}/i18n.py | 0 .../logging_config.py | 0 src/{gitea_runner_manager => grm}/registry.py | 0 src/{gitea_runner_manager => grm}/report.py | 0 .../runner_manager.py | 0 .../translations.json | 0 src/{gitea_runner_manager => grm}/ui.py | 0 tests/integration/test_lifecycle.py | 4 +- tests/integration/test_multi_instance.py | 6 +- tests/unit/test_cli.py | 164 +++++++++--------- tests/unit/test_executor.py | 18 +- tests/unit/test_gitea_client.py | 2 +- tests/unit/test_i18n.py | 2 +- tests/unit/test_logging_config.py | 10 +- tests/unit/test_registry.py | 4 +- tests/unit/test_report.py | 16 +- tests/unit/test_runner_manager.py | 4 +- tests/unit/test_ui.py | 16 +- uv.lock | 2 +- 43 files changed, 178 insertions(+), 178 deletions(-) rename src/{gitea_runner_manager => grm}/__init__.py (100%) rename src/{gitea_runner_manager => grm}/cli.py (100%) rename src/{gitea_runner_manager => grm}/exceptions.py (100%) rename src/{gitea_runner_manager => grm}/executor.py (100%) rename src/{gitea_runner_manager => grm}/gitea_client.py (100%) rename src/{gitea_runner_manager => grm}/i18n.py (100%) rename src/{gitea_runner_manager => grm}/logging_config.py (100%) rename src/{gitea_runner_manager => grm}/registry.py (100%) rename src/{gitea_runner_manager => grm}/report.py (100%) rename src/{gitea_runner_manager => grm}/runner_manager.py (100%) rename src/{gitea_runner_manager => grm}/translations.json (100%) rename src/{gitea_runner_manager => grm}/ui.py (100%) diff --git a/.devin/agents/ci-investigator/AGENT.md b/.devin/agents/ci-investigator/AGENT.md index f2ae0f7..1720c4e 100644 --- a/.devin/agents/ci-investigator/AGENT.md +++ b/.devin/agents/ci-investigator/AGENT.md @@ -99,7 +99,7 @@ Focus on the FIRST error. **Release failures:** - **git-cliff errors**: version calculation, no unreleased changes - **Lint/test during release**: release runs `make lint-ruff` and `make pytest-cov` -- **Tag/commit misalignment**: check `src/gitea_runner_manager/__init__.py` version +- **Tag/commit misalignment**: check `src/grm/__init__.py` version **Publish failures:** - **PyPI publish**: registry auth, package build errors diff --git a/.env.example b/.env.example index 9a79363..50ba187 100644 --- a/.env.example +++ b/.env.example @@ -60,4 +60,4 @@ DEVX_TASK_PREFIX=GRM # Vikunja project ID for GRM DEVX_VIKUNJA_PROJECT_ID=6 # Version file path (relative to repo root) -DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py +DEVX_VERSION_FILE=src/grm/__init__.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 03e2fe5..1e0343d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - name: Translation completeness check run: | . .venv/bin/activate 2>/dev/null || true - python3 -m devx.ci.check_translations --translations src/gitea_runner_manager/translations.json + python3 -m devx.ci.check_translations --translations src/grm/translations.json - name: Check unit test speed env: PYTHONPATH: src @@ -82,7 +82,7 @@ jobs: - name: Release dry-run validation env: PYTHONPATH: src - DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py + DEVX_VERSION_FILE: src/grm/__init__.py DEVX_TASK_PREFIX: GRM run: | . .venv/bin/activate 2>/dev/null || true diff --git a/.gitea/workflows/post-merge.yml b/.gitea/workflows/post-merge.yml index c9c2ec3..c730a58 100644 --- a/.gitea/workflows/post-merge.yml +++ b/.gitea/workflows/post-merge.yml @@ -110,7 +110,7 @@ jobs: id: release-tag env: PYTHONPATH: src - DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py + DEVX_VERSION_FILE: src/grm/__init__.py DEVX_TASK_PREFIX: GRM DEVX_VIKUNJA_PROJECT_ID: 6 run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab11048..99314f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,9 +75,9 @@ repos: - id: check-translations name: translation completeness check - entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.check_translations --translations src/gitea_runner_manager/translations.json + entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.check_translations --translations src/grm/translations.json language: system - files: ^src/gitea_runner_manager/translations\.json$ + files: ^src/grm/translations\.json$ pass_filenames: false stages: [pre-commit] diff --git a/AGENTS.md b/AGENTS.md index dc59e39..809d32c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is ## Architecture -- **Python CLI** (`src/gitea_runner_manager/`) — Click-based CLI that delegates to Ansible +- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible - **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup - **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications - **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits @@ -232,7 +232,7 @@ Vikunja task updates: `AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes. - Uses **git-cliff** to calculate the next semver version from conventional commits - - Updates `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth) + - Updates `__version__` in `src/grm/__init__.py` (single source of truth) - Updates `CHANGELOG.md` with the new version section - **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy - If lint or tests fail, **aborts immediately** — no commit, no tag @@ -284,7 +284,7 @@ via `[tool.devx.classify]` in `pyproject.toml`. - `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts **User-facing paths** (tool changes → release needed) — everything else: -- `src/gitea_runner_manager/**` — Python CLI source (except `__init__.py`) +- `src/grm/**` — Python CLI source (except `__init__.py`) - `ansible/**` — Ansible role - `pyproject.toml` — Package metadata - Any new file type not in the allowlist @@ -318,14 +318,14 @@ The codebase enforces strict separation between the GRM tool and the devx packag | Directory | Purpose | Release impact | |-----------|---------|----------------| -| `src/gitea_runner_manager/` | User-facing GRM CLI tool | Changes trigger release | +| `src/grm/` | User-facing GRM CLI tool | Changes trigger release | | `devx` package (installed from git) | Reusable CI/CD and dev tools | Not in this repo (no release impact) | | `ansible/` | Ansible role for runner setup | Changes trigger release | ### Import Rules -1. **`src/gitea_runner_manager/` NEVER imports from devx** — the GRM tool is self-contained -2. **devx MAY import from `gitea_runner_manager`** — one-way dependency (devx uses the tool's API clients, config, i18n) +1. **`src/grm/` NEVER imports from devx** — the GRM tool is self-contained +2. **devx MAY import from `grm`** — one-way dependency (devx uses the tool's API clients, config, i18n) 3. **Cross-module imports within devx** are allowed (devx modules importing from other devx modules) and must be documented 4. **`devx.gitea_cli`** is a shared wrapper around the `tea` CLI — devx modules import from it for Gitea API operations (issues, labels, PRs, releases, reviews) @@ -355,11 +355,11 @@ The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is insta ### PYTHONPATH Configuration -Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `gitea_runner_manager`: +Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `grm`: | PYTHONPATH | When to use | Example modules | |------------|-------------|-----------------| -| `src` | Module imports from `gitea_runner_manager` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.pr_review`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` | +| `src` | Module imports from `grm` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.pr_review`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` | | (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.molecule.molecule_ci_guard`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` | **In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`): @@ -370,7 +370,7 @@ Since devx is installed as a package (via `pip install` from git), it is importa run: python -m devx.ci.example ``` -**Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `gitea_runner_manager`. +**Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `grm`. ### Shared Constants @@ -403,7 +403,7 @@ ensures all merged work appears in the changelog. | `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) | | `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) | -The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version. +The version source is `__version__` in `src/grm/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version. ### Title Format Summary @@ -419,7 +419,7 @@ The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, r The devx package is configured via `DEVX_*` environment variables: - `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers - `DEVX_VIKUNJA_PROJECT_ID=6` — Vikunja project ID for task tracking -- `DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py` — Path to the version source file +- `DEVX_VERSION_FILE=src/grm/__init__.py` — Path to the version source file Change classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`, which defines the infrastructure and user-facing path patterns. diff --git a/Makefile b/Makefile index a8a5246..138d4a9 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ all: setup DEVX_PYTHON := $(BIN)/python DEVX_VENV := $(VENV) DEVX_BIN := $(BIN) -DEVX_COV_PKG := src/gitea_runner_manager +DEVX_COV_PKG := src/grm DEVX_TEST_PATHS := tests/ scripts/tests/ DEVX_LINT_PATHS := src/ scripts/ tests/ @@ -152,7 +152,7 @@ test-unit: devx-test-unit # Override devx-pytest-cov to cover both src/ and scripts/ pytest-cov: - @$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100 + @$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/grm --cov=scripts --cov-report=term-missing --cov-fail-under=100 workflow-lint: devx-workflow-lint workflow-dryrun: devx-workflow-dryrun workflow-check: devx-workflow-check diff --git a/README.md b/README.md index 14a2ac5..84d6caa 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ The registry is publicly readable — no authentication required to install. **Quick install (one-off):** ```bash -pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple +pip install grm --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple ``` **Persistent configuration (recommended):** @@ -115,7 +115,7 @@ extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/py Then install normally: ```bash -pip install gitea-runner-manager +pip install grm ``` This installs the `grm` CLI and its Python dependencies. The Ansible playbooks @@ -339,7 +339,7 @@ See the [Development Setup](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/w GRM consists of two layers: -1. **Python CLI** (`src/gitea_runner_manager/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list. +1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list. 2. **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer. diff --git a/docs/tech/architecture.md b/docs/tech/architecture.md index b47125b..f696678 100644 --- a/docs/tech/architecture.md +++ b/docs/tech/architecture.md @@ -2,7 +2,7 @@ GRM consists of two layers: -1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. +1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. 2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea. ## High-Level Design @@ -95,7 +95,7 @@ Lingering is enabled via `loginctl enable-linger` so the user's systemd services ```mermaid flowchart TD - CLI["Python CLI
src/gitea_runner_manager/
(Click)"] + CLI["Python CLI
src/grm/
(Click)"] RM["RunnerManager
runner_manager.py"] EXEC["Executor
executor.py"] REG["Registry
registry.py
~/.local/share/grm/runners.json"] @@ -207,7 +207,7 @@ From `AGENTS.md`, the project also includes: ## Python Modules -The Python CLI layer (`src/gitea_runner_manager/`) consists of the following modules: +The Python CLI layer (`src/grm/`) consists of the following modules: | Module | Description | |--------|-------------| diff --git a/docs/tech/ci-cd-workflow.md b/docs/tech/ci-cd-workflow.md index aebdce4..95497a4 100644 --- a/docs/tech/ci-cd-workflow.md +++ b/docs/tech/ci-cd-workflow.md @@ -158,7 +158,7 @@ After a PR is merged to master, the release pipeline runs automatically. - Runs `devx.ci.release` which uses **git-cliff** to: - **Checks for user-facing changes** via `devx.ci.classify_changes` — if only workflow/infrastructure files changed, the release is **skipped entirely** — no version bump, no tag, no publish - Calculate the next semver version from conventional commits since the last tag - - Update `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth) + - Update `__version__` in `src/grm/__init__.py` (single source of truth) - Update `CHANGELOG.md` with the new version section - **Run `make lint-ruff` and `make pytest-cov`** to verify the release is healthy - If lint or tests fail, **abort immediately** — no commit, no tag @@ -208,7 +208,7 @@ from accidentally skipping releases. Classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. **User-facing paths** (tool changes → release needed): -- `src/gitea_runner_manager/**` — Python CLI source +- `src/grm/**` — Python CLI source - `ansible/**` — Ansible role - `pyproject.toml` — Package metadata @@ -322,7 +322,7 @@ From `cliff.toml` `[bump]` section: - `breaking_always_bump_major = false` - `initial_tag = "0.1.0"` -The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version. +The version source is `__version__` in `src/grm/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version. ## Title Format Summary diff --git a/docs/tech/contributing.md b/docs/tech/contributing.md index 8f1bc1c..623257b 100644 --- a/docs/tech/contributing.md +++ b/docs/tech/contributing.md @@ -27,9 +27,9 @@ - **Secrets handling**: Secrets are passed via temp JSON files with `0600` permissions, never on the command line (CWE-214). Extra-vars are written to a temporary JSON file and passed via `--extra-vars @tempfile`, which is deleted after execution. This prevents secrets from being visible in the process list (`ps aux`). - **Linting**: `make lint-all` runs ruff + pyright + bandit + ansible-lint + checkmake + actionlint - **Formatting**: `ruff format` with double quotes and space indentation -- **Type checking**: `pyright` in strict mode for `src/gitea_runner_manager/` +- **Type checking**: `pyright` in strict mode for `src/grm/` - **Security scanning**: `bandit -r src/` on every PR -- **Import rules**: `src/gitea_runner_manager/` NEVER imports from devx — the GRM tool is self-contained +- **Import rules**: `src/grm/` NEVER imports from devx — the GRM tool is self-contained ## Commit Rules @@ -216,7 +216,7 @@ Not all changes require a new release. The project classifies changes using `dev - Lint config files, `.env.example`, `.gitignore` **User-facing paths** (release needed): -- `src/gitea_runner_manager/**` (except `__init__.py`) +- `src/grm/**` (except `__init__.py`) - `ansible/**` - `pyproject.toml` diff --git a/docs/tech/decision-log.md b/docs/tech/decision-log.md index 06b3e7c..5abe4f7 100644 --- a/docs/tech/decision-log.md +++ b/docs/tech/decision-log.md @@ -8,7 +8,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and ` **Date:** 2026-06-21 (v0.2.0 unreleased) -**Decision:** Use `dynamic = ["version"]` in `pyproject.toml` with setuptools `attr` to source the version from `__version__` in `src/gitea_runner_manager/__init__.py`. +**Decision:** Use `dynamic = ["version"]` in `pyproject.toml` with setuptools `attr` to source the version from `__version__` in `src/grm/__init__.py`. **Rationale:** `__init__.py` is the single source of truth for the version. The release script (`devx.ci.release`) only updates `__init__.py` — there is no need to touch `pyproject.toml`. `grm --version` reports this version directly. This eliminates version duplication across files and ensures the runtime version always matches the tagged release. @@ -84,7 +84,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and ` **Rationale:** Passing secrets as command-line arguments (e.g., `--extra-vars '{"token": "..."}'`) makes them visible in the process list (`ps aux`), which is a known security weakness (CWE-214). The `RunnerManager._extra_vars_file()` context manager writes extra-vars to a temporary file via `tempfile.mkstemp()`, sets permissions to `0600`, passes the file to Ansible via `--extra-vars @tempfile`, and deletes the file in a `finally` block — even if an exception occurs. This ensures secrets are never visible in the process list. -**Source:** `AGENTS.md` (Key Conventions), `src/gitea_runner_manager/runner_manager.py` (`_extra_vars_file` method) +**Source:** `AGENTS.md` (Key Conventions), `src/grm/runner_manager.py` (`_extra_vars_file` method) --- @@ -94,7 +94,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and ` **Decision:** Classify changed files into user-facing and workflow-only categories using `devx.ci.classify_changes`. Only user-facing changes trigger a release; workflow-only changes (CI, docs, tests, lint config) do not. -**Rationale:** Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include `src/gitea_runner_manager/**` (except `__init__.py`) and `ansible/**`. Workflow-only paths include `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`, and various config files. +**Rationale:** Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include `src/grm/**` (except `__init__.py`) and `ansible/**`. Workflow-only paths include `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`, and various config files. **Source:** `AGENTS.md` (Smart CI: User-Facing vs Workflow-Only Changes), `pyproject.toml` (`[tool.devx.classify]`) @@ -104,9 +104,9 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and ` **Date:** 2026-06-21 (v0.6.2) -**Decision:** Separate CI/CD and development tooling into the `devx` package (installed from git), keeping the GRM tool itself self-contained in `src/gitea_runner_manager/`. +**Decision:** Separate CI/CD and development tooling into the `devx` package (installed from git), keeping the GRM tool itself self-contained in `src/grm/`. -**Rationale:** The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from `gitea_runner_manager` (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the `scripts/` directory to the devx package in GRM-64. +**Rationale:** The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from `grm` (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the `scripts/` directory to the devx package in GRM-64. **Source:** `AGENTS.md` (Source Code Separation and devx Integration), `CHANGELOG.md` (0.6.2 — Refactor: "Migrate from scripts/ to devx package") diff --git a/docs/tech/development-setup.md b/docs/tech/development-setup.md index a741518..fb56a23 100644 --- a/docs/tech/development-setup.md +++ b/docs/tech/development-setup.md @@ -4,7 +4,7 @@ ``` . -├── src/gitea_runner_manager/ # Python CLI source +├── src/grm/ # Python CLI source │ ├── cli.py # Click commands │ ├── runner_manager.py # Ansible orchestration + registry integration │ ├── executor.py # Ansible subprocess execution @@ -172,7 +172,7 @@ make test-unit # Without coverage make pytest-cov # With 100% coverage enforcement ``` -The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/gitea_runner_manager/`. +The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. ### Integration tests diff --git a/docs/tech/testing-strategy.md b/docs/tech/testing-strategy.md index e0d090d..081f288 100644 --- a/docs/tech/testing-strategy.md +++ b/docs/tech/testing-strategy.md @@ -14,9 +14,9 @@ Runs pytest with 100% coverage requirement. From the `Makefile`: - `test-unit` — `pytest tests/unit/ -v --no-cov` (unit tests without coverage) -- `pytest-cov` — `pytest tests/ -v --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement) +- `pytest-cov` — `pytest tests/ -v --cov=src/grm --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement) -The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/gitea_runner_manager/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release. +The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release. ### Test speed verification diff --git a/docs/user/cli-commands.md b/docs/user/cli-commands.md index d447062..7c632fb 100644 --- a/docs/user/cli-commands.md +++ b/docs/user/cli-commands.md @@ -374,7 +374,7 @@ Show the installed GRM version. grm --version ``` -This reports the version from `__version__` in `src/gitea_runner_manager/__init__.py`, which is the single source of truth set by the automated release pipeline. +This reports the version from `__version__` in `src/grm/__init__.py`, which is the single source of truth set by the automated release pipeline. ## Environment Variables diff --git a/docs/user/faq.md b/docs/user/faq.md index c10b733..fe3af17 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -156,7 +156,7 @@ Yes. GRM is designed with security as a first-class concern: Yes: ```bash -pip install gitea-runner-manager +pip install grm ``` This installs the `grm` CLI and its Python dependencies. The Ansible playbooks and role are bundled with the package. For development or access to Make targets, clone the repository instead. diff --git a/docs/user/installation.md b/docs/user/installation.md index ff7740f..d2ce7a2 100644 --- a/docs/user/installation.md +++ b/docs/user/installation.md @@ -58,7 +58,7 @@ The registry is publicly readable — no authentication required to install. **Quick install (one-off):** ```bash -pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple +pip install grm --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple ``` **Persistent configuration (recommended):** @@ -73,7 +73,7 @@ extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/py Then install normally: ```bash -pip install gitea-runner-manager +pip install grm ``` This installs the `grm` CLI and its Python dependencies. The Ansible playbooks diff --git a/hooks/pre-push b/hooks/pre-push index 806cf7c..e248c77 100755 --- a/hooks/pre-push +++ b/hooks/pre-push @@ -36,7 +36,7 @@ elif [ -x "${HOME}/.pyenv/bin/pyenv" ]; then export PATH="${PYENV_ROOT}/bin:${PYENV_ROOT}/shims:${PATH}" eval "$("${PYENV_ROOT}/bin/pyenv" init -)" 2>/dev/null || true eval "$("${PYENV_ROOT}/bin/pyenv" virtualenv-init -)" 2>/dev/null || true - pyenv activate gitea-runner-manager 2>/dev/null || true + pyenv activate grm 2>/dev/null || true PY=python3 else PY=python3 diff --git a/pyproject.toml b/pyproject.toml index a43bac3..f39268f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "gitea-runner-manager" +name = "grm" dynamic = ["version"] description = "Lean CLI to manage Gitea Actions runners" readme = "README.md" @@ -20,10 +20,10 @@ dependencies = [ ] [project.scripts] -grm = "gitea_runner_manager.cli:cli" +grm = "grm.cli:cli" [tool.setuptools.dynamic] -version = {attr = "gitea_runner_manager.__version__"} +version = {attr = "grm.__version__"} [project.optional-dependencies] # Minimal deps for CI scripts that only need click/dotenv @@ -52,7 +52,7 @@ molecule = [ ] # Full dev environment (local development, includes everything) dev = [ - "gitea-runner-manager[ci,lint,molecule]", + "grm[ci,lint,molecule]", # Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr) "devx==0.33.3", # Non-Python dev dependency: checkmake (Makefile linter) @@ -63,12 +63,12 @@ dev = [ where = ["src"] [tool.setuptools.package-data] -gitea_runner_manager = ["translations.json"] +grm = ["translations.json"] [tool.pytest.ini_options] testpaths = ["tests", "scripts/tests"] pythonpath = ["src", "scripts"] -addopts = "--cov=src/gitea_runner_manager --cov=scripts/prune_runner_images.py --cov-report=term-missing --cov-fail-under=100" +addopts = "--cov=src/grm --cov=scripts/prune_runner_images.py --cov-report=term-missing --cov-fail-under=100" markers = [ "integration: marks tests as integration tests (not counted in coverage)", ] @@ -88,7 +88,7 @@ indent-style = "space" [tool.pyright] include = ["src"] pythonVersion = "3.12" -strict = ["src/gitea_runner_manager"] +strict = ["src/grm"] # --------------------------------------------------------------------------- # Change classification — determines which changes trigger a release @@ -128,7 +128,7 @@ infrastructure = ["scripts/**"] # but are actually infrastructure: # - __init__.py: only contains __version__ (set by release.py, not user code) infrastructure_overrides = [ - "src/gitea_runner_manager/__init__.py", + "src/grm/__init__.py", ] # User-facing overrides — safety override for broad infrastructure patterns diff --git a/src/gitea_runner_manager/__init__.py b/src/grm/__init__.py similarity index 100% rename from src/gitea_runner_manager/__init__.py rename to src/grm/__init__.py diff --git a/src/gitea_runner_manager/cli.py b/src/grm/cli.py similarity index 100% rename from src/gitea_runner_manager/cli.py rename to src/grm/cli.py diff --git a/src/gitea_runner_manager/exceptions.py b/src/grm/exceptions.py similarity index 100% rename from src/gitea_runner_manager/exceptions.py rename to src/grm/exceptions.py diff --git a/src/gitea_runner_manager/executor.py b/src/grm/executor.py similarity index 100% rename from src/gitea_runner_manager/executor.py rename to src/grm/executor.py diff --git a/src/gitea_runner_manager/gitea_client.py b/src/grm/gitea_client.py similarity index 100% rename from src/gitea_runner_manager/gitea_client.py rename to src/grm/gitea_client.py diff --git a/src/gitea_runner_manager/i18n.py b/src/grm/i18n.py similarity index 100% rename from src/gitea_runner_manager/i18n.py rename to src/grm/i18n.py diff --git a/src/gitea_runner_manager/logging_config.py b/src/grm/logging_config.py similarity index 100% rename from src/gitea_runner_manager/logging_config.py rename to src/grm/logging_config.py diff --git a/src/gitea_runner_manager/registry.py b/src/grm/registry.py similarity index 100% rename from src/gitea_runner_manager/registry.py rename to src/grm/registry.py diff --git a/src/gitea_runner_manager/report.py b/src/grm/report.py similarity index 100% rename from src/gitea_runner_manager/report.py rename to src/grm/report.py diff --git a/src/gitea_runner_manager/runner_manager.py b/src/grm/runner_manager.py similarity index 100% rename from src/gitea_runner_manager/runner_manager.py rename to src/grm/runner_manager.py diff --git a/src/gitea_runner_manager/translations.json b/src/grm/translations.json similarity index 100% rename from src/gitea_runner_manager/translations.json rename to src/grm/translations.json diff --git a/src/gitea_runner_manager/ui.py b/src/grm/ui.py similarity index 100% rename from src/gitea_runner_manager/ui.py rename to src/grm/ui.py diff --git a/tests/integration/test_lifecycle.py b/tests/integration/test_lifecycle.py index e970455..d39769e 100644 --- a/tests/integration/test_lifecycle.py +++ b/tests/integration/test_lifecycle.py @@ -5,14 +5,14 @@ from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from gitea_runner_manager.cli import cli +from grm.cli import cli @pytest.mark.integration class TestLifecycleCLI: """Test the full lifecycle CLI commands end-to-end.""" - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_start_status_stop_disable_remove(self, mock_manager_class: MagicMock) -> None: """Exercise the full lifecycle via CLI.""" mock_manager = MagicMock() diff --git a/tests/integration/test_multi_instance.py b/tests/integration/test_multi_instance.py index af53e19..54918d4 100644 --- a/tests/integration/test_multi_instance.py +++ b/tests/integration/test_multi_instance.py @@ -5,14 +5,14 @@ from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from gitea_runner_manager.cli import cli +from grm.cli import cli @pytest.mark.integration class TestMultiInstanceCLI: """Test that multiple runner instances can be managed independently.""" - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_two_instances(self, mock_manager_class: MagicMock) -> None: """Install two named instances on the same host.""" mock_manager = MagicMock() @@ -34,7 +34,7 @@ class TestMultiInstanceCLI: assert calls[0].kwargs["name"] == "runner-a" assert calls[1].kwargs["name"] == "runner-b" - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_start_stop_one_instance(self, mock_manager_class: MagicMock) -> None: """Start one instance and stop another independently.""" mock_manager = MagicMock() diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 9b8f4eb..77a3ebc 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from gitea_runner_manager import __version__ -from gitea_runner_manager.cli import cli +from grm import __version__ +from grm.cli import cli _TEST_ENV = {"GITEA_URL": "https://git.example.com", "CI_GITEA_TOKEN": ""} @@ -27,7 +27,7 @@ class TestCLI: assert result.exit_code == 0 assert __version__ in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -50,7 +50,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_no_ask_become_pass(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -73,10 +73,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_missing_url(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.install.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)") mock_manager_class.return_value = mock_manager @@ -87,7 +87,7 @@ class TestCLI: assert result.exit_code != 0 assert "GITEA_URL must be set" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_with_url_flag(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -114,10 +114,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_missing_token(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.install.side_effect = AnsibleError("GITEA_REGISTRATION_TOKEN must be set (or pass --token)") mock_manager_class.return_value = mock_manager @@ -128,7 +128,7 @@ class TestCLI: assert result.exit_code != 0 assert "GITEA_REGISTRATION_TOKEN must be set" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_with_options(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -165,7 +165,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_ask_become_pass(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -188,10 +188,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.install.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -201,7 +201,7 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_with_labels(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -226,7 +226,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_with_empty_labels(self, mock_manager_class: MagicMock) -> None: """Explicit empty string labels means 'no labels' (not 'use default').""" mock_manager = MagicMock() @@ -250,7 +250,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_labels_from_env(self, mock_manager_class: MagicMock) -> None: """Labels read from GITEA_RUNNER_LABELS env var when --labels not passed.""" mock_manager = MagicMock() @@ -274,7 +274,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_with_become_password_file(self, mock_manager_class: MagicMock) -> None: """--become-password-file passes file path to manager.""" import tempfile @@ -311,7 +311,7 @@ class TestCLI: os.unlink(pw_file) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_install_verbose(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -334,7 +334,7 @@ class TestCLI: verbose=True, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_update(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -364,7 +364,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_update_ask_become_pass(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -382,10 +382,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_update_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.update.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -395,7 +395,7 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_start(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -413,7 +413,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_start_with_override(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -431,7 +431,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_stop(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -449,7 +449,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_restart(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -467,7 +467,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_enable(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -485,7 +485,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_disable(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -505,10 +505,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_disable_missing_url(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.disable.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)") mock_manager_class.return_value = mock_manager @@ -519,7 +519,7 @@ class TestCLI: assert result.exit_code != 0 assert "GITEA_URL must be set" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_disable_with_url_flag(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -540,10 +540,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_disable_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.disable.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -553,7 +553,7 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_status(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -571,7 +571,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_remove(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -592,7 +592,7 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_remove_force_flag(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -613,10 +613,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_start_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.start.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -626,10 +626,10 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_stop_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.stop.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -639,10 +639,10 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_enable_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.enable.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -652,10 +652,10 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_status_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.status.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -665,10 +665,10 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_remove_missing_url(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.remove.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)") mock_manager_class.return_value = mock_manager @@ -679,7 +679,7 @@ class TestCLI: assert result.exit_code != 0 assert "GITEA_URL must be set" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_remove_with_url_flag(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager_class.return_value = mock_manager @@ -701,10 +701,10 @@ class TestCLI: verbose=False, ) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_remove_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.remove.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -714,7 +714,7 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.list_runners.return_value = [ @@ -736,7 +736,7 @@ class TestCLI: assert "active" in result.output mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=False) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_no_status(self, mock_manager_class: MagicMock) -> None: """--no-status skips SSH checks and shows registry only.""" mock_manager = MagicMock() @@ -758,22 +758,22 @@ class TestCLI: assert "n/a" in result.output mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=True) - @patch("gitea_runner_manager.cli.click.prompt", return_value="secret") - @patch("gitea_runner_manager.cli.sys.stdin") + @patch("grm.cli.click.prompt", return_value="secret") + @patch("grm.cli.sys.stdin") def test_collect_become_pass_tty(self, mock_stdin: MagicMock, mock_prompt: MagicMock) -> None: - from gitea_runner_manager.cli import _collect_become_pass + from grm.cli import _collect_become_pass mock_stdin.isatty.return_value = True assert _collect_become_pass(ask_become_pass=True) == "secret" - @patch("gitea_runner_manager.cli.sys.stdin") + @patch("grm.cli.sys.stdin") def test_collect_become_pass_no_ask(self, mock_stdin: MagicMock) -> None: - from gitea_runner_manager.cli import _collect_become_pass + from grm.cli import _collect_become_pass mock_stdin.isatty.return_value = True assert _collect_become_pass(ask_become_pass=False) is None - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_with_piped_become_pass(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.list_runners.return_value = [] @@ -784,7 +784,7 @@ class TestCLI: assert result.exit_code == 0 mock_manager.list_runners.assert_called_once_with(become_pass="secret", no_status=False) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_with_become_password_file(self, mock_manager_class: MagicMock) -> None: """--become-password-file reads password from file for grm list.""" import os @@ -806,7 +806,7 @@ class TestCLI: finally: os.unlink(pw_file) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_with_become_password_file_env(self, mock_manager_class: MagicMock) -> None: """GRM_BECOME_PASSWORD_FILE env var works for grm list.""" import os @@ -828,7 +828,7 @@ class TestCLI: finally: os.unlink(pw_file) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_empty(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.list_runners.return_value = [] @@ -839,10 +839,10 @@ class TestCLI: assert result.exit_code == 0 assert "No runners registered" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_list_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.list_runners.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -852,7 +852,7 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_health_all_healthy(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.health.return_value = [ @@ -868,7 +868,7 @@ class TestCLI: assert "r2" in result.output assert "yes" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_health_with_unhealthy(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.health.return_value = [ @@ -883,7 +883,7 @@ class TestCLI: assert "unhealthy" in result.output.lower() assert "r2" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_health_single_runner(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.health.return_value = [ @@ -897,7 +897,7 @@ class TestCLI: assert "r1" in result.output mock_manager.health.assert_called_once() - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_health_empty(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() mock_manager.health.return_value = [] @@ -908,10 +908,10 @@ class TestCLI: assert result.exit_code == 0 assert "No runners registered" in result.output - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_health_error(self, mock_manager_class: MagicMock) -> None: mock_manager = MagicMock() - from gitea_runner_manager.exceptions import AnsibleError + from grm.exceptions import AnsibleError mock_manager.health.side_effect = AnsibleError("fail") mock_manager_class.return_value = mock_manager @@ -921,8 +921,8 @@ class TestCLI: assert result.exit_code != 0 assert "fail" in result.output - @patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty")) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.os.getlogin", side_effect=OSError("no tty")) + @patch("grm.cli.RunnerManager") def test_default_user_fallback_on_getlogin_error( self, mock_manager_class: MagicMock, mock_getlogin: MagicMock ) -> None: @@ -937,8 +937,8 @@ class TestCLI: call_kwargs = mock_manager.install.call_args.kwargs assert call_kwargs["user"] == "testuser" - @patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty")) - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.os.getlogin", side_effect=OSError("no tty")) + @patch("grm.cli.RunnerManager") def test_default_user_fallback_to_root(self, mock_manager_class: MagicMock, mock_getlogin: MagicMock) -> None: """os.getlogin() failure with no USER env falls back to 'root'.""" mock_manager = MagicMock() @@ -951,7 +951,7 @@ class TestCLI: call_kwargs = mock_manager.install.call_args.kwargs assert call_kwargs["user"] == "root" - @patch("gitea_runner_manager.cli.RunnerManager") + @patch("grm.cli.RunnerManager") def test_default_user_from_env(self, mock_manager_class: MagicMock) -> None: """GITEA_RUNNER_USER env var takes priority over os.getlogin().""" mock_manager = MagicMock() @@ -970,20 +970,20 @@ class TestCLI: def test_get_verbose_no_context(self) -> None: """_get_verbose returns False when called outside Click context.""" - from gitea_runner_manager.cli import _get_verbose + from grm.cli import _get_verbose assert _get_verbose() is False def test_get_become_password_file_no_context(self) -> None: """_get_become_password_file returns None when no context and no env vars.""" - from gitea_runner_manager.cli import _get_become_password_file + from grm.cli import _get_become_password_file with patch.dict("os.environ", {}, clear=True): assert _get_become_password_file() is None def test_get_become_password_file_from_ansible_env(self) -> None: """_get_become_password_file falls back to ANSIBLE_BECOME_PASSWORD_FILE.""" - from gitea_runner_manager.cli import _get_become_password_file + from grm.cli import _get_become_password_file with patch.dict("os.environ", {"ANSIBLE_BECOME_PASSWORD_FILE": "/tmp/ansible.txt"}, clear=True): assert _get_become_password_file() == "/tmp/ansible.txt" @@ -994,7 +994,7 @@ class TestTriggerWorkflow: def test_trigger_workflow_success(self) -> None: runner = CliRunner(env=_TEST_ENV) - with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls: + with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls: mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.dispatch_workflow.return_value = { @@ -1022,7 +1022,7 @@ class TestTriggerWorkflow: def test_trigger_workflow_list(self) -> None: runner = CliRunner(env=_TEST_ENV) - with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls: + with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls: mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.list_workflows.return_value = [ @@ -1037,7 +1037,7 @@ class TestTriggerWorkflow: def test_trigger_workflow_list_empty(self) -> None: runner = CliRunner(env=_TEST_ENV) - with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls: + with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls: mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.list_workflows.return_value = [] @@ -1052,10 +1052,10 @@ class TestTriggerWorkflow: assert "WORKFLOW_ID" in result.output def test_trigger_workflow_api_error(self) -> None: - from gitea_runner_manager.gitea_client import GiteaAPIError + from grm.gitea_client import GiteaAPIError runner = CliRunner(env=_TEST_ENV) - with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls: + with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls: mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.dispatch_workflow.side_effect = GiteaAPIError(404, "workflow not found") @@ -1065,7 +1065,7 @@ class TestTriggerWorkflow: def test_trigger_workflow_custom_repo_and_ref(self) -> None: runner = CliRunner(env=_TEST_ENV) - with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls: + with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls: mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.dispatch_workflow.return_value = None diff --git a/tests/unit/test_executor.py b/tests/unit/test_executor.py index c9da5ab..214e07b 100644 --- a/tests/unit/test_executor.py +++ b/tests/unit/test_executor.py @@ -7,8 +7,8 @@ from unittest.mock import MagicMock, mock_open, patch import pytest -from gitea_runner_manager.exceptions import AnsibleError -from gitea_runner_manager.executor import AnsibleExecutor +from grm.exceptions import AnsibleError +from grm.executor import AnsibleExecutor def _mock_popen_process(returncode: int = 0) -> MagicMock: @@ -22,7 +22,7 @@ def _mock_popen_process(returncode: int = 0) -> MagicMock: class TestAnsibleExecutorRun: def test_run_success(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.executor.say") as mock_say: + with patch("grm.executor.say") as mock_say: executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()): mock_popen.return_value = _mock_popen_process(returncode=0) @@ -33,7 +33,7 @@ class TestAnsibleExecutorRun: assert any("Done" in msg for msg in messages) def test_run_failure(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.executor.say") as mock_say: + with patch("grm.executor.say") as mock_say: executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()): mock_popen.return_value = _mock_popen_process(returncode=1) @@ -45,7 +45,7 @@ class TestAnsibleExecutorRun: assert "exit code 1" in error_calls[0].args[0] def test_run_default_description(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.executor.say") as mock_say: + with patch("grm.executor.say") as mock_say: executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()): mock_popen.return_value = _mock_popen_process(returncode=0) @@ -75,7 +75,7 @@ class TestAnsibleExecutorRun: ' "msg": "Runner \'127.0.0.1\' is installed and running."\n' "}\n" ) - with patch("gitea_runner_manager.executor.say") as mock_say: + with patch("grm.executor.say") as mock_say: executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open(read_data=log_content)): mock_popen.return_value = _mock_popen_process(returncode=0) @@ -111,7 +111,7 @@ class TestAnsibleExecutorExtractStatus: executor = AnsibleExecutor() log_file = tmp_path / "test.log" log_file.write_text("incomplete") - with patch("gitea_runner_manager.executor.open", side_effect=OSError("read error")): + with patch("grm.executor.open", side_effect=OSError("read error")): status = executor._extract_status(log_file) assert status is None @@ -138,7 +138,7 @@ class TestAnsibleExecutorTranslation: def test_run_translated(self, tmp_path: Path) -> None: with ( patch.dict(os.environ, {"GRM_LANG": "de"}), - patch("gitea_runner_manager.executor.say") as mock_say, + patch("grm.executor.say") as mock_say, ): executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()): @@ -152,7 +152,7 @@ class TestAnsibleExecutorTranslation: def test_unsupported_lang_fallback(self, tmp_path: Path) -> None: with ( patch.dict(os.environ, {"GRM_LANG": "xx"}), - patch("gitea_runner_manager.executor.say") as mock_say, + patch("grm.executor.say") as mock_say, ): executor = AnsibleExecutor(log_dir=tmp_path) with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()): diff --git a/tests/unit/test_gitea_client.py b/tests/unit/test_gitea_client.py index 6f5b350..dc91ea4 100644 --- a/tests/unit/test_gitea_client.py +++ b/tests/unit/test_gitea_client.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch import pytest -from gitea_runner_manager.gitea_client import GiteaAPIError, GiteaWorkflowClient +from grm.gitea_client import GiteaAPIError, GiteaWorkflowClient class TestGiteaWorkflowClient: diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py index 97083ca..fdf9d2b 100644 --- a/tests/unit/test_i18n.py +++ b/tests/unit/test_i18n.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -import gitea_runner_manager.i18n as i18n_module +import grm.i18n as i18n_module class TestI18n: diff --git a/tests/unit/test_logging_config.py b/tests/unit/test_logging_config.py index 057ab0d..30111fd 100644 --- a/tests/unit/test_logging_config.py +++ b/tests/unit/test_logging_config.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -from gitea_runner_manager.logging_config import get_logger +from grm.logging_config import get_logger @pytest.fixture(autouse=True) @@ -25,7 +25,7 @@ def _cleanup_loggers() -> Generator[None, None, None]: class TestGetLogger: def test_returns_logger(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path): + with patch("grm.logging_config.Path.home", return_value=tmp_path): logger = get_logger("test_returns_logger") assert isinstance(logger, logging.Logger) @@ -33,14 +33,14 @@ class TestGetLogger: assert len(logger.handlers) == 1 # file only def test_caches_same_instance(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path): + with patch("grm.logging_config.Path.home", return_value=tmp_path): logger1 = get_logger("test_caches_same") logger2 = get_logger("test_caches_same") assert logger1 is logger2 def test_file_handler_writes(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path): + with patch("grm.logging_config.Path.home", return_value=tmp_path): logger = get_logger("test_file_writes") logger.info("hello from test") @@ -51,7 +51,7 @@ class TestGetLogger: assert "INFO" in content def test_only_file_handler(self, tmp_path: Path) -> None: - with patch("gitea_runner_manager.logging_config.Path.home", return_value=tmp_path): + with patch("grm.logging_config.Path.home", return_value=tmp_path): logger = get_logger("test_only_file") assert len(logger.handlers) == 1 diff --git a/tests/unit/test_registry.py b/tests/unit/test_registry.py index 1be215f..146748e 100644 --- a/tests/unit/test_registry.py +++ b/tests/unit/test_registry.py @@ -6,8 +6,8 @@ from pathlib import Path import pytest -from gitea_runner_manager.exceptions import GRMError -from gitea_runner_manager.registry import RunnerRegistry +from grm.exceptions import GRMError +from grm.registry import RunnerRegistry class TestRunnerRegistry: diff --git a/tests/unit/test_report.py b/tests/unit/test_report.py index f6ea628..130327f 100644 --- a/tests/unit/test_report.py +++ b/tests/unit/test_report.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest -from gitea_runner_manager.exceptions import AnsibleError -from gitea_runner_manager.report import Step, StepTracker, track_steps +from grm.exceptions import AnsibleError +from grm.report import Step, StepTracker, track_steps class TestStep: @@ -46,7 +46,7 @@ class TestStepTracker: class TestTrackSteps: def test_success(self) -> None: - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with track_steps() as tracker: tracker.begin("step1") tracker.done() @@ -61,7 +61,7 @@ class TestTrackSteps: assert any("completed" in msg for msg in messages) def test_failure_marks_step(self) -> None: - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with pytest.raises(AnsibleError, match="fail"): with track_steps() as tracker: tracker.begin("step1") @@ -78,7 +78,7 @@ class TestTrackSteps: assert any("failed" in msg for msg in messages) def test_pending_shown_on_failure(self) -> None: - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with pytest.raises(AnsibleError, match="fail"): with track_steps() as tracker: tracker.begin("step1") @@ -90,7 +90,7 @@ class TestTrackSteps: def test_multiple_in_progress_marked_failed(self) -> None: """All in-progress steps should be marked as failed on exception.""" - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with pytest.raises(AnsibleError, match="fail"): with track_steps() as tracker: tracker.begin("step1") @@ -103,7 +103,7 @@ class TestTrackSteps: assert messages.count("✗") >= 2 or sum(1 for m in messages if "✗" in m) >= 2 def test_empty_report(self) -> None: - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with track_steps(): pass @@ -112,7 +112,7 @@ class TestTrackSteps: def test_translated_report(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GRM_LANG", "bg") - with patch("gitea_runner_manager.report.say") as mock_say: + with patch("grm.report.say") as mock_say: with track_steps() as tracker: tracker.begin("step1") tracker.done() diff --git a/tests/unit/test_runner_manager.py b/tests/unit/test_runner_manager.py index b05a28f..e979de7 100644 --- a/tests/unit/test_runner_manager.py +++ b/tests/unit/test_runner_manager.py @@ -8,8 +8,8 @@ from unittest.mock import MagicMock, patch import pytest -from gitea_runner_manager.exceptions import AnsibleError -from gitea_runner_manager.runner_manager import RunnerManager +from grm.exceptions import AnsibleError +from grm.runner_manager import RunnerManager @contextmanager diff --git a/tests/unit/test_ui.py b/tests/unit/test_ui.py index 9c8bb49..5392245 100644 --- a/tests/unit/test_ui.py +++ b/tests/unit/test_ui.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -from gitea_runner_manager.ui import _console_level, say +from grm.ui import _console_level, say class TestConsoleLevel: @@ -26,15 +26,15 @@ class TestConsoleLevel: class TestSay: def test_echoes_to_console(self) -> None: - with patch("gitea_runner_manager.ui.click.echo") as mock_echo: + with patch("grm.ui.click.echo") as mock_echo: say("hello") mock_echo.assert_called_once_with("hello", err=False) def test_logs_at_info_level(self) -> None: with ( - patch("gitea_runner_manager.ui.click.echo"), - patch("gitea_runner_manager.ui.logging.getLogger") as mock_get_logger, + patch("grm.ui.click.echo"), + patch("grm.ui.logging.getLogger") as mock_get_logger, ): mock_logger = mock_get_logger.return_value say("hello") @@ -43,8 +43,8 @@ class TestSay: def test_passes_level_and_err(self) -> None: with ( - patch("gitea_runner_manager.ui.click.echo") as mock_echo, - patch("gitea_runner_manager.ui.logging.getLogger") as mock_get_logger, + patch("grm.ui.click.echo") as mock_echo, + patch("grm.ui.logging.getLogger") as mock_get_logger, ): mock_logger = mock_get_logger.return_value say("error msg", level=logging.ERROR, err=True) @@ -55,8 +55,8 @@ class TestSay: def test_suppresses_console_below_level(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GRM_LOG_LEVEL", "WARNING") with ( - patch("gitea_runner_manager.ui.click.echo") as mock_echo, - patch("gitea_runner_manager.ui.logging.getLogger") as mock_get_logger, + patch("grm.ui.click.echo") as mock_echo, + patch("grm.ui.logging.getLogger") as mock_get_logger, ): mock_logger = mock_get_logger.return_value say("debug msg", level=logging.DEBUG) diff --git a/uv.lock b/uv.lock index f428892..b905bcb 100644 --- a/uv.lock +++ b/uv.lock @@ -504,7 +504,7 @@ wheels = [ ] [[package]] -name = "gitea-runner-manager" +name = "grm" version = "0.1.0" source = { editable = "." } dependencies = [