Skip to content

Release 2.2.0: enterprise hardening #57

Release 2.2.0: enterprise hardening

Release 2.2.0: enterprise hardening #57

Workflow file for this run

name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
pull_request:
branches: [ main, develop ]
env:
DOCKER_IMAGE: noxied/wanwatcher
PYTHON_VERSION: '3.11'
jobs:
# ============================================================================
# Code Quality & Linting
# ============================================================================
lint:
name: Lint Code
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip packages
uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pylint black isort mypy types-requests
pip install -r requirements.txt
- name: Run Black (code formatting check)
run: black --check --diff wanwatcher tests scripts
- name: Run isort (import sorting check)
run: isort --check-only --diff wanwatcher tests scripts
- name: Run Flake8 (style guide)
run: flake8 wanwatcher tests scripts --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Run Flake8 (complexity)
run: flake8 wanwatcher tests scripts --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Run Pylint
run: pylint wanwatcher --exit-zero --max-line-length=127
- name: Run MyPy (type checking)
run: mypy wanwatcher --ignore-missing-imports --no-strict-optional
# ============================================================================
# Security Scanning
# ============================================================================
security:
name: Security Scan
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit safety
- name: Run Bandit (security issues)
run: bandit -r wanwatcher -f json -o bandit-report.json || true
- name: Upload Bandit report
uses: actions/upload-artifact@v6
with:
name: bandit-security-report
path: bandit-report.json
- name: Run Safety (dependency vulnerabilities)
run: |
pip install -r requirements.txt
safety check --json || true
# ============================================================================
# Unit Tests
# ============================================================================
test:
name: Run Tests
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-mock
pip install -r requirements.txt
- name: Run tests with coverage
run: pytest tests/ --cov=wanwatcher --cov-report=xml --cov-report=html
- name: Upload coverage to Codecov
if: matrix.python-version == '3.11'
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
# ============================================================================
# Configuration Validation
# ============================================================================
validate-config:
name: Validate Configuration
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Run configuration validator
run: |
# Test with minimal valid config
export DISCORD_ENABLED=true
export DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/123/test
export CHECK_INTERVAL=900
python -m wanwatcher.validation
# ============================================================================
# Docker Build & Test
# ============================================================================
docker-build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: [lint, security, test]
permissions:
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image (test)
uses: docker/build-push-action@v6
with:
context: .
load: true
push: false
tags: ${{ env.DOCKER_IMAGE }}:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Test Docker image
run: |
docker run --rm \
-e DISCORD_ENABLED=false \
-e TELEGRAM_ENABLED=false \
-e EMAIL_ENABLED=false \
${{ env.DOCKER_IMAGE }}:test \
python -c "from wanwatcher import VERSION; print(f'Version: {VERSION}')"
# Security gate: scan the freshly built image before it can be published.
# ignore-unfixed skips CVEs with no upstream patch; .trivyignore (repo
# root) holds documented exceptions for any fixable findings.
- name: Scan image for vulnerabilities (Trivy)
uses: aquasecurity/trivy-action@0.29.0
with:
image-ref: ${{ env.DOCKER_IMAGE }}:test
format: table
exit-code: "1"
ignore-unfixed: true
severity: HIGH,CRITICAL
trivyignores: .trivyignore
# ============================================================================
# Docker Push (only on tags/main)
# ============================================================================
docker-push:
name: Push Docker Image
runs-on: ubuntu-latest
needs: [docker-build]
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
permissions:
contents: read
id-token: write # required for Cosign keyless (OIDC) signing
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push multi-platform
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Generate a CycloneDX SBOM for the published image (by digest, so it is
# unambiguous across the multiple tags) and keep it as a build artifact.
- name: Generate SBOM
uses: anchore/sbom-action@v0.18.0
with:
image: ${{ env.DOCKER_IMAGE }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: wanwatcher-sbom.json
- name: Upload SBOM
uses: actions/upload-artifact@v6
with:
name: wanwatcher-sbom
path: wanwatcher-sbom.json
# Sign the image by digest with Cosign keyless (Sigstore/OIDC). Signing
# the digest covers every tag that points at it.
- name: Install Cosign
uses: sigstore/cosign-installer@v3.8.0
- name: Sign the published image
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: cosign sign --yes "${{ env.DOCKER_IMAGE }}@${DIGEST}"
# ============================================================================
# Create GitHub Release
# ============================================================================
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [docker-push]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Extract changelog for this version
id: changelog
run: |
# Extract changelog section for current version
VERSION=${{ steps.version.outputs.VERSION }}
awk "/## \[${VERSION}\]/,/## \[/" CHANGELOG.md | grep -v "## \[" > release_notes.md || echo "See CHANGELOG.md for details" > release_notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: release_notes.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}