#!/usr/bin/env bash # Publish or diff the Docker Hub repository overview. # # The overview used to live only on Docker Hub, so it silently kept serving # the pre-Apache-2.0 licence for a week after the relicense. docs/dockerhub.md # is now the source of truth; this script pushes it and, more usefully, tells # you when the live page has drifted away from it. # # scripts/dockerhub-description.sh diff # live page vs docs/dockerhub.md # scripts/dockerhub-description.sh publish # overwrite the live page # # publish needs a Docker Hub access token with write scope: # export DOCKERHUB_USERNAME=... DOCKERHUB_TOKEN=dckr_pat_... # The token is never echoed. diff needs no credentials. set -euo pipefail REPO="${DOCKERHUB_REPO:-applegater/caddyui}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Each repository has its own source file; DOCKERHUB_SOURCE overrides. case "$REPO" in applegater/caddyui-caddy) SOURCE="${DOCKERHUB_SOURCE:-$ROOT/docs/dockerhub-caddy.md}" ;; *) SOURCE="${DOCKERHUB_SOURCE:-$ROOT/docs/dockerhub.md}" ;; esac die() { printf '%s\n' "$*" >&2; exit 1; } [ -f "$SOURCE" ] || die "missing $SOURCE" # Everything after the leading HTML comment is what Docker Hub receives. rendered() { python3 - "$SOURCE" <<'PY' import re, sys text = open(sys.argv[1], encoding="utf-8").read() sys.stdout.write(re.sub(r"\A\s*\s*", "", text, count=1, flags=re.S)) PY } # The "short-description:" line inside the leading comment sets the one-liner # under the repository name (Docker Hub caps it at 100 characters). short_description() { python3 - "$SOURCE" <<'PY' import re, sys text = open(sys.argv[1], encoding="utf-8").read() head = re.match(r"\A\s*", text, flags=re.S) found = re.search(r"^short-description:\s*(.+?)\s*$", head.group(1), flags=re.M) if head else None sys.stdout.write(found.group(1) if found else "") PY } live() { curl -fsS -m 30 "https://hub.docker.com/v2/repositories/$REPO/" \ | python3 -c 'import json,sys; sys.stdout.write(json.load(sys.stdin).get("full_description") or "")' } case "${1:-diff}" in diff) live > /tmp/.hub-live.$$ || die "could not read the live description" rendered > /tmp/.hub-local.$$ if diff -u --label "docker hub (live)" /tmp/.hub-live.$$ \ --label "$(basename "$SOURCE")" /tmp/.hub-local.$$; then echo "in sync" else status=1 fi rm -f /tmp/.hub-live.$$ /tmp/.hub-local.$$ exit "${status:-0}" ;; publish) : "${DOCKERHUB_USERNAME:?set DOCKERHUB_USERNAME}" : "${DOCKERHUB_TOKEN:?set DOCKERHUB_TOKEN (Docker Hub access token, write scope)}" jwt="$( python3 - <<'PY' import json, os, sys, urllib.request body = json.dumps({ "username": os.environ["DOCKERHUB_USERNAME"], "password": os.environ["DOCKERHUB_TOKEN"], }).encode() req = urllib.request.Request( "https://hub.docker.com/v2/users/login/", data=body, headers={"Content-Type": "application/json"}, method="POST") try: with urllib.request.urlopen(req, timeout=30) as r: sys.stdout.write(json.load(r)["token"]) except Exception as exc: sys.exit("docker hub login failed: %s" % exc) PY )" || die "could not authenticate to Docker Hub" # The payload goes via a file, not a pipe: this script feeds python its # program on stdin (`python3 - < "$payload" [ -s "$payload" ] || die "refusing to publish: rendered description is empty" DOCKERHUB_JWT="$jwt" DOCKERHUB_REPO="$REPO" DOCKERHUB_PAYLOAD="$payload" \ DOCKERHUB_SHORT="$(short_description)" python3 - <<'PY' import json, os, sys, urllib.request description = open(os.environ["DOCKERHUB_PAYLOAD"], encoding="utf-8").read() if not description.strip(): sys.exit("refusing to publish an empty description") body = {"full_description": description} short = os.environ.get("DOCKERHUB_SHORT", "").strip() if short: if len(short) > 100: sys.exit("short description is %d characters; Docker Hub allows 100" % len(short)) body["description"] = short req = urllib.request.Request( "https://hub.docker.com/v2/repositories/%s/" % os.environ["DOCKERHUB_REPO"], data=json.dumps(body).encode(), headers={ "Content-Type": "application/json", "Authorization": "JWT %s" % os.environ["DOCKERHUB_JWT"], }, method="PATCH") try: with urllib.request.urlopen(req, timeout=30) as r: json.load(r) except Exception as exc: sys.exit("publish failed: %s" % exc) print("published %d characters to %s%s" % ( len(description), os.environ["DOCKERHUB_REPO"], " (+ short description)" if short else "")) PY ;; *) die "usage: ${0##*/} [diff|publish]" ;; esac