Release 2.2.0: enterprise hardening #57
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |