Public Access
DEVX-167: feat(ci): verify producer container artifact before opening dependency PR
This commit was merged in pull request #332.
This commit is contained in:
@@ -25,6 +25,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from devx.api_clients import GiteaClient
|
||||
@@ -93,6 +94,55 @@ def update_pinned_version(file_path: str, package: str, old_version: str, new_ve
|
||||
return changed
|
||||
|
||||
|
||||
def resolve_container_digest(api_url: str, owner: str, name: str, tag: str, token: str) -> str:
|
||||
"""Resolve the OCI digest for a container image tag via the packages API.
|
||||
|
||||
Implements REQ-1: dependency PRs must only be opened after the producer
|
||||
artifact exists — this raises ClickException when the tag is missing or
|
||||
the registry call fails, so the PR is never opened against an artifact
|
||||
that has not been published. The sha256 of the stored ``manifest.json``
|
||||
blob is the manifest content digest (what ``docker pull`` reports).
|
||||
"""
|
||||
url = f"{api_url}/packages/{owner}/container/{name}/{tag}/files"
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=30) # nosec B310
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
status = e.response.status_code if e.response is not None else "?"
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Container artifact {owner}/{name}:{tag} not found or unreadable (HTTP {status}). "
|
||||
"Refusing to open a dependency PR for an unpublished artifact.",
|
||||
owner=owner,
|
||||
name=name,
|
||||
tag=tag,
|
||||
status=status,
|
||||
)
|
||||
) from e
|
||||
except requests.RequestException as e:
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Registry lookup failed for {owner}/{name}:{tag}: {error}",
|
||||
owner=owner,
|
||||
name=name,
|
||||
tag=tag,
|
||||
error=e,
|
||||
)
|
||||
) from e
|
||||
for f in resp.json():
|
||||
if f.get("name") == "manifest.json" and f.get("sha256"):
|
||||
return f"sha256:{f['sha256']}"
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Registry returned no manifest blob for {owner}/{name}:{tag}.",
|
||||
owner=owner,
|
||||
name=name,
|
||||
tag=tag,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_vikunja_task(title: str, description: str) -> str | None:
|
||||
"""Create a Vikunja task and return its identifier (e.g., OBL-INFRA-531)."""
|
||||
try:
|
||||
@@ -113,6 +163,24 @@ def create_vikunja_task(title: str, description: str) -> str | None:
|
||||
@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-run-id", default="", help=_("CI run ID that triggered the publish"))
|
||||
@click.option(
|
||||
"--verify-container",
|
||||
default="",
|
||||
help=_(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--container-tag",
|
||||
default="",
|
||||
help=_(
|
||||
"Container tag to verify with --verify-container (default: --new-version). "
|
||||
"Use when the image tag differs from the release version, e.g. a package "
|
||||
"__version__ tag vs a release git tag."
|
||||
),
|
||||
)
|
||||
@click.option("--dry-run", is_flag=True, default=False, help=_("Show what would be done without creating PR"))
|
||||
def cli(
|
||||
repo: str,
|
||||
@@ -120,6 +188,8 @@ def cli(
|
||||
new_version: str,
|
||||
source_repo: str,
|
||||
source_run_id: str,
|
||||
verify_container: str,
|
||||
container_tag: str,
|
||||
dry_run: bool,
|
||||
) -> None:
|
||||
"""Create an infra PR to bump a pinned dependency version."""
|
||||
@@ -129,9 +199,29 @@ def cli(
|
||||
owner, repo_name = repo.split("/", 1)
|
||||
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
||||
|
||||
# Implements: REQ-1 — this tool runs from the *producer* repo's CI, so
|
||||
# every file lookup and git operation must happen inside a clone of the
|
||||
# target repo, not the producer checkout in CWD.
|
||||
# Implements: REQ-1 — verify the producer artifact exists and resolve its
|
||||
# digest before any branch/PR work begins.
|
||||
image_digest = ""
|
||||
if verify_container:
|
||||
if "/" not in verify_container:
|
||||
raise click.ClickException(
|
||||
_("Invalid container format: {container} (expected owner/name)", container=verify_container)
|
||||
)
|
||||
c_owner, c_name = verify_container.split("/", 1)
|
||||
image_tag = container_tag or new_version
|
||||
image_digest = resolve_container_digest(GITEA_API_URL, c_owner, c_name, image_tag, token)
|
||||
click.echo(
|
||||
_(
|
||||
"[dep-pr] Verified {container}:{version} -> {digest}",
|
||||
container=verify_container,
|
||||
version=image_tag,
|
||||
digest=image_digest,
|
||||
)
|
||||
)
|
||||
|
||||
# Clone the target repo — this tool runs from the *producer* repo's CI,
|
||||
# so every file lookup and git operation must happen inside a clone of
|
||||
# the target repo, not the producer checkout in CWD.
|
||||
workdir = Path(tempfile.mkdtemp(prefix="dep-pr-"))
|
||||
clone_url = f"{GITEA_API_URL.removesuffix('/api/v1')}/{repo}.git"
|
||||
auth_cfg = f"http.extraHeader=Authorization: token {token}"
|
||||
@@ -203,7 +293,6 @@ def cli(
|
||||
|
||||
if 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))
|
||||
|
||||
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}"
|
||||
subprocess.run(["git", "commit", "-m", commit_msg], check=True, cwd=workdir) # nosec B603 B607
|
||||
|
||||
@@ -1343,6 +1343,30 @@
|
||||
"ru": "Настройка входа tea '{name}' для {url}...",
|
||||
"zh": "正在为 {url} 配置 tea 登录 '{name}'..."
|
||||
},
|
||||
"Container artifact {owner}/{name}:{tag} not found or unreadable (HTTP {status}). Refusing to open a dependency PR for an unpublished artifact.": {
|
||||
"bg": "Артефактът на контейнера {owner}/{name}:{tag} не е намерен или не може да бъде прочетен (HTTP {status}). Отказвам да отворя PR за зависимост за непубликуван артефакт.",
|
||||
"de": "Container-Artefakt {owner}/{name}:{tag} nicht gefunden oder nicht lesbar (HTTP {status}). Kein Dependency-PR für ein unveröffentlichtes Artefakt.",
|
||||
"en": "Container artifact {owner}/{name}:{tag} not found or unreadable (HTTP {status}). Refusing to open a dependency PR for an unpublished artifact.",
|
||||
"pl": "Artefakt kontenera {owner}/{name}:{tag} nie został znaleziony lub jest nieczytelny (HTTP {status}). Odmawiam otwarcia PR zależności dla nieopublikowanego artefaktu.",
|
||||
"ru": "Артефакт контейнера {owner}/{name}:{tag} не найден или недоступен для чтения (HTTP {status}). Отказ открывать PR зависимости для неопубликованного артефакта.",
|
||||
"zh": "容器构件 {owner}/{name}:{tag} 未找到或不可读 (HTTP {status})。拒绝为未发布的构件创建依赖 PR。"
|
||||
},
|
||||
"Container tag to verify with --verify-container (default: --new-version). Use when the image tag differs from the release version, e.g. a package __version__ tag vs a release git tag.": {
|
||||
"bg": "Таг на контейнер за проверка с --verify-container (по подразбиране: --new-version). Използвайте, когато тагът на изображението се различава от версията на изданието, напр. таг на __version__ на пакет срещу git таг на издание.",
|
||||
"de": "Container-Tag, der mit --verify-container geprüft wird (Standard: --new-version). Zu verwenden, wenn sich das Image-Tag von der Release-Version unterscheidet, z. B. ein __version__-Tag eines Pakets vs. ein Release-Git-Tag.",
|
||||
"en": "Container tag to verify with --verify-container (default: --new-version). Use when the image tag differs from the release version, e.g. a package __version__ tag vs a release git tag.",
|
||||
"pl": "Tag kontenera do weryfikacji z --verify-container (domyślnie: --new-version). Użyj, gdy tag obrazu różni się od wersji wydania, np. tag __version__ pakietu a tag git wydania.",
|
||||
"ru": "Тег контейнера для проверки с --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.": {
|
||||
"bg": "Контейнер за проверка преди отваряне на PR (собственик/име). Разрешава OCI дайджеста на тага, отговарящ на --new-version (или --container-tag); PR се отказва, ако артефактът липсва или е нечетим.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"ru": "Контейнер для проверки перед открытием PR (владелец/имя). Разрешает OCI-дайджест тега, соответствующего --new-version (или --container-tag); PR отклоняется, если артефакт отсутствует или недоступен.",
|
||||
"zh": "在打开 PR 前要验证的容器(所有者/名称)。解析与 --new-version(或 --container-tag)匹配标签的 OCI 摘要;当工件缺失或不可读时拒绝创建 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.",
|
||||
"de": "PR-Nummer konnte nicht ermittelt werden. Mit --pr explizit angeben,\noder den Befehl von einem Branch mit offenem PR ausführen.",
|
||||
@@ -1991,6 +2015,14 @@
|
||||
"ru": "Интеграционные тесты пройдены.",
|
||||
"zh": "集成测试通过。"
|
||||
},
|
||||
"Invalid container format: {container} (expected owner/name)": {
|
||||
"bg": "Невалиден формат на контейнер: {container} (очаква се owner/name)",
|
||||
"de": "Ungültiges Container-Format: {container} (erwartet owner/name)",
|
||||
"en": "Invalid container format: {container} (expected owner/name)",
|
||||
"pl": "Nieprawidłowy format kontenera: {container} (oczekiwano owner/name)",
|
||||
"ru": "Неверный формат контейнера: {container} (ожидается owner/name)",
|
||||
"zh": "容器格式无效:{container}(应为 owner/name)"
|
||||
},
|
||||
"Invalid repo format: {repo}": {
|
||||
"bg": "Невалиден формат на репозитория: {repo}",
|
||||
"de": "Ungültiges Repo-Format: {repo}",
|
||||
@@ -2887,6 +2919,22 @@
|
||||
"ru": "Вход в реестр не удался: {error}",
|
||||
"zh": "注册表登录失败:{error}"
|
||||
},
|
||||
"Registry lookup failed for {owner}/{name}:{tag}: {error}": {
|
||||
"bg": "Неуспешно търсене в регистъра за {owner}/{name}:{tag}: {error}",
|
||||
"de": "Registry-Abfrage für {owner}/{name}:{tag} fehlgeschlagen: {error}",
|
||||
"en": "Registry lookup failed for {owner}/{name}:{tag}: {error}",
|
||||
"pl": "Wyszukiwanie w rejestrze nie powiodło się dla {owner}/{name}:{tag}: {error}",
|
||||
"ru": "Ошибка поиска в реестре для {owner}/{name}:{tag}: {error}",
|
||||
"zh": "注册表查询 {owner}/{name}:{tag} 失败:{error}"
|
||||
},
|
||||
"Registry returned no manifest blob for {owner}/{name}:{tag}.": {
|
||||
"bg": "Регистърът не върна manifest blob за {owner}/{name}:{tag}.",
|
||||
"de": "Registry hat keinen Manifest-Blob für {owner}/{name}:{tag} zurückgegeben.",
|
||||
"en": "Registry returned no manifest blob for {owner}/{name}:{tag}.",
|
||||
"pl": "Rejestr nie zwrócił blobu manifestu dla {owner}/{name}:{tag}.",
|
||||
"ru": "Реестр не вернул blob манифеста для {owner}/{name}:{tag}.",
|
||||
"zh": "注册表未返回 {owner}/{name}:{tag} 的清单 blob。"
|
||||
},
|
||||
"Regular merge commit — running all post-merge jobs.": {
|
||||
"bg": "Обикновен merge комит — изпълняват се всички post-merge задачи.",
|
||||
"de": "Regulärer Merge-Commit — alle Post-Merge-Jobs werden ausgeführt.",
|
||||
@@ -3727,6 +3775,14 @@
|
||||
"ru": "[dep-pr] PR уже существует: #{number}",
|
||||
"zh": "[dep-pr] PR 已存在:#{number}"
|
||||
},
|
||||
"[dep-pr] Verified {container}:{version} -> {digest}": {
|
||||
"bg": "[dep-pr] Проверено {container}:{version} -> {digest}",
|
||||
"de": "[dep-pr] Verifiziert {container}:{version} -> {digest}",
|
||||
"en": "[dep-pr] Verified {container}:{version} -> {digest}",
|
||||
"pl": "[dep-pr] Zweryfikowano {container}:{version} -> {digest}",
|
||||
"ru": "[dep-pr] Проверено {container}:{version} -> {digest}",
|
||||
"zh": "[dep-pr] 已验证 {container}:{version} -> {digest}"
|
||||
},
|
||||
"[dep-pr] {pkg} already at {version} — no PR needed.": {
|
||||
"bg": "[dep-pr] {pkg} вече е на {version} — не е нужен PR.",
|
||||
"de": "[dep-pr] {pkg} bereits auf {version} — kein PR nötig.",
|
||||
|
||||
Reference in New Issue
Block a user