Compare commits

...
12 Commits
Author SHA1 Message Date
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
devx-ci-bot 9170546a31 release: v0.22.0
Post-merge / detect-type (push) Successful in 8s
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 33s
Post-merge / badges (push) Successful in 24s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
2026-06-27 15:06:08 +00:00
emil 91f59076a1 DEVX-81: feat: document CI_GITEA_TOKEN scopes and add CI_GITEA_USERNAME to env var table
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / release (push) Successful in 20s
Post-merge / badges (push) Successful in 26s
Post-merge / publish (push) Successful in 14s
2026-06-27 15:05:37 +00:00
gitea-actions-bot ca310fe442 chore: update badge URLs to commit c8ba6417 [skip ci] 2026-06-27 15:00:55 +00:00
emil decba5ec77 DEVX-80: chore: update badge URLs to commit 20bcfe96 [skip ci] 2026-06-27 14:56:57 +00:00
gitea-actions-bot 082df1d893 chore: update badge URLs to commit 20bcfe96 [skip ci] 2026-06-27 13:47:51 +00:00
gitea-actions-bot 08f58e551b chore: update badge URLs to commit 8203528c [skip ci] 2026-06-27 13:47:06 +00:00
48 changed files with 362 additions and 292 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Gitea API token (required for CI scripts that interact with Gitea)
# Create at: https://git.oblachno.oblachno.fyi/user/settings/applications
REPO_TOKEN=
CI_GITEA_TOKEN=
# Vikunja API token (required for post-merge task updates)
# Create at: https://work.oblachno.oblachno.fyi/settings/tokens
+8 -8
View File
@@ -54,19 +54,19 @@ jobs:
fetch-depth: 0
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
run: make setup-release
- name: Docker registry login
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: |
. .venv/bin/activate
echo "$REPO_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$REGISTRY_USERNAME" --password-stdin
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
- name: Build and push tier images
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
PYTHONPATH: src
run: |
. .venv/bin/activate
@@ -95,7 +95,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
@@ -119,7 +119,7 @@ jobs:
run: make setup-ci
- name: Clean up old image versions
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
+4 -4
View File
@@ -103,7 +103,7 @@ jobs:
fetch-depth: 0
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
run: make setup-image
- name: Release dry-run validation
env:
@@ -127,7 +127,7 @@ jobs:
run: make setup-image
- name: Run automated PR review
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
set -euo pipefail
@@ -157,12 +157,12 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Set up environment
run: make setup-image
- name: Squash merge with task ID
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8"
PYTHONPATH: src
+13 -12
View File
@@ -93,10 +93,10 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
run: make setup-image
- name: Configure git
run: |
@@ -113,7 +113,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
@@ -137,11 +137,12 @@ 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
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
@@ -150,7 +151,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
@@ -178,7 +179,7 @@ jobs:
run: make setup-image
- name: Sync documentation to wiki
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
@@ -186,7 +187,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
@@ -210,7 +211,7 @@ jobs:
with:
fetch-depth: 0
ref: master
token: ${{ secrets.REPO_TOKEN }}
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Fetch latest master
run: |
git fetch origin master
@@ -226,7 +227,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
@@ -262,7 +263,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
@@ -287,7 +288,7 @@ jobs:
run: make setup-image
- name: Ensure branch protection and labels
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
@@ -295,7 +296,7 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
+3 -3
View File
@@ -17,7 +17,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)
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `REPO_TOKEN`)
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `CI_GITEA_TOKEN`)
## Workflow Verification (Before Push)
@@ -263,7 +263,7 @@ so `.:src` is not needed. The `src` directory is the sole import root.
The `tea` Gitea CLI tool is used for Gitea API interactions. It is installed
by `python -m devx.tools.install_tools` and configured by
`python -m devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
`python -m devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
**`devx.gitea_cli.TeaCLI`** — Python wrapper around `tea` CLI with JSON output parsing:
- `create_issue()` — Create issues with labels
@@ -358,7 +358,7 @@ devx uses environment variables with `.env` file fallback for configuration.
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_VIKUNJA_PROJECT_ID` | `6` | Vikunja project ID |
| `DEVX_LANG` | `en` | Language for i18n (en, bg) |
| `REPO_TOKEN` | (from .env) | Gitea API token |
| `CI_GITEA_TOKEN` | (from .env) | Gitea API token |
| `VIKUNJA_TOKEN` | (from .env) | Vikunja API token |
### Per-Project Overrides
+13
View File
@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file.
## [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
- Document CI_GITEA_TOKEN scopes and add CI_GITEA_USERNAME to env var table
## [0.21.2] - 2026-06-27
### Bug Fixes
+7 -5
View File
@@ -33,6 +33,8 @@ setup-release: $(VENV)/bin/activate .env
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate && pip install -e . --no-deps 2>/dev/null; \
export PATH="$$HOME/.local/bin:$$PATH"; \
python3 -m devx.tools.install_tools --tool hadolint 2>/dev/null || true; \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
.env:
@@ -101,12 +103,12 @@ lint-all: lint workflow-lint lint-dockerfiles
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
+21 -7
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/3a45f8e1896d9e0e07620fda23c78073514a14b9/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/python.svg)](https://www.python.org/downloads/)
## Why devx?
@@ -326,10 +326,24 @@ The config system loads `.env` automatically via `python-dotenv`.
| `DEVX_DOCS_DIR` | `docs` | Documentation directory (used by sync_wiki) |
| `DEVX_STATUS_CHECKS` | `CI / quality (pull_request)` | Comma-separated status check contexts |
| `DEVX_PYPI_REGISTRY_URL` | — | Gitea PyPI registry URL (used by publish) |
| `REPO_TOKEN` | — | Gitea API token |
| `CI_GITEA_TOKEN` | — | Gitea API token (see scopes below) |
| `CI_GITEA_USERNAME` | — | Gitea username for registry authentication |
| `VIKUNJA_TOKEN` | — | Vikunja API token |
| `PYPI_TOKEN` | — | Standard PyPI token (takes precedence over Gitea registry) |
#### CI_GITEA_TOKEN scopes
The `CI_GITEA_TOKEN` is a single Gitea Personal Access Token used across all
workflows. It requires these scopes:
| Scope | Purpose |
|-------|---------|
| `read:repository` | Read repos, PRs, issues, branches |
| `write:repository` | Push commits, merge PRs, create tags/releases, create issues, set branch protection, push wiki |
| `read:package` | Pull packages from Gitea PyPI registry, pull Docker images |
| `write:package` | Publish packages to Gitea PyPI registry, push Docker images |
| `read:organization` | Query org-level runners for molecule test distribution |
### Per-project overrides
Projects using devx can override the default API URLs and language by setting
+7 -7
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/3a45f8e1896d9e0e07620fda23c78073514a14b9/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/3a45f8e1896d9e0e07620fda23c78073514a14b9/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7471d49059023809dc04f376db0006c18498b270/python.svg)](https://www.python.org/downloads/)
## Overview
@@ -149,7 +149,7 @@ fallback. Key variables:
| `DEVX_REPO_NAME` | **(must be set)** | Repository name |
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh, pl) |
| `REPO_TOKEN` | — | Gitea API token |
| `CI_GITEA_TOKEN` | — | Gitea API token |
| `VIKUNJA_TOKEN` | — | Vikunja API token |
See [AGENTS.md](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/AGENTS.md)
+2 -2
View File
@@ -245,7 +245,7 @@ for retrying on git push failures.
Creates a Gitea issue when a CI workflow fails. Uses the `tea` CLI for issue
creation with failure labels. Supports `--auto-login` to configure the tea
CLI login profile from `REPO_TOKEN` and `DEVX_GITEA_API_URL` before creating
CLI login profile from `CI_GITEA_TOKEN` and `DEVX_GITEA_API_URL` before creating
the issue.
### `post_merge.py`
@@ -569,7 +569,7 @@ push_badges.py:
The `tea` Gitea CLI tool is used for Gitea API interactions where tea provides
reliable, official support. It is installed by
`python -m devx.tools.install_tools` and configured by
`python -m devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
`python -m devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
`devx.gitea_cli.TeaCLI` wraps tea with JSON output parsing. Operations that
tea does not support (wiki management, commit status, runner discovery,
+2 -2
View File
@@ -305,7 +305,7 @@ post-merge workflow when it creates and pushes a new version tag.
and the project itself
2. **Install CI tools** — git-cliff and tea via
`python -m devx.tools.install_tools`
3. **Configure tea login**`tea login add` using `REPO_TOKEN`
3. **Configure tea login**`tea login add` using `CI_GITEA_TOKEN`
4. **Build and publish**`python -m devx.ci.publish <tag> <owner/repo>`:
- Build the package with `python -m build`
- Publish to the Gitea PyPI registry (default) using `twine upload
@@ -377,7 +377,7 @@ python -m devx.ci.pr_review <pr_number> <owner/repo>
Creates a Gitea issue when a CI workflow fails. Uses the tea CLI for issue
creation with failure labels. Supports `--auto-login` to configure the tea
CLI login profile from `REPO_TOKEN`.
CLI login profile from `CI_GITEA_TOKEN`.
```bash
python -m devx.ci.notify_failure --repo <owner/repo> --run-id <id> \
+3 -3
View File
@@ -148,7 +148,7 @@ devx ci integration-guard -- -x -v --tb=short test_a.py
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `REPO_TOKEN` — API token with repo access
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
@@ -171,7 +171,7 @@ Options:
- `--run-id <id>` — CI run ID (required)
- `--workflow <name>` — workflow name (required)
- `--commit <sha>` — commit SHA (required)
- `--auto-login` — configure tea CLI login from `REPO_TOKEN` before creating
- `--auto-login` — configure tea CLI login from `CI_GITEA_TOKEN` before creating
the issue
### `devx ci post-merge`
@@ -462,7 +462,7 @@ Options:
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `REPO_TOKEN` — API token with repo access
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.21.2"
__version__ = "0.22.1"
+3 -3
View File
@@ -17,7 +17,7 @@ This allows the PR title to be a human-friendly Vikunja task title
while the squashed commit follows conventional commits.
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number>
CI_GITEA_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number>
"""
import os
@@ -196,9 +196,9 @@ def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
@click.argument("repo")
@click.argument("pr_number")
def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
# Validate PR number is an integer
try:
+7 -5
View File
@@ -15,7 +15,7 @@ Exit code 1 = NOT ready — fix issues before pushing.
Usage::
# CI (with VIKUNJA_TOKEN and REPO_TOKEN):
# CI (with VIKUNJA_TOKEN and CI_GITEA_TOKEN):
python3 -m devx.ci.check_auto_merge_ready \\
--branch "$HEAD_REF" \\
--pr-title "$PR_TITLE" \\
@@ -34,7 +34,7 @@ skipped (with a warning) — this allows local pre-push hooks to run
without CI secrets. In CI, the token is always set and the check is
mandatory.
If ``REPO_TOKEN`` is not set and ``--pr-number`` is not provided, only
If ``CI_GITEA_TOKEN`` is not set and ``--pr-number`` is not provided, only
branch-name and PR-title-format checks run (local mode).
"""
@@ -98,9 +98,9 @@ def is_branch_behind_master(branch: str) -> bool:
def get_pr_title_from_gitea(repo: str, pr_number: int) -> str | None:
"""Fetch the PR title from the Gitea API.
Returns ``None`` if ``REPO_TOKEN`` is not set or the PR cannot be fetched.
Returns ``None`` if ``CI_GITEA_TOKEN`` is not set or the PR cannot be fetched.
"""
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token or "/" not in repo:
return None
owner, repo_name = repo.split("/", 1)
@@ -189,7 +189,9 @@ def cli(
if pr_title is None:
# Local mode without PR — only validate branch name
if pr_number is not None:
raise click.ClickException(_("Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found)."))
raise click.ClickException(
_("Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).")
)
click.echo("[pre-merge-check] No PR title provided — running branch-name-only check (local mode).")
click.echo("[pre-merge-check] Branch name OK. Push to create PR, then CI will validate the title.")
return
+1 -1
View File
@@ -148,7 +148,7 @@ def main(
output_indices: bool,
github_output: bool,
) -> None:
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if owner is None:
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
+3 -3
View File
@@ -17,7 +17,7 @@ Usage::
Environment variables:
GITEA_URL Base URL of the Gitea instance.
REPO_TOKEN API token with repo access.
CI_GITEA_TOKEN API token with repo access.
RUN_ID Workflow run ID (GITHUB_RUN_ID).
JOB_NAME Base job name (GITHUB_JOB), e.g. "integration-tests".
MATRIX_INDEX Current matrix index (runner-index).
@@ -49,7 +49,7 @@ POLL_INTERVAL = 10
def cli(pytest_args: tuple[str, ...]) -> None:
"""Run pytest with cross-runner failure detection."""
gitea_url = os.environ.get("GITEA_URL", "")
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
run_id = int(os.environ.get("RUN_ID", "0"))
job_name = os.environ.get("JOB_NAME", "integration-tests")
current_index = int(os.environ.get("MATRIX_INDEX", "0"))
@@ -59,7 +59,7 @@ def cli(pytest_args: tuple[str, ...]) -> None:
owner, repo = "oblachno-oss", "devx"
if not all([gitea_url, token, run_id]):
click.echo(_("GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
click.echo(_("GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
stop_event = threading.Event()
failed_event = threading.Event()
+5 -5
View File
@@ -6,7 +6,7 @@ otherwise go unnoticed in the Actions tab. Uses the ``tea`` Gitea CLI
for issue creation tea must be installed and configured.
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.notify_failure \
CI_GITEA_TOKEN=<token> python3 -m devx.ci.notify_failure \
--repo <owner/repo> \
--run-id <run_id> \
--workflow <workflow_name> \
@@ -14,7 +14,7 @@ Usage:
--auto-login
With ``--auto-login``, the script configures the tea CLI login profile
from ``REPO_TOKEN`` and ``DEVX_GITEA_API_URL`` before creating the issue,
from ``CI_GITEA_TOKEN`` and ``DEVX_GITEA_API_URL`` before creating the issue,
eliminating the need for a separate ``tea login add`` step in the workflow.
"""
@@ -71,12 +71,12 @@ def _create_issue_via_tea(repo: str, title: str, body: str) -> int:
"--auto-login",
is_flag=True,
default=False,
help="Configure tea CLI login from REPO_TOKEN before creating the issue.",
help="Configure tea CLI login from CI_GITEA_TOKEN before creating the issue.",
)
def main(repo: str, run_id: str, workflow: str, commit: str, auto_login: bool) -> None:
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
if auto_login:
configure_tea_login()
+3 -3
View File
@@ -17,7 +17,7 @@ Checks performed:
8. Commit conventions conventional commit format on branch commits
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.pr_review <pr_number> <owner/repo>
CI_GITEA_TOKEN=<token> python3 -m devx.ci.pr_review <pr_number> <owner/repo>
"""
from __future__ import annotations
@@ -526,9 +526,9 @@ def post_review(client: GiteaClient, pr_number: str, result: ReviewResult) -> di
@click.option("--dry-run", is_flag=True, default=False, help="Print review without posting.")
def main(pr_number: str, repo: str, dry_run: bool) -> None:
"""Run automated PR review and post results to Gitea."""
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
owner, repo_name = repo.split("/")
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
+6 -6
View File
@@ -9,14 +9,14 @@ Publishing destinations (checked in order):
``DEVX_PYPI_REGISTRY_URL`` env var is set, or ``GITEA_API_URL``
is converted to a packages URL). Uses ``twine upload
--repository-url <url> -u <token> -p <token>`` with the
``REPO_TOKEN`` as both username and password.
``CI_GITEA_TOKEN`` as both username and password.
2. **Standard PyPI** if ``PYPI_TOKEN`` is set. Uses the standard
``twine upload -u __token__ -p <token>`` flow.
3. **Skip** if neither is configured, only the Gitea release is created.
Usage:
REPO_TOKEN=<token> [PYPI_TOKEN=<token>] python3 -m devx.ci.publish <tag> <repo>
REPO_TOKEN=<token> python3 -m devx.ci.publish <tag> <repo> --registry-url https://git.example.com/api/packages/owner/pypi
CI_GITEA_TOKEN=<token> [PYPI_TOKEN=<token>] python3 -m devx.ci.publish <tag> <repo>
CI_GITEA_TOKEN=<token> python3 -m devx.ci.publish <tag> <repo> --registry-url https://git.example.com/api/packages/owner/pypi
"""
import os
@@ -225,7 +225,7 @@ def is_release_commit(tag: str) -> bool:
"--auto-login",
is_flag=True,
default=False,
help="Configure tea CLI login from REPO_TOKEN before creating the Gitea release. "
help="Configure tea CLI login from CI_GITEA_TOKEN before creating the Gitea release. "
"Eliminates the need for a separate tea login step in containerized CI jobs.",
)
def main(
@@ -253,9 +253,9 @@ def main(
if not tag:
raise click.ClickException(_("Tag is required (or use --from-tag)."))
gitea_token = os.environ.get("REPO_TOKEN", "")
gitea_token = os.environ.get("CI_GITEA_TOKEN", "")
if not gitea_token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
pypi_token = os.environ.get("PYPI_TOKEN", "")
+1 -1
View File
@@ -29,7 +29,7 @@ version. This prevents duplicate release commits (a common issue when CI
checkouts don't fetch tags) and ensures tag/version/commit alignment.
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.release [--dry-run] [--skip-tests]
CI_GITEA_TOKEN=<token> python3 -m devx.ci.release [--dry-run] [--skip-tests]
python3 -m devx.ci.release --verify # Check tag/version/release alignment
"""
+3 -3
View File
@@ -14,7 +14,7 @@ Gitea 1.26 wiki API endpoints (all use content_base64, NOT content):
- Delete: DELETE /repos/{owner}/{repo}/wiki/page/{sub_url}
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.sync_wiki [--dry-run] [--repo owner/repo]
CI_GITEA_TOKEN=<token> python3 -m devx.ci.sync_wiki [--dry-run] [--repo owner/repo]
"""
from __future__ import annotations
@@ -225,9 +225,9 @@ def verify_wiki_integrity(
help="Full integrity check: verify page count, missing pages, stale pages, and content. Implies --verify.",
)
def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
if repo is None:
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
+4 -4
View File
@@ -56,10 +56,10 @@ class TeaCLIError(Exception):
def configure_tea_login(login_name: str = "devx") -> None:
"""Configure tea CLI login from REPO_TOKEN and DEVX_GITEA_API_URL.
"""Configure tea CLI login from CI_GITEA_TOKEN and DEVX_GITEA_API_URL.
Idempotent: if a login with the same name already exists, it is not re-added.
Skips silently if tea is not installed or REPO_TOKEN is not set.
Skips silently if tea is not installed or CI_GITEA_TOKEN is not set.
Used by CI scripts (publish, notify_failure) that need tea login but
run in containerized environments where ``make setup`` was not called.
@@ -69,9 +69,9 @@ def configure_tea_login(login_name: str = "devx") -> None:
click.echo(_("tea not installed — skipping login configuration."))
return
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
click.echo(_("REPO_TOKEN not set — skipping login configuration."))
click.echo(_("CI_GITEA_TOKEN not set — skipping login configuration."))
return
gitea_url = GITEA_API_URL.replace("/api/v1", "")
+13 -13
View File
@@ -54,11 +54,11 @@ DEVX_WORKFLOW_DIR ?= .gitea/workflows
# PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured.
# Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]'
# GITEA_PYPI_USER can be set in .env, as an env var, or as a Make variable.
DEVX_PIP_INSTALL := if [ -z "$$REPO_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
REPO_TOKEN="$${REPO_TOKEN:-$$GITEA_REGISTRY_TOKEN}"; \
_PYPI_USER="$${DEVX_GITEA_PYPI_USER:-$${GITEA_PYPI_USER}}"; \
if [ -n "$$REPO_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$REPO_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
# CI_GITEA_USERNAME can be set in .env, as an env var, or as a Make variable.
DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
_PYPI_USER="$${CI_GITEA_USERNAME:-emil}"; \
if [ -n "$$CI_GITEA_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
$(DEVX_BIN)/pip
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr devx-check-config
@@ -96,12 +96,12 @@ devx-push-with-pr: devx-push devx-create-pr
# ── Environment setup ─────────────────────────────────────────────────────────
# Configure Gitea private PyPI registry so pip can find devx and other
# private packages. In CI, REPO_TOKEN is set as a secret. Locally, it's in .env.
# private packages. In CI, CI_GITEA_TOKEN is set as a secret. Locally, it's in .env.
devx-configure-gitea-pypi:
@if [ -z "$$REPO_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
REPO_TOKEN="$${REPO_TOKEN:-$$GITEA_REGISTRY_TOKEN}"; \
if [ -z "$$REPO_TOKEN" ]; then echo "[configure-gitea-pypi] REPO_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (REPO_TOKEN present)."
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)."
# Create .env from .env.example if it doesn't exist
devx-env:
@@ -174,7 +174,7 @@ devx-workflow-check: devx-workflow-lint devx-workflow-dryrun
# Notify on CI failure — creates a Gitea issue via devx.ci.notify_failure.
# Usage: make devx-notify-failure WORKFLOW=post-merge/release
# Requires: REPO_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA
# Requires: CI_GITEA_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA
devx-notify-failure:
@. $(DEVX_VENV)/bin/activate 2>/dev/null || true; \
export PATH="$(HOME)/.local/bin:$$PATH"; \
@@ -268,8 +268,8 @@ devx-clean:
devx-setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv $(DEVX_VENV); . $(DEVX_BIN)/activate; \
_U="$${DEVX_GITEA_PYPI_USER:-$${GITEA_PYPI_USER:-emil}}"; \
if [ -n "$$REPO_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$_U:$$REPO_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
_U="$${CI_GITEA_USERNAME:-emil}"; \
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$_U:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
echo "[devx-setup-image] Linked /opt/venv$(if $(EXTRAS), with [$(EXTRAS)],)."; \
else echo "[devx-setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
+1 -1
View File
@@ -142,7 +142,7 @@ def main(
output_indices: bool,
github_output: bool,
) -> None:
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if owner is None:
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
+3 -3
View File
@@ -22,7 +22,7 @@ Usage::
Environment variables:
GITEA_URL Base URL of the Gitea instance.
REPO_TOKEN API token with repo access.
CI_GITEA_TOKEN API token with repo access.
RUN_ID Workflow run ID (GITHUB_RUN_ID).
JOB_NAME Base job name (GITHUB_JOB), e.g. "molecule-tests".
MATRIX_INDEX Current matrix index (runner-index).
@@ -163,7 +163,7 @@ def resolve_role_dir(role: str, roles_root: Path | None, repo_root: Path) -> Pat
def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None:
"""Run molecule pairs sequentially, stop if another CI runner fails."""
gitea_url = os.environ.get("GITEA_URL", "")
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
run_id = int(os.environ.get("RUN_ID", "0"))
job_name = os.environ.get("JOB_NAME", "molecule-tests")
current_index = int(os.environ.get("MATRIX_INDEX", "0"))
@@ -173,7 +173,7 @@ def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None:
owner, repo = "oblachno-oss", "devx"
if not all([gitea_url, token, run_id]):
click.echo(_("GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
click.echo(_("GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
# When devx is installed as a pip package, __file__ resolves to the
# site-packages directory, not the repo root. Use GITHUB_WORKSPACE
+6 -11
View File
@@ -34,9 +34,8 @@ The manifest file is a JSON list of dicts, each with:
- ``context``: build context directory (optional, defaults to repo root)
- ``tags``: list of tags (optional, defaults to ``["latest"]``)
Registry authentication uses ``REPO_TOKEN`` (or ``GITEA_REGISTRY_TOKEN``)
and ``REGISTRY_USERNAME`` (or ``GITEA_REGISTRY_USERNAME``) environment
variables, matching the existing CI workflow patterns.
Registry authentication uses ``CI_GITEA_TOKEN`` and ``CI_GITEA_USERNAME``
environment variables, matching the existing CI workflow patterns.
"""
from __future__ import annotations
@@ -221,13 +220,9 @@ def push_image(
def _get_registry_creds() -> tuple[str, str]:
"""Get registry credentials from environment variables.
Supports both REPO_TOKEN/GITEA_REGISTRY_TOKEN and
REGISTRY_USERNAME/GITEA_REGISTRY_USERNAME patterns.
"""
token = os.environ.get("REPO_TOKEN") or os.environ.get("GITEA_REGISTRY_TOKEN", "")
username = os.environ.get("REGISTRY_USERNAME") or os.environ.get("GITEA_REGISTRY_USERNAME", "")
"""Get registry credentials from environment variables."""
token = os.environ.get("CI_GITEA_TOKEN", "")
username = os.environ.get("CI_GITEA_USERNAME", "")
return username, token
@@ -312,7 +307,7 @@ def main(
username, token = _get_registry_creds()
if not token or not username:
raise click.ClickException(
_("Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars")
_("Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars")
)
if not registry_login(registry, username, token, dry_run=dry_run):
raise click.ClickException(_("Registry login failed"))
+3 -3
View File
@@ -28,7 +28,7 @@ Usage::
--keep 2 \\
--dry-run
Authentication uses ``REPO_TOKEN`` environment variable.
Authentication uses ``CI_GITEA_TOKEN`` environment variable.
"""
from __future__ import annotations
@@ -164,9 +164,9 @@ def main(
api_url: str | None,
) -> None:
"""Clean up old Docker image versions from a Gitea registry."""
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("REPO_TOKEN environment variable required"))
raise click.ClickException(_("CI_GITEA_TOKEN environment variable required"))
base_url = api_url or GITEA_API_URL
total_deleted = 0
+4 -4
View File
@@ -6,8 +6,8 @@ The ``tea`` CLI is used for label creation if available, with a
fallback to ``GiteaClient`` if tea is not installed.
Usage:
REPO_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo
REPO_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo --owner my-org
CI_GITEA_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo
CI_GITEA_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo --owner my-org
"""
from __future__ import annotations
@@ -102,7 +102,7 @@ def configure_repo(
api_url: Gitea API base URL. If None, uses ``GITEA_API_URL`` from config.
"""
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
url = api_url or GITEA_API_URL
client = GiteaClient(url, token, owner, repo)
@@ -148,7 +148,7 @@ def configure_repo(
)
def main(repo: str | None, owner: str | None, branch: str, api_url: str | None) -> None:
"""Configure branch protection and repository settings via the Gitea API."""
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if repo is None:
repo = os.environ.get("DEVX_REPO_NAME", "")
+2 -2
View File
@@ -123,9 +123,9 @@ def create_pr(
),
)
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("REPO_TOKEN is not set. Required to create a PR."))
raise click.ClickException(_("CI_GITEA_TOKEN is not set. Required to create a PR."))
vikunja_title = get_vikunja_task_title(task_id)
pr_title = f"{task_id}: {vikunja_title}"
+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}")
+4 -4
View File
@@ -64,19 +64,19 @@ def _install_ansible_collections(bin_dir: str) -> None:
def _configure_tea_login() -> None:
"""Configure tea CLI login from .env if REPO_TOKEN is set.
"""Configure tea CLI login from .env if CI_GITEA_TOKEN is set.
Idempotent: if a login with the same name already exists, it is not re-added.
Skips if tea is not installed or REPO_TOKEN is not set.
Skips if tea is not installed or CI_GITEA_TOKEN is not set.
"""
tea_bin = shutil.which("tea")
if tea_bin is None:
click.echo("tea: not installed — run 'make install-tools' to install it.")
return
token = os.environ.get("REPO_TOKEN", "")
token = os.environ.get("CI_GITEA_TOKEN", "")
if not token:
click.echo("tea: REPO_TOKEN not set — skipping login configuration.")
click.echo("tea: CI_GITEA_TOKEN not set — skipping login configuration.")
return
api_url = os.environ.get("DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1")
+49 -49
View File
@@ -535,13 +535,13 @@
"ru": "Could not extract conventional commit message from PR commits.",
"zh": "Could not extract conventional commit message from PR commits."
},
"Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).": {
"bg": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
"de": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
"en": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
"pl": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
"ru": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
"zh": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found)."
"Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).": {
"bg": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"de": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"en": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"pl": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"ru": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"zh": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found)."
},
"Could not find Vikunja task {task_id} in project {project_id}.": {
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}.",
@@ -655,13 +655,13 @@
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
},
"ERROR: REPO_TOKEN is not set.": {
"bg": "ГРЕШКА: REPO_TOKEN не е зададен.",
"de": "FEHLER: REPO_TOKEN ist nicht gesetzt.",
"en": "ERROR: REPO_TOKEN is not set.",
"pl": "BŁĄD: REPO_TOKEN nie jest ustawiony.",
"ru": "ОШИБКА: REPO_TOKEN не задан.",
"zh": "错误:未设置 REPO_TOKEN。"
"ERROR: CI_GITEA_TOKEN is not set.": {
"bg": "ГРЕШКА: CI_GITEA_TOKEN не е зададен.",
"de": "FEHLER: CI_GITEA_TOKEN ist nicht gesetzt.",
"en": "ERROR: CI_GITEA_TOKEN is not set.",
"pl": "BŁĄD: CI_GITEA_TOKEN nie jest ustawiony.",
"ru": "ОШИБКА: CI_GITEA_TOKEN не задан.",
"zh": "错误:未设置 CI_GITEA_TOKEN。"
},
"ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": {
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
@@ -759,13 +759,13 @@
"ru": "Found {count} stale documentation reference(s)",
"zh": "Found {count} stale documentation reference(s)"
},
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"pl": "GITEA_URL/REPO_TOKEN/RUN_ID nie ustawione; uruchamianie bez anulowania między runnerami.",
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
"GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
"bg": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"de": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"en": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"pl": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID nie ustawione; uruchamianie bez anulowania między runnerami.",
"ru": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"zh": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."
},
"Generated {file} with prefix '{prefix}'.": {
"bg": "Generated {file} with prefix '{prefix}'.",
@@ -1383,29 +1383,29 @@
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
},
"REPO_TOKEN environment variable required": {
"bg": "REPO_TOKEN environment variable required",
"de": "REPO_TOKEN environment variable required",
"en": "REPO_TOKEN environment variable required",
"pl": "REPO_TOKEN environment variable required",
"ru": "REPO_TOKEN environment variable required",
"zh": "REPO_TOKEN environment variable required"
"CI_GITEA_TOKEN environment variable required": {
"bg": "CI_GITEA_TOKEN environment variable required",
"de": "CI_GITEA_TOKEN environment variable required",
"en": "CI_GITEA_TOKEN environment variable required",
"pl": "CI_GITEA_TOKEN environment variable required",
"ru": "CI_GITEA_TOKEN environment variable required",
"zh": "CI_GITEA_TOKEN environment variable required"
},
"REPO_TOKEN is not set. Required to create a PR.": {
"bg": "REPO_TOKEN не е зададен. Необходим за създаване на PR.",
"de": "REPO_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
"en": "REPO_TOKEN is not set. Required to create a PR.",
"pl": "REPO_TOKEN nie jest ustawiony. Wymagany do utworzenia PR.",
"ru": "REPO_TOKEN не установлен. Требуется для создания PR.",
"zh": "REPO_TOKEN 未设置。创建 PR 所需。"
"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.",
"en": "CI_GITEA_TOKEN is not set. Required to create a PR.",
"pl": "CI_GITEA_TOKEN nie jest ustawiony. Wymagany do utworzenia PR.",
"ru": "CI_GITEA_TOKEN не установлен. Требуется для создания PR.",
"zh": "CI_GITEA_TOKEN 未设置。创建 PR 所需。"
},
"Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars": {
"bg": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
"de": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
"en": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
"pl": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
"ru": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
"zh": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars"
"Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": {
"bg": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"de": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"en": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"pl": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"ru": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"zh": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars"
},
"Registry login failed": {
"bg": "Registry login failed",
@@ -2007,13 +2007,13 @@
"ru": "tea not installed — skipping login configuration.",
"zh": "tea not installed — skipping login configuration."
},
"REPO_TOKEN not set — skipping login configuration.": {
"bg": "REPO_TOKEN not set — skipping login configuration.",
"de": "REPO_TOKEN not set — skipping login configuration.",
"en": "REPO_TOKEN not set — skipping login configuration.",
"pl": "REPO_TOKEN not set — skipping login configuration.",
"ru": "REPO_TOKEN not set — skipping login configuration.",
"zh": "REPO_TOKEN not set — skipping login configuration."
"CI_GITEA_TOKEN not set — skipping login configuration.": {
"bg": "CI_GITEA_TOKEN not set — skipping login configuration.",
"de": "CI_GITEA_TOKEN not set — skipping login configuration.",
"en": "CI_GITEA_TOKEN not set — skipping login configuration.",
"pl": "CI_GITEA_TOKEN not set — skipping login configuration.",
"ru": "CI_GITEA_TOKEN not set — skipping login configuration.",
"zh": "CI_GITEA_TOKEN not set — skipping login configuration."
},
"tea login '{name}' already configured.": {
"bg": "tea login '{name}' already configured.",
+11 -11
View File
@@ -217,7 +217,7 @@ class TestRunCmd:
class TestMain:
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
@patch("devx.ci.auto_merge.GiteaClient")
def test_full_merge_flow(
@@ -239,14 +239,14 @@ class TestMain:
assert result.exit_code == 0, result.output
mock_client.merge_pr.assert_called_once_with(7, "DEVX-19: fix: resolve timeout")
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
def test_no_token_raises(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: test", "owner/repo", "7"])
assert result.exit_code != 0
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.GiteaClient")
def test_no_task_id_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
@@ -256,7 +256,7 @@ class TestMain:
assert result.exit_code != 0
assert "No task ID" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.GiteaClient")
def test_invalid_pr_title_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
@@ -266,7 +266,7 @@ class TestMain:
assert result.exit_code != 0
assert "format" in result.output.lower()
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
@patch("devx.ci.auto_merge.GiteaClient")
def test_merge_behind_master_raises_no_rebase(
@@ -298,7 +298,7 @@ class TestMain:
# Must NOT have called merge_pr twice (no retry after rebase)
assert mock_client.merge_pr.call_count == 1
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
@patch("devx.ci.auto_merge.GiteaClient")
def test_merge_failure_raises(
@@ -321,7 +321,7 @@ class TestMain:
assert result.exit_code != 0
assert "Merge failed" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
@patch("devx.ci.auto_merge.GiteaClient")
def test_no_conventional_msg_raises(
@@ -342,7 +342,7 @@ class TestMain:
assert result.exit_code != 0
assert "conventional commit" in result.output.lower()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
def test_invalid_pr_number_raises(self, tmp_path, monkeypatch) -> None:
"""Non-integer PR number should raise."""
monkeypatch.chdir(tmp_path)
@@ -351,7 +351,7 @@ class TestMain:
assert result.exit_code != 0
assert "PR number must be an integer" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
def test_invalid_repo_format_raises(self, tmp_path, monkeypatch) -> None:
"""Repo without owner/name should raise."""
monkeypatch.chdir(tmp_path)
@@ -360,7 +360,7 @@ class TestMain:
assert result.exit_code != 0
assert "owner/name" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
@patch("devx.ci.auto_merge.GiteaClient")
def test_merge_behind_master_does_not_force_push(
+7 -7
View File
@@ -442,7 +442,7 @@ class TestCLIBuildImage:
dockerfile.touch()
runner = CliRunner()
login_result = MagicMock(returncode=1, stderr="auth failed", stdout="")
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake", "CI_GITEA_USERNAME": "user"}):
with patch("devx.tools.build_image.subprocess.run", return_value=login_result):
result = runner.invoke(
build_image.main,
@@ -458,7 +458,7 @@ class TestCLIBuildImage:
build_result = MagicMock(returncode=0)
login_result = MagicMock(returncode=0, stderr="", stdout="")
push_result = MagicMock(returncode=1, stderr="push failed", stdout="")
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake", "CI_GITEA_USERNAME": "user"}):
with patch(
"devx.tools.build_image.subprocess.run",
side_effect=[login_result, build_result, push_result],
@@ -482,7 +482,7 @@ class TestCLICleanImages:
{"version": "0.3.0", "created_at": "2025-03-01"},
]
mock_resp.raise_for_status = MagicMock()
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
result = runner.invoke(
clean_main,
@@ -511,7 +511,7 @@ class TestCLICleanImages:
mock_resp = MagicMock()
mock_resp.json.return_value = []
mock_resp.raise_for_status = MagicMock()
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
result = runner.invoke(
clean_main,
@@ -532,7 +532,7 @@ class TestCLICleanImages:
]
list_resp.raise_for_status = MagicMock()
delete_resp = MagicMock(status_code=204)
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
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(
@@ -548,7 +548,7 @@ class TestCLICleanImages:
from devx.tools.clean_images import main as clean_main
runner = CliRunner()
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
with patch(
"devx.tools.clean_images.requests.get",
side_effect=req.ConnectionError("network down"),
@@ -572,7 +572,7 @@ class TestCLICleanImages:
]
list_resp.raise_for_status = MagicMock()
delete_resp = MagicMock(status_code=500)
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
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(
+3 -3
View File
@@ -78,7 +78,7 @@ class TestGetPrTitleFromGitea:
assert get_pr_title_from_gitea("owner/repo", 1) is None
def test_returns_none_with_invalid_repo(self) -> None:
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
assert get_pr_title_from_gitea("invalid", 1) is None
@patch("devx.ci.check_auto_merge_ready.GiteaClient")
@@ -86,7 +86,7 @@ class TestGetPrTitleFromGitea:
mock_client = MagicMock()
mock_client.get_pr.return_value = {"title": "DEVX-1: Fix bug"}
mock_client_cls.return_value = mock_client
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
result = get_pr_title_from_gitea("owner/repo", 1)
assert result == "DEVX-1: Fix bug"
@@ -95,7 +95,7 @@ class TestGetPrTitleFromGitea:
mock_client = MagicMock()
mock_client.get_pr.side_effect = Exception("API error")
mock_client_cls.return_value = mock_client
with patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True):
result = get_pr_title_from_gitea("owner/repo", 1)
assert result is None
+2 -2
View File
@@ -248,8 +248,8 @@ class TestDevxI18n:
import devx.i18n
importlib.reload(devx.i18n)
# "ERROR: REPO_TOKEN is not set." has a German translation
result = devx.i18n._("ERROR: REPO_TOKEN is not set.")
# "ERROR: CI_GITEA_TOKEN is not set." has a German translation
result = devx.i18n._("ERROR: CI_GITEA_TOKEN is not set.")
assert "FEHLER" in result
# Restore
+12 -12
View File
@@ -47,7 +47,7 @@ class TestDefaultConfigs:
class TestConfigureRepo:
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_configure_repo_success(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -58,7 +58,7 @@ class TestConfigureRepo:
mock_client.ensure_branch_protection.assert_called_once()
mock_client.update_repo_settings.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_configure_repo_api_error(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -69,10 +69,10 @@ class TestConfigureRepo:
configure_repo(token="tok", owner="owner", repo="repo")
def test_configure_repo_no_token(self) -> None:
with pytest.raises(click.ClickException, match="REPO_TOKEN"):
with pytest.raises(click.ClickException, match="CI_GITEA_TOKEN"):
configure_repo(token="", owner="owner", repo="repo")
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_configure_repo_custom_configs(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -102,7 +102,7 @@ class TestConfigureRepo:
class TestMain:
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "DEVX_REPO_NAME": "myrepo"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "myrepo"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_success_with_env_repo(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -114,7 +114,7 @@ class TestMain:
mock_client.ensure_branch_protection.assert_called_once()
mock_client.update_repo_settings.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_success_with_cli_repo(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -125,7 +125,7 @@ class TestMain:
assert result.exit_code == 0
mock_client.ensure_branch_protection.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_api_error(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -142,16 +142,16 @@ class TestMain:
runner = CliRunner()
result = runner.invoke(main, ["--repo", "myrepo"])
assert result.exit_code != 0
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
def test_main_no_repo(self) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code != 0
assert "Repository name not specified" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_custom_branch(self, mock_client_cls: MagicMock) -> None:
mock_client = MagicMock()
@@ -164,7 +164,7 @@ class TestMain:
args = mock_client.ensure_branch_protection.call_args
assert args[0][0] == "develop"
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "DEVX_REPO_NAME": "oblachno/infra"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "oblachno/infra"}, clear=True)
@patch("devx.tools.configure_repo.GiteaClient")
def test_main_parses_owner_repo_from_env(self, mock_client_cls: MagicMock) -> None:
"""DEVX_REPO_NAME with 'owner/repo' format should be split."""
@@ -181,7 +181,7 @@ class TestMain:
@patch.dict(
"os.environ",
{"REPO_TOKEN": "tok", "DEVX_REPO_NAME": "infra", "DEVX_REPO_OWNER": "oblachno"},
{"CI_GITEA_TOKEN": "tok", "DEVX_REPO_NAME": "infra", "DEVX_REPO_OWNER": "oblachno"},
clear=True,
)
@patch("devx.tools.configure_repo.REPO_OWNER", "oblachno")
+4 -4
View File
@@ -95,7 +95,7 @@ class TestCreatePr:
@patch("devx.tools.create_pr.GiteaClient")
@patch("devx.tools.create_pr.get_vikunja_task_title", return_value="Add feature")
@patch("devx.tools.create_pr.find_existing_pr", return_value=None)
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
def test_creates_new_pr(self, mock_find: MagicMock, mock_title: MagicMock, mock_gitea: MagicMock) -> None:
mock_client = MagicMock()
mock_client.create_pr.return_value = {"number": 15, "html_url": "https://git.example.com/pr/15"}
@@ -112,7 +112,7 @@ class TestCreatePr:
@patch("devx.tools.create_pr.GiteaClient")
@patch("devx.tools.create_pr.get_vikunja_task_title", return_value="Add feature")
@patch("devx.tools.create_pr.find_existing_pr")
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
def test_existing_pr_idempotent(self, mock_find: MagicMock, mock_title: MagicMock, mock_gitea: MagicMock) -> None:
mock_find.return_value = {"number": 10, "html_url": "https://git.example.com/pr/10"}
mock_client = MagicMock()
@@ -123,10 +123,10 @@ class TestCreatePr:
@patch.dict("os.environ", {}, clear=True)
def test_no_repo_token(self) -> None:
with pytest.raises(click.ClickException, match="REPO_TOKEN"):
with pytest.raises(click.ClickException, match="CI_GITEA_TOKEN"):
create_pr("DEVX-42-fix", "master", "", "owner", "repo")
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
def test_no_task_id_in_branch(self) -> None:
with pytest.raises(click.ClickException, match="does not contain a task ID"):
create_pr("feature-branch", "master", "", "owner", "repo")
+4 -4
View File
@@ -362,19 +362,19 @@ class TestWhoami:
class TestConfigureTeaLogin:
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
def test_no_token_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no token prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value=None)
def test_no_tea_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no tea binary prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
def test_configures_login_when_not_present(self, mock_subprocess: MagicMock, mock_which: MagicMock) -> None:
@@ -384,7 +384,7 @@ class TestConfigureTeaLogin:
configure_tea_login()
assert mock_subprocess.call_count >= 2 # login list + login add + login default
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
def test_skips_when_already_configured(self, mock_subprocess: MagicMock, mock_which: MagicMock) -> None:
+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()
+4 -4
View File
@@ -101,7 +101,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
@@ -148,7 +148,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
@@ -193,7 +193,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "integration-tests",
"MATRIX_INDEX": "0",
@@ -239,7 +239,7 @@ class TestCli:
assert "without cross-runner cancellation" in result.output
def test_partial_env_vars_runs_without_polling(self) -> None:
"""Only GITEA_URL set (missing REPO_TOKEN and RUN_ID) — should skip polling."""
"""Only GITEA_URL set (missing CI_GITEA_TOKEN and RUN_ID) — should skip polling."""
with (
patch.dict(
os.environ,
+5 -5
View File
@@ -220,7 +220,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
@@ -288,7 +288,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
@@ -325,7 +325,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
@@ -371,7 +371,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
@@ -418,7 +418,7 @@ class TestCli:
os.environ,
{
"GITEA_URL": "https://gitea.example",
"REPO_TOKEN": "token",
"CI_GITEA_TOKEN": "token",
"RUN_ID": "123",
"JOB_NAME": "molecule-tests",
"MATRIX_INDEX": "0",
+15 -15
View File
@@ -9,7 +9,7 @@ from devx.gitea_cli import TeaCLIError, configure_tea_login
class TestNotifyFailure:
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.notify_failure.TeaCLI")
def test_creates_issue_with_tea(self, mock_tea_cls: MagicMock) -> None:
mock_tea = MagicMock()
@@ -36,7 +36,7 @@ class TestNotifyFailure:
mock_tea.create_issue.assert_called_once()
mock_tea.add_label.assert_called_once_with("owner/repo", 42, ["bug"])
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.notify_failure.TeaCLI")
def test_tea_creates_issue_without_bug_label(self, mock_tea_cls: MagicMock) -> None:
mock_tea = MagicMock()
@@ -53,7 +53,7 @@ class TestNotifyFailure:
assert "issue #43" in result.output
mock_tea.add_label.assert_not_called()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.notify_failure.TeaCLI")
def test_tea_error_raises(self, mock_tea_cls: MagicMock) -> None:
"""When tea fails, the workflow fails — no fallback."""
@@ -70,7 +70,7 @@ class TestNotifyFailure:
assert result.exit_code != 0
assert "tea" in result.output.lower()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.notify_failure.TeaCLI")
def test_tea_list_labels_error_continues_without_labels(self, mock_tea_cls: MagicMock) -> None:
"""If listing labels fails via tea, issue is still created without labels."""
@@ -87,7 +87,7 @@ class TestNotifyFailure:
assert result.exit_code == 0
assert "issue #50" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.notify_failure.TeaCLI")
def test_tea_add_label_error_is_ignored(self, mock_tea_cls: MagicMock) -> None:
"""If adding label fails via tea, issue is still reported as created."""
@@ -105,7 +105,7 @@ class TestNotifyFailure:
assert result.exit_code == 0
assert "issue #51" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(
@@ -113,9 +113,9 @@ class TestNotifyFailure:
["--repo", "owner/repo", "--run-id", "1", "--workflow", "release", "--commit", "abc"],
)
assert result.exit_code != 0
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value=None)
@patch("devx.ci.notify_failure.TeaCLI")
def test_auto_login_no_tea_skips(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
@@ -133,11 +133,11 @@ class TestNotifyFailure:
assert result.exit_code == 0
assert "issue #60" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.ci.notify_failure.TeaCLI")
def test_auto_login_no_token_skips_login(self, mock_tea_cls: MagicMock, mock_which: MagicMock) -> None:
"""--auto-login with no REPO_TOKEN skips login but raises before creating issue."""
"""--auto-login with no CI_GITEA_TOKEN skips login but raises before creating issue."""
mock_tea = MagicMock()
mock_tea_cls.return_value = mock_tea
@@ -147,23 +147,23 @@ class TestNotifyFailure:
["--repo", "owner/repo", "--run-id", "1", "--workflow", "release", "--commit", "abc", "--auto-login"],
)
assert result.exit_code != 0
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
class TestConfigureTeaLogin:
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
def test_no_token_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no token prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value=None)
def test_no_tea_skips(self, mock_which: MagicMock) -> None:
"""configure_tea_login with no tea binary prints skip message and returns."""
configure_tea_login()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
@patch("devx.ci.notify_failure.TeaCLI")
@@ -191,7 +191,7 @@ class TestConfigureTeaLogin:
# tea login add was called
assert mock_subprocess.call_count >= 2
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.gitea_cli.shutil.which", return_value="/usr/bin/tea")
@patch("devx.gitea_cli.subprocess.run")
@patch("devx.ci.notify_failure.TeaCLI")
+6 -6
View File
@@ -683,7 +683,7 @@ class TestMain:
def test_dry_run_does_not_post(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = ReviewResult()
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm", "--dry-run"], env={"REPO_TOKEN": "fake"})
result = runner.invoke(main, ["42", "oblachno-oss/grm", "--dry-run"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "[dry-run]" in result.output
mock_client_class.return_value.create_review.assert_not_called()
@@ -694,7 +694,7 @@ class TestMain:
mock_run.return_value = ReviewResult()
mock_client_class.return_value.create_review.return_value = {"id": 123}
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"REPO_TOKEN": "fake"})
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #123" in result.output
mock_client_class.return_value.create_review.assert_called_once()
@@ -710,7 +710,7 @@ class TestMain:
{"id": 124},
]
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"REPO_TOKEN": "fake"})
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #124" in result.output
assert client.create_review.call_count == 2
@@ -723,14 +723,14 @@ class TestMain:
client = mock_client_class.return_value
client.create_review.side_effect = APIError(500, "Internal server error")
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"REPO_TOKEN": "fake"})
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code != 0
def test_no_token_raises(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"REPO_TOKEN": ""})
result = runner.invoke(main, ["42", "oblachno-oss/grm"], env={"CI_GITEA_TOKEN": ""})
assert result.exit_code != 0
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
def test_main_module_block() -> None:
+19 -19
View File
@@ -165,7 +165,7 @@ class TestDefaultGiteaRegistryUrl:
class TestMain:
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -190,7 +190,7 @@ class TestMain:
"owner/repo", tag="v1.0.0", title="v1.0.0", body="Release notes"
)
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"}, clear=True)
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_gitea_registry")
@@ -213,7 +213,7 @@ class TestMain:
mock_gitea_publish.assert_called_once()
mock_tea.create_release.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"}, clear=True)
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_gitea_registry")
@@ -239,7 +239,7 @@ class TestMain:
@patch.dict(
"os.environ",
{"REPO_TOKEN": "gitea-tok", "DEVX_PYPI_REGISTRY_URL": "https://env.registry.com/pypi"},
{"CI_GITEA_TOKEN": "gitea-tok", "DEVX_PYPI_REGISTRY_URL": "https://env.registry.com/pypi"},
clear=True,
)
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@@ -262,7 +262,7 @@ class TestMain:
assert result.exit_code == 0
mock_gitea_publish.assert_called_once_with("https://env.registry.com/pypi", "gitea-tok")
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"}, clear=True)
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.build_package")
@@ -284,14 +284,14 @@ class TestMain:
assert "PYPI_TOKEN not set" in result.output
mock_tea.create_release.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
def test_missing_repo_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 1
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -305,7 +305,7 @@ class TestMain:
assert result.exit_code == 1
assert "build" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -326,7 +326,7 @@ class TestMain:
"owner/repo", tag="v1.0.0", title="v1.0.0", body="Release notes"
)
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -343,7 +343,7 @@ class TestMain:
assert result.exit_code == 1
assert "Release creation failed" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.build_package")
@@ -361,7 +361,7 @@ class TestMain:
mock_build.assert_not_called()
mock_tea.create_release.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -379,7 +379,7 @@ class TestMain:
assert "already exists" in result.output
mock_tea.create_release.assert_not_called()
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@@ -395,7 +395,7 @@ class TestMain:
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 0
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_gitea_registry")
@@ -419,7 +419,7 @@ class TestMain:
assert result.exit_code == 0
assert "already exists" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_gitea_registry")
@@ -521,7 +521,7 @@ class TestFromTag:
@patch("devx.ci.publish.is_release_commit", return_value=True)
@patch("devx.ci.publish.get_latest_tag", return_value="v1.0.0")
def test_from_tag_publishes(self, _mock_tag: MagicMock, _mock_rel: MagicMock) -> None:
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake"}):
with patch("devx.ci.publish.TeaCLI") as mock_tea_cls:
mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
@@ -535,7 +535,7 @@ class TestFromTag:
@patch("devx.ci.publish.is_release_commit", return_value=True)
@patch("devx.ci.publish.get_latest_tag", return_value="v1.0.0")
def test_from_tag_publishes_no_repo_arg(self, _mock_tag: MagicMock, _mock_rel: MagicMock) -> None:
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "GITHUB_REPOSITORY": "owner/repo"}):
with patch.dict("os.environ", {"CI_GITEA_TOKEN": "fake", "GITHUB_REPOSITORY": "owner/repo"}):
with patch("devx.ci.publish.TeaCLI") as mock_tea_cls:
mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
@@ -556,7 +556,7 @@ class TestFromTag:
class TestPublishAutoLogin:
"""Tests for --auto-login flag in publish."""
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.publish.configure_tea_login")
@patch("devx.ci.publish.TeaCLI")
def test_auto_login_calls_configure(self, mock_tea_cls: MagicMock, mock_login: MagicMock) -> None:
@@ -571,7 +571,7 @@ class TestPublishAutoLogin:
assert result.exit_code == 0
mock_login.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.publish.configure_tea_login")
@patch("devx.ci.publish.TeaCLI")
def test_no_auto_login_skips_configure(self, mock_tea_cls: MagicMock, mock_login: MagicMock) -> None:
+4 -4
View File
@@ -120,7 +120,7 @@ class TestConfigureTeaLogin:
@patch("devx.tools.setup.subprocess.run")
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok123"}, clear=True)
def test_login_already_exists(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stdout="devx\ngrm\n", stderr="")
_configure_tea_login()
@@ -130,7 +130,7 @@ class TestConfigureTeaLogin:
@patch("devx.tools.setup.subprocess.run")
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok123"}, clear=True)
def test_login_add_success(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
list_result = MagicMock(returncode=0, stdout="", stderr="")
add_result = MagicMock(returncode=0, stdout="", stderr="")
@@ -143,7 +143,7 @@ class TestConfigureTeaLogin:
@patch("devx.tools.setup.subprocess.run")
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok123"}, clear=True)
def test_login_add_failure(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
list_result = MagicMock(returncode=0, stdout="", stderr="")
add_result = MagicMock(returncode=1, stdout="", stderr="auth failed")
@@ -155,7 +155,7 @@ class TestConfigureTeaLogin:
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
@patch.dict(
"os.environ",
{"REPO_TOKEN": "tok123", "DEVX_GITEA_API_URL": "https://custom.example.com/api/v1"},
{"CI_GITEA_TOKEN": "tok123", "DEVX_GITEA_API_URL": "https://custom.example.com/api/v1"},
clear=True,
)
def test_custom_gitea_url(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
+17 -15
View File
@@ -286,7 +286,7 @@ class TestVerifyWikiIntegrity:
class TestMain:
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.sync_wiki.MAPPING_FILE")
@patch("devx.ci.sync_wiki.DOCS_DIR")
@patch("devx.ci.sync_wiki.GiteaClient")
@@ -301,14 +301,16 @@ class TestMain:
assert result.exit_code == 0
assert "dry-run" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo"])
assert result.exit_code == 1
assert "REPO_TOKEN" in result.output
assert "CI_GITEA_TOKEN" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "DEVX_REPO_OWNER": "me", "DEVX_REPO_NAME": "myrepo"}, clear=True)
@patch.dict(
"os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_OWNER": "me", "DEVX_REPO_NAME": "myrepo"}, clear=True
)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_auto_detect_repo(self, mock_client_cls: MagicMock) -> None:
"""Test that repo is auto-detected from env vars when --repo is not passed."""
@@ -322,7 +324,7 @@ class TestMain:
assert result.exit_code == 0
mock_client_cls.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_missing_mapping_file(self, mock_client_cls: MagicMock) -> None:
"""Test that missing mapping.json exits with error."""
@@ -333,7 +335,7 @@ class TestMain:
assert result.exit_code == 1
assert "mapping.json" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_existing_pages_message(self, mock_client_cls: MagicMock) -> None:
"""Test that existing wiki pages are reported."""
@@ -347,7 +349,7 @@ class TestMain:
assert result.exit_code == 0
assert "existing wiki pages" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_file_not_found_fails(self, mock_client_cls: MagicMock) -> None:
"""Test that missing doc files cause an error, not a warning."""
@@ -361,7 +363,7 @@ class TestMain:
assert result.exit_code != 0
assert "not found" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_empty_doc_file_fails(self, mock_client_cls: MagicMock) -> None:
"""Test that empty doc files cause an error, not a warning."""
@@ -375,7 +377,7 @@ class TestMain:
assert result.exit_code != 0
assert "empty" in result.output.lower()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_create_and_update(self, mock_client_cls: MagicMock) -> None:
"""Test that pages are created and updated correctly (non-dry-run)."""
@@ -393,7 +395,7 @@ class TestMain:
assert "Created: 1" in result.output
assert "Updated: 1" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_passes(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify passes when content matches."""
@@ -413,7 +415,7 @@ class TestMain:
assert result.exit_code == 0
assert "Verification passed" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_fails_on_empty_content(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify fails when wiki pages have empty content."""
@@ -430,7 +432,7 @@ class TestMain:
assert result.exit_code == 1
assert "FAIL" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify is skipped during dry-run."""
@@ -444,7 +446,7 @@ class TestMain:
assert result.exit_code == 0
assert "Verification" not in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_passes(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict passes when integrity check succeeds."""
@@ -461,7 +463,7 @@ class TestMain:
assert result.exit_code == 0
assert "Integrity check passed" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_fails_on_integrity_issues(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict fails when integrity check finds issues."""
@@ -483,7 +485,7 @@ class TestMain:
assert "Missing page: FAQ" in result.output
assert "Stale page: Old-Page" in result.output
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict verification is skipped during dry-run."""