Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ jobs:
with:
enable-cache: true
- name: Generate build matrix
id: set-matrix
run: |
FORCE=$(if git log --pretty=format:"%s" HEAD^..HEAD | grep -q '\[force\]'; then echo "--force"; else echo ""; fi)
uv run dpn $FORCE build-matrix --event ${{ github.event_name }}
id: set-matrix


deploy:
name: ${{ matrix.key }}
Expand All @@ -34,6 +35,7 @@ jobs:
strategy:
matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
steps:
# Setup
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
Expand All @@ -49,25 +51,47 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# Build
- name: Build image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: dockerfiles/${{ matrix.key }}.Dockerfile
load: true
tags: nikolaik/python-nodejs:${{ matrix.key }}

# Test
- name: Run smoke tests
run: |
docker run --rm nikolaik/python-nodejs:${{ matrix.key }} sh -c "node --version && npm --version && yarn --version && python --version && pip --version && pipenv --version && poetry --version && uv --version"

# Push image
- name: Push image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
id: build-and-push
with:
context: .
file: dockerfiles/${{ matrix.key }}.Dockerfile
platforms: ${{ join(matrix.platforms) }}
push: true
tags: nikolaik/python-nodejs:${{ matrix.key }}

# Store build context
- name: Add digest to build context
run: |
mkdir builds/
digest="${{ steps.build-and-push.outputs.digest }}"
echo '${{ toJSON(matrix) }}' | jq --arg digest "$digest" '. +={"digest": $digest}' >> "builds/${{ matrix.key }}.json"

- name: Upload build context
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-${{ matrix.key }}
path: builds/*
if-no-files-found: error
retention-days: 1

release:
name: Update versions.json and README.md
runs-on: ubuntu-latest
Expand All @@ -77,9 +101,17 @@ jobs:
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
enable-cache: true

- name: Download metadata for builds
uses: actions/download-artifact@v5
with:
path: builds
pattern: build-*
merge-multiple: true

- name: Update versions.json and README.md, then commit and push changes (if any)
run: |
uv run dpn --verbose release
uv run dpn --verbose release --builds-dir builds/
clean_checkout=$(git status --porcelain)
if [[ -n "${clean_checkout}" ]]; then
git config --global user.name "Nikolai Kristiansen" > /dev/null 2>&1
Expand Down
36 changes: 28 additions & 8 deletions src/docker_python_nodejs/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import logging
from pathlib import Path
from typing import Literal, cast

from .build_matrix import build_matrix
Expand All @@ -9,6 +10,7 @@
from .versions import (
decide_version_combinations,
find_new_or_updated,
load_build_contexts,
persist_versions,
supported_versions,
)
Expand All @@ -25,30 +27,30 @@ class CLIArgs(argparse.Namespace):

context: str # dockerfile command arg
event: str # build-matrix command arg
builds_dir: Path # release command arg


def run_dockerfile(args: CLIArgs) -> None:
render_dockerfile_with_context(args.context, args.dry_run)


def run_build_matrix(args: CLIArgs) -> None:
suported_python_versions, suported_nodejs_versions = supported_versions()
versions = decide_version_combinations(args.distros, suported_python_versions, suported_nodejs_versions)
supported_python_versions, supported_nodejs_versions = supported_versions()
versions = decide_version_combinations(args.distros, supported_python_versions, supported_nodejs_versions)
new_or_updated = find_new_or_updated(versions, args.force)
build_matrix(new_or_updated, args.event)


def run_release(args: CLIArgs) -> None:
suported_python_versions, suported_nodejs_versions = supported_versions()
versions = decide_version_combinations(args.distros, suported_python_versions, suported_nodejs_versions)
versions = load_build_contexts(args.builds_dir)
new_or_updated = find_new_or_updated(versions, args.force)

supported_python_versions, supported_nodejs_versions = supported_versions()
if not new_or_updated:
logger.info("No new or updated versions")
return

persist_versions(versions, args.dry_run)
update_dynamic_readme(versions, suported_python_versions, suported_nodejs_versions, args.dry_run)
update_dynamic_readme(versions, supported_python_versions, supported_nodejs_versions, args.dry_run)


def main(args: CLIArgs) -> None:
Expand Down Expand Up @@ -84,9 +86,11 @@ def parse_args() -> CLIArgs:
parser.add_argument("--verbose", action="store_true", help="Enable debug logging")

subparsers = parser.add_subparsers(dest="command", help="Sub-commands")

# Dockerfile command
parser_dockerfile = subparsers.add_parser("dockerfile", help="Render a dockerfile based on version config")
parser_dockerfile.add_argument("--context", default="", help="Dockerfile version config")

# Build matrix command
parser_build_matrix = subparsers.add_parser("build-matrix", help="Generate CI build matrix")
parser_build_matrix.add_argument(
Expand All @@ -95,6 +99,22 @@ def parse_args() -> CLIArgs:
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
help="GitHub Action event name (github.event_name)",
)

# Release command
subparsers.add_parser("release", help="Persist versions and make a release")
return cast(CLIArgs, parser.parse_args())
parser_release = subparsers.add_parser("release", help="Persist versions and make a release")
parser_release.add_argument(
"--builds-dir",
type=Path,
required=True,
help="Builds directory with build context JSON files",
)

cli_args = cast("CLIArgs", parser.parse_args())
if cli_args.command == "release":
if not cli_args.builds_dir.exists():
parser.error(f"Builds directory {cli_args.builds_dir.as_posix()} does not exist")

if not cli_args.builds_dir.is_dir():
parser.error(f"Builds directory {cli_args.builds_dir.as_posix()} is not a directory")

return cli_args
30 changes: 21 additions & 9 deletions src/docker_python_nodejs/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import re
from dataclasses import dataclass
from pathlib import Path

import requests
from bs4 import BeautifulSoup
Expand Down Expand Up @@ -39,20 +40,16 @@ class LanguageVersion:
canonical_version: str
key: str
distro: str
image: str | None = None


@dataclass
class NodeJsVersion(LanguageVersion):
pass
image: str | None = None


@dataclass
class PythonVersion(LanguageVersion):
canonical_version: str
key: str
image: str
distro: str


@dataclass
Expand All @@ -67,6 +64,7 @@ class BuildVersion:
nodejs_canonical: str
distro: str
platforms: list[str]
digest: str = ""


def _is_platform_image(platform: str, image: DockerImageDict) -> bool:
Expand Down Expand Up @@ -165,11 +163,11 @@ def fetch_supported_nodejs_versions() -> list[SupportedVersion]:


def supported_versions() -> tuple[list[SupportedVersion], list[SupportedVersion]]:
suported_python_versions = scrape_supported_python_versions()
suported_nodejs_versions = fetch_supported_nodejs_versions()
supported_versions = format_supported_versions(suported_python_versions, suported_nodejs_versions)
supported_python_versions = scrape_supported_python_versions()
supported_nodejs_versions = fetch_supported_nodejs_versions()
supported_versions = format_supported_versions(supported_python_versions, supported_nodejs_versions)
logger.debug(f"Found the following supported versions:\n{supported_versions}")
return suported_python_versions, suported_nodejs_versions
return supported_python_versions, supported_nodejs_versions


def _has_arch_files(files: list[str], distro: str) -> bool:
Expand Down Expand Up @@ -286,3 +284,17 @@ def find_new_or_updated(
new_or_updated.append(ver)

return new_or_updated


def load_build_contexts(builds_dir: Path) -> list[BuildVersion]:
"""Find JSON files with build contexts and return the corresponding BuildVersion list"""
logger.info(f"Loading builds metadata from {builds_dir.as_posix()}")
versions: list[BuildVersion] = []

for build_file in builds_dir.glob("*.json"):
with build_file.open() as fp:
build_data = json.load(fp)
version = BuildVersion(**build_data)
versions.append(version)

return versions
13 changes: 13 additions & 0 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
decide_nodejs_versions,
decide_version_combinations,
fetch_supported_nodejs_versions,
load_build_contexts,
scrape_supported_python_versions,
)

Expand Down Expand Up @@ -251,3 +252,15 @@ def test_decide_nodejs_versions(
versions = decide_nodejs_versions(distros, supported_node_versions)

assert len(supported_node_versions) * len(distros) == len(versions)


def test_load_build_contexts(build_version: BuildVersion, tmp_path: Path) -> None:
ver = build_version

file_path = tmp_path / f"{ver.key}.json"
content = json.dumps(dataclasses.asdict(ver))
file_path.write_text(content)

versions = load_build_contexts(tmp_path)
assert len(versions) == 1
assert versions[0].key == ver.key