From c8f699bc42396fa45ba94b25671b3617be707983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Sun, 16 Nov 2025 21:57:13 +0100 Subject: [PATCH 1/4] feat(ci): add versioned build, signing and release workflows - Enhance Docker build workflow with multi-repo tags, PR security gate, metadata, and cosign signing for main and version tags - Introduce release workflow gated by authorized actor and main HEAD tag for automated GitHub Releases - Add VERSION file and version bumping/generation scripts for SemVer management and Docker tags - Expose application version in Dockerfile, runtime (main.py), docker-compose, and document versioning/Docker images in README --- .github/workflows/build-and-publish.yaml | 157 ++++++++++++++++++----- .github/workflows/release.yml | 95 ++++++++++++++ Dockerfile | 14 ++ README.md | 45 ++++++- VERSION | 1 + docker-compose.yaml | 4 + main.py | 17 ++- scripts/bump-version.sh | 93 ++++++++++++++ scripts/generate-tags.sh | 62 +++++++++ 9 files changed, 448 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 VERSION create mode 100755 scripts/bump-version.sh create mode 100755 scripts/generate-tags.sh diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 2757052..6231c8b 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -1,19 +1,34 @@ -name: Build and Push Docker image +# This workflow builds and pushes a Docker image to GHCR and Docker Hub +# under multiple repository aliases (auto_docker_proxy and traefik_network_connector). +# +# Triggers: +# 1. Push to 'main' branch (tags as 'latest') +# 2. Push of tags 'v*.*.*' (tags as SemVer) +# 3. Pull Requests (builds 'pr-XXX' tag, only if author is trusted or PR is approved) +# +# Features: +# - Multi-platform build +# - Multi-registry push (GHCR & Docker Hub) +# - GitHub Actions cache +# - Cosign OIDC signing for main/tag pushes +name: Build and Push Docker (Multi-Repo Alias) on: push: branches: - main + tags: + - 'v*.*.*' # Trigger on version tags like v1.0.0 pull_request: types: [opened, synchronize, reopened] jobs: - build-and-push: + build-push-sign: runs-on: ubuntu-latest permissions: contents: read packages: write - id-token: write # For cosign + id-token: write # Required for OIDC signing steps: - name: Checkout code @@ -35,19 +50,23 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # --- PR Security Checks (Start) --- - name: Determine if PR author is trusted if: ${{ github.event_name == 'pull_request' }} id: check-author + shell: bash run: | + # Define a list of trusted authors. Consider using a GitHub Team for larger projects. trusted_authors=("obeone") - if [[ " ${trusted_authors[@]} " =~ " ${GITHUB_ACTOR} " ]]; then - echo "Author is trusted" - echo "::set-output name=trusted::true" - else - echo "Author is not trusted" - echo "::set-output name=trusted::false" - fi - + trusted="false" + for author in "${trusted_authors[@]}"; do + if [[ "$author" == "$GITHUB_ACTOR" ]]; then + trusted="true" + break + fi + done + echo "Author ($GITHUB_ACTOR) trusted: $trusted" + echo "trusted=$trusted" >> "$GITHUB_OUTPUT" - name: Check for admin approval if: ${{ github.event_name == 'pull_request' }} @@ -56,51 +75,119 @@ jobs: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} + # Check for approval by an organization OWNER or MEMBER. + APPROVALS=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews \ + --jq '.[] | select(.state=="APPROVED" and (.author_association=="OWNER" or .author_association=="MEMBER")) | .user.login') - # Fetch the reviews for the pull request - APPROVALS=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews --jq '.[] | select(.state=="APPROVED" and .author_association=="ADMIN") | .user.login') - - # Check if any of the reviews are from an admin if [[ -n "$APPROVALS" ]]; then - echo "PR is approved by an admin" - echo "::set-output name=approved::true" + echo "PR is approved by an organization member: $APPROVALS" + echo "approved=true" >> "$GITHUB_OUTPUT" else - echo "PR is not approved by an admin" - echo "::set-output name=approved::false" + echo "PR is not approved by an organization member." + echo "approved=false" >> "$GITHUB_OUTPUT" fi + # --- PR Security Checks (End) --- - - name: Build and push to GHCR and Docker Hub - if: ${{ github.event_name == 'push' || (steps.check-author.outputs.trusted == 'true' || steps.check-approval.outputs.approved == 'true') }} - uses: docker/build-push-action@v5 + - name: Security Gatekeeper + id: gatekeeper + shell: bash + run: | + # This step consolidates the logic to determine if the build should proceed. + # The build will run if any of the following conditions are met: + # 1. The event is a 'push' (to 'main' branch or a tag). + # 2. The event is a 'pull_request' AND the author is explicitly trusted. + # 3. The event is a 'pull_request' AND the PR has been approved by an administrator. + + if [[ "${{ github.event_name }}" == "push" ]]; then + echo "Event is 'push', proceeding with build." + echo "run_build=true" >> "$GITHUB_OUTPUT" + elif [[ "${{ steps.check-author.outputs.trusted }}" == "true" ]]; then + echo "PR author is trusted, proceeding with build." + echo "run_build=true" >> "$GITHUB_OUTPUT" + elif [[ "${{ steps.check-approval.outputs.approved }}" == "true" ]]; then + echo "PR is approved by an admin, proceeding with build." + echo "run_build=true" >> "$GITHUB_OUTPUT" + else + echo "PR event does not meet security criteria. Build will be skipped." + echo "run_build=false" >> "$GITHUB_OUTPUT" + fi + + - name: Docker metadata (multi-repo) + id: meta + uses: docker/metadata-action@v5 + with: + # Define all four image names. The generated tags will be applied to each of them. + images: | + ghcr.io/obeone/auto_docker_proxy + docker.io/obeoneorg/auto_docker_proxy + ghcr.io/obeone/traefik_network_connector + docker.io/obeoneorg/traefik_network_connector + tags: | + # For pushes to the 'main' branch, tag the image as 'latest'. + type=ref,event=branch,enable=${{ github.ref_name == 'main' }},prefix=,suffix=latest + # For 'v*.*.*' tags, generate SemVer tags (e.g., v1.2.3, v1.2, v1). + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # For 'pull_request' events, tag the image as 'pr-XXX' (where XXX is the PR number). + type=ref,event=pr + + - name: Build and push (multi-repo) + if: steps.gatekeeper.outputs.run_build == 'true' id: build-and-push + uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile push: true - cache-from: type=gha - cache-to: type=gha,mode=max - tags: | - ghcr.io/obeone/auto_docker_proxy:${{ github.event_name == 'push' && 'latest' || 'pr-${{ github.head_ref }}' }} - docker.io/obeoneorg/auto_docker_proxy:${{ github.event_name == 'push' && 'latest' || 'pr-${{ github.head_ref }}' }} - ghcr.io/obeone/traefik_network_connector:${{ github.event_name == 'push' && 'latest' || 'pr-${{ github.head_ref }}' }} - docker.io/obeoneorg/traefik_network_connector:${{ github.event_name == 'push' && 'latest' || 'pr-${{ github.head_ref }}' }} platforms: | linux/amd64 linux/arm64 linux/i386 linux/armhf linux/armel + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + # Pass the clean version (e.g., 1.0.0) extracted from metadata to the Dockerfile. + VERSION=${{ steps.meta.outputs.version }} - name: Set up cosign + if: steps.gatekeeper.outputs.run_build == 'true' uses: sigstore/cosign-installer@v3 - name: Sign the container image with cosign - if: ${{ github.event_name == 'push' || (steps.check-author.outputs.trusted == 'true' || steps.check-approval.outputs.approved == 'true') }} - run: | - cosign sign --yes ghcr.io/obeone/auto_docker_proxy@${DIGEST} - cosign sign --yes docker.io/obeoneorg/auto_docker_proxy@${DIGEST} - cosign sign --yes ghcr.io/obeone/traefik_network_connector@${DIGEST} - cosign sign --yes docker.io/obeoneorg/traefik_network_connector@${DIGEST} + # This step only signs official images, which are those built from 'main' branch pushes or tag pushes. + if: >- + ${{ + steps.gatekeeper.outputs.run_build == 'true' && + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + }} env: COSIGN_EXPERIMENTAL: true + # Retrieve the image digest from the previous build step. DIGEST: ${{ steps.build-and-push.outputs.digest }} + shell: bash + run: | + if [ -z "${DIGEST}" ]; then + echo "Digest is empty, aborting image signing." + exit 1 + fi + + echo "Signing digest: ${DIGEST}" + + # List all four base image names that need to be signed. + IMAGES=( + "ghcr.io/obeone/auto_docker_proxy" + "docker.io/obeoneorg/auto_docker_proxy" + "ghcr.io/obeone/traefik_network_connector" + "docker.io/obeoneorg/traefik_network_connector" + ) + + for image in "${IMAGES[@]}"; do + echo "Signing ${image}@${DIGEST}" + cosign sign --yes "${image}@${DIGEST}" + done \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dabe4d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +# This GitHub Actions workflow automates the release process for the project. +# It is triggered by pushing a Git tag that adheres to the semantic versioning +# pattern "v*.*.*" (e.g., v1.0.0, v2.1.5). + +# This workflow incorporates security measures: +# 1. The 'release' job will only execute if the triggering actor is on an allow-list. +# 2. The release creation step will only proceed if the pushed tag points to the HEAD of the 'main' branch. + +name: Create Release + +on: + push: + tags: + - 'v*.*.*' # Trigger on semantic version tags (e.g., v1.2.3) + +jobs: + release: + # This job runs on the latest available version of Ubuntu. + runs-on: ubuntu-latest + + # Define the necessary permissions for this job. + # 'contents: write' is crucial for creating and publishing GitHub Releases. + permissions: + contents: write + + steps: + # Step 1: Check out the repository's code. + # `fetch-depth: 0` is required to retrieve the complete Git history, + # which is essential for accurate changelog generation and comparing commit SHAs. + - name: Checkout repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Step 2: Fetch the 'main' branch. + # This action is critical for obtaining the latest commit SHA of the 'origin/main' + # branch, which is then used for comparison against the pushed tag's SHA. + - name: Fetch main branch + run: git fetch origin main + + # Step 3: Validate release conditions: Authorized Actor and Tag on Main. + # This step performs essential validations to ensure that the release + # process only proceeds under specific, secure conditions. + - name: Check release conditions (Tag on main & Authorized actor) + id: check_conditions + run: | + TAG_SHA=$(git rev-parse ${{ github.ref }}) + MAIN_SHA=$(git rev-parse origin/main) + ACTOR=${{ github.actor }} + + echo "Tag SHA: $TAG_SHA" + echo "Main SHA: $MAIN_SHA" + echo "Actor: $ACTOR" + + # --- AUTHORIZATION CHECK --- + # Ensures that only an explicitly authorized actor can trigger a release. + if [[ "$ACTOR" != "obeone" ]]; then + echo "::error::Actor '$ACTOR' is not authorized to create releases. Skipping release." + echo "authorized=false" >> $GITHUB_OUTPUT + exit 1 # Fail the job if the actor is unauthorized + fi + + # --- BRANCH CHECK --- + # Verifies that the pushed tag points directly to the HEAD of the 'main' branch. + if [ "$TAG_SHA" != "$MAIN_SHA" ]; then + echo "::error::Tag ${{ github.ref_name }} does not point to the HEAD of the 'main' branch. Skipping release." + echo "on_main=false" >> $GITHUB_OUTPUT + exit 1 # Fail the job if the tag is not on main + fi + + echo "All conditions met. Proceeding with the release process." + echo "authorized=true" >> $GITHUB_OUTPUT" + echo "on_main=true" >> $GITHUB_OUTPUT" + + # Step 4: Create a GitHub Release and Generate Changelog. + # This step is conditionally executed only if the 'check_conditions' step + # successfully validated both the authorized actor and the tag's branch. + # The 'ncipollo/release-action' is utilized as a robust and actively maintained + # solution for creating releases, replacing the deprecated 'actions/create-release'. + - name: Create GitHub Release and Generate Changelog + if: steps.check_conditions.outputs.authorized == 'true' && steps.check_conditions.outputs.on_main == 'true' + uses: ncipollo/release-action@v1 + with: + # The action automatically infers the tag name from the Git reference (github.ref_name). + name: Release ${{ github.ref_name }} + + # Enables the automatic generation of release notes, leveraging the action's built-in capabilities. + generateReleaseNotes: true + + draft: false # Publishes the release immediately, rather than as a draft. + prerelease: false # Designates the release as a full, stable release. + + # The GITHUB_TOKEN is automatically provided by GitHub Actions, + # granting the necessary permissions for creating the release. + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 44f73e3..5a2d623 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,25 @@ +# Use the official Python image as a parent image FROM python:3.12-slim +# Set the working directory in the container WORKDIR /usr/src/app +# Set an argument for the version +ARG VERSION=unknown + +# Copy the requirements file and install dependencies COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt +# Copy the rest of the application's code COPY . . +# Set the entrypoint for the container ENTRYPOINT [ "/usr/local/bin/python", "/usr/src/app/main.py" ] +# --- Metadata --- +# Set the maintainer label LABEL maintainer="obeone " LABEL description="Automatically connect traefik to docker's services networks which is needed" LABEL project="traefik_network_connector" @@ -17,3 +27,7 @@ LABEL url="https://github.com/obeone/traefik_network_connector" LABEL vcs-url="https://github.com/obeone/traefik_network_connector" LABEL keywords="docker, docker compose, traefik, reverse proxy, network automation, dynamic configuration, TLS support, container management" LABEL org.opencontainers.image.source https://github.com/obeone/traefik_network_connector +LABEL org.opencontainers.image.version=$VERSION + +# Set environment variable for the application version +ENV APP_VERSION=$VERSION diff --git a/README.md b/README.md index f5d6a18..9097714 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is useful if you have, for example, one traefik proxy which handle incoming - [Table of Contents](#table-of-contents) - [Features](#features) - [Requirements](#requirements) - - [Usage](#usage) + - [Quick start](#quick-start) - [Configuration](#configuration) - [Configuration File](#configuration-file) - [Command Line Arguments](#command-line-arguments) @@ -23,6 +23,8 @@ This is useful if you have, for example, one traefik proxy which handle incoming - [Systemd Service Setup](#systemd-service-setup) - [Docker socket proxy setup](#docker-socket-proxy-setup) - [How It Works](#how-it-works) + - [Versioning & Release process](#versioning--release-process) + - [Docker images](#docker-images) - [TLS Configuration](#tls-configuration) - [FAQs / Troubleshooting](#faqs--troubleshooting) - [Contributing](#contributing) @@ -45,7 +47,7 @@ If you want to run it outside containers, make sure you have the following insta - Python 3.6+ - See `requirements.txt` for details. -## Usage +## Quick start Start the container : @@ -57,7 +59,7 @@ docker run -d \ obeoneorg/traefik_network_connector:latest ``` -And it's ok ! +The application version running inside the container is available through the `APP_VERSION` environment variable. For more details about the configuration options, refer to the [Configuration](#configuration) section. @@ -182,6 +184,41 @@ proxy exposes the Docker API via TCP, the Docker host must be configured accordi - **Connecting Traefik**: When a container with the specified label is created, it connects Traefik to its network if not already connected. If a config specified label is present (default is `autoproxy.networks`) only these networks will be connected to. - **Disconnecting Traefik**: If a container is destroyed, it checks if Traefik should be disconnected from its network, based on other containers' usage of the network. +## Versioning & Release process + +This project follows semantic versioning. The release process is automated using GitHub Actions. + +- The version is managed in the `VERSION` file in the format `X.Y.Z`. +- To bump the version, use the [`scripts/bump-version.sh`](./scripts/bump-version.sh:0) script. This script updates the `VERSION` file, commits the change, and creates a new Git tag. + ```bash + # Usage: ./scripts/bump-version.sh [major|minor|patch] + ./scripts/bump-version.sh minor + ``` +- Pushing a tag in the format `vX.Y.Z` (e.g., `v1.2.3`) triggers the [`.github/workflows/release.yml`](./.github/workflows/release.yml:0) workflow. +- The workflow automatically builds and pushes a multi-platform Docker image to Docker Hub and creates a new GitHub Release. + +## Docker images + +Docker images are stored on [Docker Hub](https://hub.docker.com/r/obeoneorg/auto-docker-proxy). + +- **Pulling an image:** + ```bash + docker pull obeoneorg/auto-docker-proxy:v1.2 + ``` +- **Tagging strategy:** For a release `v1.2.3`, the following tags are generated: + - `v1.2.3`, `1.2.3` + - `v1.2`, `1.2` + - `v1`, `1` + - `latest` +- **Using the version in `docker-compose`:** The application version is passed as a build argument to the Docker image and is available as an environment variable `APP_VERSION`. You can use it in your `docker-compose.yaml` file like this: + ```yaml + services: + my-service: + image: obeoneorg/auto-docker-proxy:v1.2.3 + environment: + - VERSION=${APP_VERSION} + ``` + ## TLS Configuration To ensure secure communication with the Docker server, configure the following TLS settings in `config.yaml`: @@ -232,4 +269,4 @@ For support or queries, please open an issue on this repository. We aim to respo Primarily powered by my new brain, GPT-4o, with some crucial tweaks and oversight from my secondary brain. -Check out more of my work on [GitHub](https://github.com/obeone). +Check out more of my work on [GitHub](https://github.com/obeone). \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 7d276c7..bbdb79c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,6 +14,10 @@ services: - /var/run/docker.sock:/var/run/docker.sock auto_docker_proxy: + build: + context: . + args: + VERSION: "${VERSION:-0.0.0}" image: obeoneorg/traefik_network_connector volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/main.py b/main.py index 743d106..e8b7c9e 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,18 @@ import logging from config import config, app_logger import re +import os + +# --- Version --- + +# Read the version from the VERSION file +try: + with open("VERSION") as version_file: + __version__ = version_file.read().strip() +except FileNotFoundError: + __version__ = "0.0.0" + +# --- Cache --- # Initialize the cache for container details container_cache = {} @@ -248,8 +260,11 @@ def monitor_events(): del container_cache[event["id"]] if __name__ == "__main__": + # Display the version + print(f"Version: {__version__}") + # Connect to all relevant networks on startup connect_to_all_relevant_networks() - # Start monitoring events loop‹≤ + # Start monitoring events loop monitor_events() diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..8fa6344 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# +# bump-version.sh +# +# This script bumps the version in the VERSION file according to SemVer. +# It takes one argument: patch, minor, or major. +# It also handles git commit and tag creation. +# + +set -euo pipefail + +# --- Functions --- + +# Function to display help message +usage() { + cat < 0.1.1) + minor Increment the minor version (e.g., 0.1.0 -> 0.2.0) + major Increment the major version (e.g., 0.1.0 -> 1.0.0) + +Options: + --help Display this help message and exit +EOF +} + +# --- Main script --- + +# Check for --help argument +if [[ "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +# Check for correct number of arguments +if [ "$#" -ne 1 ]; then + usage + exit 1 +fi + +# Check for valid argument +BUMP_TYPE=$1 +if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" ]]; then + usage + exit 1 +fi + +# Read the current version from the VERSION file +VERSION_FILE="VERSION" +if [ ! -f "$VERSION_FILE" ]; then + echo "Error: VERSION file not found!" + exit 1 +fi +CURRENT_VERSION=$(cat "$VERSION_FILE") + +# Split the version into major, minor, and patch parts +IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" +MAJOR=${VERSION_PARTS[0]} +MINOR=${VERSION_PARTS[1]} +PATCH=${VERSION_PARTS[2]} + +# Increment the correct part of the version +case "$BUMP_TYPE" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; +esac + +# Assemble the new version +NEW_VERSION="$MAJOR.$MINOR.$PATCH" + +# Write the new version to the VERSION file +echo "$NEW_VERSION" > "$VERSION_FILE" + +# Print the new version to stdout +echo "$NEW_VERSION" + +# Commit the new version and create a git tag +git add "$VERSION_FILE" +git commit -m "Bump version to $NEW_VERSION" +git tag "v$NEW_VERSION" \ No newline at end of file diff --git a/scripts/generate-tags.sh b/scripts/generate-tags.sh new file mode 100755 index 0000000..0b5f6e6 --- /dev/null +++ b/scripts/generate-tags.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# +# Generate Docker tags from a SemVer Git tag. +# +# This script takes a Git tag in the format "vX.Y.Z" and generates a list +# of Docker tags that should be applied to the image. The output is a +# comma-separated list of tags suitable for docker/build-push-action's +# `tags` input. +# +# Usage: +# ./scripts/generate-tags.sh [ ...] +# +# Example: +# ./scripts/generate-tags.sh v1.2.3 ghcr.io/obeone/app docker.io/obeoneorg/app +# Output: +# ghcr.io/obeone/app:v1.2.3,ghcr.io/obeone/app:v1.2,ghcr.io/obeone/app:v1,\\ +# ghcr.io/obeone/app:1.2.3,ghcr.io/obeone/app:1.2,ghcr.io/obeone/app:1,\\ +# docker.io/obeoneorg/app:v1.2.3,... + +set -euo pipefail + +# --- Main execution --- + +if [ "$#" -lt 2 ]; then + echo "Usage: $0 [ ...]" >&2 + exit 1 +fi + +GIT_TAG="$1" +shift +REPOS=("$@") + +# Remove the 'v' prefix to get the plain version number. +PLAIN_VERSION=${GIT_TAG#v} + +# Split the version into major, minor, and patch components. +MAJOR=$(echo "$PLAIN_VERSION" | cut -d. -f1) +MINOR=$(echo "$PLAIN_VERSION" | cut -d. -f2) +PATCH=$(echo "$PLAIN_VERSION" | cut -d. -f3) + +# Check if all version components are present. +if [ -z "$MAJOR" ] || [ -z "$MINOR" ] || [ -z "$PATCH" ]; then + echo "Error: Invalid tag format. Expected vX.Y.Z, but got $GIT_TAG" >&2 + exit 1 +fi + +TAGS=() + +for repo in "${REPOS[@]}"; do + TAGS+=( + "${repo}:${GIT_TAG}" + "${repo}:v${MAJOR}.${MINOR}" + "${repo}:v${MAJOR}" + "${repo}:${PLAIN_VERSION}" + "${repo}:${MAJOR}.${MINOR}" + "${repo}:${MAJOR}" + ) +done + +# Join with commas for docker/build-push-action `tags` input +IFS=',' +printf '%s' "${TAGS[*]}" From 9a805ac0fca5c55ffc6604aeb173c63c725010f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Tue, 18 Nov 2025 03:37:41 +0100 Subject: [PATCH 2/4] chore(ci): add dependabot config and update workflows - Add Dependabot configuration for GitHub Actions with weekly update schedule and grouped PRs - Update Docker tag configuration to use raw latest tag on main branch - Bump docker/build-push-action from v5 to v6 - Adjust build platforms (replace armhf/armel with linux/arm/v7) - Fix release workflow outputs quoting issue - Enhance release notes with Docker image pull instructions and supported platforms --- .github/dependabot.yml | 30 ++++++++++++++++++++++ .github/workflows/build-and-publish.yaml | 9 +++---- .github/workflows/release.yml | 32 ++++++++++++++++++++---- 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a95c1eb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +# Dependabot configuration for keeping GitHub Actions up to date +# This configuration enables automatic updates for GitHub Actions dependencies +# to ensure the workflows use the latest secure versions. + +version: 2 +updates: + # Monitor GitHub Actions dependencies + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates weekly on Mondays + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Europe/Paris" + # Limit the number of open pull requests + open-pull-requests-limit: 5 + # Add labels to PRs + labels: + - "dependencies" + - "github-actions" + # Commit message configuration + commit-message: + prefix: "chore(deps)" + include: "scope" + # Group all GitHub Actions updates into a single PR + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 6231c8b..1b04137 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -124,7 +124,7 @@ jobs: docker.io/obeoneorg/traefik_network_connector tags: | # For pushes to the 'main' branch, tag the image as 'latest'. - type=ref,event=branch,enable=${{ github.ref_name == 'main' }},prefix=,suffix=latest + type=raw,value=latest,enable=${{ github.ref_name == 'main' }} # For 'v*.*.*' tags, generate SemVer tags (e.g., v1.2.3, v1.2, v1). type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} @@ -135,7 +135,7 @@ jobs: - name: Build and push (multi-repo) if: steps.gatekeeper.outputs.run_build == 'true' id: build-and-push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile @@ -144,8 +144,7 @@ jobs: linux/amd64 linux/arm64 linux/i386 - linux/armhf - linux/armel + linux/arm/v7 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha @@ -190,4 +189,4 @@ jobs: for image in "${IMAGES[@]}"; do echo "Signing ${image}@${DIGEST}" cosign sign --yes "${image}@${DIGEST}" - done \ No newline at end of file + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dabe4d2..18339a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,8 +69,8 @@ jobs: fi echo "All conditions met. Proceeding with the release process." - echo "authorized=true" >> $GITHUB_OUTPUT" - echo "on_main=true" >> $GITHUB_OUTPUT" + echo "authorized=true" >> $GITHUB_OUTPUT + echo "on_main=true" >> $GITHUB_OUTPUT # Step 4: Create a GitHub Release and Generate Changelog. # This step is conditionally executed only if the 'check_conditions' step @@ -83,13 +83,35 @@ jobs: with: # The action automatically infers the tag name from the Git reference (github.ref_name). name: Release ${{ github.ref_name }} - + # Enables the automatic generation of release notes, leveraging the action's built-in capabilities. generateReleaseNotes: true - + + # Add Docker image links to release body + body: | + ## Docker Images + + This release is available as multi-platform Docker images on both GitHub Container Registry and Docker Hub: + + ### GitHub Container Registry (GHCR) + ```bash + docker pull ghcr.io/obeone/auto_docker_proxy:${{ github.ref_name }} + docker pull ghcr.io/obeone/traefik_network_connector:${{ github.ref_name }} + ``` + + ### Docker Hub + ```bash + docker pull obeoneorg/auto_docker_proxy:${{ github.ref_name }} + docker pull obeoneorg/traefik_network_connector:${{ github.ref_name }} + ``` + + **Supported Platforms:** linux/amd64, linux/arm64, linux/arm/v7 + + All images are signed with [Cosign](https://github.com/sigstore/cosign) for supply chain security. + draft: false # Publishes the release immediately, rather than as a draft. prerelease: false # Designates the release as a full, stable release. - + # The GITHUB_TOKEN is automatically provided by GitHub Actions, # granting the necessary permissions for creating the release. token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 0c3104dc8fc1bb96ba5b9a0fa90c547f6f4a12eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Tue, 18 Nov 2025 05:09:45 +0100 Subject: [PATCH 3/4] fix(ci): remove duplicate entries in build workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove duplicate job declarations, duplicate if conditions, duplicate shell directives, and duplicate comments in the build-and-publish workflow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/build-and-publish.yaml | 45 ------------------------ 1 file changed, 45 deletions(-) diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 42ad3f6..1b04137 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -12,20 +12,6 @@ # - GitHub Actions cache # - Cosign OIDC signing for main/tag pushes name: Build and Push Docker (Multi-Repo Alias) -# This workflow builds and pushes a Docker image to GHCR and Docker Hub -# under multiple repository aliases (auto_docker_proxy and traefik_network_connector). -# -# Triggers: -# 1. Push to 'main' branch (tags as 'latest') -# 2. Push of tags 'v*.*.*' (tags as SemVer) -# 3. Pull Requests (builds 'pr-XXX' tag, only if author is trusted or PR is approved) -# -# Features: -# - Multi-platform build -# - Multi-registry push (GHCR & Docker Hub) -# - GitHub Actions cache -# - Cosign OIDC signing for main/tag pushes -name: Build and Push Docker (Multi-Repo Alias) on: push: @@ -33,20 +19,16 @@ on: - main tags: - 'v*.*.*' # Trigger on version tags like v1.0.0 - tags: - - 'v*.*.*' # Trigger on version tags like v1.0.0 pull_request: types: [opened, synchronize, reopened] jobs: - build-push-sign: build-push-sign: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write # Required for OIDC signing - id-token: write # Required for OIDC signing steps: - name: Checkout code @@ -68,15 +50,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # --- PR Security Checks (Start) --- # --- PR Security Checks (Start) --- - name: Determine if PR author is trusted if: ${{ github.event_name == 'pull_request' }} id: check-author shell: bash - shell: bash run: | - # Define a list of trusted authors. Consider using a GitHub Team for larger projects. # Define a list of trusted authors. Consider using a GitHub Team for larger projects. trusted_authors=("obeone") trusted="false" @@ -88,15 +67,6 @@ jobs: done echo "Author ($GITHUB_ACTOR) trusted: $trusted" echo "trusted=$trusted" >> "$GITHUB_OUTPUT" - trusted="false" - for author in "${trusted_authors[@]}"; do - if [[ "$author" == "$GITHUB_ACTOR" ]]; then - trusted="true" - break - fi - done - echo "Author ($GITHUB_ACTOR) trusted: $trusted" - echo "trusted=$trusted" >> "$GITHUB_OUTPUT" - name: Check for admin approval if: ${{ github.event_name == 'pull_request' }} @@ -109,15 +79,9 @@ jobs: APPROVALS=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews \ --jq '.[] | select(.state=="APPROVED" and (.author_association=="OWNER" or .author_association=="MEMBER")) | .user.login') - # Check for approval by an organization OWNER or MEMBER. - APPROVALS=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews \ - --jq '.[] | select(.state=="APPROVED" and (.author_association=="OWNER" or .author_association=="MEMBER")) | .user.login') - if [[ -n "$APPROVALS" ]]; then echo "PR is approved by an organization member: $APPROVALS" echo "approved=true" >> "$GITHUB_OUTPUT" - echo "PR is approved by an organization member: $APPROVALS" - echo "approved=true" >> "$GITHUB_OUTPUT" else echo "PR is not approved by an organization member." echo "approved=false" >> "$GITHUB_OUTPUT" @@ -190,19 +154,11 @@ jobs: VERSION=${{ steps.meta.outputs.version }} - name: Set up cosign - if: steps.gatekeeper.outputs.run_build == 'true' if: steps.gatekeeper.outputs.run_build == 'true' uses: sigstore/cosign-installer@v3 - name: Sign the container image with cosign # This step only signs official images, which are those built from 'main' branch pushes or tag pushes. - if: >- - ${{ - steps.gatekeeper.outputs.run_build == 'true' && - github.event_name == 'push' && - (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - }} - # This step only signs official images, which are those built from 'main' branch pushes or tag pushes. if: >- ${{ steps.gatekeeper.outputs.run_build == 'true' && @@ -212,7 +168,6 @@ jobs: env: COSIGN_EXPERIMENTAL: true # Retrieve the image digest from the previous build step. - # Retrieve the image digest from the previous build step. DIGEST: ${{ steps.build-and-push.outputs.digest }} shell: bash run: | From 0fcbb04b13dbe90f378025933eb58e0736544426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Compagnon?= Date: Tue, 18 Nov 2025 05:13:31 +0100 Subject: [PATCH 4/4] chore(gitignore): ignore claude configuration directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3f83f97..96da046 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ venv.bak/ # Misc .history /test + +# Claude +/.claude