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
157 changes: 122 additions & 35 deletions .github/workflows/build-and-publish.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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' }}
Expand All @@ -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
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
# 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 <obeone@obeone.org>"
LABEL description="Automatically connect traefik to docker's services networks which is needed"
LABEL project="traefik_network_connector"
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
Loading
Loading