Skip to content

Commit 935727e

Browse files
committed
Merge branch 'upstream-PR/01-fix-latest-tag-missing-for-arm64' into upstream-PR/all-PR-combined
2 parents 3d39cce + c0ee392 commit 935727e

7 files changed

Lines changed: 142 additions & 37 deletions

File tree

.github/workflows/build.yaml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,37 @@ jobs:
107107
if-no-files-found: error
108108
retention-days: 1
109109

110+
tag-latest:
111+
name: Point latest at canonical build
112+
runs-on: ubuntu-latest
113+
needs: [deploy]
114+
steps:
115+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
116+
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7
117+
with:
118+
enable-cache: true
119+
- name: Download metadata for builds
120+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
121+
with:
122+
path: builds
123+
pattern: build-*
124+
merge-multiple: true
125+
- name: Login to Docker Hub
126+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
127+
with:
128+
username: ${{ secrets.DOCKERHUB_USERNAME }}
129+
password: ${{ secrets.DOCKERHUB_TOKEN }}
130+
- name: Install regctl
131+
uses: regclient/actions/regctl-installer@main
132+
- name: Point latest to canonical image
133+
run: |
134+
latest_tag="$(uv run dpn latest-key --builds-dir builds/)"
135+
regctl image copy "${IMAGE_NAME}:${latest_tag}" "${IMAGE_NAME}:latest"
136+
110137
release:
111138
name: Update versions.json and README.md
112139
runs-on: ubuntu-latest
113-
needs: [deploy]
140+
needs: [deploy, tag-latest]
114141
steps:
115142
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
116143
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # 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: 24 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,18 @@ def decide_version_combinations(
256268
return version_combinations(nodejs_versions, python_versions)
257269

258270

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

tests/test_all.py

Lines changed: 57 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,59 @@ 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+
def test_latest_tag_key_matches_latest_sources() -> None:
296+
versions = [
297+
BuildVersion(
298+
key="python3.14-nodejs25",
299+
python="3.14",
300+
python_canonical="3.14.3",
301+
python_image="3.14.3-trixie",
302+
nodejs="25",
303+
nodejs_canonical="25.8.1",
304+
distro="trixie",
305+
platforms=["linux/amd64", "linux/arm64"],
306+
),
307+
BuildVersion(
308+
key="python3.14-nodejs24-bookworm",
309+
python="3.14",
310+
python_canonical="3.14.3",
311+
python_image="3.14.3-bookworm",
312+
nodejs="24",
313+
nodejs_canonical="24.14.0",
314+
distro="bookworm",
315+
platforms=["linux/amd64", "linux/arm64"],
316+
),
317+
]
318+
319+
with (
320+
mock.patch("docker_python_nodejs.versions._latest_python_minor", return_value="3.14"),
321+
mock.patch("docker_python_nodejs.versions.fetch_latest_nodejs_version", return_value="v25.8.1"),
322+
):
323+
assert latest_tag_key(versions) == "python3.14-nodejs25"
324+
325+
326+
def test_latest_tag_key_fails_if_canonical_build_is_missing() -> None:
327+
versions = [
328+
BuildVersion(
329+
key="python3.14-nodejs24",
330+
python="3.14",
331+
python_canonical="3.14.3",
332+
python_image="3.14.3-trixie",
333+
nodejs="24",
334+
nodejs_canonical="24.14.0",
335+
distro="trixie",
336+
platforms=["linux/amd64", "linux/arm64"],
337+
),
338+
]
339+
340+
with (
341+
mock.patch("docker_python_nodejs.versions._latest_python_minor", return_value="3.14"),
342+
mock.patch("docker_python_nodejs.versions.fetch_latest_nodejs_version", return_value="v25.8.1"),
343+
pytest.raises(
344+
ValueError,
345+
match=r"Computed latest tag 'python3\.14-nodejs25' was not part of the current build set",
346+
),
347+
):
348+
latest_tag_key(versions)

0 commit comments

Comments
 (0)