diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bb65d7c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,238 @@ +name: Build & Deploy +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + inputs: + promote: + description: 'Promote to production tags after build' + required: false + type: boolean + default: false + +jobs: + build: + name: Build Images + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - python_version: "3.9" + debian_version: "bullseye" + - python_version: "3.10" + debian_version: "bullseye" + - python_version: "3.11" + debian_version: "bookworm" + - python_version: "3.12" + debian_version: "bookworm" + - python_version: "3.13" + debian_version: "bookworm" + - python_version: "3.14" + debian_version: "bookworm" + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if image already exists + id: check + run: | + IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }}" + echo "Checking if ${IMAGE} already exists..." + + if docker manifest inspect "${IMAGE}" >/dev/null 2>&1; then + echo "✅ Image already exists, skipping build" + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "❌ Image does not exist, will build" + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + if: steps.check.outputs.exists == 'false' + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/python-container-builder + tags: | + type=sha,prefix=${{ matrix.python_version }}-,format=long + type=ref,event=pr,prefix=pr-,suffix=-${{ matrix.python_version }} + + - name: Build and push Docker images + if: steps.check.outputs.exists == 'false' + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: | + DEBIAN_VERSION=${{ matrix.debian_version }} + PYTHON_VERSION=${{ matrix.python_version }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=python-${{ matrix.python_version }} + cache-to: type=gha,mode=max,scope=python-${{ matrix.python_version }} + provenance: true + sbom: true + outputs: type=image,name=python-container-builder,annotation-index.org.opencontainers.image.description=Build your Python distroless containers with this + + promote: + name: Promote to Production + runs-on: ubuntu-latest + needs: [build, test, security-scan] + # Promote on: + # 1. Normal merge to main (not force push) + # 2. Manual workflow dispatch with promote flag enabled + # CRITICAL: Only runs if build, test, AND security-scan all succeed + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main' && !github.event.forced) || + (github.event_name == 'workflow_dispatch' && inputs.promote == true) + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Promote commit SHA to version tag + run: | + # Get the full commit SHA + COMMIT_SHA="${{ github.sha }}" + + # Source image with commit SHA + SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${COMMIT_SHA}" + + # Destination tags + VERSION_TAG="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}" + + echo "Promoting ${SOURCE_IMAGE} to ${VERSION_TAG}" + + # Re-tag the existing image (no rebuild) + docker buildx imagetools create \ + "${SOURCE_IMAGE}" \ + --tag "${VERSION_TAG}" + + echo "✅ Successfully promoted ${{ matrix.python_version }} to production" + + - name: Promote latest tag + if: matrix.python_version == '3.14' + run: | + COMMIT_SHA="${{ github.sha }}" + SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${COMMIT_SHA}" + LATEST_TAG="ghcr.io/${{ github.repository_owner }}/python-container-builder:latest" + + echo "Promoting ${SOURCE_IMAGE} to ${LATEST_TAG}" + + docker buildx imagetools create \ + "${SOURCE_IMAGE}" \ + --tag "${LATEST_TAG}" + + echo "✅ Successfully promoted latest tag" + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + security-events: write + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ghcr.io/${{ github.repository_owner }}/python-container-builder:3.14-${{ github.sha }} + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + - name: Print Trivy results summary + uses: aquasecurity/trivy-action@master + if: always() + with: + image-ref: ghcr.io/${{ github.repository_owner }}/python-container-builder:3.14-${{ github.sha }} + format: 'table' + severity: 'CRITICAL,HIGH' + + test: + name: Test Images + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - name: Test Python version + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} python --version + + - name: Test uv is installed + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} uv --version + + - name: Test poetry is installed + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} poetry --version + + - name: Test pipenv is installed + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} pipenv --version + + - name: Test pdm is installed + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} pdm --version + + - name: Test pip is installed in venv + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} /.venv/bin/pip --version + + - name: Test venv is created + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c 'test -d /.venv && echo "venv exists"' + + - name: Test package installation with uv + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c 'uv pip install requests && /.venv/bin/python -c "import requests; print(f\"requests {requests.__version__} imported successfully\")"' + + - name: Test package installation with pip + run: | + docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c '/.venv/bin/pip install click && /.venv/bin/python -c "import click; print(f\"click {click.__version__} imported successfully\")"' diff --git a/.github/workflows/check-versions.yml b/.github/workflows/check-versions.yml new file mode 100644 index 0000000..6645cb9 --- /dev/null +++ b/.github/workflows/check-versions.yml @@ -0,0 +1,218 @@ +name: Check Python Versions + +on: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC + workflow_dispatch: + +jobs: + check-versions: + name: Check for Python version updates + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check current Python versions + id: check + run: | + echo "Checking Python versions from official images..." + + # Initialize arrays + declare -A current_versions + declare -A new_versions + + # Current minor versions we support + MINOR_VERSIONS=("3.9" "3.10" "3.11" "3.12" "3.13" "3.14") + + # Check each version + for minor in "${MINOR_VERSIONS[@]}"; do + # Determine debian version + if [[ "$minor" == "3.9" || "$minor" == "3.10" ]]; then + debian="bullseye" + else + debian="bookworm" + fi + + echo "Checking Python ${minor} (${debian})..." + + # Get actual version from official image + version=$(docker run --rm python:${minor}-slim-${debian} python --version 2>&1 | grep -oP 'Python \K[\d.]+' || echo "") + + if [ -n "$version" ]; then + current_versions[$minor]=$version + echo " Found: $version" + else + echo " ERROR: Could not detect version for ${minor}" + current_versions[$minor]="unknown" + fi + done + + # Check for Python 3.15 (future version) + echo "Checking for Python 3.15..." + if docker pull python:3.15-slim-bookworm 2>/dev/null; then + version=$(docker run --rm python:3.15-slim-bookworm python --version 2>&1 | grep -oP 'Python \K[\d.]+' || echo "") + if [ -n "$version" ]; then + echo " NEW VERSION FOUND: Python 3.15 ($version)" + current_versions["3.15"]=$version + echo "new_minor_version=3.15" >> $GITHUB_OUTPUT + fi + else + echo " Python 3.15 not yet available" + fi + + # Save versions to file for next step + for minor in "${!current_versions[@]}"; do + echo "${minor}=${current_versions[$minor]}" >> /tmp/versions.txt + done + + # Output for debugging + cat /tmp/versions.txt + + - name: Update README with versions + id: update-readme + run: | + echo "Updating README with current versions..." + + # Read versions + declare -A versions + while IFS='=' read -r minor version; do + versions[$minor]=$version + done < /tmp/versions.txt + + # Create backup + cp README.md README.md.backup + + # Update the table in README + # This updates the "Python Source" column with actual versions + for minor in "${!versions[@]}"; do + version="${versions[$minor]}" + if [[ "$minor" == "3.9" || "$minor" == "3.10" ]]; then + debian="bullseye" + else + debian="bookworm" + fi + + # Check if this is the latest Python version (has ":latest" tag) + # Currently 3.14, but will be 3.15, 3.16, etc. in the future + if grep -q "| ${minor} | \`:${minor}\` or \`:latest\` |" README.md; then + # Handle version with :latest tag + # First try to update existing version + sed -i "s/| ${minor} | \`:${minor}\` or \`:latest\` | \`python:${minor}-slim-${debian}\` ([^)]*) |/| ${minor} | \`:${minor}\` or \`:latest\` | \`python:${minor}-slim-${debian}\` (${version}) |/g" README.md + # If no existing version, add it + sed -i "s/| ${minor} | \`:${minor}\` or \`:latest\` | \`python:${minor}-slim-${debian}\` |/| ${minor} | \`:${minor}\` or \`:latest\` | \`python:${minor}-slim-${debian}\` (${version}) |/g" README.md + else + # Handle regular version without :latest tag + # First try to update existing version + sed -i "s/| ${minor} | \`:${minor}\` | \`python:${minor}-slim-${debian}\` ([^)]*) |/| ${minor} | \`:${minor}\` | \`python:${minor}-slim-${debian}\` (${version}) |/g" README.md + # If no existing version, add it + sed -i "s/| ${minor} | \`:${minor}\` | \`python:${minor}-slim-${debian}\` |/| ${minor} | \`:${minor}\` | \`python:${minor}-slim-${debian}\` (${version}) |/g" README.md + fi + done + + # Check if there were changes + if ! diff -q README.md README.md.backup > /dev/null 2>&1; then + echo "changes_detected=true" >> $GITHUB_OUTPUT + echo "Changes detected in README" + + # Show diff + echo "Diff:" + diff README.md.backup README.md || true + else + echo "changes_detected=false" >> $GITHUB_OUTPUT + echo "No changes detected" + fi + + rm README.md.backup + + - name: Update workflow matrix if new minor version found + if: steps.check.outputs.new_minor_version + run: | + NEW_VERSION="${{ steps.check.outputs.new_minor_version }}" + echo "Adding Python ${NEW_VERSION} to workflow matrix..." + + # Read the version from temp file + VERSION=$(grep "^${NEW_VERSION}=" /tmp/versions.txt | cut -d= -f2) + + # Add to build.yml matrix + WORKFLOW_FILE=".github/workflows/build.yml" + + # Insert new version in matrix (before the last item) + # Find the line with "3.14" and add the new version after it + sed -i "/python_version: \"3.14\"/a\\ - python_version: \"${NEW_VERSION}\"\n debian_version: \"bookworm\"" $WORKFLOW_FILE + + # Update the test matrix + sed -i "s/python_version: \[\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"\]/python_version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\", \"${NEW_VERSION}\"]/" $WORKFLOW_FILE + + # Update latest tag logic (3.14 -> new version) + sed -i "s/matrix.python_version == '3.14'/matrix.python_version == '${NEW_VERSION}'/" $WORKFLOW_FILE + + echo "Workflow updated to include Python ${NEW_VERSION}" + + # Update README table to add new minor version + echo "Adding Python ${NEW_VERSION} to README table..." + + # Find the current "latest" version line (currently 3.14) + CURRENT_LATEST=$(grep -E "\\| [0-9.]+ \\| \`:[0-9.]+\` or \`:latest\`" README.md | sed -E 's/.*\| ([0-9.]+) \| .*/\1/' | head -1) + + if [ -n "$CURRENT_LATEST" ]; then + echo "Current latest version: ${CURRENT_LATEST}" + + # Remove ":latest" tag from current latest version + sed -i "s/| ${CURRENT_LATEST} | \`:${CURRENT_LATEST}\` or \`:latest\` |/| ${CURRENT_LATEST} | \`:${CURRENT_LATEST}\` |/g" README.md + + # Get the distroless version for the current latest (to use as template) + DISTROLESS=$(grep "| ${CURRENT_LATEST} |" README.md | sed -E 's/.*gcr.io\/distroless\/(python3-debian[0-9]+).*/\1/' | head -1) + DISTROLESS="gcr.io/distroless/${DISTROLESS}" + + # Add new row after current latest with :latest tag + # The new version will use bookworm (Debian 12) as it's a recent Python version + NEW_ROW="| ${NEW_VERSION} | \`:${NEW_VERSION}\` or \`:latest\` | \`python:${NEW_VERSION}-slim-bookworm\` (${VERSION}) | \`debian:bookworm-slim\` | \`${DISTROLESS}\` |" + + # Insert new row after the previous latest (GNU sed syntax) + sed -i "/| ${CURRENT_LATEST} |/a\\${NEW_ROW}" README.md + + echo "Added new row for Python ${NEW_VERSION} with :latest tag" + echo "Removed :latest tag from Python ${CURRENT_LATEST}" + else + echo "Warning: Could not find current latest version in README" + fi + + - name: Create Pull Request + if: steps.update-readme.outputs.changes_detected == 'true' || steps.check.outputs.new_minor_version + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + chore: Update Python versions + + - Updated README with current Python versions + ${{ steps.check.outputs.new_minor_version && format('- Added Python {0} to build matrix', steps.check.outputs.new_minor_version) || '' }} + branch: update-python-versions + delete-branch: true + title: 'chore: Update Python versions' + body: | + ## Python Version Update + + This PR updates the Python version information: + + ### Changes + - ✅ Updated README with current Python versions from official images + ${{ steps.check.outputs.new_minor_version && format('- ✅ **NEW**: Added Python {0} to build matrix', steps.check.outputs.new_minor_version) || '' }} + + ### Version Details + ``` + $(cat /tmp/versions.txt) + ``` + + ### What happens next? + 1. Review the changes + 2. Merge this PR to trigger a new build with updated versions + 3. Images will be built and pushed automatically + + --- + 🤖 Auto-generated by weekly version check workflow + labels: dependencies,automated diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index b31a38d..0000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Build & Deploy Nightly -on: - schedule: - - cron: '0 6 * * *' - workflow_dispatch: - -jobs: - nightly: - name: Build & Deploy nightly - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - strategy: - matrix: - include: - - python_version: "3.9" - debian_version: "bullseye" - - python_version: "3.10" - debian_version: "bullseye" - - python_version: "3.11" - debian_version: "bookworm" - - python_version: "3.12" - debian_version: "bookworm" - - python_version: "3.13" - debian_version: "bookworm" - - python_version: "3.14" - debian_version: "bookworm" - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push Docker images - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - DEBIAN_VERSION=${{ matrix.debian_version }} - PYTHON_VERSION=${{ matrix.python_version }} - tags: | - ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} - ${{ matrix.python_version == '3.14' && format('ghcr.io/{0}/python-container-builder:latest', github.repository_owner) || '' }} - provenance: false - outputs: type=image,name=python-container-builder,annotation-index.org.opencontainers.image.description=build your Python distroless containers with this - - security-scan: - name: Security Scan - runs-on: ubuntu-latest - needs: nightly - permissions: - contents: read - security-events: write - steps: - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: ghcr.io/${{ github.repository_owner }}/python-container-builder:latest - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH' - - - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 - if: always() - with: - sarif_file: 'trivy-results.sarif' - - - name: Print Trivy results summary - uses: aquasecurity/trivy-action@master - if: always() - with: - image-ref: ghcr.io/${{ github.repository_owner }}/python-container-builder:latest - format: 'table' - severity: 'CRITICAL,HIGH' - - test: - name: Test Images - runs-on: ubuntu-latest - needs: nightly - permissions: - contents: read - strategy: - matrix: - python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - steps: - - name: Test Python version - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} python --version - - - name: Test uv is installed - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} uv --version - - - name: Test poetry is installed - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} poetry --version - - - name: Test pipenv is installed - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} pipenv --version - - - name: Test pdm is installed - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} pdm --version - - - name: Test venv is created - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'test -d /.venv && echo "venv exists"' - - - name: Test package installation with uv - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'uv pip install requests && python -c "import requests; print(f\"requests {requests.__version__} imported successfully\")"' - - - name: Test package installation with pip - run: | - docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'pip install click && python -c "import click; print(f\"click {click.__version__} imported successfully\")"' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3718917..e040635 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,5 +38,6 @@ RUN uv venv ENV VIRTUAL_ENV=/.venv ENV PATH="/bin:$VIRTUAL_ENV/bin:$PATH" -# Install modern Python package managers for completeness -RUN uv pip install --no-cache poetry pipenv pdm \ No newline at end of file +# Install pip in the virtualenv for standard pip usage +# Then install other modern Python package managers +RUN uv pip install --no-cache pip poetry pipenv pdm \ No newline at end of file diff --git a/README.md b/README.md index cc196d7..096fe54 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Python Container Builder -[![Build & Deploy Nightly](https://github.com/jski/python-container-builder/actions/workflows/nightly.yml/badge.svg?branch=main)](https://github.com/jski/python-container-builder/actions/workflows/nightly.yml) +[![Build & Deploy](https://github.com/jski/python-container-builder/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jski/python-container-builder/actions/workflows/build.yml) [![Python Versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/jski/python-container-builder) [![Platform Support](https://img.shields.io/badge/platform-linux%2Famd64%20%7C%20linux%2Farm64-blue)](https://github.com/jski/python-container-builder) @@ -95,13 +95,13 @@ Use the appropriate image tag and distroless runtime for your Python version: | Build Image | Distroless Runtime | Use Case | |-------------|-------------------|----------| -| `:3.14` or `:latest` | `python3-debian12` | Latest features | +| `:3.14` | `python3-debian12` | Latest features | | `:3.12` | `python3-debian12` | Recommended for most projects | | `:3.11` | `python3-debian12` | Long-term stable | | `:3.10` | `python3-debian11` | Older projects | | `:3.9` | `python3-debian11` | Legacy compatibility | -> **Note**: When using Python 3.9 or 3.10, use `gcr.io/distroless/python3-debian11` as your runtime. For Python 3.11+, use `gcr.io/distroless/python3-debian12`. +> **Note**: When using Python 3.9 or 3.10, use `gcr.io/distroless/python3-debian11` as your runtime. For Python 3.11+, use `gcr.io/distroless/python3-debian12`. `latest` is an alias for the tag `:3.14`. ### How It Works 1. Choose your Python version and declare the corresponding base image as the top FROM line in your Dockerfile (e.g., `:3.12`, `:3.14`, or `:latest`).