diff --git a/AGENTS.md b/AGENTS.md index 1e216bb..d9589f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,5 +17,5 @@ the add-an-image workflow. - `update` finds `IMAGES` with a regex. Keep `IMAGES = (` and its closing `)` at column 0 with only indented, non-blank lines between them. Otherwise - `update` silently writes nothing. + `update` exits with an error. - Pin digests only for architectures in `ARCHS`. diff --git a/README.md b/README.md index cb905f7..26e7176 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,6 @@ with mutable tags, such as `latest` or `14`, pick up new digests. Run `python3 main.py update --dry-run` to print the rewritten file without saving it. -`update` fails with HTTP 400 when it reaches a single-architecture entry, such -as `checkr/flagr:latest`. Until that is fixed, add the new entry's digests by -hand. Get them with `docker buildx imagetools inspect :`. - CI runs `python3 main.py sync --dry-run` on pull requests to list the images it would push. After merge, CI runs `sync` on `main` and pushes each image that is missing one of its pinned digests on ghcr.io. diff --git a/main.py b/main.py index 949ebbd..760d3d1 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from typing import Mapping from typing import NamedTuple -ARCHS = frozenset(('amd64', 'arm64', 'arm64/v8')) +ARCHS = frozenset(('amd64', 'arm64')) LIST = 'application/vnd.docker.distribution.manifest.list.v2+json' SINGLE = 'application/vnd.docker.distribution.manifest.v2+json' @@ -60,12 +60,12 @@ def _digests(registry: str, image: str, tag: str) -> list[tuple[str, str]]: req = urllib.request.Request( f'https://{registry}/v2/{image}/manifests/{tag}', headers={ - 'Authorization': f'Bearer {token}', # annoyingly, even if we only "Accept" the list, docker.io will # send us a single manifest 'Accept': f'{LIST}, {INDEX}, {SINGLE};q=.9', }, ) + req.add_unredirected_header('Authorization', f'Bearer {token}') resp = urllib.request.urlopen(req) ret = json.load(resp) if resp.headers['Content-Type'] in {LIST, INDEX}: @@ -77,8 +77,10 @@ def _digests(registry: str, image: str, tag: str) -> list[tuple[str, str]]: blob = ret['config']['digest'] req = urllib.request.Request( f'https://{registry}/v2/{image}/blobs/{blob}', - headers={'Authorization': f'Bearer {token}'}, ) + # registries redirect blob downloads to object storage, which + # rejects requests that also carry the registry's bearer token + req.add_unredirected_header('Authorization', f'Bearer {token}') ret = json.load(urllib.request.urlopen(req)) return [(ret['architecture'], resp.headers['Docker-Content-Digest'])] else: @@ -549,7 +551,11 @@ def main() -> int: with open(__file__) as f: src = f.read() - src = re.sub(r'IMAGES = \(\n( +.+\n)+\)', '\n'.join(lines), src) + src, n = re.subn( + r'IMAGES = \(\n(?: +\S.*\n)+\)', '\n'.join(lines), src, + ) + if n != 1: + raise SystemExit(f'expected 1 IMAGES block, found {n}') if args.dry_run: print(src)