Compare commits

...
Author SHA1 Message Date
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2652352205 refactor: remove deprecated devx.ci.discover_runners wrapper
All workflows now use devx.molecule.discover_runners directly. The
deprecated wrapper (added in Phase 1c for backward compatibility) is
no longer referenced by any workflow in devx, grm, or infra.

- Delete src/devx/ci/discover_runners.py
- Remove `devx ci discover-runners` CLI subcommand
- Update tests to import from devx.molecule.discover_runners
- Remove TestDeprecationWrapper class
- Update docs, README, AGENTS.md, ADR-0002

DEVX-160

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-13 01:35:51 +02:00
gitea-actions-bot 10de782d93 chore: update badge URLs to commit 642ecf8c [skip ci] 2026-08-12 21:59:23 +00:00
devx-ci-bot bfb3a4d862 release: v0.50.6 [skip ci] 2026-08-12 21:58:35 +00:00
emil User e167be1890 DEVX-159: fix: add GitHub mirror fallback for actionlint and vale downloads 2026-08-12 21:55:52 +00:00
15 changed files with 126 additions and 150 deletions
+1 -2
View File
@@ -167,7 +167,6 @@ src/devx/
│ ├── cancel_superseded_runs.py # Cancel in-flight CI runs for the same PR branch │ ├── cancel_superseded_runs.py # Cancel in-flight CI runs for the same PR branch
│ ├── check_workflow_artifact_deps.py # Verify artifact download jobs depend on upload jobs │ ├── check_workflow_artifact_deps.py # Verify artifact download jobs depend on upload jobs
│ ├── check_workflow_tofu_init.py # Verify tofu-state jobs have a tofu-init step │ ├── check_workflow_tofu_init.py # Verify tofu-state jobs have a tofu-init step
│ ├── discover_runners.py # Deprecated wrapper → molecule/discover_runners
│ └── wait_for_checks.py # Poll Gitea Actions for job completion (replaces inline shell polling) │ └── wait_for_checks.py # Poll Gitea Actions for job completion (replaces inline shell polling)
├── tools/ # Developer tooling modules (run locally or by CI) ├── tools/ # Developer tooling modules (run locally or by CI)
│ ├── setup.py # Environment setup (venv, deps, hooks) │ ├── setup.py # Environment setup (venv, deps, hooks)
@@ -220,7 +219,7 @@ src/devx/
│ ├── ui.py # say() — unified click.echo + logging output │ ├── ui.py # say() — unified click.echo + logging output
│ └── jinja.py # Jinja2 environment helpers + Ansible-compatible filters │ └── jinja.py # Jinja2 environment helpers + Ansible-compatible filters
└── molecule/ # Optional molecule testing helpers (for Ansible projects) └── molecule/ # Optional molecule testing helpers (for Ansible projects)
├── discover_runners.py # Dynamic Gitea runner discovery (canonical; ci/discover_runners is a deprecated wrapper) ├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role) ├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role)
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast (--roles-root) ├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast (--roles-root)
├── molecule_all.py # Run all molecule scenarios locally ├── molecule_all.py # Run all molecule scenarios locally
+14
View File
@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [Unreleased]
### Breaking Changes
- Remove deprecated `devx.ci.discover_runners` wrapper. All workflows
now use `devx.molecule.discover_runners` directly. The `devx ci
discover-runners` CLI subcommand has also been removed.
## [0.50.6] - 2026-08-12
### Bug Fixes
- Add GitHub mirror fallback for actionlint and vale downloads
## [0.50.5] - 2026-08-12 ## [0.50.5] - 2026-08-12
### Bug Fixes ### Bug Fixes
+10 -10
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/python.svg)](https://www.python.org/downloads/)
## Why devx? ## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.50.5", "devx>=0.50.6",
] ]
[tool.pip] [tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
``` ```
> **Note:** If your project requires a specific devx version, pin it in > **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.50.5"`) or use a version constraint > `dependencies` (for example, `"devx==0.50.6"`) or use a version constraint
> (for example, `"devx>=0.50.5,<0.51"`). > (for example, `"devx>=0.50.6,<0.51"`).
### Optional extras ### Optional extras
@@ -172,7 +172,7 @@ python -m devx.ci.notify_failure --repo oblachno-oss/devx --run-id 123 \
--workflow ci --commit abc123 --auto-login --workflow ci --commit abc123 --auto-login
# Discover available Gitea Actions runners # Discover available Gitea Actions runners
python -m devx.ci.discover_runners --owner oblachno-oss --repo devx --indices python -m devx.molecule.discover_runners --owner oblachno-oss --repo devx --indices
# Distribute files across parallel runners (round-robin) # Distribute files across parallel runners (round-robin)
python -m devx.ci.distribute_files --pattern "tests/integration/test_*.py" \ python -m devx.ci.distribute_files --pattern "tests/integration/test_*.py" \
@@ -81,17 +81,13 @@ Extract the inline polling logic into `devx.ci.wait_for_checks`:
This replaces the inline shell polling in `grm` `ci.yml` with a This replaces the inline shell polling in `grm` `ci.yml` with a
reusable, testable Python module. reusable, testable Python module.
### 3. Deprecated `ci/discover_runners` Wrapper ### 3. Removed `ci/discover_runners` Wrapper
Merge the `ci/discover_runners` implementation (with its better error Merged the `ci/discover_runners` implementation (with its better error
logging) into `molecule/discover_runners` as the canonical version. logging) into `molecule/discover_runners` as the canonical version.
Make `ci/discover_runners` a deprecated wrapper that: The `ci/discover_runners` wrapper was deprecated in Phase 1c and
**removed in Phase 2d** (DEVX-160). All workflows now use
- Re-exports all public symbols from `molecule.discover_runners` `devx.molecule.discover_runners` directly.
- Emits a `DeprecationWarning` when run as `__main__`
- Preserves backward compatibility for existing workflow references
New code should import from `devx.molecule.discover_runners` directly.
## Consequences ## Consequences
+8 -8
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/1d748408e5f94048f931d58d7741cff665a5bbdb/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/642ecf8c14c883fa864f41b6828d8a1524c13b3e/python.svg)](https://www.python.org/downloads/)
## Overview ## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.50.5", "devx>=0.50.6",
] ]
[tool.pip] [tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple" extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
``` ```
Pin a specific version if needed: `"devx==0.50.5"` or `"devx>=0.50.5,<0.51"`. Pin a specific version if needed: `"devx==0.50.6"` or `"devx>=0.50.6,<0.51"`.
### Optional extras ### Optional extras
+1 -1
View File
@@ -409,7 +409,7 @@ Intended for local development; CI uses the parallel matrix instead.
### `molecule/discover_runners.py` ### `molecule/discover_runners.py`
Discovers available Gitea Actions runners for molecule tests. This is the Discovers available Gitea Actions runners for molecule tests. This is the
canonical implementation; `devx.ci.discover_runners` is a deprecated wrapper canonical implementation (formerly `devx.ci.discover_runners`, removed in v0.51.0)
that re-exports from this module. Queries runners at repository, that re-exports from this module. Queries runners at repository,
organization, and instance (administrator) levels, with warnings logged organization, and instance (administrator) levels, with warnings logged
to stderr on non-200 responses (except 403 on instance-level, which is to stderr on non-200 responses (except 403 on instance-level, which is
+1 -1
View File
@@ -456,7 +456,7 @@ instance levels. Falls back to `MOLECULE_RUNNERS` repo variable or
`DEFAULT_MAX_RUNNERS` (3). `DEFAULT_MAX_RUNNERS` (3).
```bash ```bash
python -m devx.ci.discover_runners --owner <owner> --repo <repo> [--count] [--indices] python -m devx.molecule.discover_runners --owner <owner> --repo <repo> [--count] [--indices]
``` ```
### `detect_release_commit.py` ### `detect_release_commit.py`
+3 -3
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.50.5", "devx>=0.50.6",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"devx>=0.50.5", "devx>=0.50.6",
] ]
``` ```
@@ -142,7 +142,7 @@ infrastructure = [
- `devx.ci.notify_failure` — Create Gitea issues on CI failures - `devx.ci.notify_failure` — Create Gitea issues on CI failures
- `devx.ci.distribute_files` — Parallel test file distribution - `devx.ci.distribute_files` — Parallel test file distribution
- `devx.ci.distribute_items` — Parallel item distribution across runners - `devx.ci.distribute_items` — Parallel item distribution across runners
- `devx.ci.discover_runners` — Dynamic runner discovery via Gitea API - `devx.molecule.discover_runners` — Dynamic runner discovery via Gitea API
### Development Tools (`devx.tools.*`) ### Development Tools (`devx.tools.*`)
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects.""" """devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.50.5" __version__ = "0.50.6"
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""Discover available Gitea Actions runners for dynamic job distribution.
.. deprecated:: Phase 1c
Use :mod:`devx.molecule.discover_runners` instead. This module is a
thin wrapper that re-exports the canonical implementation from
:mod:`devx.molecule.discover_runners` for backward compatibility
with existing workflow references and Makefile targets.
The canonical implementation lives in
:mod:`devx.molecule.discover_runners` because runner discovery is
primarily used by the molecule test distribution pipeline. CI
workflows that still reference ``python -m devx.ci.discover_runners``
will continue to work via this wrapper, but new code should import
from :mod:`devx.molecule.discover_runners` directly.
Usage:
python3 -m devx.ci.discover_runners --owner oblachno-oss --repo devx
python3 -m devx.ci.discover_runners --indices
python3 -m devx.ci.discover_runners --count
"""
from __future__ import annotations
import sys
import warnings
from devx.molecule.discover_runners import ( # noqa: F401 — re-exported for backward compat
DEFAULT_MAX_RUNNERS,
generate_indices,
get_runner_count,
main,
query_runners,
)
_DEPRECATION_MSG = (
"devx.ci.discover_runners is deprecated; use devx.molecule.discover_runners instead. "
"This wrapper will be removed in a future release."
)
def _emit_deprecation_warning() -> None:
"""Emit a DeprecationWarning when this module is imported for CLI use."""
warnings.warn(_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
if __name__ == "__main__": # pragma: no cover
_emit_deprecation_warning()
sys.exit(main())
-7
View File
@@ -81,13 +81,6 @@ def ci_detect_release_commit(args: tuple[str, ...]) -> None:
_run_module("devx.ci.detect_release_commit", list(args)) _run_module("devx.ci.detect_release_commit", list(args))
@ci.command("discover-runners")
@click.argument("args", nargs=-1)
def ci_discover_runners(args: tuple[str, ...]) -> None:
"""Discover available Gitea Actions runners."""
_run_module("devx.ci.discover_runners", list(args))
@ci.command("doc-coverage") @ci.command("doc-coverage")
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def ci_doc_coverage(args: tuple[str, ...]) -> None: def ci_doc_coverage(args: tuple[str, ...]) -> None:
+35 -21
View File
@@ -131,26 +131,37 @@ def _download_with_fallback(urls: list[str], binary_name: str) -> Path:
raise click.ClickException(f"Failed to download {binary_name} from all URLs: {'; '.join(errors)}") raise click.ClickException(f"Failed to download {binary_name} from all URLs: {'; '.join(errors)}")
def _download_and_extract_tarball(url: str, binary_name: str) -> Path: def _download_and_extract_tarball(url: str, binary_name: str, *, fallback_urls: list[str] | None = None) -> Path:
"""Download a tarball, extract the binary, and install it to TARGET_DIR. """Download a tarball, extract the binary, and install it to TARGET_DIR.
Returns the path to the installed binary. Returns the path to the installed binary. Falls back to ``fallback_urls``
if the primary ``url`` fails all retries.
""" """
target_dir = _ensure_target_dir() target_dir = _ensure_target_dir()
dest = target_dir / binary_name dest = target_dir / binary_name
with tempfile.TemporaryDirectory() as tmpdir: urls = [url, *(fallback_urls or [])]
tarball = Path(tmpdir) / "archive.tar.gz" errors: list[str] = []
_download(url, tarball) for try_url in urls:
with tarfile.open(tarball, "r:gz") as tar: with tempfile.TemporaryDirectory() as tmpdir:
tar.extractall(tmpdir) # nosec B202 tarball = Path(tmpdir) / "archive.tar.gz"
# Find the binary in the extracted tree try:
extracted = Path(tmpdir).rglob(binary_name) _download(try_url, tarball)
found = next(extracted, None) except Exception as exc: # noqa: BLE001
if found is None: errors.append(f"{try_url}: {exc}")
raise click.ClickException(f"Binary {binary_name} not found in archive from {url}") click.echo(f" {binary_name}: fallback — {exc}")
shutil.copy2(found, dest) continue
dest.chmod(0o755) with tarfile.open(tarball, "r:gz") as tar:
return dest tar.extractall(tmpdir) # nosec B202
# Find the binary in the extracted tree
extracted = Path(tmpdir).rglob(binary_name)
found = next(extracted, None)
if found is None:
errors.append(f"{try_url}: binary not found in archive")
continue
shutil.copy2(found, dest)
dest.chmod(0o755)
return dest
raise click.ClickException(f"Failed to download {binary_name} from all URLs: {'; '.join(errors)}")
def _download_binary(url: str, binary_name: str) -> Path: def _download_binary(url: str, binary_name: str) -> Path:
@@ -178,11 +189,12 @@ def install_actionlint() -> bool:
click.echo("actionlint: already installed") click.echo("actionlint: already installed")
return True return True
arch = _arch() arch = _arch()
url = ( path = (
f"https://github.com/rhysd/actionlint/releases/download/" f"rhysd/actionlint/releases/download/v{ACTIONLINT_VERSION}/actionlint_{ACTIONLINT_VERSION}_linux_{arch}.tar.gz"
f"v{ACTIONLINT_VERSION}/actionlint_{ACTIONLINT_VERSION}_linux_{arch}.tar.gz"
) )
dest = _download_and_extract_tarball(url, "actionlint") url = f"https://github.com/{path}"
fallback = [f"https://ghproxy.com/{path}"]
dest = _download_and_extract_tarball(url, "actionlint", fallback_urls=fallback)
click.echo(f"actionlint: installed to {dest}") click.echo(f"actionlint: installed to {dest}")
return True return True
@@ -276,8 +288,10 @@ def install_vale() -> bool:
return True return True
machine = platform.machine().lower() machine = platform.machine().lower()
arch = "64-bit" if machine in {"x86_64", "amd64"} else "arm64" arch = "64-bit" if machine in {"x86_64", "amd64"} else "arm64"
url = f"https://github.com/errata-ai/vale/releases/download/v{VALE_VERSION}/vale_{VALE_VERSION}_Linux_{arch}.tar.gz" path = f"errata-ai/vale/releases/download/v{VALE_VERSION}/vale_{VALE_VERSION}_Linux_{arch}.tar.gz"
dest = _download_and_extract_tarball(url, "vale") url = f"https://github.com/{path}"
fallback = [f"https://ghproxy.com/{path}"]
dest = _download_and_extract_tarball(url, "vale", fallback_urls=fallback)
click.echo(f"vale: installed to {dest}") click.echo(f"vale: installed to {dest}")
return True return True
-7
View File
@@ -73,13 +73,6 @@ class TestCiCommands:
assert result.exit_code == 0 assert result.exit_code == 0
mock_run.assert_called_once_with("devx.ci.detect_release_commit", []) mock_run.assert_called_once_with("devx.ci.detect_release_commit", [])
@patch("devx.cli._run_module")
def test_ci_discover_runners(self, mock_run: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["ci", "discover-runners", "positional"])
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.ci.discover_runners", ["positional"])
@patch("devx.cli._run_module") @patch("devx.cli._run_module")
def test_ci_doc_coverage(self, mock_run: MagicMock) -> None: def test_ci_doc_coverage(self, mock_run: MagicMock) -> None:
runner = CliRunner() runner = CliRunner()
+3 -31
View File
@@ -1,8 +1,6 @@
"""Unit tests for devx.ci.discover_runners (deprecated wrapper). """Unit tests for devx.molecule.discover_runners.
The wrapper re-exports from devx.molecule.discover_runners; these tests Tests verify the canonical implementation by patching the requests module.
verify backward compatibility by importing through the wrapper and
patching the canonical implementation's requests module.
""" """
import json import json
@@ -13,7 +11,7 @@ import click
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from devx.ci.discover_runners import ( from devx.molecule.discover_runners import (
DEFAULT_MAX_RUNNERS, DEFAULT_MAX_RUNNERS,
generate_indices, generate_indices,
get_runner_count, get_runner_count,
@@ -266,29 +264,3 @@ class TestMain:
assert result.output.strip() == "3" assert result.output.strip() == "3"
args, _ = mock_count.call_args args, _ = mock_count.call_args
assert args[1] is None # token passed as None when missing assert args[1] is None # token passed as None when missing
class TestDeprecationWrapper:
def test_re_exports_canonical_symbols(self) -> None:
"""The wrapper re-exports the canonical implementation's symbols."""
from devx.ci import discover_runners as ci_mod
from devx.molecule import discover_runners as mol_mod
assert ci_mod.query_runners is mol_mod.query_runners
assert ci_mod.get_runner_count is mol_mod.get_runner_count
assert ci_mod.generate_indices is mol_mod.generate_indices
assert ci_mod.main is mol_mod.main
assert ci_mod.DEFAULT_MAX_RUNNERS is mol_mod.DEFAULT_MAX_RUNNERS
def test_emit_deprecation_warning(self) -> None:
"""_emit_deprecation_warning issues a DeprecationWarning."""
import warnings
from devx.ci.discover_runners import _emit_deprecation_warning
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
_emit_deprecation_warning()
assert len(caught) == 1
assert issubclass(caught[0].category, DeprecationWarning)
assert "deprecated" in str(caught[0].message)
+44
View File
@@ -201,6 +201,50 @@ class TestDownloadAndExtractTarball:
with pytest.raises(ClickException, match="not found in archive"): with pytest.raises(ClickException, match="not found in archive"):
install_tools._download_and_extract_tarball("https://example.com/actionlint.tar.gz", "actionlint") install_tools._download_and_extract_tarball("https://example.com/actionlint.tar.gz", "actionlint")
def test_fallback_url_succeeds(self, tmp_path: Path) -> None:
import io
import tarfile
tarball_path = tmp_path / "archive.tar.gz"
binary_content = b"fake binary"
with tarfile.open(tarball_path, "w:gz") as tar:
info = tarfile.TarInfo(name="actionlint")
info.size = len(binary_content)
tar.addfile(info, io.BytesIO(binary_content))
target_dir = tmp_path / "bin"
target_dir.mkdir()
tarball_bytes = tarball_path.read_bytes()
def fake_download(url: str, dest: Path) -> None:
if "primary" in url:
raise OSError("connection refused")
Path(dest).write_bytes(tarball_bytes)
with patch.object(install_tools, "TARGET_DIR", target_dir):
with patch.object(install_tools, "_download", side_effect=fake_download):
result = install_tools._download_and_extract_tarball(
"https://primary.com/actionlint.tar.gz",
"actionlint",
fallback_urls=["https://fallback.com/actionlint.tar.gz"],
)
assert result == target_dir / "actionlint"
assert result.read_bytes() == binary_content
def test_all_urls_fail(self, tmp_path: Path) -> None:
target_dir = tmp_path / "bin"
target_dir.mkdir()
with patch.object(install_tools, "TARGET_DIR", target_dir):
with patch.object(install_tools, "_download", side_effect=OSError("connection refused")):
with pytest.raises(ClickException, match="Failed to download"):
install_tools._download_and_extract_tarball(
"https://primary.com/actionlint.tar.gz",
"actionlint",
fallback_urls=["https://fallback.com/actionlint.tar.gz"],
)
class TestInstallActionlint: class TestInstallActionlint:
def test_already_installed(self) -> None: def test_already_installed(self) -> None: