diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a709ff9..651a269 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -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 }} @@ -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: @@ -49,6 +51,8 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Build - name: Build image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 with: @@ -56,11 +60,16 @@ jobs: 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 @@ -68,6 +77,21 @@ jobs: 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 @@ -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 diff --git a/src/docker_python_nodejs/cli.py b/src/docker_python_nodejs/cli.py index 295c202..5e02af7 100644 --- a/src/docker_python_nodejs/cli.py +++ b/src/docker_python_nodejs/cli.py @@ -1,5 +1,6 @@ import argparse import logging +from pathlib import Path from typing import Literal, cast from .build_matrix import build_matrix @@ -9,6 +10,7 @@ from .versions import ( decide_version_combinations, find_new_or_updated, + load_build_contexts, persist_versions, supported_versions, ) @@ -25,6 +27,7 @@ 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: @@ -32,23 +35,22 @@ def run_dockerfile(args: CLIArgs) -> None: 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: @@ -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( @@ -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 diff --git a/src/docker_python_nodejs/versions.py b/src/docker_python_nodejs/versions.py index a6e9796..ce4a93d 100644 --- a/src/docker_python_nodejs/versions.py +++ b/src/docker_python_nodejs/versions.py @@ -4,6 +4,7 @@ import logging import re from dataclasses import dataclass +from pathlib import Path import requests from bs4 import BeautifulSoup @@ -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 @@ -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: @@ -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: @@ -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 diff --git a/tests/test_all.py b/tests/test_all.py index 3d02e7e..2701531 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -15,6 +15,7 @@ decide_nodejs_versions, decide_version_combinations, fetch_supported_nodejs_versions, + load_build_contexts, scrape_supported_python_versions, ) @@ -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