From f7103ef54f90ffdfcf17800593af8cede8583fb9 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 4 Aug 2026 14:32:20 +0200 Subject: [PATCH] Make package publication retries hash-safe --- .gitea/workflows/module-package-release.yml | 88 +++++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/module-package-release.yml b/.gitea/workflows/module-package-release.yml index 69dca09..ef7ae89 100644 --- a/.gitea/workflows/module-package-release.yml +++ b/.gitea/workflows/module-package-release.yml @@ -163,6 +163,78 @@ jobs: with: name: module-packages-${{ gitea.ref_name }} path: dist/package-artifacts.json + - name: Check immutable registry state + shell: bash + env: + PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "$PACKAGE_TOKEN" + python - <<'PY' + import hashlib + import json + import os + from pathlib import Path + import tomllib + from urllib.error import HTTPError + from urllib.parse import quote + from urllib.request import Request, urlopen + + api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN" + token = os.environ["PACKAGE_TOKEN"] + + def should_publish(kind, name, version, path): + package_url = "/".join( + (api_root, kind, quote(name, safe=""), quote(version, safe=""), "files") + ) + request = Request( + package_url, + headers={"Accept": "application/json", "Authorization": f"token {token}"}, + ) + try: + with urlopen(request, timeout=30) as response: + files = json.load(response) + except HTTPError as exc: + if exc.code == 404: + print(f"{kind} package {name}=={version} is not published yet") + return True + raise + if not isinstance(files, list) or len(files) != 1: + raise SystemExit( + f"immutable {kind} package {name}=={version} has an unexpected file set" + ) + expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest() + if files[0].get("sha256") != expected_sha256: + raise SystemExit( + f"immutable {kind} package {name}=={version} already exists with a different SHA-256" + ) + print(f"verified existing {kind} package {name}=={version} ({expected_sha256})") + return False + + project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"] + wheels = tuple(Path("dist").glob("*.whl")) + if len(wheels) != 1: + raise SystemExit("release build must contain exactly one wheel") + publish_pypi = should_publish( + "pypi", str(project["name"]), str(project["version"]), wheels[0] + ) + + tarballs = tuple(Path("dist").glob("*.tgz")) + if len(tarballs) > 1: + raise SystemExit("release build must contain at most one npm package") + publish_npm = False + if tarballs: + webui = json.loads( + Path(".package-webui/package.json").read_text(encoding="utf-8") + ) + publish_npm = should_publish( + "npm", str(webui["name"]), str(webui["version"]), tarballs[0] + ) + + with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file: + env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n") + env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n") + PY - name: Publish wheel and WebUI package shell: bash env: @@ -172,13 +244,17 @@ jobs: set -euo pipefail test -n "$PACKAGE_USERNAME" test -n "$PACKAGE_TOKEN" - TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \ - python -m twine upload --non-interactive \ - --repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \ - dist/*.whl + if [[ "$PUBLISH_PYPI" == 1 ]]; then + TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \ + python -m twine upload --non-interactive \ + --repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \ + dist/*.whl + else + echo "Exact wheel is already present; skipping immutable retry." + fi shopt -s nullglob webui_packages=(dist/*.tgz) - if (( ${#webui_packages[@]} )); then + if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then npmrc="$(mktemp)" trap 'rm -f "$npmrc"' EXIT chmod 600 "$npmrc" @@ -189,4 +265,6 @@ jobs: NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \ --ignore-scripts --access public \ --registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/ + elif (( ${#webui_packages[@]} )); then + echo "Exact WebUI package is already present; skipping immutable retry." fi