Compare commits

..
12 Commits
Author SHA1 Message Date
devx-ci-bot d47e3833bc release: v0.51.9 [skip ci] 2026-08-26 18:11:18 +00:00
emil c00e9e3d7b DEVX-166: fix: exclude docs/plans/* from PR size check
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m25s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 18:10:34 +00:00
gitea-actions-bot 40e95bc96f chore: update badge URLs to commit ae0cb5b8 [skip ci] 2026-08-26 18:06:00 +00:00
devx-ci-bot 3aa9681404 release: v0.51.8 [skip ci] 2026-08-26 18:04:45 +00:00
emil e96f63cb40 DEVX-165: fix: accept deps: as valid conventional commit type
Post-merge / detect-and-configure (push) Successful in 14s
Post-merge / release-and-maintain (push) Successful in 1m46s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 18:03:57 +00:00
gitea-actions-bot 6d6c8cceec chore: update badge URLs to commit 2f14bdfd [skip ci] 2026-08-26 14:29:30 +00:00
devx-ci-bot d9489b5387 release: v0.51.7 [skip ci] 2026-08-26 14:28:39 +00:00
kireto 35f7bc92cd DEVX-164: fix: increase HTTP 500 retry count to 5 with longer backoff and visible logging
Post-merge / detect-and-configure (push) Successful in 11s
Post-merge / release-and-maintain (push) Successful in 1m20s
2026-08-26 14:27:57 +00:00
gitea-actions-bot 55bcd8fa01 chore: update badge URLs to commit f474ba2f [skip ci] 2026-08-26 14:11:33 +00:00
devx-ci-bot 990f70845c release: v0.51.6 [skip ci] 2026-08-26 14:10:44 +00:00
kireto 149e8846b8 DEVX-163: fix: use stderr=STDOUT to capture all docker push output in one stream
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m18s
2026-08-26 14:09:59 +00:00
gitea-actions-bot a7cdebc0dc chore: update badge URLs to commit 73dcdbaf [skip ci] 2026-08-26 13:42:00 +00:00
17 changed files with 202 additions and 71 deletions
+24
View File
@@ -2,6 +2,30 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.51.9] - 2026-08-26
### Bug Fixes
- Exclude docs/plans/* from PR size check
## [0.51.8] - 2026-08-26
### Bug Fixes
- Accept deps: as valid conventional commit type
## [0.51.7] - 2026-08-26
### Bug Fixes
- Increase HTTP 500 retry count to 5 with longer backoff and visible logging
## [0.51.6] - 2026-08-26
### Bug Fixes
- Use stderr=STDOUT to capture all docker push output in one stream
## [0.51.5] - 2026-08-26 ## [0.51.5] - 2026-08-26
### Bug Fixes ### Bug Fixes
+9 -9
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/python.svg)](https://www.python.org/downloads/)
## Why devx? ## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.51.5", "devx>=0.51.9",
] ]
[tool.pip] [tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
``` ```
> **Note:** If your project requires a specific devx version, pin it in > **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.51.5"`) or use a version constraint > `dependencies` (for example, `"devx==0.51.9"`) or use a version constraint
> (for example, `"devx>=0.51.5,<0.52"`). > (for example, `"devx>=0.51.9,<0.52"`).
### Optional extras ### Optional extras
+8 -8
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/7660d5019b904710f01bf56fc45e0b9d4d87b53d/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/ae0cb5b8a4b82578eb74b43d632e1c0c04abcacb/python.svg)](https://www.python.org/downloads/)
## Overview ## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.51.5", "devx>=0.51.9",
] ]
[tool.pip] [tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple" extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
``` ```
Pin a specific version if needed: `"devx==0.51.5"` or `"devx>=0.51.5,<0.52"`. Pin a specific version if needed: `"devx==0.51.9"` or `"devx>=0.51.9,<0.52"`.
### Optional extras ### Optional extras
+34
View File
@@ -0,0 +1,34 @@
# DEVX-164: Increase HTTP 500 retry count and backoff for docker push
## Problem
The HTTP 500 retry logic (DEVX-162, DEVX-163) works correctly — 3 retry
attempts are made. But all 3 attempts fail because the Gitea registry's
"offset mismatch" race condition needs more than ~15s to recover. The
current backoff is 5s-20s with 3 attempts (total ~15s of waiting).
## Approach
Increase retry count from 3 to 5 and backoff from 5-20s to 10-60s,
giving the registry up to ~2 minutes to recover. Add visible logging
between retry attempts so the CI logs show the retry happening.
REQ-1: Increase retry count from 3 to 5
REQ-2: Increase backoff from 5-20s to 10-60s exponential
REQ-3: Add visible logging between retry attempts (click.echo)
REQ-4: All tests pass with 100% coverage
## Test Plan
- Unit tests verify retry count and backoff parameters
- Unit tests verify logging output on retry
- Manual: trigger build-images workflow and verify retries visible in logs
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Increase retry count from 3 to 5
- [x] REQ-2: Increase backoff from 5-20s to 10-60s exponential
- [x] REQ-3: Add visible logging between retry attempts (click.echo)
- [x] REQ-4: All tests pass with 100% coverage
+31
View File
@@ -0,0 +1,31 @@
# DEVX-165: Accept deps: as valid conventional commit type
## Problem
The commit validator rejects `deps:` as a conventional commit type, causing
post-merge CI failures on grm and sso-bridge repos where automated dependency
bump PRs use `deps: bump devx...` as the commit message.
## Approach
REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
REQ-2: Update the allowed types list in the error message in
`src/devx/ci/validate_commit_msg.py`
REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
and `tests/unit/test_validate_commit_msg.py`
## Test Plan
- `make pytest-cov` passes with 100% coverage
- `make lint-all` passes
## Deploy Plan
- Merge to master → post-merge auto-publishes new devx version
- grm and sso-bridge bump devx version to pick up the fix
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
- [x] REQ-2: Update the allowed types list in the error message in
`src/devx/ci/validate_commit_msg.py`
- [x] REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
and `tests/unit/test_validate_commit_msg.py`
+27
View File
@@ -0,0 +1,27 @@
# DEVX-166: Exclude docs/plans/* from PR size check
## Problem
Planning docs in `docs/plans/` are legitimately large (700+ lines) but
fail the PR size check (max 500 lines). This blocks PRs that only add
planning documents.
## Approach
REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
`src/devx/ci/check_pr_size.py`
REQ-2: Add test coverage for the new exclusion pattern
## Test Plan
- `make pytest-cov` passes with 100% coverage
- `make lint-all` passes
## Deploy Plan
- Merge to master → post-merge auto-publishes new devx version
- Infra PR #1179 picks up the fix once devx is bumped
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
`src/devx/ci/check_pr_size.py`
- [x] REQ-2: Add test coverage for the new exclusion pattern
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.51.5", "devx>=0.51.9",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"devx>=0.51.5", "devx>=0.51.9",
] ]
``` ```
+1 -1
View File
@@ -6,4 +6,4 @@ create_dependency_pr, auto_merge, release, publish), developer tooling
molecule testing helpers for Ansible projects. molecule testing helpers for Ansible projects.
""" """
__version__ = "0.51.5" __version__ = "0.51.9"
+1
View File
@@ -36,6 +36,7 @@ DEFAULT_EXCLUDED_PATTERNS = [
"CHANGELOG.md", "CHANGELOG.md",
"README.md", "README.md",
"docs/index.md", "docs/index.md",
"docs/plans/*",
"*.svg", "*.svg",
"uv.lock", "uv.lock",
"poetry.lock", "poetry.lock",
+1 -1
View File
@@ -118,7 +118,7 @@ def main(commit_msg_file: str | None, branch: str | None, from_git: bool) -> Non
" Expected: <type>: <description>\n" " Expected: <type>: <description>\n"
" Got: {subject}\n" " Got: {subject}\n"
" Allowed types: feat, fix, chore, docs, style, refactor,\n" " Allowed types: feat, fix, chore, docs, style, refactor,\n"
" perf, test, ci, build, revert, BREAKING CHANGE", " perf, test, ci, build, deps, revert, BREAKING CHANGE",
subject=subject, subject=subject,
) )
) )
+1 -1
View File
@@ -93,4 +93,4 @@ RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
RETRY_STATUS_CODES = {429, 500, 502, 503, 504} RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
# Conventional commit regex — used by validate_commit_msg.py # Conventional commit regex — used by validate_commit_msg.py
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?: .+") CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|deps)(\(.+\))?: .+")
+21 -10
View File
@@ -269,17 +269,19 @@ def _run_push(cmd: list[str]) -> subprocess.CompletedProcess[str]:
Docker sends push progress/errors to both stdout and stderr depending Docker sends push progress/errors to both stdout and stderr depending
on the error type, so both streams are checked for the 500 status. on the error type, so both streams are checked for the 500 status.
Uses stderr=STDOUT to merge both streams into stdout, ensuring all
output is captured in one place (docker push output behavior varies
depending on TTY detection).
""" """
result = subprocess.run( # nosec B603 result = subprocess.run( # nosec B603
cmd, cmd,
capture_output=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True, text=True,
check=False, check=False,
) )
if result.returncode != 0: if result.returncode != 0 and "500" in (result.stdout or ""):
combined = f"{result.stderr}\n{result.stdout}" raise PushHTTP500Error(result.stdout.strip())
if "500" in combined:
raise PushHTTP500Error(combined.strip())
return result return result
@@ -313,9 +315,17 @@ def push_image(
click.echo(f"Pushing {ft}...") click.echo(f"Pushing {ft}...")
@retry( @retry(
stop=stop_after_attempt(3), stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=5, min=5, max=20), wait=wait_exponential(multiplier=10, min=10, max=60),
retry=retry_if_exception_type(PushHTTP500Error), retry=retry_if_exception_type(PushHTTP500Error),
before_sleep=lambda retry_state: click.echo(
_(
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
wait=retry_state.next_action.sleep if retry_state.next_action else 0,
attempt=retry_state.attempt_number + 1,
),
err=True,
),
reraise=True, reraise=True,
) )
def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]: def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]:
@@ -334,7 +344,7 @@ def push_image(
if result.returncode == 0: if result.returncode == 0:
click.echo(f"Pushed {ft}") click.echo(f"Pushed {ft}")
continue continue
combined_output = f"{result.stderr}\n{result.stdout}".strip() combined_output = (result.stdout or "").strip()
# Gitea #31964: push fails because tag already exists. # Gitea #31964: push fails because tag already exists.
# Delete the old manifest and retry once. # Delete the old manifest and retry once.
if username and token and "already exists" in combined_output.lower(): if username and token and "already exists" in combined_output.lower():
@@ -350,14 +360,15 @@ def push_image(
click.echo(f" Retrying push {ft}...") click.echo(f" Retrying push {ft}...")
result = subprocess.run( # nosec B603 result = subprocess.run( # nosec B603
cmd, cmd,
capture_output=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True, text=True,
check=False, check=False,
) )
if result.returncode == 0: if result.returncode == 0:
click.echo(f"Pushed {ft} (after retry)") click.echo(f"Pushed {ft} (after retry)")
continue continue
combined_output = f"{result.stderr}\n{result.stdout}".strip() combined_output = (result.stdout or "").strip()
click.echo( click.echo(
_("Push failed for {tag}: {error}", tag=ft, error=combined_output), _("Push failed for {tag}: {error}", tag=ft, error=combined_output),
err=True, err=True,
+15 -7
View File
@@ -2999,13 +2999,13 @@
"PR number for label check": "PR number for label check", "PR number for label check": "PR number for label check",
"Repo (owner/name) for label check": "Repo (owner/name) for label check" "Repo (owner/name) for label check": "Repo (owner/name) for label check"
}, },
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": { "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE": {
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"pl": "Ups! Wiadomość commit musi być w formacie conventional commit.\n Oczekiwano: <typ>: <opis>\n Otrzymano: {subject}\n Dozwolone typy: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "pl": "Ups! Wiadomość commit musi być w formacie conventional commit.\n Oczekiwano: <typ>: <opis>\n Otrzymano: {subject}\n Dozwolone typy: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"PR number for label check": "PR number for label check", "PR number for label check": "PR number for label check",
"Repo (owner/name) for label check": "Repo (owner/name) for label check" "Repo (owner/name) for label check": "Repo (owner/name) for label check"
}, },
@@ -5152,5 +5152,13 @@
"pl": "PR has 'refactoring' label — size check bypassed.", "pl": "PR has 'refactoring' label — size check bypassed.",
"ru": "PR has 'refactoring' label — size check bypassed.", "ru": "PR has 'refactoring' label — size check bypassed.",
"zh": "PR has 'refactoring' label — size check bypassed." "zh": "PR has 'refactoring' label — size check bypassed."
},
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...": {
"en": "HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
"bg": " HTTP 500 от регистъра, повторен опит след {wait:.0f}с (опит {attempt}/5)...",
"de": " HTTP 500 vom Registry, Wiederholung in {wait:.0f}s (Versuch {attempt}/5)...",
"pl": " HTTP 500 z rejestru, ponawianie za {wait:.0f}s (próba {attempt}/5)...",
"ru": " HTTP 500 от реестра, повтор через {wait:.0f}с (попытка {attempt}/5)...",
"zh": " 注册表返回 HTTP 500{wait:.0f}秒后重试(第{attempt}/5次尝试)..."
} }
} }
+19 -32
View File
@@ -198,7 +198,7 @@ class TestBuildImage:
class TestPushImage: class TestPushImage:
def test_success(self) -> None: def test_success(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
mock_result = MagicMock(returncode=0, stderr="", stdout="") mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run: with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
assert push_image(spec, "git.example.com") is True assert push_image(spec, "git.example.com") is True
assert mock_run.call_count == 2 assert mock_run.call_count == 2
@@ -206,8 +206,8 @@ class TestPushImage:
def test_partial_failure(self) -> None: def test_partial_failure(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
results = [ results = [
MagicMock(returncode=0, stderr="", stdout=""), MagicMock(returncode=0, stdout=""),
MagicMock(returncode=1, stderr="push failed", stdout=""), MagicMock(returncode=1, stdout="push failed"),
] ]
with patch("devx.tools.build_image.subprocess.run", side_effect=results): with patch("devx.tools.build_image.subprocess.run", side_effect=results):
assert push_image(spec, "git.example.com") is False assert push_image(spec, "git.example.com") is False
@@ -221,7 +221,7 @@ class TestPushImage:
def test_no_delete_on_success_with_creds(self) -> None: def test_no_delete_on_success_with_creds(self) -> None:
"""Push-first: no delete needed when push succeeds.""" """Push-first: no delete needed when push succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stderr="", stdout="") mock_result = MagicMock(returncode=0, stdout="")
with ( with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result), patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del, patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -231,7 +231,7 @@ class TestPushImage:
def test_no_delete_without_creds(self) -> None: def test_no_delete_without_creds(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stderr="", stdout="") mock_result = MagicMock(returncode=0, stdout="")
with ( with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result), patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del, patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -243,8 +243,8 @@ class TestPushImage:
"""Gitea #31964: push fails with 'already exists', delete + retry.""" """Gitea #31964: push fails with 'already exists', delete + retry."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [ results = [
MagicMock(returncode=1, stderr="package version already exists", stdout=""), MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=0, stderr="", stdout=""), MagicMock(returncode=0, stdout=""),
] ]
with ( with (
patch("devx.tools.build_image.subprocess.run", side_effect=results), patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -263,7 +263,7 @@ class TestPushImage:
def test_no_delete_on_non_already_exists_failure(self) -> None: def test_no_delete_on_non_already_exists_failure(self) -> None:
"""Push fails for other reasons (non-500) — old manifest preserved.""" """Push fails for other reasons (non-500) — old manifest preserved."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=1, stderr="denied: requested access to the resource is denied", stdout="") mock_result = MagicMock(returncode=1, stdout="denied: requested access to the resource is denied")
with ( with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result), patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del, patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -275,8 +275,8 @@ class TestPushImage:
"""Gitea #31964 retry also fails — both pushes fail.""" """Gitea #31964 retry also fails — both pushes fail."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [ results = [
MagicMock(returncode=1, stderr="package version already exists", stdout=""), MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=1, stderr="push failed again", stdout=""), MagicMock(returncode=1, stdout="push failed again"),
] ]
with ( with (
patch("devx.tools.build_image.subprocess.run", side_effect=results), patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -288,8 +288,8 @@ class TestPushImage:
"""HTTP 500 from registry race condition — retry succeeds.""" """HTTP 500 from registry race condition — retry succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [ results = [
MagicMock(returncode=1, stderr="", stdout="received unexpected HTTP status: 500 Internal Server Error"), MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error"),
MagicMock(returncode=0, stderr="", stdout=""), MagicMock(returncode=0, stdout=""),
] ]
with ( with (
patch("devx.tools.build_image.subprocess.run", side_effect=results), patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -302,9 +302,7 @@ class TestPushImage:
def test_http_500_retries_all_fail(self) -> None: def test_http_500_retries_all_fail(self) -> None:
"""HTTP 500 retries exhausted — push fails, no delete attempted.""" """HTTP 500 retries exhausted — push fails, no delete attempted."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"]) spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock( mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
returncode=1, stderr="", stdout="received unexpected HTTP status: 500 Internal Server Error"
)
with ( with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result), patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del, patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -313,31 +311,20 @@ class TestPushImage:
assert push_image(spec, "git.example.com", username="user", token="tok") is False assert push_image(spec, "git.example.com", username="user", token="tok") is False
mock_del.assert_not_called() mock_del.assert_not_called()
def test_run_push_raises_on_500_stderr(self) -> None: def test_run_push_raises_on_500(self) -> None:
"""_run_push raises PushHTTP500Error when stderr contains 500.""" """_run_push raises PushHTTP500Error when stdout contains 500."""
from devx.tools.build_image import _run_push from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=1, stderr="HTTP 500 Internal Server Error", stdout="") mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
with pytest.raises(PushHTTP500Error, match="HTTP 500"):
_run_push(["docker", "push", "img:latest"])
def test_run_push_raises_on_500_stdout(self) -> None:
"""_run_push raises PushHTTP500Error when stdout contains 500 (docker sends to stdout)."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(
returncode=1, stderr="", stdout="received unexpected HTTP status: 500 Internal Server Error"
)
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result): with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
with pytest.raises(PushHTTP500Error, match="500"): with pytest.raises(PushHTTP500Error, match="500"):
_run_push(["docker", "push", "img:latest"]) _run_push(["docker", "push", "img:latest"])
def test_run_push_no_raise_on_non_500(self) -> None: def test_run_push_no_raise_on_non_500(self) -> None:
"""_run_push returns result when stderr has no 500.""" """_run_push returns result when stdout has no 500."""
from devx.tools.build_image import _run_push from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=1, stderr="denied: access denied", stdout="") mock_result = MagicMock(returncode=1, stdout="denied: access denied")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result): with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"]) result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 1 assert result.returncode == 1
@@ -346,7 +333,7 @@ class TestPushImage:
"""_run_push returns result on success.""" """_run_push returns result on success."""
from devx.tools.build_image import _run_push from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=0, stderr="", stdout="") mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result): with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"]) result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 0 assert result.returncode == 0
+6
View File
@@ -26,6 +26,12 @@ class TestIsExcluded:
def test_excludes_readme(self) -> None: def test_excludes_readme(self) -> None:
assert is_excluded("README.md", ["README.md"]) assert is_excluded("README.md", ["README.md"])
def test_excludes_plans_glob(self) -> None:
assert is_excluded("docs/plans/sso-bridge-full-extraction.md", ["docs/plans/*"])
def test_does_not_exclude_specs(self) -> None:
assert not is_excluded("docs/specs/DEVX-165.md", ["docs/plans/*"])
class TestCheckSize: class TestCheckSize:
def test_under_limits_passes(self) -> None: def test_under_limits_passes(self) -> None:
+1
View File
@@ -38,6 +38,7 @@ class TestConfigConstants:
def test_conventional_re(self) -> None: def test_conventional_re(self) -> None:
assert CONVENTIONAL_RE.match("feat: add feature") assert CONVENTIONAL_RE.match("feat: add feature")
assert CONVENTIONAL_RE.match("fix(scope): bug fix") assert CONVENTIONAL_RE.match("fix(scope): bug fix")
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
assert not CONVENTIONAL_RE.match("random message") assert not CONVENTIONAL_RE.match("random message")
assert not CONVENTIONAL_RE.match("feat:") assert not CONVENTIONAL_RE.match("feat:")
assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something") assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something")
+1
View File
@@ -30,6 +30,7 @@ class TestHelpers:
assert CONVENTIONAL_RE.match("test: add tests") assert CONVENTIONAL_RE.match("test: add tests")
assert CONVENTIONAL_RE.match("ci: update workflow") assert CONVENTIONAL_RE.match("ci: update workflow")
assert CONVENTIONAL_RE.match("build: update deps") assert CONVENTIONAL_RE.match("build: update deps")
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
assert CONVENTIONAL_RE.match("revert: undo change") assert CONVENTIONAL_RE.match("revert: undo change")
def test_conventional_re_allows_scope(self) -> None: def test_conventional_re_allows_scope(self) -> None: