Skip to content

Commit 08d5175

Browse files
committed
Make latest a post-build multi-arch alias
Stop tagging latest during the matrix build and create it afterwards from the already-pushed canonical default-distro image instead. The canonical tag is now derived from the same inputs as the legacy latest image: python:<DEFAULT_DISTRO> and Node.js dist/latest. The workflow fails if that computed tag was not part of the current build set, which makes drift explicit. Fixes nikolaik#263. Remove the root Dockerfile because it only existed for Docker Hub's separate automated latest build path. Keeping that file would preserve a second, divergent publishing path for latest while GitHub Actions now owns latest as an alias to an existing multi-arch build. After this change, any Docker Hub automated build or autobuild rule that still targets the deleted root Dockerfile must be disabled in Docker Hub settings. That configuration is external to this repository and is not removed automatically by this commit.
1 parent 22ef8e0 commit 08d5175

7 files changed

Lines changed: 185 additions & 37 deletions

File tree

.github/workflows/build.yaml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,37 @@ jobs:
102102
if-no-files-found: error
103103
retention-days: 1
104104

105+
tag-latest:
106+
name: Point latest at canonical build
107+
runs-on: ubuntu-latest
108+
needs: [deploy]
109+
steps:
110+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
111+
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7
112+
with:
113+
enable-cache: true
114+
- name: Download metadata for builds
115+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
116+
with:
117+
path: builds
118+
pattern: build-*
119+
merge-multiple: true
120+
- name: Set up Docker Buildx
121+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
122+
- name: Login to Docker Hub
123+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
124+
with:
125+
username: ${{ secrets.DOCKERHUB_USERNAME }}
126+
password: ${{ secrets.DOCKERHUB_TOKEN }}
127+
- name: Point latest to canonical image
128+
run: |
129+
latest_tag="$(uv run dpn latest-key --builds-dir builds/)"
130+
docker buildx imagetools create --tag "${IMAGE_NAME}:latest" "${IMAGE_NAME}:${latest_tag}"
131+
105132
release:
106133
name: Update versions.json and README.md
107134
runs-on: ubuntu-latest
108-
needs: [deploy]
135+
needs: [deploy, tag-latest]
109136
steps:
110137
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
111138
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7

Dockerfile

Lines changed: 0 additions & 32 deletions
This file was deleted.

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ Versions are kept up to date using official sources. For Python we scrape the _S
139139
```bash
140140
# Pull from Docker Hub
141141
docker pull nikolaik/python-nodejs:latest
142-
# Build from GitHub
143-
docker build -t nikolaik/python-nodejs github.com/nikolaik/docker-python-nodejs
144142
# Run image
145143
docker run -it nikolaik/python-nodejs bash
146144
```

src/docker_python_nodejs/cli.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .versions import (
1111
decide_version_combinations,
1212
find_new_or_updated,
13+
latest_tag_key,
1314
load_build_contexts,
1415
load_versions,
1516
persist_versions,
@@ -23,7 +24,7 @@ class CLIArgs(argparse.Namespace):
2324
dry_run: bool
2425
distros: list[str]
2526
verbose: bool
26-
command: Literal["dockerfile", "build-matrix", "release"]
27+
command: Literal["dockerfile", "build-matrix", "release", "latest-key"]
2728
force: bool # build-matrix and release command arg
2829

2930
context: str # dockerfile command arg
@@ -69,6 +70,11 @@ def run_release(args: CLIArgs) -> None:
6970
update_dynamic_readme(versions, supported_python_versions, supported_nodejs_versions, args.dry_run)
7071

7172

73+
def run_latest_key(args: CLIArgs) -> None:
74+
builds = load_build_contexts(args.builds_dir)
75+
print(latest_tag_key(list(builds.values())))
76+
77+
7278
def main(args: CLIArgs) -> None:
7379
if args.dry_run:
7480
logger.debug("Dry run, outputting only.")
@@ -79,6 +85,8 @@ def main(args: CLIArgs) -> None:
7985
run_build_matrix(args)
8086
elif args.command == "release":
8187
run_release(args)
88+
elif args.command == "latest-key":
89+
run_latest_key(args)
8290

8391

8492
def parse_args() -> CLIArgs:
@@ -125,8 +133,19 @@ def parse_args() -> CLIArgs:
125133
help="Builds directory with build context JSON files",
126134
)
127135

136+
parser_latest_key = subparsers.add_parser(
137+
"latest-key",
138+
help="Print the built tag that should also be published as latest",
139+
)
140+
parser_latest_key.add_argument(
141+
"--builds-dir",
142+
type=Path,
143+
required=True,
144+
help="Builds directory with build context JSON files",
145+
)
146+
128147
cli_args = cast("CLIArgs", parser.parse_args())
129-
if cli_args.command == "release":
148+
if cli_args.command in {"release", "latest-key"}:
130149
if not cli_args.builds_dir.exists():
131150
parser.error(f"Builds directory {cli_args.builds_dir.as_posix()} does not exist")
132151

src/docker_python_nodejs/nodejs_versions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import re
23
from typing import TypedDict
34

45
import requests
@@ -29,6 +30,17 @@ def fetch_node_unofficial_releases() -> list[NodeRelease]:
2930
return data
3031

3132

33+
def fetch_latest_nodejs_version() -> str:
34+
url = "https://nodejs.org/dist/latest/SHASUMS256.txt"
35+
res = requests.get(url, timeout=10.0)
36+
res.raise_for_status()
37+
match = re.search(r"node-(v\d+\.\d+\.\d+)-", res.text)
38+
if not match:
39+
msg = "Could not determine latest Node.js version from SHASUMS256.txt"
40+
raise ValueError(msg)
41+
return match.group(1)
42+
43+
3244
class ReleaseScheduleItem(TypedDict):
3345
start: str
3446
lts: str

src/docker_python_nodejs/versions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from .docker_hub import DockerImageDict, DockerTagDict, fetch_tags
1616
from .nodejs_versions import (
17+
fetch_latest_nodejs_version,
1718
fetch_node_releases,
1819
fetch_node_unofficial_releases,
1920
fetch_nodejs_release_schedule,
@@ -105,6 +106,17 @@ def _latest_patch(tags: list[DockerTagDict], ver: str, distro: str) -> str | Non
105106
return sorted(tags, key=lambda x: Version.parse(x["name"]), reverse=True)[0]["name"] if tags else None
106107

107108

109+
def _latest_python_minor(distro: str) -> str:
110+
python_patch_re = re.compile(rf"^(\d+\.\d+\.\d+)-{distro}$")
111+
tags = [tag["name"] for tag in fetch_tags("python") if python_patch_re.match(tag["name"])]
112+
if not tags:
113+
msg = f"Could not determine latest Python version for distro '{distro}'"
114+
raise ValueError(msg)
115+
116+
latest_patch = sorted(tags, key=lambda x: Version.parse(x.removesuffix(f"-{distro}")), reverse=True)[0]
117+
return ".".join(latest_patch.removesuffix(f"-{distro}").split(".")[:2])
118+
119+
108120
def scrape_supported_python_versions() -> list[SupportedVersion]:
109121
"""Scrape supported python versions (risky)."""
110122
versions = []
@@ -256,6 +268,19 @@ def decide_version_combinations(
256268
return version_combinations(nodejs_versions, python_versions)
257269

258270

271+
def latest_tag_key(versions: list[BuildVersion]) -> str:
272+
"""Return the built tag that matches the legacy `latest` Dockerfile semantics."""
273+
python_minor = _latest_python_minor(DEFAULT_DISTRO)
274+
node_major = fetch_latest_nodejs_version().removeprefix("v").split(".")[0]
275+
key = f"python{python_minor}-nodejs{node_major}"
276+
277+
if key not in {version.key for version in versions}:
278+
msg = f"Computed latest tag '{key}' was not part of the current build set"
279+
raise ValueError(msg)
280+
281+
return key
282+
283+
259284
def persist_versions(versions: list[BuildVersion], dry_run: bool = False) -> None:
260285
if dry_run:
261286
logger.debug(versions)

tests/test_all.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
decide_version_combinations,
1919
fetch_supported_nodejs_versions,
2020
find_new_or_updated,
21+
latest_tag_key,
2122
load_build_contexts,
2223
scrape_supported_python_versions,
2324
)
@@ -289,3 +290,101 @@ def test_find_new_or_updated_with_digest() -> None:
289290
res = find_new_or_updated([new], {existing.key: existing})
290291

291292
assert len(res) == 0
293+
294+
295+
@responses.activate
296+
def test_latest_tag_key_matches_legacy_latest_sources() -> None:
297+
responses.add(
298+
method="GET",
299+
url="https://registry.hub.docker.com/v2/namespaces/library/repositories/python/tags?page=1&page_size=100",
300+
json={
301+
"count": 3,
302+
"next": None,
303+
"previous": None,
304+
"results": [
305+
{
306+
"name": "3.13.12-trixie",
307+
"images": [{"os": "linux", "architecture": "amd64"}, {"os": "linux", "architecture": "arm64"}],
308+
},
309+
{
310+
"name": "3.14.3-trixie",
311+
"images": [{"os": "linux", "architecture": "amd64"}, {"os": "linux", "architecture": "arm64"}],
312+
},
313+
{
314+
"name": "3.14.3-alpine",
315+
"images": [{"os": "linux", "architecture": "amd64"}],
316+
},
317+
],
318+
},
319+
)
320+
responses.add(
321+
method="GET",
322+
url="https://nodejs.org/dist/latest/SHASUMS256.txt",
323+
body="deadbeef node-v25.8.1-linux-x64.tar.xz\n",
324+
)
325+
versions = [
326+
BuildVersion(
327+
key="python3.14-nodejs25",
328+
python="3.14",
329+
python_canonical="3.14.3",
330+
python_image="3.14.3-trixie",
331+
nodejs="25",
332+
nodejs_canonical="25.8.1",
333+
distro="trixie",
334+
platforms=["linux/amd64", "linux/arm64"],
335+
),
336+
BuildVersion(
337+
key="python3.14-nodejs24-bookworm",
338+
python="3.14",
339+
python_canonical="3.14.3",
340+
python_image="3.14.3-bookworm",
341+
nodejs="24",
342+
nodejs_canonical="24.14.0",
343+
distro="bookworm",
344+
platforms=["linux/amd64", "linux/arm64"],
345+
),
346+
]
347+
348+
assert latest_tag_key(versions) == "python3.14-nodejs25"
349+
350+
351+
@responses.activate
352+
def test_latest_tag_key_fails_if_canonical_build_is_missing() -> None:
353+
responses.add(
354+
method="GET",
355+
url="https://registry.hub.docker.com/v2/namespaces/library/repositories/python/tags?page=1&page_size=100",
356+
json={
357+
"count": 1,
358+
"next": None,
359+
"previous": None,
360+
"results": [
361+
{
362+
"name": "3.14.3-trixie",
363+
"images": [{"os": "linux", "architecture": "amd64"}, {"os": "linux", "architecture": "arm64"}],
364+
},
365+
],
366+
},
367+
)
368+
responses.add(
369+
method="GET",
370+
url="https://nodejs.org/dist/latest/SHASUMS256.txt",
371+
body="deadbeef node-v25.8.1-linux-x64.tar.xz\n",
372+
)
373+
versions = [
374+
BuildVersion(
375+
key="python3.14-nodejs24",
376+
python="3.14",
377+
python_canonical="3.14.3",
378+
python_image="3.14.3-trixie",
379+
nodejs="24",
380+
nodejs_canonical="24.14.0",
381+
distro="trixie",
382+
platforms=["linux/amd64", "linux/arm64"],
383+
),
384+
]
385+
386+
with pytest.raises(
387+
ValueError,
388+
match=r"Computed latest tag 'python3\.14-nodejs25' was not part of the current build set",
389+
):
390+
latest_tag_key(versions)

0 commit comments

Comments
 (0)