Public Access
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175068782b | ||
|
|
14296ffaac |
@@ -2,24 +2,6 @@
|
|||||||
|
|
||||||
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.1] - 2026-08-25
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Delete existing manifest before push (Gitea #31964 workaround)
|
|
||||||
|
|
||||||
## [0.51.0] - 2026-08-25
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Add role defaults path to create_dependency_pr search
|
|
||||||
|
|
||||||
## [0.50.2] - 2026-08-24
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Update check_pr_size usage example with --repo and --pr-number args
|
|
||||||
|
|
||||||
## [0.50.1] - 2026-08-15
|
## [0.50.1] - 2026-08-15
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -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.51.1",
|
"devx>=0.50.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[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.1"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.50.1"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.51.1,<0.52"`).
|
> (for example, `"devx>=0.50.1,<0.51"`).
|
||||||
|
|
||||||
### 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.51.1",
|
"devx>=0.50.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[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.1"` or `"devx>=0.51.1,<0.52"`.
|
Pin a specific version if needed: `"devx==0.50.1"` or `"devx>=0.50.1,<0.51"`.
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# DEVX-157: Add role defaults path to create_dependency_pr search
|
# DEVX-157: Add role defaults path to create_dependency_pr search
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
`create_dependency_pr` only searches `pyproject.toml` and the infra images vars file for pinned versions. The sso-bridge role pins its version in its role defaults file via `sso_bridge_version`, which is not searched.
|
`create_dependency_pr` only searches `pyproject.toml` and `ansible/group_vars/all/images.yml` for pinned versions. The sso-bridge role pins its version in `ansible/roles/sso_bridge/defaults/main.yml` via `sso_bridge_version: "0.1.0"`, which is not searched.
|
||||||
|
|
||||||
## Approach
|
## Approach
|
||||||
Add the sso-bridge role defaults path to the search paths.
|
Add `ansible/roles/sso_bridge/defaults/main.yml` to the search paths.
|
||||||
|
|
||||||
REQ-1: Add ROLE_DEFAULTS_PATH constant pointing to the sso-bridge role defaults file
|
REQ-1: Add ROLE_DEFAULTS_PATH constant pointing to ansible/roles/sso_bridge/defaults/main.yml
|
||||||
REQ-2: Include ROLE_DEFAULTS_PATH in the search loop
|
REQ-2: Include ROLE_DEFAULTS_PATH in the search loop
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# DEVX-158: Fix build-images workflow: delete existing manifest before push
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
Gitea 1.27 has a known bug (#31964) where pushing a Docker image tag that
|
|
||||||
already exists in the container registry fails with HTTP 500 "package
|
|
||||||
version already exists." The build-images workflow has been failing for weeks because
|
|
||||||
every push to `ci-base:latest`, `ci-quality:latest`, and `ci-full:latest`
|
|
||||||
hits this error.
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
Add a `delete_remote_manifest` function that deletes the existing manifest
|
|
||||||
via the Docker registry v2 API before pushing. This works around the Gitea
|
|
||||||
bug by ensuring the tag doesn't exist when the push starts.
|
|
||||||
|
|
||||||
REQ-1: Add `delete_remote_manifest` function using Docker registry v2 API
|
|
||||||
REQ-2: Call `delete_remote_manifest` before each `docker push` in `push_image`
|
|
||||||
REQ-3: Pass registry credentials from `main` to `push_image`
|
|
||||||
REQ-4: Handle errors gracefully — never block the push if delete fails
|
|
||||||
REQ-5: 100% test coverage for new code
|
|
||||||
|
|
||||||
## Test Plan
|
|
||||||
- Unit tests for `delete_remote_manifest` (success, 404, 500, network error)
|
|
||||||
- Unit tests for `push_image` with and without credentials
|
|
||||||
- Verify existing tests still pass
|
|
||||||
|
|
||||||
## Deploy Plan
|
|
||||||
- Merge to master, auto-release new devx version
|
|
||||||
- The build-images workflow will use the new code on the next run
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
- Revert the merge commit
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- [x] REQ-1: `delete_remote_manifest` function added
|
|
||||||
- [x] REQ-2: Called before each push in `push_image`
|
|
||||||
- [x] REQ-3: Credentials passed from `main` to `push_image`
|
|
||||||
- [x] REQ-4: Errors don't block the push (returns True on failure)
|
|
||||||
- [x] REQ-5: 100% test coverage
|
|
||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.51.1",
|
"devx>=0.50.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.51.1",
|
"devx>=0.50.1",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.1"
|
__version__ = "0.50.1"
|
||||||
|
|||||||
@@ -40,12 +40,9 @@ and ``CI_GITEA_USERNAME`` environment variables, matching the existing CI workfl
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -191,76 +188,11 @@ def build_image(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def delete_remote_manifest(
|
|
||||||
registry: str,
|
|
||||||
name: str,
|
|
||||||
tag: str,
|
|
||||||
username: str,
|
|
||||||
token: str,
|
|
||||||
*,
|
|
||||||
dry_run: bool = False,
|
|
||||||
) -> bool:
|
|
||||||
"""Delete an existing manifest from the Gitea container registry.
|
|
||||||
|
|
||||||
Gitea 1.27 has a bug (#31964) where pushing a tag that already exists
|
|
||||||
fails with HTTP 500 "package version already exists". This function
|
|
||||||
deletes the existing manifest before the push to work around it.
|
|
||||||
|
|
||||||
Returns True if deleted or not found, False on unexpected errors.
|
|
||||||
"""
|
|
||||||
manifest_url = f"https://{registry}/v2/{name}/manifests/{tag}"
|
|
||||||
if dry_run:
|
|
||||||
click.echo(f"[dry-run] DELETE {manifest_url}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# First, get the digest via HEAD
|
|
||||||
req = urllib.request.Request(manifest_url, method="HEAD") # nosec B310
|
|
||||||
auth_str = f"{username}:{token}"
|
|
||||||
req.add_header("Authorization", f"Basic {base64.b64encode(auth_str.encode()).decode()}")
|
|
||||||
req.add_header("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp: # nosec B310
|
|
||||||
digest = resp.headers.get("Docker-Content-Digest")
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
if e.code == 404:
|
|
||||||
return True # Tag doesn't exist — nothing to delete
|
|
||||||
if e.code == 405:
|
|
||||||
# HEAD not supported — try GET with a range
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
click.echo(f" Warning: HEAD {tag} returned {e.code}", err=True)
|
|
||||||
return True # Don't block the push
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
click.echo(f" Warning: HEAD {tag} failed: {e}", err=True)
|
|
||||||
return True # Don't block the push
|
|
||||||
else:
|
|
||||||
if not digest:
|
|
||||||
return True
|
|
||||||
# Delete by digest
|
|
||||||
del_url = f"https://{registry}/v2/{name}/manifests/{digest}"
|
|
||||||
del_req = urllib.request.Request(del_url, method="DELETE") # nosec B310
|
|
||||||
del_req.add_header("Authorization", f"Basic {base64.b64encode(auth_str.encode()).decode()}")
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(del_req, timeout=30) as resp: # nosec B310
|
|
||||||
click.echo(f" Deleted existing {tag} (digest: {digest[:19]}...)")
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
if e.code == 404:
|
|
||||||
return True # Already gone
|
|
||||||
click.echo(f" Warning: DELETE {tag} returned {e.code}", err=True)
|
|
||||||
return True # Don't block the push
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
click.echo(f" Warning: DELETE {tag} failed: {e}", err=True)
|
|
||||||
return True
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def push_image(
|
def push_image(
|
||||||
spec: ImageSpec,
|
spec: ImageSpec,
|
||||||
registry: str,
|
registry: str,
|
||||||
*,
|
*,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
username: str = "",
|
|
||||||
token: str = "",
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Push all tags of a Docker image to the registry.
|
"""Push all tags of a Docker image to the registry.
|
||||||
|
|
||||||
@@ -268,17 +200,7 @@ def push_image(
|
|||||||
"""
|
"""
|
||||||
full_tags = [build_full_tag(registry, spec.name, t) for t in spec.tags]
|
full_tags = [build_full_tag(registry, spec.name, t) for t in spec.tags]
|
||||||
all_ok = True
|
all_ok = True
|
||||||
for ft, tag in zip(full_tags, spec.tags, strict=False):
|
for ft in full_tags:
|
||||||
# Workaround for Gitea #31964: delete existing tag before push
|
|
||||||
if username and token:
|
|
||||||
delete_remote_manifest(
|
|
||||||
registry,
|
|
||||||
spec.name,
|
|
||||||
tag,
|
|
||||||
username,
|
|
||||||
token,
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
cmd = ["docker", "push", ft]
|
cmd = ["docker", "push", ft]
|
||||||
if dry_run:
|
if dry_run:
|
||||||
click.echo(f"[dry-run] {' '.join(cmd)}")
|
click.echo(f"[dry-run] {' '.join(cmd)}")
|
||||||
@@ -398,15 +320,11 @@ def main(
|
|||||||
raise click.ClickException(_("Registry login failed"))
|
raise click.ClickException(_("Registry login failed"))
|
||||||
|
|
||||||
failed: list[str] = []
|
failed: list[str] = []
|
||||||
push_username = "" # nosec B105
|
|
||||||
push_token = "" # nosec B105
|
|
||||||
if push:
|
|
||||||
push_username, push_token = _get_registry_creds()
|
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
if not build_image(spec, registry, dry_run=dry_run, pull=pull):
|
if not build_image(spec, registry, dry_run=dry_run, pull=pull):
|
||||||
failed.append(spec.name)
|
failed.append(spec.name)
|
||||||
continue
|
continue
|
||||||
if push and not push_image(spec, registry, dry_run=dry_run, username=push_username, token=push_token): # type: ignore[arg-type]
|
if push and not push_image(spec, registry, dry_run=dry_run): # type: ignore[arg-type]
|
||||||
failed.append(spec.name)
|
failed.append(spec.name)
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import devx.tools.build_image as build_image
|
|||||||
from devx.tools.build_image import (
|
from devx.tools.build_image import (
|
||||||
ImageSpec,
|
ImageSpec,
|
||||||
build_full_tag,
|
build_full_tag,
|
||||||
delete_remote_manifest,
|
|
||||||
load_manifest,
|
load_manifest,
|
||||||
push_image,
|
push_image,
|
||||||
registry_login,
|
registry_login,
|
||||||
@@ -217,119 +216,6 @@ class TestPushImage:
|
|||||||
assert push_image(spec, "git.example.com", dry_run=True) is True
|
assert push_image(spec, "git.example.com", dry_run=True) is True
|
||||||
mock_run.assert_not_called()
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
def test_delete_before_push_with_creds(self) -> None:
|
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
|
||||||
with (
|
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
|
||||||
patch("devx.tools.build_image.delete_remote_manifest", return_value=True) as mock_del,
|
|
||||||
):
|
|
||||||
assert push_image(spec, "git.example.com", username="user", token="tok") is True
|
|
||||||
mock_del.assert_called_once_with(
|
|
||||||
"git.example.com",
|
|
||||||
"ci-base",
|
|
||||||
"latest",
|
|
||||||
"user",
|
|
||||||
"tok",
|
|
||||||
dry_run=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_delete_without_creds(self) -> None:
|
|
||||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
|
||||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
|
||||||
with (
|
|
||||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
|
||||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
|
||||||
):
|
|
||||||
assert push_image(spec, "git.example.com") is True
|
|
||||||
mock_del.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteRemoteManifest:
|
|
||||||
def test_dry_run(self) -> None:
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t", dry_run=True) is True
|
|
||||||
|
|
||||||
def test_tag_not_found(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = urllib.error.HTTPError("url", 404, "Not Found", {}, None)
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_delete_success(self) -> None:
|
|
||||||
mock_head_resp = MagicMock()
|
|
||||||
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
|
|
||||||
mock_del_resp = MagicMock()
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = [mock_head_resp, mock_del_resp]
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_delete_404_treated_as_success(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
mock_head_resp = MagicMock()
|
|
||||||
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = [
|
|
||||||
mock_head_resp,
|
|
||||||
urllib.error.HTTPError("url", 404, "Not Found", {}, None),
|
|
||||||
]
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_head_error_does_not_block(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = urllib.error.HTTPError("url", 500, "Server Error", {}, None)
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_url_error_does_not_block(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = urllib.error.URLError("network down")
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_no_digest_does_not_block(self) -> None:
|
|
||||||
mock_head_resp = MagicMock()
|
|
||||||
mock_head_resp.__enter__.return_value.headers.get.return_value = None
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.return_value = mock_head_resp
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_head_405_passes_through(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = urllib.error.HTTPError("url", 405, "Method Not Allowed", {}, None)
|
|
||||||
# 405 falls through with pass, digest never set, returns True
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
assert mock_urlopen.call_count == 1
|
|
||||||
|
|
||||||
def test_delete_500_does_not_block(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
mock_head_resp = MagicMock()
|
|
||||||
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = [
|
|
||||||
mock_head_resp,
|
|
||||||
urllib.error.HTTPError("url", 500, "Server Error", {}, None),
|
|
||||||
]
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
def test_delete_url_error_does_not_block(self) -> None:
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
mock_head_resp = MagicMock()
|
|
||||||
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
|
|
||||||
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
|
|
||||||
mock_urlopen.side_effect = [
|
|
||||||
mock_head_resp,
|
|
||||||
urllib.error.URLError("network down"),
|
|
||||||
]
|
|
||||||
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestSortVersions:
|
class TestSortVersions:
|
||||||
def test_sort_by_created_at_desc(self) -> None:
|
def test_sort_by_created_at_desc(self) -> None:
|
||||||
@@ -702,20 +588,11 @@ class TestCLIBuildImage:
|
|||||||
"devx.tools.build_image.subprocess.run",
|
"devx.tools.build_image.subprocess.run",
|
||||||
side_effect=[login_result, build_result, push_result],
|
side_effect=[login_result, build_result, push_result],
|
||||||
):
|
):
|
||||||
with patch("devx.tools.build_image.delete_remote_manifest", return_value=True):
|
result = runner.invoke(
|
||||||
result = runner.invoke(
|
build_image.main,
|
||||||
build_image.main,
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
||||||
[
|
)
|
||||||
"--dockerfile",
|
assert result.exit_code != 0
|
||||||
str(dockerfile),
|
|
||||||
"--name",
|
|
||||||
"ci-base",
|
|
||||||
"--push",
|
|
||||||
"--registry",
|
|
||||||
"git.example.com",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
assert result.exit_code != 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLICleanImages:
|
class TestCLICleanImages:
|
||||||
|
|||||||
Reference in New Issue
Block a user