Compare commits

..
23 Commits
Author SHA1 Message Date
devx-ci-bot 33cfbb0f41 release: v0.23.3 [skip ci] 2026-06-27 21:41:26 +00:00
emil 598238e4d6 DEVX-88: fix: correct clean_images delete URL and add retry with error handling
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 18s
Build Images / detect-type (push) Successful in 36s
Post-merge / release (push) Successful in 25s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / badges (push) Successful in 29s
Post-merge / publish (push) Successful in 19s
Build Images / build-and-push (push) Successful in 2m55s
Build Images / cleanup (push) Successful in 5m47s
2026-06-27 21:40:49 +00:00
gitea-actions-bot 925b99b7db chore: update badge URLs to commit dd2ca2f7 [skip ci] 2026-06-27 21:20:07 +00:00
devx-ci-bot 16ed48bd26 release: v0.23.2 [skip ci] 2026-06-27 21:19:53 +00:00
emil 3b0500164b DEVX-87: fix: add skip-ci flag to release commits and concurrency to build-images
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 15s
Post-merge / vikunja (push) Successful in 13s
Build Images / detect-type (push) Successful in 31s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Successful in 25s
Post-merge / badges (push) Successful in 36s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m50s
Build Images / cleanup (push) Successful in 5m24s
2026-06-27 21:19:17 +00:00
gitea-actions-bot 00a44ec5dc chore: update badge URLs to commit b62d5c3d [skip ci] 2026-06-27 19:42:25 +00:00
gitea-actions-bot b385c57621 chore: update badge URLs to commit 3e6359f8 [skip ci] 2026-06-27 19:41:36 +00:00
devx-ci-bot 3181b24f5e release: v0.23.1
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 44s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 32s
2026-06-27 19:40:59 +00:00
emil bc8478220c DEVX-86: fix: add rsync to ci-full image for molecule_docker
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / detect-type (push) Successful in 34s
Post-merge / release (push) Successful in 28s
Post-merge / sync-wiki (push) Successful in 23s
Post-merge / vikunja (push) Successful in 18s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / badges (push) Successful in 44s
Post-merge / publish (push) Successful in 18s
Build Images / build-and-push (push) Successful in 3m45s
Build Images / cleanup (push) Successful in 2m17s
2026-06-27 19:40:20 +00:00
gitea-actions-bot a568c0899f chore: update badge URLs to commit 0f22063f [skip ci] 2026-06-27 18:06:46 +00:00
gitea-actions-bot 4216698ca8 chore: update badge URLs to commit 6823dfce [skip ci] 2026-06-27 18:06:11 +00:00
devx-ci-bot f3685b9029 release: v0.23.0
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 38s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 28s
2026-06-27 18:06:01 +00:00
emil 2e5470236e DEVX-85: feat: add devx-lint-dockerfiles to devx.mak, alias setup-image
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 11s
Build Images / detect-type (push) Successful in 36s
Post-merge / release (push) Successful in 26s
Post-merge / badges (push) Successful in 31s
Post-merge / publish (push) Successful in 17s
Build Images / build-and-push (push) Successful in 2m58s
Build Images / cleanup (push) Successful in 1m25s
2026-06-27 18:05:25 +00:00
gitea-actions-bot d2dcf8f7c4 chore: update badge URLs to commit 1d6085cf [skip ci] 2026-06-27 17:25:13 +00:00
emil 357e07a9a6 DEVX-84: refactor: remove hadolint on-the-fly install from setup-image
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 11s
Post-merge / vikunja (push) Successful in 12s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / badges (push) Successful in 27s
2026-06-27 17:24:34 +00:00
gitea-actions-bot ffb3976224 chore: update badge URLs to commit 22ed0d7b [skip ci] 2026-06-27 16:44:47 +00:00
gitea-actions-bot ad75b22f2a chore: update badge URLs to commit b800f158 [skip ci] 2026-06-27 16:44:13 +00:00
devx-ci-bot 37f867f6d8 release: v0.22.1
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 39s
Build Images / build-and-push (push) Has been skipped
Post-merge / badges (push) Successful in 26s
Build Images / cleanup (push) Has been skipped
2026-06-27 16:44:03 +00:00
emil 233a0bc055 DEVX-83: fix: fail lint-dockerfiles when hadolint is missing
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 17s
Build Images / detect-type (push) Successful in 34s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / release (push) Successful in 29s
Post-merge / badges (push) Successful in 37s
Post-merge / publish (push) Successful in 18s
Build Images / build-and-push (push) Successful in 3m1s
Build Images / cleanup (push) Successful in 4m21s
2026-06-27 16:43:22 +00:00
gitea-actions-bot 1a28f5dcc5 chore: update badge URLs to commit 7471d490 [skip ci] 2026-06-27 16:17:12 +00:00
emil 3af60af439 DEVX-82: fix: checkout release tag in publish job
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / release (push) Successful in 13s
Post-merge / vikunja (push) Successful in 12s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 20s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / badges (push) Successful in 32s
2026-06-27 16:16:25 +00:00
gitea-actions-bot e181104e1c chore: update badge URLs to commit 6a571fb4 [skip ci] 2026-06-27 15:06:45 +00:00
gitea-actions-bot ee1826fb34 chore: update badge URLs to commit 477726f7 [skip ci] 2026-06-27 15:06:17 +00:00
17 changed files with 326 additions and 51 deletions
+4
View File
@@ -23,6 +23,10 @@ on:
- src/devx/**
workflow_dispatch:
concurrency:
group: build-images
cancel-in-progress: false
jobs:
detect-type:
runs-on: docker
+1
View File
@@ -137,6 +137,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release.outputs.tag }}
- name: Set up environment
run: make setup-image EXTRAS=release
- name: Build and publish release
+8 -6
View File
@@ -4,8 +4,8 @@
```bash
make setup # Create venv, install deps, set up hooks, install CI tools
make install-tools # Install actionlint, git-cliff, act_runner to ~/.local/bin
make lint-all # ruff + pyright + bandit + actionlint
make install-tools # Install actionlint, git-cliff, act_runner, tea, hadolint to ~/.local/bin
make lint-all # ruff + pyright + bandit + actionlint + lint-dockerfiles
make pytest-cov # Unit tests with 100% coverage enforcement
make test-unit # Unit tests without coverage
make workflow-lint # Static lint of .gitea/workflows/*.yml (actionlint)
@@ -16,7 +16,7 @@ make clean # Remove caches, build artifacts, coverage data
`make setup` automatically installs all development tools:
- **Python deps** via `python -m devx.tools.setup` (pip install -e .[dev], pre-commit hooks)
- **actionlint, git-cliff, act_runner, tea** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
- **actionlint, git-cliff, act_runner, tea, hadolint** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `CI_GITEA_TOKEN`)
## Workflow Verification (Before Push)
@@ -73,7 +73,7 @@ src/devx/
│ └── doc_coverage.py # Documentation coverage check
├── tools/ # Developer tooling modules (run locally or by CI)
│ ├── setup.py # Environment setup (venv, deps, hooks)
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea, hadolint
│ ├── install_checkmake.py # Install checkmake (Makefile linter)
│ ├── build_image.py # Build and push Docker images to Gitea registry
│ ├── clean_images.py # Clean up old Docker image versions from Gitea registry
@@ -404,7 +404,7 @@ projects.
| `devx-venv` | Create Python venv with version check |
| `devx-activate-scripts` | Create shell/fish/zsh activate scripts |
| `devx-install-hooks` | Set git hooks path to hooks/ |
| `devx-install-tools` | Install actionlint, git-cliff, act_runner, tea |
| `devx-install-tools` | Install actionlint, git-cliff, act_runner, tea, hadolint |
| `devx-install-checkmake` | Install checkmake (Makefile linter) |
| `devx-checkmake` | Lint Makefiles with checkmake |
| `devx-workflow-lint` | Static lint of Gitea Actions YAML (actionlint) |
@@ -428,6 +428,7 @@ projects.
| `devx-pre-push` | Run lint + tests before push |
| `devx-clean` | Remove caches, build artifacts, coverage data |
| `devx-setup-image` | Link /opt/venv + install project (for pre-built image CI jobs) |
| `devx-lint-dockerfiles` | Lint Dockerfiles with hadolint (fail-fast, parameterized by `DEVX_DOCKERFILE_PATHS`) |
| `devx-build-images` | Build Docker images from manifest (no push) |
| `devx-push-images` | Build and push Docker images to Gitea registry |
| `devx-build-images-dry-run` | Show what would be built/pushed |
@@ -441,6 +442,7 @@ projects.
- `DEVX_COV_PKG` — coverage package (default: `src/devx`)
- `DEVX_TEST_PATHS` — pytest paths (default: `tests/`)
- `DEVX_PR_BASE` — PR base branch (default: `master`)
- `DEVX_DOCKERFILE_PATHS` — directory to search for Dockerfiles (default: `docker`)
- `DEVX_GITEA_REGISTRY` — registry URL (default: `git.oblachno.oblachno.fyi`)
- `DEVX_IMAGE_MANIFEST` — path to JSON manifest (default: `docker/images.json`)
- `DEVX_IMAGE_OWNER` — package owner for cleanup (default: `oblachno-oss`)
@@ -453,7 +455,7 @@ to eliminate the 40-120s setup tax on every CI job:
| Image | Contains | Used by jobs |
|-------|----------|-------------|
| `ci-base-latest` | Python 3.12 + devx[ci] + tea | detect-changes, detect-type, validate-commit-msg, pr-review, auto-merge, sync-wiki, vikunja, configure-repo |
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake | quality, badges |
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake + hadolint | quality, badges |
| `ci-full-latest` | ci-quality + devx[release,molecule,deploy] + git-cliff + OpenTofu | release, publish, release-dry-run, molecule-tests, deploy jobs |
**Build process** (in `build-images.yml` workflow):
+35
View File
@@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file.
## [0.23.3] - 2026-06-27
### Bug Fixes
- Correct clean_images delete URL and add retry with error handling
## [0.23.2] - 2026-06-27
### Bug Fixes
- Add skip-ci flag to release commits and concurrency to build-images
## [0.23.1] - 2026-06-27
### Bug Fixes
- Add rsync to ci-full image for molecule_docker
## [0.23.0] - 2026-06-27
### Features
- Add devx-lint-dockerfiles to devx.mak, alias setup-image
### Refactor
- Remove hadolint on-the-fly install from setup-image
## [0.22.1] - 2026-06-27
### Bug Fixes
- Checkout release tag in publish job
- Fail lint-dockerfiles when hadolint is missing
## [0.22.0] - 2026-06-27
### Features
+10 -5
View File
@@ -31,6 +31,9 @@ setup-release: $(VENV)/bin/activate .env
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint,release" --no-pre-commit
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
# Note: Not aliased to devx-setup-image because devx's own CI images may have
# an older devx.mak that doesn't yet define devx-setup-image. Consumer repos
# (grm, infra) can safely alias to devx-setup-image since they install devx from PyPI.
setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate && pip install -e . --no-deps 2>/dev/null; \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
@@ -99,14 +102,16 @@ git-push: devx-push
lint-all: lint workflow-lint lint-dockerfiles
@echo "[lint-all] All linting checks passed."
# Note: Not aliased to devx-lint-dockerfiles for the same reason as setup-image —
# devx's own CI images may have an older devx.mak. Consumer repos can safely alias.
lint-dockerfiles:
@echo "[lint-dockerfiles] Linting Dockerfiles with hadolint..."
@if command -v hadolint >/dev/null 2>&1; then \
find docker -name 'Dockerfile*' -exec hadolint {} +; \
echo "[lint-dockerfiles] All Dockerfiles passed."; \
else \
echo "[lint-dockerfiles] hadolint not found — skipping (install with: pip install hadolint or download from GitHub)"; \
@if ! command -v hadolint >/dev/null 2>&1; then \
echo "[lint-dockerfiles] ERROR: hadolint not found. Install from https://github.com/hadolint/hadolint/releases" >&2; \
exit 1; \
fi
@find docker -name 'Dockerfile*' -exec hadolint {} +
@echo "[lint-dockerfiles] All Dockerfiles passed."
test-unit: devx-test-unit
+6 -6
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/c8ba6417503aafcc1e25d2b402d89c9981a41107/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/python.svg)](https://www.python.org/downloads/)
## Why devx?
+4
View File
@@ -11,6 +11,10 @@ FROM git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install rsync (required by molecule_docker for file sync between host and test containers)
RUN apt-get update && apt-get install -y --no-install-recommends rsync \
&& rm -rf /var/lib/apt/lists/*
# Install devx[release,molecule,deploy] from local source
COPY . /tmp/devx
RUN pip install --no-cache-dir /tmp/devx[release,molecule,deploy] \
+6 -6
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/c8ba6417503aafcc1e25d2b402d89c9981a41107/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/c8ba6417503aafcc1e25d2b402d89c9981a41107/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/dd2ca2f7c67b539173bf3c3be45f42917bde6c48/python.svg)](https://www.python.org/downloads/)
## Overview
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.22.0"
__version__ = "0.23.3"
+2 -2
View File
@@ -279,7 +279,7 @@ def commit_release_changes(new_version: str) -> bool:
if status.returncode == 0:
click.echo(_("No staged changes — version and changelog already up to date."))
return False
run_cmd(["git", "commit", "--no-verify", "-m", f"release: v{new_version}"])
run_cmd(["git", "commit", "--no-verify", "-m", f"release: v{new_version} [skip ci]"])
return True
@@ -705,7 +705,7 @@ def main(dry_run: bool, skip_tests: bool, verify: bool) -> None:
click.echo(_("\n[dry-run] Changelog:\n{changelog}", changelog=changelog))
click.echo(_("[dry-run] Would update {init}", init=INIT_FILE))
click.echo(_("[dry-run] Would update {changelog_file}", changelog_file=CHANGELOG_FILE))
click.echo(_("[dry-run] Would commit: release: v{version}", version=new_version))
click.echo(_("[dry-run] Would commit: release: v{version} [skip ci]", version=new_version))
click.echo(_("[dry-run] Would push commit to master"))
click.echo(_("[dry-run] Would create tag: v{version}", version=new_version))
return
+21 -1
View File
@@ -51,6 +51,7 @@ DEVX_GITEA_PYPI_HOST ?= git.oblachno.oblachno.fyi
DEVX_GITEA_PYPI_ORG ?= oblachno-oss
DEVX_ACTIONLINT_CFG ?= .gitea/actionlint.yaml
DEVX_WORKFLOW_DIR ?= .gitea/workflows
DEVX_DOCKERFILE_PATHS ?= docker
# PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured.
# Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]'
@@ -69,7 +70,7 @@ DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi;
.PHONY: devx-clean devx-pre-push
.PHONY: devx-check-mutable-globals devx-check-dep-docs devx-check-test-coverage devx-check-docs devx-check-test-speed
.PHONY: devx-test-unit devx-pytest-cov
.PHONY: devx-setup-image
.PHONY: devx-setup-image devx-lint-dockerfiles
# ── Vikunja task and PR management ────────────────────────────────────────────
@@ -251,6 +252,25 @@ devx-clean:
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
@rm -rf .coverage htmlcov/ dist/ build/ *.egg-info/ .molecule/ 2>/dev/null || true
# ── Dockerfile linting ────────────────────────────────────────────────────────
#
# Lint Dockerfiles with hadolint. Fails fast if hadolint is not installed
# (no silent skip). Set DEVX_DOCKERFILE_PATHS to the directory containing
# your Dockerfiles (default: docker).
#
# Usage:
# make devx-lint-dockerfiles (lints docker/ directory)
# make devx-lint-dockerfiles DEVX_DOCKERFILE_PATHS=ansible (lints ansible/)
devx-lint-dockerfiles:
@echo "[devx-lint-dockerfiles] Linting Dockerfiles with hadolint..."
@if ! command -v hadolint >/dev/null 2>&1; then \
echo "[devx-lint-dockerfiles] ERROR: hadolint not found. Install from https://github.com/hadolint/hadolint/releases" >&2; \
exit 1; \
fi
@find $(DEVX_DOCKERFILE_PATHS) -name 'Dockerfile*' -exec hadolint {} +
@echo "[devx-lint-dockerfiles] All Dockerfiles passed."
# ── Pre-built image setup ─────────────────────────────────────────────────────
#
# When running inside a pre-built Docker runner image (ci-base, ci-quality,
+34 -4
View File
@@ -34,6 +34,7 @@ Authentication uses ``CI_GITEA_TOKEN`` environment variable.
from __future__ import annotations
import os
import time
from typing import Any
import click
@@ -85,15 +86,37 @@ def delete_package_version(
token: str,
*,
timeout: int = 30,
package_type: str = "container",
max_retries: int = 3,
) -> bool:
"""Delete a specific version of a container package.
Uses the Gitea API endpoint ``DELETE /packages/{owner}/{type}/{name}/{version}``.
Retries on transient failures (5xx, timeouts) up to ``max_retries`` times.
Returns True on success, False on failure.
"""
url = f"{api_url}/packages/{owner}/{name}/{version}"
url = f"{api_url}/packages/{owner}/{package_type}/{name}/{version}"
headers = {"Authorization": f"token {token}"}
resp = requests.delete(url, headers=headers, timeout=timeout)
return resp.status_code in (204, 200)
for attempt in range(max_retries):
try:
resp = requests.delete(url, headers=headers, timeout=timeout)
except requests.RequestException:
if attempt < max_retries - 1:
time.sleep(2**attempt)
continue
return False
if resp.status_code in (204, 200):
return True
# 404 means already deleted — treat as success
if resp.status_code == 404:
return True
# 5xx is transient — retry
if 500 <= resp.status_code < 600 and attempt < max_retries - 1:
time.sleep(2**attempt)
continue
return False
return False
def sort_versions_by_date(
@@ -171,6 +194,7 @@ def main(
total_deleted = 0
total_kept = 0
total_failed = 0
for name in names:
click.echo(f"\n{'=' * 60}")
click.echo(f"Package: {owner}/{name}")
@@ -182,6 +206,7 @@ def main(
_("Failed to list versions for {name}: {error}", name=name, error=exc),
err=True,
)
total_failed += 1
continue
if not versions:
@@ -203,6 +228,7 @@ def main(
continue
deleted_count = 0
failed_count = 0
for v in to_delete:
version = str(v.get("version", ""))
if delete_package_version(base_url, owner, name, version, token):
@@ -210,11 +236,15 @@ def main(
deleted_count += 1
else:
click.echo(f" FAILED to delete: {version}", err=True)
failed_count += 1
total_deleted += deleted_count
total_kept += kept_count
total_failed += failed_count
click.echo(f"\nDone. Deleted {total_deleted}, kept {total_kept}.")
click.echo(f"\nDone. Deleted {total_deleted}, kept {total_kept}, failed {total_failed}.")
if total_failed > 0:
raise click.ClickException(_("Failed to delete {count} image version(s)", count=total_failed))
if __name__ == "__main__": # pragma: no cover
+19 -1
View File
@@ -6,6 +6,7 @@ Handles installation of:
- git-cliff (changelog generator)
- act_runner (Gitea Actions local runner, optional)
- tea (Gitea CLI official command-line tool for Gitea API operations)
- hadolint (Dockerfile linter)
Each tool is installed to ``~/.local/bin`` if not already on PATH.
Idempotent: skips tools that are already available.
@@ -39,6 +40,8 @@ ACT_RUNNER_VERSION = "0.2.11"
TEA_VERSION = "0.14.1"
HADOLINT_VERSION = "2.12.0"
def _arch() -> str:
"""Return the architecture string used by release assets."""
@@ -161,7 +164,20 @@ def install_tea() -> bool:
return True
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea"]
def install_hadolint() -> bool:
"""Install hadolint if not already present. Returns True if installed/skipped."""
if _is_installed("hadolint"):
click.echo("hadolint: already installed")
return True
machine = platform.machine().lower()
arch = "x86_64" if machine in {"x86_64", "amd64"} else "arm64"
url = f"https://github.com/hadolint/hadolint/releases/download/v{HADOLINT_VERSION}/hadolint-Linux-{arch}"
dest = _download_binary(url, "hadolint")
click.echo(f"hadolint: installed to {dest}")
return True
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea", "hadolint"]
def _install_tool(name: str) -> bool:
@@ -174,6 +190,8 @@ def _install_tool(name: str) -> bool:
return install_act_runner()
if name == "tea":
return install_tea()
if name == "hadolint":
return install_hadolint()
raise click.ClickException(f"Unknown tool: {name}")
+15 -7
View File
@@ -1391,6 +1391,14 @@
"ru": "CI_GITEA_TOKEN environment variable required",
"zh": "CI_GITEA_TOKEN environment variable required"
},
"Failed to delete {count} image version(s)": {
"bg": "Failed to delete {count} image version(s)",
"de": "Failed to delete {count} image version(s)",
"en": "Failed to delete {count} image version(s)",
"pl": "Failed to delete {count} image version(s)",
"ru": "Failed to delete {count} image version(s)",
"zh": "Failed to delete {count} image version(s)"
},
"CI_GITEA_TOKEN is not set. Required to create a PR.": {
"bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.",
"de": "CI_GITEA_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
@@ -1799,13 +1807,13 @@
"ru": "[check_test_coverage] No changed files to check.",
"zh": "[check_test_coverage] No changed files to check."
},
"[dry-run] Would commit: release: v{version}": {
"bg": "[dry-run] Would commit: release: v{version}",
"de": "[dry-run] Would commit: release: v{version}",
"en": "[dry-run] Would commit: release: v{version}",
"pl": "[dry-run] Utworzono by commit: release: v{version}",
"ru": "[dry-run] Would commit: release: v{version}",
"zh": "[dry-run] Would commit: release: v{version}"
"[dry-run] Would commit: release: v{version} [skip ci]": {
"bg": "[dry-run] Would commit: release: v{version} [skip ci]",
"de": "[dry-run] Would commit: release: v{version} [skip ci]",
"en": "[dry-run] Would commit: release: v{version} [skip ci]",
"pl": "[dry-run] Utworzono by commit: release: v{version} [skip ci]",
"ru": "[dry-run] Would commit: release: v{version} [skip ci]",
"zh": "[dry-run] Would commit: release: v{version} [skip ci]"
},
"[dry-run] Would create tag: v{version}": {
"bg": "[dry-run] Would create tag: v{version}",
+134 -9
View File
@@ -321,6 +321,25 @@ class TestCleanImagesAPI:
from devx.tools.clean_images import delete_package_version
mock_resp = MagicMock(status_code=204)
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp) as mock_del:
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
)
is True
)
# Verify URL includes container type
url = mock_del.call_args.args[0]
assert "/container/" in url
def test_delete_package_version_404_treated_as_success(self) -> None:
from devx.tools.clean_images import delete_package_version
mock_resp = MagicMock(status_code=404)
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
assert (
delete_package_version(
@@ -336,7 +355,7 @@ class TestCleanImagesAPI:
def test_delete_package_version_failure(self) -> None:
from devx.tools.clean_images import delete_package_version
mock_resp = MagicMock(status_code=404)
mock_resp = MagicMock(status_code=403)
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
assert (
delete_package_version(
@@ -349,6 +368,110 @@ class TestCleanImagesAPI:
is False
)
def test_delete_package_version_retries_on_5xx(self) -> None:
from devx.tools.clean_images import delete_package_version
responses = [
MagicMock(status_code=500),
MagicMock(status_code=502),
MagicMock(status_code=204),
]
with patch("devx.tools.clean_images.requests.delete", side_effect=responses):
with patch("devx.tools.clean_images.time.sleep"):
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
max_retries=3,
)
is True
)
def test_delete_package_version_retries_on_exception(self) -> None:
import requests as req
from devx.tools.clean_images import delete_package_version
responses = [
req.ConnectionError("network down"),
MagicMock(status_code=204),
]
with patch("devx.tools.clean_images.requests.delete", side_effect=responses):
with patch("devx.tools.clean_images.time.sleep"):
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
max_retries=3,
)
is True
)
def test_delete_package_version_exhausts_retries_on_exception(self) -> None:
import requests as req
from devx.tools.clean_images import delete_package_version
with patch(
"devx.tools.clean_images.requests.delete",
side_effect=req.ConnectionError("network down"),
):
with patch("devx.tools.clean_images.time.sleep"):
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
max_retries=2,
)
is False
)
def test_delete_package_version_exhausts_retries_on_5xx(self) -> None:
from devx.tools.clean_images import delete_package_version
with patch(
"devx.tools.clean_images.requests.delete",
return_value=MagicMock(status_code=500),
):
with patch("devx.tools.clean_images.time.sleep"):
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
max_retries=2,
)
is False
)
def test_delete_package_version_zero_retries(self) -> None:
from devx.tools.clean_images import delete_package_version
with patch("devx.tools.clean_images.requests.delete") as mock_del:
assert (
delete_package_version(
"https://git.example.com/api/v1",
"oblachno-oss",
"ci-base",
"0.1.0",
"token",
max_retries=0,
)
is False
)
mock_del.assert_not_called()
class TestCLIBuildImage:
def test_single_image_build(self, tmp_path: Path) -> None:
@@ -557,7 +680,7 @@ class TestCLICleanImages:
clean_main,
["--owner", "oblachno-oss", "--name", "ci-base", "--dry-run"],
)
assert result.exit_code == 0
assert result.exit_code != 0
assert "Failed to list" in result.output
def test_delete_failure_in_cli(self) -> None:
@@ -571,13 +694,15 @@ class TestCLICleanImages:
{"version": "0.3.0", "created_at": "2025-03-01"},
]
list_resp.raise_for_status = MagicMock()
delete_resp = MagicMock(status_code=500)
delete_resp = MagicMock(status_code=403)
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
with patch("devx.tools.clean_images.requests.get", return_value=list_resp):
with patch("devx.tools.clean_images.requests.delete", return_value=delete_resp):
result = runner.invoke(
clean_main,
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
)
assert result.exit_code == 0
assert "FAILED" in result.output
with patch("devx.tools.clean_images.time.sleep"):
result = runner.invoke(
clean_main,
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
)
assert result.exit_code != 0
assert "FAILED" in result.output
assert "failed" in result.output.lower()
+24 -1
View File
@@ -222,6 +222,24 @@ class TestInstallTea:
assert (tmp_path / "tea").exists()
class TestInstallHadolint:
def test_already_installed(self) -> None:
with patch.object(install_tools, "_is_installed", return_value=True):
assert install_tools.install_hadolint() is True
def test_install(self, tmp_path: Path) -> None:
def _write_file(url: str, path: Path) -> tuple[str, None]:
Path(path).write_bytes(b"binary")
return str(path), None
with patch.object(install_tools, "_is_installed", return_value=False):
with patch.object(install_tools, "TARGET_DIR", tmp_path):
with patch.object(platform, "machine", return_value="x86_64"):
with patch.object(install_tools, "_download", side_effect=_write_file):
assert install_tools.install_hadolint() is True
assert (tmp_path / "hadolint").exists()
class TestListTools:
def test_list(self, tmp_path: Path) -> None:
with patch.object(install_tools, "TARGET_DIR", tmp_path):
@@ -251,6 +269,11 @@ class TestInstallTool:
assert install_tools._install_tool("tea") is True
mock.assert_called_once()
def test_hadolint(self) -> None:
with patch.object(install_tools, "install_hadolint", return_value=True) as mock:
assert install_tools._install_tool("hadolint") is True
mock.assert_called_once()
def test_unknown_tool(self) -> None:
with pytest.raises(ClickException, match="Unknown tool"):
install_tools._install_tool("unknown")
@@ -269,7 +292,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 == 4
assert mock_install.call_count == 5
def test_install_specific_tool(self) -> None:
runner = CliRunner()
+2 -2
View File
@@ -802,7 +802,7 @@ class TestCommitReleaseChanges:
assert result is True
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
assert ["git", "add", "src/devx/__init__.py", "CHANGELOG.md"] in calls
assert ["git", "commit", "--no-verify", "-m", "release: v0.2.0"] in calls
assert ["git", "commit", "--no-verify", "-m", "release: v0.2.0 [skip ci]"] in calls
@patch("devx.ci.release.run_cmd")
def test_skips_when_no_changes(self, mock_run_cmd: MagicMock) -> None:
@@ -811,7 +811,7 @@ class TestCommitReleaseChanges:
result = commit_release_changes("0.1.0")
assert result is False
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
assert ["git", "commit", "--no-verify", "-m", "release: v0.1.0"] not in calls
assert ["git", "commit", "--no-verify", "-m", "release: v0.1.0 [skip ci]"] not in calls
class TestCreateAndPushTag: