From 5d28ae38727aea0f55b318fe31b68ac14f1e3f56 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 6 Oct 2026 16:34:05 +0200 Subject: [PATCH 1/3] chore(tmp): dry run Docker Hub deletion Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tmp-dockerhub-delete.yml | 110 +++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/tmp-dockerhub-delete.yml diff --git a/.github/workflows/tmp-dockerhub-delete.yml b/.github/workflows/tmp-dockerhub-delete.yml new file mode 100644 index 000000000..e108c443c --- /dev/null +++ b/.github/workflows/tmp-dockerhub-delete.yml @@ -0,0 +1,110 @@ +name: "tmp: Docker Hub deletion (PYSDK-156)" + +on: + push: + branches: [chore/pysdk-156-dockerhub-delete] + +permissions: {} + +jobs: + delete: + runs-on: ubuntu-latest + env: + MODE: dry-run # dry-run | canary | all + NAMESPACE: helmuthva + REPOS: aignostics-python-sdk aignostics-python-sdk-slim + CANARY_TAG: "0.2.96" + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + steps: + - name: Delete Docker Hub tags + shell: python + run: | + import json, os, sys, time, urllib.error, urllib.request + + HUB = "https://hub.docker.com" + MODE = os.environ["MODE"] + NS = os.environ["NAMESPACE"] + OK = (200, 202, 204) + + def call(method, url, token=None, body=None): + data = json.dumps(body).encode() if body is not None else None + for _ in range(5): + req = urllib.request.Request(url, data=data, method=method) + req.add_header("Content-Type", "application/json") + if token: + req.add_header("Authorization", f"Bearer {token}") + try: + with urllib.request.urlopen(req) as r: + raw = r.read() + return r.status, (json.loads(raw) if raw else None) + except urllib.error.HTTPError as e: + if e.code == 429: + time.sleep(int(e.headers.get("Retry-After", "30"))) + continue + return e.code, None + return 429, None + + user, secret = os.environ["DOCKER_USERNAME"], os.environ["DOCKER_PASSWORD"] + status, body = call("POST", f"{HUB}/v2/auth/token", body={"identifier": user, "secret": secret}) + token = (body or {}).get("access_token") + if not token: + status, body = call("POST", f"{HUB}/v2/users/login", body={"username": user, "password": secret}) + token = (body or {}).get("token") + if not token: + sys.exit(f"::error::Docker Hub login failed ({status})") + print("Docker Hub login OK") + + def list_tags(repo): + url, names = f"{HUB}/v2/namespaces/{NS}/repositories/{repo}/tags?page_size=100", [] + while url: + status, body = call("GET", url, token) + if status == 404: + return [] + if status != 200: + sys.exit(f"::error::list {repo} failed ({status})") + names += [t["name"] for t in body["results"]] + url = body.get("next") + return names + + def delete(paths): + # Current endpoint first, legacy endpoint second. + for path in paths: + status, _ = call("DELETE", HUB + path, token) + if status not in (404, 405): + return status + return status + + for repo in os.environ["REPOS"].split(): + names = list_tags(repo) + print(f"{repo}: {len(names)} tags") + if MODE == "dry-run": + continue + targets = [os.environ["CANARY_TAG"]] if MODE == "canary" else names + for tag in targets: + status = delete([ + f"/v2/namespaces/{NS}/repositories/{repo}/tags/{tag}", + f"/v2/repositories/{NS}/{repo}/tags/{tag}/", + ]) + print(f"DELETE {repo}:{tag} -> {status}") + if status not in OK: + sys.exit(f"::error::DELETE {repo}:{tag} failed ({status}). Stop.") + if MODE == "all": + status = delete([ + f"/v2/namespaces/{NS}/repositories/{repo}", + f"/v2/repositories/{NS}/{repo}/", + ]) + level = "notice" if status in OK else "warning" + print(f"::{level}::DELETE repository {repo} -> {status}") + + - name: Verify anonymously + if: env.MODE == 'all' + run: | + for repo in $REPOS; do + code=$(curl -s -o body.json -w "%{http_code}" \ + "https://hub.docker.com/v2/namespaces/$NAMESPACE/repositories/$repo/tags?page_size=1") + if [ "$code" = "404" ]; then echo "$repo: repository gone"; continue; fi + count=$(jq .count body.json) + echo "$repo: $count tags" + [ "$count" = "0" ] || { echo "::error::$repo still has tags"; exit 1; } + done From f095e69f66b3d10d40afad777c3d0e55b69e36f9 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 09:43:03 +0200 Subject: [PATCH 2/3] chore(tmp): delete canary Docker Hub tag Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tmp-dockerhub-delete.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tmp-dockerhub-delete.yml b/.github/workflows/tmp-dockerhub-delete.yml index e108c443c..60b5df0f7 100644 --- a/.github/workflows/tmp-dockerhub-delete.yml +++ b/.github/workflows/tmp-dockerhub-delete.yml @@ -10,7 +10,7 @@ jobs: delete: runs-on: ubuntu-latest env: - MODE: dry-run # dry-run | canary | all + MODE: canary # dry-run | canary | all NAMESPACE: helmuthva REPOS: aignostics-python-sdk aignostics-python-sdk-slim CANARY_TAG: "0.2.96" From 3e827e04c32deeaec1d0491c8e693426f59b5792 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 09:45:07 +0200 Subject: [PATCH 3/3] chore(tmp): delete all Docker Hub tags Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tmp-dockerhub-delete.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tmp-dockerhub-delete.yml b/.github/workflows/tmp-dockerhub-delete.yml index 60b5df0f7..8473410cb 100644 --- a/.github/workflows/tmp-dockerhub-delete.yml +++ b/.github/workflows/tmp-dockerhub-delete.yml @@ -10,7 +10,7 @@ jobs: delete: runs-on: ubuntu-latest env: - MODE: canary # dry-run | canary | all + MODE: all # dry-run | canary | all NAMESPACE: helmuthva REPOS: aignostics-python-sdk aignostics-python-sdk-slim CANARY_TAG: "0.2.96"