Compare commits

..
3 Commits
Author SHA1 Message Date
devx-ci-bot 8fcac10286 release: v0.47.0 [skip ci] 2026-07-14 23:19:15 +00:00
emil c62c560c85 DEVX-141: feat: add promtool to install_tools for alert rule validation
Post-merge / detect-and-configure (push) Successful in 14s
Post-merge / release-and-maintain (push) Successful in 1m5s
2026-07-14 23:18:29 +00:00
gitea-actions-bot 08b781f978 chore: update badge URLs to commit 75024199 [skip ci] 2026-07-14 16:30:59 +00:00
7 changed files with 98 additions and 22 deletions
+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## [0.47.0] - 2026-07-14
### Features
- Add promtool to install_tools for alert rule validation
## [0.46.0] - 2026-07-14
### Features
+9 -9
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)
[![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/b6a7c5d7fcb500db7b56eccab165090fbe62b609/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/python.svg)](https://www.python.org/downloads/)
## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml
[project]
dependencies = [
"devx>=0.46.0",
"devx>=0.47.0",
]
[tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
```
> **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.46.0"`) or use a version constraint
> (for example, `"devx>=0.46.0,<0.47"`).
> `dependencies` (for example, `"devx==0.47.0"`) or use a version constraint
> (for example, `"devx>=0.47.0,<0.48"`).
### Optional extras
+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)
[![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/b6a7c5d7fcb500db7b56eccab165090fbe62b609/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/b6a7c5d7fcb500db7b56eccab165090fbe62b609/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7502419923835a8aeaa5b6de655b63a1b83cc816/python.svg)](https://www.python.org/downloads/)
## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml
[project]
dependencies = [
"devx>=0.46.0",
"devx>=0.47.0",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.46.0"` or `"devx>=0.46.0,<0.47"`.
Pin a specific version if needed: `"devx==0.47.0"` or `"devx>=0.47.0,<0.48"`.
### Optional extras
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml
[project]
dependencies = [
"devx>=0.46.0",
"devx>=0.47.0",
]
[project.optional-dependencies]
dev = [
"devx>=0.46.0",
"devx>=0.47.0",
]
```
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.46.0"
__version__ = "0.47.0"
+25 -1
View File
@@ -8,6 +8,7 @@ Handles installation of:
- tea (Gitea CLI — official command-line tool for Gitea API operations)
- hadolint (Dockerfile linter)
- vale (prose linter for documentation quality)
- promtool (Prometheus rule validator)
Each tool is installed to ``~/.local/bin`` if not already on PATH.
Idempotent: skips tools that are already available.
@@ -47,6 +48,8 @@ TOFU_VERSION = "1.12.3"
VALE_VERSION = "3.15.1"
PROMTOOL_VERSION = "3.5.5"
def _arch() -> str:
"""Return the architecture string used by release assets (delegates to shared utility)."""
@@ -212,7 +215,26 @@ def install_vale() -> bool:
return True
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea", "hadolint", "tofu", "vale"]
def install_promtool() -> bool:
"""Install promtool (Prometheus rule validator) if not already present.
Downloads the official Prometheus release tarball from GitHub and
extracts the ``promtool`` binary to ``~/.local/bin``.
"""
if _is_installed("promtool"):
click.echo("promtool: already installed")
return True
arch = _arch()
url = (
f"https://github.com/prometheus/prometheus/releases/download/"
f"v{PROMTOOL_VERSION}/prometheus-{PROMTOOL_VERSION}.linux-{arch}.tar.gz"
)
dest = _download_and_extract_tarball(url, "promtool")
click.echo(f"promtool: installed to {dest}")
return True
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea", "hadolint", "tofu", "vale", "promtool"]
def _install_tool(name: str) -> bool:
@@ -231,6 +253,8 @@ def _install_tool(name: str) -> bool:
return install_tofu()
if name == "vale":
return install_vale()
if name == "promtool":
return install_promtool()
raise click.ClickException(f"Unknown tool: {name}")
+47 -1
View File
@@ -297,6 +297,47 @@ class TestInstallVale:
assert (tmp_path / "vale").exists()
class TestInstallPromtool:
def test_already_installed(self) -> None:
with patch.object(install_tools, "_is_installed", return_value=True):
assert install_tools.install_promtool() is True
def test_install(self, tmp_path: Path) -> None:
import io
import tarfile
tarball_path = tmp_path / "archive.tar.gz"
binary_content = b"fake promtool"
with tarfile.open(tarball_path, "w:gz") as tar:
info = tarfile.TarInfo(name="promtool")
info.size = len(binary_content)
tar.addfile(info, io.BytesIO(binary_content))
with patch.object(install_tools, "_is_installed", return_value=False):
with patch.object(install_tools, "TARGET_DIR", tmp_path):
with patch.object(install_tools, "_arch", return_value="amd64"):
with patch.object(
install_tools,
"_download",
side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()),
):
assert install_tools.install_promtool() is True
assert (tmp_path / "promtool").exists()
def test_url_contains_version(self, tmp_path: Path) -> None:
"""Verify the download URL includes the correct promtool version."""
captured_url = []
def fake_extract(url: str, binary_name: str) -> Path:
captured_url.append(url)
return tmp_path / binary_name
with patch.object(install_tools, "_is_installed", return_value=False):
with patch.object(install_tools, "_download_and_extract_tarball", side_effect=fake_extract):
install_tools.install_promtool()
assert any(f"v{install_tools.PROMTOOL_VERSION}" in url for url in captured_url)
class TestListTools:
def test_list(self, tmp_path: Path) -> None:
with patch.object(install_tools, "TARGET_DIR", tmp_path):
@@ -341,6 +382,11 @@ class TestInstallTool:
assert install_tools._install_tool("vale") is True
mock.assert_called_once()
def test_promtool(self) -> None:
with patch.object(install_tools, "install_promtool", return_value=True) as mock:
assert install_tools._install_tool("promtool") is True
mock.assert_called_once()
def test_unknown_tool(self) -> None:
with pytest.raises(ClickException, match="Unknown tool"):
install_tools._install_tool("unknown")
@@ -359,7 +405,7 @@ class TestMain:
with patch.object(install_tools, "_install_tool", return_value=True) as mock_install:
result = runner.invoke(install_tools.main, [])
assert result.exit_code == 0
assert mock_install.call_count == 7
assert mock_install.call_count == 8
def test_install_specific_tool(self) -> None:
runner = CliRunner()