Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dfd616cfd | ||
|
|
49ad1868bc | ||
|
|
7268c2b4ac |
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
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.53.0] - 2026-09-19
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(ci)* Manifest-aware dependency PRs and cleanup protection
|
||||||
|
|
||||||
## [0.52.0] - 2026-09-19
|
## [0.52.0] - 2026-09-19
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ quality badges.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](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.52.0",
|
"devx>=0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[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.52.0"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.53.0"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.52.0,<0.53"`).
|
> (for example, `"devx>=0.53.0,<0.54"`).
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](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.52.0",
|
"devx>=0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[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.52.0"` or `"devx>=0.52.0,<0.53"`.
|
Pin a specific version if needed: `"devx==0.53.0"` or `"devx>=0.53.0,<0.54"`.
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
+51
-19
@@ -1,31 +1,63 @@
|
|||||||
# DEVX-165: Accept deps: as valid conventional commit type
|
# DEVX-165: Manifest-aware dependency PRs and registry cleanup protection
|
||||||
|
|
||||||
## Problem
|
## 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
|
S03 (OBL-INFRA-548 REQ-3) requires an immutable delivery contract: infra
|
||||||
bump PRs use `deps: bump devx...` as the commit message.
|
pins the sso-bridge release as {version, git ref, image tag, OCI digest}
|
||||||
|
in a JSON manifest. Two gaps in devx block that:
|
||||||
|
|
||||||
|
1. `create_dependency_pr` only regex-bumps a version string in
|
||||||
|
pyproject/ansible vars — it cannot update a structured manifest with
|
||||||
|
the resolved image digest, and it does not verify the producer
|
||||||
|
artifact exists before opening the PR.
|
||||||
|
2. `clean_images` deletes all but the newest N tags — a tag/digest that
|
||||||
|
infra still pins gets deleted once newer releases land, breaking
|
||||||
|
deploys.
|
||||||
|
|
||||||
## Approach
|
## 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
|
REQ-1: `create_dependency_pr` gains `--manifest <path>` +
|
||||||
`src/devx/ci/validate_commit_msg.py`
|
`--verify-container <owner/name>` + `--container-tag` +
|
||||||
REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
|
`--source-ref`: before opening the PR it resolves the container tag's
|
||||||
and `tests/unit/test_validate_commit_msg.py`
|
OCI digest via the packages API (`manifest.json` blob sha256), then
|
||||||
|
updates manifest fields `{version, git_ref, image_tag, image_digest,
|
||||||
|
source_run_id, updated_at}` in the PR branch instead of a regex bump.
|
||||||
|
`--container-tag` decouples the image tag from the release version
|
||||||
|
(sso-bridge images tag `__init__.py.__version__`, not the git tag).
|
||||||
|
|
||||||
|
REQ-1b: `create_dependency_pr` clones the *target* repo into a tempdir
|
||||||
|
and performs all file lookups and git operations inside it. Previously
|
||||||
|
it operated on CWD — the producer repo's own checkout — so file updates
|
||||||
|
silently targeted the wrong repo and the whole dep-PR path no-oped
|
||||||
|
behind `|| echo warning`.
|
||||||
|
|
||||||
|
REQ-2: `clean_images` gains `--protect` (repeatable): named versions are
|
||||||
|
never deleted regardless of `--keep` trimming.
|
||||||
|
|
||||||
|
REQ-3: Regression tests for manifest update, digest resolution,
|
||||||
|
verify-then-PR ordering, and protect filtering.
|
||||||
|
|
||||||
|
## Files Affected
|
||||||
|
|
||||||
|
- `src/devx/ci/create_dependency_pr.py`
|
||||||
|
- `src/devx/tools/clean_images.py`
|
||||||
|
- `tests/unit/test_create_dependency_pr.py`
|
||||||
|
- `tests/unit/test_clean_images.py`
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
- `make pytest-cov` passes with 100% coverage
|
|
||||||
- `make lint-all` passes
|
- New unit tests per REQ; `make pytest-cov`, `make lint-all`.
|
||||||
|
|
||||||
## Deploy Plan
|
## Deploy Plan
|
||||||
- Merge to master → post-merge auto-publishes new devx version
|
|
||||||
- grm and sso-bridge bump devx version to pick up the fix
|
- Merge → devx release → consumer repos pick up via dependency PRs.
|
||||||
|
|
||||||
## Rollback Plan
|
## Rollback Plan
|
||||||
- Revert the merge commit
|
|
||||||
|
- Revert; regex version bump and unprotected cleanup return.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## 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
|
- [x] REQ-1: Manifest update + pre-PR OCI digest verification
|
||||||
`src/devx/ci/validate_commit_msg.py`
|
- [x] REQ-2: `--protect` exempts versions from cleanup
|
||||||
- [x] REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
|
- [x] REQ-3: Regression tests added and passing
|
||||||
and `tests/unit/test_validate_commit_msg.py`
|
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.52.0",
|
"devx>=0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.52.0",
|
"devx>=0.53.0",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.52.0"
|
__version__ = "0.53.0"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -143,6 +144,54 @@ def resolve_container_digest(api_url: str, owner: str, name: str, tag: str, toke
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_manifest(file_path: str, section: str, fields: dict[str, str]) -> bool:
|
||||||
|
"""Update a release-manifest JSON section in place. Returns True if changed.
|
||||||
|
|
||||||
|
Implements REQ-1: manifest fields record the immutable release contract
|
||||||
|
(version, git ref, image tag, resolved OCI digest) in the target repo.
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise click.ClickException(_("Manifest file not found: {file}", file=file_path))
|
||||||
|
try:
|
||||||
|
manifest = _json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except _json.JSONDecodeError as e:
|
||||||
|
raise click.ClickException(_("Manifest file {file} is not valid JSON: {error}", file=file_path, error=e)) from e
|
||||||
|
existing = manifest.get(section)
|
||||||
|
if not isinstance(existing, dict):
|
||||||
|
raise click.ClickException(
|
||||||
|
_("Manifest file {file} has no object section {section}", file=file_path, section=section)
|
||||||
|
)
|
||||||
|
changed = False
|
||||||
|
for k, v in fields.items():
|
||||||
|
if existing.get(k) != v:
|
||||||
|
existing[k] = v
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
path.write_text(_json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def read_manifest_version(file_path: str, section: str) -> str | None:
|
||||||
|
"""Read the currently pinned version from a release manifest section."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
manifest = _json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except _json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
existing = manifest.get(section)
|
||||||
|
if isinstance(existing, dict):
|
||||||
|
version = existing.get("version")
|
||||||
|
return str(version) if version is not None else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_vikunja_task(title: str, description: str) -> str | None:
|
def create_vikunja_task(title: str, description: str) -> str | None:
|
||||||
"""Create a Vikunja task and return its identifier (e.g., OBL-INFRA-531)."""
|
"""Create a Vikunja task and return its identifier (e.g., OBL-INFRA-531)."""
|
||||||
try:
|
try:
|
||||||
@@ -163,15 +212,28 @@ def create_vikunja_task(title: str, description: str) -> str | None:
|
|||||||
@click.option("--new-version", required=True, help=_("New version to pin"))
|
@click.option("--new-version", required=True, help=_("New version to pin"))
|
||||||
@click.option("--source-repo", required=True, help=_("Source repo that published (owner/name)"))
|
@click.option("--source-repo", required=True, help=_("Source repo that published (owner/name)"))
|
||||||
@click.option("--source-run-id", default="", help=_("CI run ID that triggered the publish"))
|
@click.option("--source-run-id", default="", help=_("CI run ID that triggered the publish"))
|
||||||
|
@click.option(
|
||||||
|
"--manifest",
|
||||||
|
"manifest_path",
|
||||||
|
default="",
|
||||||
|
help=_(
|
||||||
|
"Path to a release-manifest JSON in the target repo. When set, the PR updates "
|
||||||
|
"the manifest section named after --package instead of a regex version bump."
|
||||||
|
),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--verify-container",
|
"--verify-container",
|
||||||
default="",
|
default="",
|
||||||
help=_(
|
help=_(
|
||||||
"Container to verify before opening the PR (owner/name). Resolves the OCI "
|
"Container to verify before opening the PR (owner/name). Resolves the OCI "
|
||||||
"digest of the tag matching --new-version (or --container-tag); the PR is "
|
"digest of the tag matching --new-version and records it in the manifest."
|
||||||
"refused when the artifact is missing or unreadable."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--source-ref",
|
||||||
|
default="",
|
||||||
|
help=_("Git ref of the producer release (default: v<new-version>), recorded in the manifest."),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--container-tag",
|
"--container-tag",
|
||||||
default="",
|
default="",
|
||||||
@@ -188,7 +250,9 @@ def cli(
|
|||||||
new_version: str,
|
new_version: str,
|
||||||
source_repo: str,
|
source_repo: str,
|
||||||
source_run_id: str,
|
source_run_id: str,
|
||||||
|
manifest_path: str,
|
||||||
verify_container: str,
|
verify_container: str,
|
||||||
|
source_ref: str,
|
||||||
container_tag: str,
|
container_tag: str,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -234,11 +298,15 @@ def cli(
|
|||||||
# Find current pinned version
|
# Find current pinned version
|
||||||
old_version = None
|
old_version = None
|
||||||
changed_file = None
|
changed_file = None
|
||||||
for f in [PYPROJECT_PATH, IMAGES_YML_PATH, ROLE_DEFAULTS_PATH]:
|
if manifest_path:
|
||||||
old_version = find_pinned_version(package, str(workdir / f))
|
old_version = read_manifest_version(str(workdir / manifest_path), package)
|
||||||
if old_version:
|
changed_file = manifest_path if old_version else None
|
||||||
changed_file = f
|
else:
|
||||||
break
|
for f in [PYPROJECT_PATH, IMAGES_YML_PATH, ROLE_DEFAULTS_PATH]:
|
||||||
|
old_version = find_pinned_version(package, str(workdir / f))
|
||||||
|
if old_version:
|
||||||
|
changed_file = f
|
||||||
|
break
|
||||||
|
|
||||||
if not old_version:
|
if not old_version:
|
||||||
click.echo(_("[dep-pr] Could not find pinned version for {pkg} in infra repo.", pkg=package))
|
click.echo(_("[dep-pr] Could not find pinned version for {pkg} in infra repo.", pkg=package))
|
||||||
@@ -291,8 +359,23 @@ def cli(
|
|||||||
subprocess.run(["git", "fetch", "origin", f"{branch_name}"], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
|
subprocess.run(["git", "fetch", "origin", f"{branch_name}"], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
|
||||||
subprocess.run(["git", "checkout", branch_name], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
|
subprocess.run(["git", "checkout", branch_name], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
|
||||||
|
|
||||||
if not changed_file or not update_pinned_version(str(workdir / changed_file), package, old_version, new_version):
|
if manifest_path:
|
||||||
|
fields = {
|
||||||
|
"version": new_version,
|
||||||
|
"git_ref": source_ref or f"v{new_version}",
|
||||||
|
"image_tag": container_tag or new_version,
|
||||||
|
"updated_at": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
if image_digest:
|
||||||
|
fields["image_digest"] = image_digest
|
||||||
|
if source_run_id:
|
||||||
|
fields["source_run_id"] = source_run_id
|
||||||
|
if not update_manifest(str(workdir / manifest_path), package, fields):
|
||||||
|
raise click.ClickException(_("Failed to update {file}", file=manifest_path))
|
||||||
|
elif not changed_file or not update_pinned_version(str(workdir / changed_file), package, old_version, new_version):
|
||||||
raise click.ClickException(_("Failed to update {file}", file=changed_file))
|
raise click.ClickException(_("Failed to update {file}", file=changed_file))
|
||||||
|
|
||||||
|
assert changed_file is not None # nosec B101 — narrowed by the early exit above
|
||||||
subprocess.run(["git", "add", changed_file], check=True, cwd=workdir) # nosec B603 B607
|
subprocess.run(["git", "add", changed_file], check=True, cwd=workdir) # nosec B603 B607
|
||||||
commit_msg = f"deps: bump {package} from {old_version} to {new_version}"
|
commit_msg = f"deps: bump {package} from {old_version} to {new_version}"
|
||||||
subprocess.run(["git", "commit", "-m", commit_msg], check=True, cwd=workdir) # nosec B603 B607
|
subprocess.run(["git", "commit", "-m", commit_msg], check=True, cwd=workdir) # nosec B603 B607
|
||||||
|
|||||||
@@ -150,15 +150,20 @@ def sort_versions_by_date(
|
|||||||
def select_for_deletion(
|
def select_for_deletion(
|
||||||
versions: list[dict[str, Any]],
|
versions: list[dict[str, Any]],
|
||||||
keep: int,
|
keep: int,
|
||||||
|
protect: frozenset[str] = frozenset(),
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Select versions to delete, keeping the most recent ``keep`` versions.
|
"""Select versions to delete, keeping the most recent ``keep`` versions.
|
||||||
|
|
||||||
Versions named ``latest`` are always preserved.
|
Versions named ``latest`` and any version listed in ``protect`` are
|
||||||
|
always preserved. Implements REQ-2 (DEVX-165): tags/digests that a
|
||||||
|
release manifest still pins must survive registry cleanup.
|
||||||
"""
|
"""
|
||||||
sorted_versions = sort_versions_by_date(versions)
|
sorted_versions = sort_versions_by_date(versions)
|
||||||
to_delete = sorted_versions[keep:]
|
to_delete = sorted_versions[keep:]
|
||||||
# Always preserve 'latest' tag
|
# Always preserve 'latest' tag
|
||||||
to_delete = [v for v in to_delete if v.get("version") != "latest"]
|
to_delete = [v for v in to_delete if v.get("version") != "latest"]
|
||||||
|
# Preserve explicitly protected versions (e.g., pinned by a release manifest)
|
||||||
|
to_delete = [v for v in to_delete if v.get("version") not in protect]
|
||||||
return to_delete
|
return to_delete
|
||||||
|
|
||||||
|
|
||||||
@@ -182,6 +187,12 @@ def select_for_deletion(
|
|||||||
show_default=True,
|
show_default=True,
|
||||||
help="Number of recent versions to keep (excluding 'latest').",
|
help="Number of recent versions to keep (excluding 'latest').",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--protect",
|
||||||
|
"protect",
|
||||||
|
multiple=True,
|
||||||
|
help="Version/tag to never delete (e.g., pinned by a release manifest). Can be repeated.",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -197,6 +208,7 @@ def main(
|
|||||||
owner: str | None,
|
owner: str | None,
|
||||||
names: tuple[str, ...],
|
names: tuple[str, ...],
|
||||||
keep: int,
|
keep: int,
|
||||||
|
protect: tuple[str, ...],
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
api_url: str | None,
|
api_url: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -238,7 +250,7 @@ def main(
|
|||||||
_(" {version} (created: {created})", version=v.get("version", "?"), created=v.get("created_at", "?"))
|
_(" {version} (created: {created})", version=v.get("version", "?"), created=v.get("created_at", "?"))
|
||||||
)
|
)
|
||||||
|
|
||||||
to_delete = select_for_deletion(versions, keep)
|
to_delete = select_for_deletion(versions, keep, frozenset(protect))
|
||||||
kept_count = len(versions) - len(to_delete)
|
kept_count = len(versions) - len(to_delete)
|
||||||
click.echo(_("\nKeeping {kept}, would delete {count}", kept=kept_count, count=len(to_delete)))
|
click.echo(_("\nKeeping {kept}, would delete {count}", kept=kept_count, count=len(to_delete)))
|
||||||
|
|
||||||
|
|||||||
@@ -1359,13 +1359,13 @@
|
|||||||
"ru": "Тег контейнера для проверки с --verify-container (по умолчанию: --new-version). Используйте, когда тег образа отличается от версии релиза, например тег __version__ пакета против git-тега релиза.",
|
"ru": "Тег контейнера для проверки с --verify-container (по умолчанию: --new-version). Используйте, когда тег образа отличается от версии релиза, например тег __version__ пакета против git-тега релиза.",
|
||||||
"zh": "用 --verify-container 验证的容器标签(默认:--new-version)。当镜像标签与发布版本不同时使用,例如包的 __version__ 标签与发布 git 标签。"
|
"zh": "用 --verify-container 验证的容器标签(默认:--new-version)。当镜像标签与发布版本不同时使用,例如包的 __version__ 标签与发布 git 标签。"
|
||||||
},
|
},
|
||||||
"Container to verify before opening the PR (owner/name). Resolves the OCI digest of the tag matching --new-version (or --container-tag); the PR is refused when the artifact is missing or unreadable.": {
|
"Container to verify before opening the PR (owner/name). Resolves the OCI digest of the tag matching --new-version and records it in the manifest.": {
|
||||||
"bg": "Контейнер за проверка преди отваряне на PR (собственик/име). Разрешава OCI дайджеста на тага, отговарящ на --new-version (или --container-tag); PR се отказва, ако артефактът липсва или е нечетим.",
|
"bg": "Контейнер за проверка преди отваряне на PR (owner/name). Разрешава OCI дайджеста на тага, съвпадащ с --new-version, и го записва в манифеста.",
|
||||||
"de": "Container zur Verifizierung vor dem Öffnen des PR (owner/name). Löst den OCI-Digest des zu --new-version (oder --container-tag) passenden Tags auf; der PR wird abgelehnt, wenn das Artefakt fehlt oder unlesbar ist.",
|
"de": "Container, der vor dem Öffnen des PR verifiziert wird (owner/name). Ermittelt den OCI-Digest des zu --new-version passenden Tags und trägt ihn ins Manifest ein.",
|
||||||
"en": "Container to verify before opening the PR (owner/name). Resolves the OCI digest of the tag matching --new-version (or --container-tag); the PR is refused when the artifact is missing or unreadable.",
|
"en": "Container to verify before opening the PR (owner/name). Resolves the OCI digest of the tag matching --new-version and records it in the manifest.",
|
||||||
"pl": "Kontener do weryfikacji przed otwarciem PR (właściciel/nazwa). Rozwiązuje skrót OCI tagu pasującego do --new-version (lub --container-tag); PR jest odrzucany, gdy artefakt nie istnieje lub jest nieczytelny.",
|
"pl": "Kontener do zweryfikowania przed otwarciem PR (owner/name). Rozwiązuje skrót OCI tagu pasującego do --new-version i zapisuje go w manifeście.",
|
||||||
"ru": "Контейнер для проверки перед открытием PR (владелец/имя). Разрешает OCI-дайджест тега, соответствующего --new-version (или --container-tag); PR отклоняется, если артефакт отсутствует или недоступен.",
|
"ru": "Контейнер для проверки перед открытием PR (owner/name). Разрешает OCI-дайджест тега, соответствующего --new-version, и записывает его в манифест.",
|
||||||
"zh": "在打开 PR 前要验证的容器(所有者/名称)。解析与 --new-version(或 --container-tag)匹配标签的 OCI 摘要;当工件缺失或不可读时拒绝创建 PR。"
|
"zh": "在打开 PR 前要验证的容器 (owner/name)。解析与 --new-version 匹配的标签的 OCI 摘要并记录到清单中。"
|
||||||
},
|
},
|
||||||
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
|
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
|
||||||
"bg": "Не може да се определи номерът на PR. Използвайте --pr, за да го зададете изрично,\nили изпълнете командата от клон с отворен PR.",
|
"bg": "Не може да се определи номерът на PR. Използвайте --pr, за да го зададете изрично,\nили изпълнете командата от клон с отворен PR.",
|
||||||
@@ -1847,6 +1847,14 @@
|
|||||||
"ru": "Генерация значков в {out}...",
|
"ru": "Генерация значков в {out}...",
|
||||||
"zh": "正在 {out} 中生成徽章..."
|
"zh": "正在 {out} 中生成徽章..."
|
||||||
},
|
},
|
||||||
|
"Git ref of the producer release (default: v<new-version>), recorded in the manifest.": {
|
||||||
|
"bg": "Git референция на продуцентското издание (по подразбиране: v<new-version>), записана в манифеста.",
|
||||||
|
"de": "Git-Ref des Producer-Releases (Standard: v<new-version>), im Manifest verzeichnet.",
|
||||||
|
"en": "Git ref of the producer release (default: v<new-version>), recorded in the manifest.",
|
||||||
|
"pl": "Referencja git wydania producenta (domyślnie: v<new-version>), zapisana w manifeście.",
|
||||||
|
"ru": "Git-ссылка релиза производителя (по умолчанию: v<new-version>), записанная в манифест.",
|
||||||
|
"zh": "生产者发布的 git 引用(默认:v<new-version>),记录在清单中。"
|
||||||
|
},
|
||||||
"Git tag or ref that was deployed": {
|
"Git tag or ref that was deployed": {
|
||||||
"bg": "Git таг или референция, която беше разгърната",
|
"bg": "Git таг или референция, която беше разгърната",
|
||||||
"de": "Git-Tag oder Ref, der bereitgestellt wurde",
|
"de": "Git-Tag oder Ref, der bereitgestellt wurde",
|
||||||
@@ -2103,6 +2111,14 @@
|
|||||||
"ru": "Цикл с {count} итерациями в тесте '{test}' — используйте property-based тестирование (hypothesis) или уменьшите до <= {max} итераций.",
|
"ru": "Цикл с {count} итерациями в тесте '{test}' — используйте property-based тестирование (hypothesis) или уменьшите до <= {max} итераций.",
|
||||||
"zh": "测试 '{test}' 中有 {count} 次迭代的循环 — 考虑使用基于属性的测试 (hypothesis) 或减少到 <= {max} 次迭代。"
|
"zh": "测试 '{test}' 中有 {count} 次迭代的循环 — 考虑使用基于属性的测试 (hypothesis) 或减少到 <= {max} 次迭代。"
|
||||||
},
|
},
|
||||||
|
"Manifest file not found: {file}": {
|
||||||
|
"bg": "Файлът на манифеста не е намерен: {file}",
|
||||||
|
"de": "Manifest-Datei nicht gefunden: {file}",
|
||||||
|
"en": "Manifest file not found: {file}",
|
||||||
|
"pl": "Nie znaleziono pliku manifestu: {file}",
|
||||||
|
"ru": "Файл манифеста не найден: {file}",
|
||||||
|
"zh": "未找到清单文件:{file}"
|
||||||
|
},
|
||||||
"Manifest file not found: {path}": {
|
"Manifest file not found: {path}": {
|
||||||
"bg": "Файлът на манифеста не е намерен: {path}",
|
"bg": "Файлът на манифеста не е намерен: {path}",
|
||||||
"de": "Manifestdatei nicht gefunden: {path}",
|
"de": "Manifestdatei nicht gefunden: {path}",
|
||||||
@@ -2111,6 +2127,22 @@
|
|||||||
"ru": "Файл манифеста не найден: {path}",
|
"ru": "Файл манифеста не найден: {path}",
|
||||||
"zh": "未找到清单文件:{path}"
|
"zh": "未找到清单文件:{path}"
|
||||||
},
|
},
|
||||||
|
"Manifest file {file} has no object section {section}": {
|
||||||
|
"bg": "Файлът на манифеста {file} няма обектна секция {section}",
|
||||||
|
"de": "Manifest-Datei {file} hat keinen Objektabschnitt {section}",
|
||||||
|
"en": "Manifest file {file} has no object section {section}",
|
||||||
|
"pl": "Plik manifestu {file} nie ma sekcji obiektu {section}",
|
||||||
|
"ru": "Файл манифеста {file} не содержит объектного раздела {section}",
|
||||||
|
"zh": "清单文件 {file} 没有对象节 {section}"
|
||||||
|
},
|
||||||
|
"Manifest file {file} is not valid JSON: {error}": {
|
||||||
|
"bg": "Файлът на манифеста {file} не е валиден JSON: {error}",
|
||||||
|
"de": "Manifest-Datei {file} ist kein gültiges JSON: {error}",
|
||||||
|
"en": "Manifest file {file} is not valid JSON: {error}",
|
||||||
|
"pl": "Plik manifestu {file} nie jest prawidłowym JSON: {error}",
|
||||||
|
"ru": "Файл манифеста {file} не является допустимым JSON: {error}",
|
||||||
|
"zh": "清单文件 {file} 不是有效的 JSON:{error}"
|
||||||
|
},
|
||||||
"Manifest must be a JSON list": {
|
"Manifest must be a JSON list": {
|
||||||
"bg": "Манифестът трябва да е JSON списък",
|
"bg": "Манифестът трябва да е JSON списък",
|
||||||
"de": "Manifest muss eine JSON-Liste sein",
|
"de": "Manifest muss eine JSON-Liste sein",
|
||||||
@@ -2703,6 +2735,14 @@
|
|||||||
"ru": "Извлечён owner={owner}, repo={repo} из DEVX_REPO_NAME",
|
"ru": "Извлечён owner={owner}, repo={repo} из DEVX_REPO_NAME",
|
||||||
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
|
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
|
||||||
},
|
},
|
||||||
|
"Path to a release-manifest JSON in the target repo. When set, the PR updates the manifest section named after --package instead of a regex version bump.": {
|
||||||
|
"bg": "Път към release-manifest JSON в целевото хранилище. Когато е зададен, PR актуализира секцията на манифеста, наречена след --package, вместо regex bump на версията.",
|
||||||
|
"de": "Pfad zu einer Release-Manifest-JSON im Ziel-Repo. Wenn gesetzt, aktualisiert der PR den nach --package benannten Manifest-Abschnitt statt eines Regex-Versionsbumps.",
|
||||||
|
"en": "Path to a release-manifest JSON in the target repo. When set, the PR updates the manifest section named after --package instead of a regex version bump.",
|
||||||
|
"pl": "Ścieżka do pliku JSON manifestu wydania w docelowym repozytorium. Po ustawieniu PR aktualizuje sekcję manifestu nazwaną po --package zamiast podbicia wersji regexem.",
|
||||||
|
"ru": "Путь к JSON манифеста релиза в целевом репозитории. Если задан, PR обновляет раздел манифеста, названный по --package, вместо повышения версии по regex.",
|
||||||
|
"zh": "目标仓库中发布清单 JSON 的路径。设置后,PR 更新以 --package 命名的清单节,而不是正则版本提升。"
|
||||||
|
},
|
||||||
"Path to pyproject.toml (default: pyproject.toml in CWD).": {
|
"Path to pyproject.toml (default: pyproject.toml in CWD).": {
|
||||||
"bg": "Път до pyproject.toml (по подразбиране: pyproject.toml в CWD).",
|
"bg": "Път до pyproject.toml (по подразбиране: pyproject.toml в CWD).",
|
||||||
"de": "Pfad zu pyproject.toml (Standard: pyproject.toml im CWD).",
|
"de": "Pfad zu pyproject.toml (Standard: pyproject.toml im CWD).",
|
||||||
|
|||||||
@@ -485,6 +485,38 @@ class TestSelectForDeletion:
|
|||||||
to_delete = select_for_deletion(versions, keep=5)
|
to_delete = select_for_deletion(versions, keep=5)
|
||||||
assert len(to_delete) == 0
|
assert len(to_delete) == 0
|
||||||
|
|
||||||
|
def test_protect_exempts_version(self) -> None:
|
||||||
|
"""REQ-2: a manifest-pinned version survives cleanup beyond --keep."""
|
||||||
|
versions = [
|
||||||
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||||
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||||
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||||
|
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||||
|
]
|
||||||
|
to_delete = select_for_deletion(versions, keep=2, protect=frozenset({"0.1.0"}))
|
||||||
|
deleted = {v["version"] for v in to_delete}
|
||||||
|
assert "0.1.0" not in deleted
|
||||||
|
assert deleted == {"0.2.0"}
|
||||||
|
|
||||||
|
def test_protect_multiple_versions(self) -> None:
|
||||||
|
versions = [
|
||||||
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||||
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||||
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||||
|
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||||
|
]
|
||||||
|
to_delete = select_for_deletion(versions, keep=1, protect=frozenset({"0.1.0", "0.2.0"}))
|
||||||
|
assert {v["version"] for v in to_delete} == {"0.3.0"}
|
||||||
|
|
||||||
|
def test_protect_default_empty_behaves_as_before(self) -> None:
|
||||||
|
versions = [
|
||||||
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||||
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||||
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||||
|
]
|
||||||
|
to_delete = select_for_deletion(versions, keep=2)
|
||||||
|
assert {v["version"] for v in to_delete} == {"0.1.0"}
|
||||||
|
|
||||||
|
|
||||||
class TestCleanImagesAPI:
|
class TestCleanImagesAPI:
|
||||||
"""Tests for the clean_images module's API functions."""
|
"""Tests for the clean_images module's API functions."""
|
||||||
|
|||||||
@@ -244,14 +244,119 @@ class TestResolveContainerDigest:
|
|||||||
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok")
|
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok")
|
||||||
|
|
||||||
|
|
||||||
class TestCliVerifyContainer:
|
class TestManifestHelpers:
|
||||||
"""REQ-1: artifact verification gates the dependency PR."""
|
"""REQ-1: manifest read/update helpers."""
|
||||||
|
|
||||||
|
def test_read_version(self, tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from devx.ci.create_dependency_pr import read_manifest_version
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text(json.dumps({"schema_version": 1, "sso_bridge": {"version": "0.9.0"}}))
|
||||||
|
assert read_manifest_version(str(p), "sso_bridge") == "0.9.0"
|
||||||
|
|
||||||
|
def test_read_version_missing_file(self, tmp_path: Path) -> None:
|
||||||
|
from devx.ci.create_dependency_pr import read_manifest_version
|
||||||
|
|
||||||
|
assert read_manifest_version(str(tmp_path / "nope.json"), "sso_bridge") is None
|
||||||
|
|
||||||
|
def test_read_version_bad_json(self, tmp_path: Path) -> None:
|
||||||
|
from devx.ci.create_dependency_pr import read_manifest_version
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text("not json{")
|
||||||
|
assert read_manifest_version(str(p), "sso_bridge") is None
|
||||||
|
|
||||||
|
def test_read_version_missing_section(self, tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from devx.ci.create_dependency_pr import read_manifest_version
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text(json.dumps({"schema_version": 1, "other": "x"}))
|
||||||
|
assert read_manifest_version(str(p), "sso_bridge") is None
|
||||||
|
|
||||||
|
def test_update_manifest_fields(self, tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from devx.ci.create_dependency_pr import update_manifest
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text(json.dumps({"schema_version": 1, "sso_bridge": {"version": "0.9.0"}}))
|
||||||
|
changed = update_manifest(
|
||||||
|
str(p),
|
||||||
|
"sso_bridge",
|
||||||
|
{"version": "0.9.1", "git_ref": "v0.9.1", "image_digest": "sha256:x"},
|
||||||
|
)
|
||||||
|
assert changed is True
|
||||||
|
data = json.loads(p.read_text())
|
||||||
|
assert data["sso_bridge"]["version"] == "0.9.1"
|
||||||
|
assert data["sso_bridge"]["git_ref"] == "v0.9.1"
|
||||||
|
assert data["sso_bridge"]["image_digest"] == "sha256:x"
|
||||||
|
|
||||||
|
def test_update_manifest_no_change(self, tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from devx.ci.create_dependency_pr import update_manifest
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text(json.dumps({"sso_bridge": {"version": "0.9.1"}}))
|
||||||
|
assert update_manifest(str(p), "sso_bridge", {"version": "0.9.1"}) is False
|
||||||
|
|
||||||
|
def test_update_manifest_missing_file(self, tmp_path: Path) -> None:
|
||||||
|
from devx.ci.create_dependency_pr import update_manifest
|
||||||
|
|
||||||
|
with pytest.raises(click.ClickException, match="not found"):
|
||||||
|
update_manifest(str(tmp_path / "nope.json"), "sso_bridge", {"version": "1"})
|
||||||
|
|
||||||
|
def test_update_manifest_bad_json(self, tmp_path: Path) -> None:
|
||||||
|
from devx.ci.create_dependency_pr import update_manifest
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text("broken{")
|
||||||
|
with pytest.raises(click.ClickException, match="not valid JSON"):
|
||||||
|
update_manifest(str(p), "sso_bridge", {"version": "1"})
|
||||||
|
|
||||||
|
def test_update_manifest_missing_section(self, tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from devx.ci.create_dependency_pr import update_manifest
|
||||||
|
|
||||||
|
p = tmp_path / "m.json"
|
||||||
|
p.write_text(json.dumps({"schema_version": 1}))
|
||||||
|
with pytest.raises(click.ClickException, match="no object section"):
|
||||||
|
update_manifest(str(p), "sso_bridge", {"version": "1"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestCliManifestMode:
|
||||||
|
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||||
|
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||||
|
def test_manifest_same_version_no_pr(self, mock_token: MagicMock, mock_read: MagicMock) -> None:
|
||||||
|
mock_token.return_value = "fake-token"
|
||||||
|
mock_read.return_value = "0.9.1"
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--package",
|
||||||
|
"sso_bridge",
|
||||||
|
"--new-version",
|
||||||
|
"0.9.1",
|
||||||
|
"--source-repo",
|
||||||
|
"oblachno/sso-bridge",
|
||||||
|
"--manifest",
|
||||||
|
"deploy/sso-bridge-release.json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "no pr needed" in result.output.lower()
|
||||||
|
|
||||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||||
def test_verify_container_runs_before_lookup(
|
def test_verify_container_runs_before_lookup(
|
||||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Verification failure aborts before the version lookup/PR steps."""
|
"""Verification failure aborts before the version lookup/PR steps."""
|
||||||
mock_token.return_value = "fake-token"
|
mock_token.return_value = "fake-token"
|
||||||
@@ -266,21 +371,23 @@ class TestCliVerifyContainer:
|
|||||||
"0.9.1",
|
"0.9.1",
|
||||||
"--source-repo",
|
"--source-repo",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
|
"--manifest",
|
||||||
|
"deploy/m.json",
|
||||||
"--verify-container",
|
"--verify-container",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
mock_find.assert_not_called()
|
mock_read.assert_not_called()
|
||||||
|
|
||||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||||
def test_verify_container_resolves_digest(
|
def test_verify_container_resolves_digest(
|
||||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_token.return_value = "fake-token"
|
mock_token.return_value = "fake-token"
|
||||||
mock_find.return_value = "0.9.1"
|
mock_read.return_value = "0.9.1"
|
||||||
mock_digest.return_value = "sha256:abc"
|
mock_digest.return_value = "sha256:abc"
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -292,6 +399,8 @@ class TestCliVerifyContainer:
|
|||||||
"0.9.1",
|
"0.9.1",
|
||||||
"--source-repo",
|
"--source-repo",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
|
"--manifest",
|
||||||
|
"deploy/m.json",
|
||||||
"--verify-container",
|
"--verify-container",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
],
|
],
|
||||||
@@ -321,14 +430,14 @@ class TestCliVerifyContainer:
|
|||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
|
|
||||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||||
def test_container_tag_overrides_new_version(
|
def test_container_tag_overrides_new_version(
|
||||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""--container-tag selects the image tag when it differs from version."""
|
"""--container-tag selects the image tag when it differs from version."""
|
||||||
mock_token.return_value = "fake-token"
|
mock_token.return_value = "fake-token"
|
||||||
mock_find.return_value = "0.9.1"
|
mock_read.return_value = "0.9.1"
|
||||||
mock_digest.return_value = "sha256:abc"
|
mock_digest.return_value = "sha256:abc"
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -340,6 +449,8 @@ class TestCliVerifyContainer:
|
|||||||
"0.9.1",
|
"0.9.1",
|
||||||
"--source-repo",
|
"--source-repo",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
|
"--manifest",
|
||||||
|
"deploy/m.json",
|
||||||
"--verify-container",
|
"--verify-container",
|
||||||
"oblachno/sso-bridge",
|
"oblachno/sso-bridge",
|
||||||
"--container-tag",
|
"--container-tag",
|
||||||
|
|||||||
Reference in New Issue
Block a user