Skip to content

Commit 0e96017

Browse files
committed
Publish latest from the canonical deploy manifest
1 parent e247afd commit 0e96017

7 files changed

Lines changed: 173 additions & 38 deletions

File tree

.github/workflows/build.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ jobs:
104104
refs+=("${IMAGE_NAME}:${{ matrix.key }}-arm64")
105105
fi
106106
docker manifest create "${IMAGE_NAME}:${{ matrix.key }}" "${refs[@]}"
107+
if [[ "${{ matrix.is_latest }}" == "true" ]]; then
108+
docker manifest create "${IMAGE_NAME}:latest" "${refs[@]}"
109+
fi
107110
108111
- name: Login to Docker Hub
109112
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
@@ -112,15 +115,22 @@ jobs:
112115
password: ${{ secrets.DOCKERHUB_TOKEN }}
113116

114117
- name: Push multi-arch manifest
115-
run: docker manifest push "${IMAGE_NAME}:${{ matrix.key }}"
118+
id: push-manifest
119+
run: |
120+
digest="$(docker manifest push "${IMAGE_NAME}:${{ matrix.key }}" | tail -n1)"
121+
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
122+
123+
- name: Push latest manifest
124+
if: matrix.is_latest
125+
run: docker manifest push "${IMAGE_NAME}:latest"
116126

117127
- name: Set up Docker Buildx
118128
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
119129

120130
- name: Add digest to build context
121131
run: |
122132
mkdir builds/
123-
digest="$(docker buildx imagetools inspect "${IMAGE_NAME}:${{ matrix.key }}" | awk '/^Digest:/ {print $2}')"
133+
digest="${{ steps.push-manifest.outputs.digest }}"
124134
echo '${{ toJSON(matrix) }}' | jq --arg digest "$digest" '. +={"digest": $digest}' >> "builds/${{ matrix.key }}.json"
125135
126136
- name: Upload build context

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/build_matrix.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
if TYPE_CHECKING:
1010
from .versions import BuildVersion
1111

12+
from .versions import latest_tag_key
13+
1214
CI_EVENT_SCHEDULED = "scheduled"
1315

1416
logger = logging.getLogger("dpn")
@@ -29,7 +31,12 @@ def _github_action_set_output(key: str, value: str) -> None:
2931

3032

3133
def _build_matrix_json(new_or_updated: list[BuildVersion]) -> str:
32-
return json.dumps({"include": [dataclasses.asdict(ver) for ver in new_or_updated]}) if new_or_updated else ""
34+
if not new_or_updated:
35+
return ""
36+
37+
latest_key = latest_tag_key(new_or_updated)
38+
include = [dataclasses.asdict(ver) | {"is_latest": ver.key == latest_key} for ver in new_or_updated]
39+
return json.dumps({"include": include})
3340

3441

3542
def _build_arch_matrix_json(new_or_updated: list[BuildVersion]) -> str:

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: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
import responses
1010

11-
from docker_python_nodejs.build_matrix import _build_arch_matrix_json
11+
from docker_python_nodejs.build_matrix import _build_arch_matrix_json, _build_matrix_json
1212
from docker_python_nodejs.dockerfiles import render_dockerfile_with_context
1313
from docker_python_nodejs.readme import update_dynamic_readme
1414
from docker_python_nodejs.settings import BASE_PATH, DOCKERFILES_PATH
@@ -19,6 +19,7 @@
1919
decide_version_combinations,
2020
fetch_supported_nodejs_versions,
2121
find_new_or_updated,
22+
latest_tag_key,
2223
load_build_contexts,
2324
scrape_supported_python_versions,
2425
)
@@ -327,3 +328,118 @@ def test_build_arch_matrix_json(build_version: BuildVersion) -> None:
327328
},
328329
],
329330
}
331+
332+
333+
def test_build_matrix_json_marks_latest() -> None:
334+
versions = [
335+
BuildVersion(
336+
key="python3.14-nodejs25",
337+
python="3.14",
338+
python_canonical="3.14.3",
339+
python_image="3.14.3-trixie",
340+
nodejs="25",
341+
nodejs_canonical="25.8.1",
342+
distro="trixie",
343+
platforms=["linux/amd64", "linux/arm64"],
344+
),
345+
BuildVersion(
346+
key="python3.14-nodejs24-bookworm",
347+
python="3.14",
348+
python_canonical="3.14.3",
349+
python_image="3.14.3-bookworm",
350+
nodejs="24",
351+
nodejs_canonical="24.14.0",
352+
distro="bookworm",
353+
platforms=["linux/amd64", "linux/arm64"],
354+
),
355+
]
356+
357+
with (
358+
mock.patch("docker_python_nodejs.build_matrix.latest_tag_key", return_value="python3.14-nodejs25"),
359+
):
360+
matrix = json.loads(_build_matrix_json(versions))
361+
362+
assert matrix == {
363+
"include": [
364+
{
365+
"key": "python3.14-nodejs25",
366+
"python": "3.14",
367+
"python_canonical": "3.14.3",
368+
"python_image": "3.14.3-trixie",
369+
"nodejs": "25",
370+
"nodejs_canonical": "25.8.1",
371+
"distro": "trixie",
372+
"platforms": ["linux/amd64", "linux/arm64"],
373+
"digest": "",
374+
"is_latest": True,
375+
},
376+
{
377+
"key": "python3.14-nodejs24-bookworm",
378+
"python": "3.14",
379+
"python_canonical": "3.14.3",
380+
"python_image": "3.14.3-bookworm",
381+
"nodejs": "24",
382+
"nodejs_canonical": "24.14.0",
383+
"distro": "bookworm",
384+
"platforms": ["linux/amd64", "linux/arm64"],
385+
"digest": "",
386+
"is_latest": False,
387+
},
388+
],
389+
}
390+
391+
392+
def test_latest_tag_key_matches_latest_sources() -> None:
393+
versions = [
394+
BuildVersion(
395+
key="python3.14-nodejs25",
396+
python="3.14",
397+
python_canonical="3.14.3",
398+
python_image="3.14.3-trixie",
399+
nodejs="25",
400+
nodejs_canonical="25.8.1",
401+
distro="trixie",
402+
platforms=["linux/amd64", "linux/arm64"],
403+
),
404+
BuildVersion(
405+
key="python3.14-nodejs24-bookworm",
406+
python="3.14",
407+
python_canonical="3.14.3",
408+
python_image="3.14.3-bookworm",
409+
nodejs="24",
410+
nodejs_canonical="24.14.0",
411+
distro="bookworm",
412+
platforms=["linux/amd64", "linux/arm64"],
413+
),
414+
]
415+
416+
with (
417+
mock.patch("docker_python_nodejs.versions._latest_python_minor", return_value="3.14"),
418+
mock.patch("docker_python_nodejs.versions.fetch_latest_nodejs_version", return_value="v25.8.1"),
419+
):
420+
assert latest_tag_key(versions) == "python3.14-nodejs25"
421+
422+
423+
def test_latest_tag_key_fails_if_canonical_build_is_missing() -> None:
424+
versions = [
425+
BuildVersion(
426+
key="python3.14-nodejs24",
427+
python="3.14",
428+
python_canonical="3.14.3",
429+
python_image="3.14.3-trixie",
430+
nodejs="24",
431+
nodejs_canonical="24.14.0",
432+
distro="trixie",
433+
platforms=["linux/amd64", "linux/arm64"],
434+
),
435+
]
436+
437+
with (
438+
mock.patch("docker_python_nodejs.versions._latest_python_minor", return_value="3.14"),
439+
mock.patch("docker_python_nodejs.versions.fetch_latest_nodejs_version", return_value="v25.8.1"),
440+
pytest.raises(
441+
ValueError,
442+
match=r"Computed latest tag 'python3\.14-nodejs25' was not part of the current build set",
443+
),
444+
):
445+
latest_tag_key(versions)

0 commit comments

Comments
 (0)