Module 4: Secrets and Environment Management

8. Project — Secure Pipeline

Overview

This project integrates everything you learned in Module 4. You're going to take Module 3's AI Quality Gate pipeline and secure it: configure GitHub Secrets for the API keys, create two GitHub Environments (staging and production) with separate secrets, generate .env files dynamically in CI, and verify that the secrets never get exposed in logs or artifacts.

The result is a pipeline that can run AI-specific checks safely in CI — prompt regression testing with real API keys, cost estimation, and quality gates — with no risk of exposing credentials.


What you're going to build

Push / PR
    │
    ├── verify-secrets (verifies the secrets exist)
    │
    ├── lint-and-test (no secrets — generic checks)
    │   ├── ruff check
    │   ├── mypy
    │   └── pytest
    │
    ├── ai-checks-staging (environment: staging)
    │   ├── Generate .env
    │   ├── Verify secrets
    │   ├── Prompt evaluation
    │   ├── Cost estimation
    │   └── Cleanup .env
    │
    ├── ai-checks-production (environment: production)
    │   ├── Requires approval ← protection rule
    │   ├── Generate .env
    │   ├── Prompt evaluation
    │   └── Cost estimation
    │
    └── security-audit (verifies there are no leaks)
        ├── Check logs for secret patterns
        └── Verify .env not in artifacts

Artifacts generated:

  • Test results (JUnit XML)
  • Evaluation reports (JSON) — staging and production
  • Cost reports (JSON)
  • Security audit report

Security checks:

  • Secrets verified before using them
  • .env generated dynamically and cleaned up afterward
  • Logs audited to detect secret patterns
  • Protection rules on production

Project structure

my-ai-project/
├── .github/
│   └── workflows/
│       └── secure-ai-pipeline.yml    # The secure workflow
├── .gitignore                         # .env excluded
├── .env.example                       # Template without real values
├── src/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── prompts.py
│   └── cost.py
├── tests/
│   ├── __init__.py
│   ├── test_main.py
│   ├── test_prompts.py
│   └── test_cost.py
├── scripts/
│   ├── evaluate_prompts.py
│   ├── estimate_cost.py
│   ├── verify_secrets.py              # Secret verification
│   ├── generate_env.py                # .env generator
│   └── security_audit.py             # Security audit
├── baselines/
│   └── prompt-baseline.json
├── Dockerfile
├── requirements.txt
├── requirements-dev.txt
└── pyproject.toml

Step 1: Configure GitHub Secrets

Repository secrets (for jobs without an environment)

# The shared secret (fallback)
gh secret set OPENAI_API_KEY
# Enter your API key when it asks

Environment secrets

# Create the secrets for staging
gh secret set OPENAI_API_KEY --env staging
# Enter the staging key (limited budget)

# Create the secrets for production
gh secret set OPENAI_API_KEY --env production
# Enter the production key

# Verify
gh secret list
gh secret list --env staging
gh secret list --env production

Expected output

# gh secret list
NAME              UPDATED
OPENAI_API_KEY    2026-03-08

# gh secret list --env staging
NAME              UPDATED
OPENAI_API_KEY    2026-03-08

# gh secret list --env production
NAME              UPDATED
OPENAI_API_KEY    2026-03-08

Step 2: Configure GitHub Environments

Staging

Settings → Environments → New environment
  Name: staging
  → Configure environment
  
  Deployment branches: All branches
  Required reviewers: No
  Wait timer: 0 minutes
  
  Environment secrets:
    OPENAI_API_KEY = sk-staging-... (budget: $10/month)

Production

Settings → Environments → New environment
  Name: production
  → Configure environment
  
  Deployment branches: Selected branches → main
  Required reviewers: Yes → @your-username
  Wait timer: 5 minutes
  
  Environment secrets:
    OPENAI_API_KEY = sk-prod-... (budget: $500/month)

Step 3: The supporting scripts

scripts/verify_secrets.py

#!/usr/bin/env python3
"""
Verify that the required secrets are available.
It doesn't print values — it only verifies existence and format.
"""
import os
import sys
import json
from datetime import datetime, timezone


REQUIRED_SECRETS = {
    "OPENAI_API_KEY": {
        "prefix": "sk-",
        "min_length": 20,
    },
}


def verify_secret(name: str, config: dict) -> dict:
    value = os.environ.get(name, "")

    if not value:
        return {"name": name, "status": "MISSING", "detail": "Not set"}

    if config.get("prefix") and not value.startswith(config["prefix"]):
        return {
            "name": name,
            "status": "INVALID_FORMAT",
            "detail": f"Expected prefix '{config['prefix']}'",
        }

    if config.get("min_length") and len(value) < config["min_length"]:
        return {
            "name": name,
            "status": "TOO_SHORT",
            "detail": f"Expected >= {config['min_length']} chars, got {len(value)}",
        }

    return {
        "name": name,
        "status": "OK",
        "detail": f"Set ({len(value)} chars)",
    }


def main() -> int:
    app_env = os.environ.get("APP_ENV", "unknown")
    print(f"Verifying secrets for environment: {app_env}")
    print("=" * 50)

    results = []
    all_ok = True

    for name, config in REQUIRED_SECRETS.items():
        result = verify_secret(name, config)
        results.append(result)
        status_icon = "PASS" if result["status"] == "OK" else "FAIL"
        print(f"  [{status_icon}] {result['name']}: {result['detail']}")
        if result["status"] != "OK":
            all_ok = False

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "environment": app_env,
        "results": results,
        "all_ok": all_ok,
    }

    os.makedirs("results", exist_ok=True)
    with open(f"results/secret-verification-{app_env}.json", "w") as f:
        json.dump(report, f, indent=2)

    if all_ok:
        print(f"\nAll required secrets verified for {app_env}.")
        return 0

    print(f"\nSome secrets are missing for {app_env}.")
    return 1


if __name__ == "__main__":
    sys.exit(main())

scripts/generate_env.py

#!/usr/bin/env python3
"""
Generate .env from the system's environment variables and defaults.
Designed to run in CI where the secrets come from the workflow.
"""
import os
import sys


DEFAULTS = {
    "APP_ENV": "development",
    "MODEL": "gpt-4o-mini",
    "MAX_TOKENS": "500",
    "TEMPERATURE": "0.7",
    "COST_THRESHOLD": "5.00",
    "DAILY_REQUEST_ESTIMATE": "10000",
    "LOG_LEVEL": "debug",
}

SECRETS_FROM_ENV = [
    "OPENAI_API_KEY",
]


def generate_env_file(output_path: str = ".env") -> None:
    lines = ["# Auto-generated for CI — DO NOT COMMIT", ""]

    for key, default in DEFAULTS.items():
        value = os.environ.get(key, default)
        lines.append(f"{key}={value}")

    lines.append("")
    for key in SECRETS_FROM_ENV:
        value = os.environ.get(key, "")
        if value:
            lines.append(f"{key}={value}")
        else:
            lines.append(f"# {key}= (not available)")

    with open(output_path, "w") as f:
        f.write("\n".join(lines) + "\n")

    var_count = sum(1 for line in lines if "=" in line and not line.startswith("#"))
    print(f"Generated {output_path} with {var_count} variables")


if __name__ == "__main__":
    output = sys.argv[1] if len(sys.argv) > 1 else ".env"
    generate_env_file(output)

scripts/security_audit.py

#!/usr/bin/env python3
"""
Post-pipeline security audit.
Checks that no secrets leaked into artifacts or generated files.
"""
import os
import re
import sys
import json
from datetime import datetime, timezone
from pathlib import Path


SECRET_PATTERNS = [
    r"sk-[a-zA-Z0-9]{20,}",
    r"sk-proj-[a-zA-Z0-9]{20,}",
    r"sk-ant-[a-zA-Z0-9]{20,}",
    r"ghp_[a-zA-Z0-9]{36}",
    r"gho_[a-zA-Z0-9]{36}",
    r"AKIA[A-Z0-9]{16}",
]


def scan_file(filepath: str) -> list[dict]:
    findings = []

    try:
        with open(filepath) as f:
            content = f.read()
    except (UnicodeDecodeError, PermissionError):
        return findings

    for pattern in SECRET_PATTERNS:
        matches = re.finditer(pattern, content)
        for match in matches:
            findings.append({
                "file": filepath,
                "pattern": pattern,
                "position": match.start(),
                "preview": match.group()[:8] + "...",
            })

    return findings


def scan_directory(directory: str, extensions: list[str]) -> list[dict]:
    all_findings = []

    for ext in extensions:
        for filepath in Path(directory).rglob(f"*{ext}"):
            findings = scan_file(str(filepath))
            all_findings.extend(findings)

    return all_findings


def check_env_files() -> list[dict]:
    findings = []
    env_files = list(Path(".").glob(".env*"))

    for env_file in env_files:
        name = str(env_file)
        if name == ".env.example":
            continue

        if env_file.exists() and env_file.stat().st_size > 0:
            findings.append({
                "file": name,
                "issue": "Non-example .env file exists",
                "severity": "HIGH",
            })

    return findings


def main() -> int:
    print("Security Audit")
    print("=" * 50)

    all_findings = []

    print("\n1. Scanning results/ for secret patterns...")
    if Path("results").exists():
        scan_findings = scan_directory("results", [".json", ".xml", ".txt", ".log"])
        all_findings.extend(scan_findings)
        print(f"   Found {len(scan_findings)} potential secrets in results/")
    else:
        print("   results/ directory not found — skipping")

    print("\n2. Checking for .env files that should not exist...")
    env_findings = check_env_files()
    print(f"   Found {len(env_findings)} .env issues")

    print("\n3. Verifying .gitignore includes .env...")
    gitignore_ok = False
    if Path(".gitignore").exists():
        with open(".gitignore") as f:
            gitignore_content = f.read()
        gitignore_ok = ".env" in gitignore_content
    print(f"   .env in .gitignore: {'YES' if gitignore_ok else 'NO'}")
    if not gitignore_ok:
        all_findings.append({
            "file": ".gitignore",
            "issue": ".env not in .gitignore",
            "severity": "CRITICAL",
        })

    total_issues = len(all_findings) + len(env_findings)
    print(f"\n{'=' * 50}")
    print(f"Total issues found: {total_issues}")

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "secret_scan_findings": all_findings,
        "env_file_findings": env_findings,
        "gitignore_ok": gitignore_ok,
        "total_issues": total_issues,
        "passed": total_issues == 0,
    }

    os.makedirs("results", exist_ok=True)
    with open("results/security-audit.json", "w") as f:
        json.dump(report, f, indent=2)
    print(f"\nReport saved to results/security-audit.json")

    if total_issues > 0:
        print("\nSECURITY AUDIT FAILED — review findings above")
        for finding in all_findings:
            print(f"  - {finding.get('file', 'unknown')}: {finding.get('issue', finding.get('preview', ''))}")
        for finding in env_findings:
            print(f"  - {finding['file']}: {finding['issue']}")
        return 1

    print("\nSECURITY AUDIT PASSED")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Step 4: The .env.example file

# .env.example — Commit this file to the repo
# Copy it as .env and fill in the real values

# Required
OPENAI_API_KEY=sk-your-key-here

# Application
APP_ENV=development
MODEL=gpt-4o-mini
MAX_TOKENS=500
TEMPERATURE=0.7

# Cost control
COST_THRESHOLD=5.00
DAILY_REQUEST_ESTIMATE=10000

# Logging
LOG_LEVEL=debug

Step 5: The complete workflow

.github/workflows/secure-ai-pipeline.yml

name: Secure AI Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

env:
  PYTHON_VERSION: "3.12"
  MODEL: "gpt-4o-mini"
  COST_THRESHOLD: "5.00"

permissions:
  contents: read
  pull-requests: write

jobs:
  # ──────────────────────────────────────────────
  # Job 1: Lint and Test (no secrets needed)
  # ──────────────────────────────────────────────
  lint-and-test:
    name: Lint & Test
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: "pip"

      - name: Install dev dependencies
        run: pip install -r requirements-dev.txt

      - name: Run ruff linter
        run: ruff check src/ tests/ scripts/

      - name: Run mypy type checker
        run: mypy src/ --ignore-missing-imports

      - name: Run unit tests
        run: |
          mkdir -p results/tests
          pytest tests/ \
            -v \
            --timeout=30 \
            --junitxml=results/tests/report.xml \
            --cov=src \
            --cov-report=term-missing

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: results/tests/
          retention-days: 30

  # ──────────────────────────────────────────────
  # Job 2: AI Checks in Staging
  # ──────────────────────────────────────────────
  ai-checks-staging:
    name: AI Checks (Staging)
    runs-on: ubuntu-latest
    needs: [lint-and-test]
    environment: staging
    timeout-minutes: 10

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: "pip"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Verify secrets
        run: python scripts/verify_secrets.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          APP_ENV: staging

      - name: Generate .env for staging
        run: python scripts/generate_env.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          APP_ENV: staging
          LOG_LEVEL: warning
          COST_THRESHOLD: "10.00"

      - name: Run prompt evaluation
        run: |
          python scripts/evaluate_prompts.py \
            --baseline baselines/prompt-baseline.json \
            --output results/staging-evaluation.json
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

      - name: Run cost estimation
        run: |
          python scripts/estimate_cost.py \
            --output results/staging-cost-report.json \
            --threshold ${{ env.COST_THRESHOLD }}

      - name: Cleanup .env
        if: always()
        run: rm -f .env

      - name: Upload staging results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: staging-results
          path: |
            results/staging-evaluation.json
            results/staging-cost-report.json
            results/secret-verification-staging.json
          retention-days: 30

  # ──────────────────────────────────────────────
  # Job 3: AI Checks in Production
  # ──────────────────────────────────────────────
  ai-checks-production:
    name: AI Checks (Production)
    runs-on: ubuntu-latest
    needs: [ai-checks-staging]
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://api.example.com
    timeout-minutes: 10

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: "pip"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Verify secrets
        run: python scripts/verify_secrets.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          APP_ENV: production

      - name: Generate .env for production
        run: python scripts/generate_env.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          APP_ENV: production
          LOG_LEVEL: error
          COST_THRESHOLD: "500.00"

      - name: Run prompt evaluation
        run: |
          python scripts/evaluate_prompts.py \
            --baseline baselines/prompt-baseline.json \
            --output results/production-evaluation.json
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

      - name: Run cost estimation
        run: |
          python scripts/estimate_cost.py \
            --output results/production-cost-report.json \
            --threshold 500.00

      - name: Cleanup .env
        if: always()
        run: rm -f .env

      - name: Upload production results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: production-results
          path: |
            results/production-evaluation.json
            results/production-cost-report.json
            results/secret-verification-production.json
          retention-days: 90

  # ──────────────────────────────────────────────
  # Job 4: Security Audit
  # ──────────────────────────────────────────────
  security-audit:
    name: Security Audit
    runs-on: ubuntu-latest
    needs: [ai-checks-staging]
    if: always()
    timeout-minutes: 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Download staging results
        uses: actions/download-artifact@v4
        with:
          name: staging-results
          path: results/
        continue-on-error: true

      - name: Run security audit
        run: python scripts/security_audit.py

      - name: Upload audit report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: security-audit
          path: results/security-audit.json
          retention-days: 90

  # ──────────────────────────────────────────────
  # Job 5: Pipeline Summary
  # ──────────────────────────────────────────────
  summary:
    name: Pipeline Summary
    runs-on: ubuntu-latest
    needs: [lint-and-test, ai-checks-staging, ai-checks-production, security-audit]
    if: always()

    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: all-results/
        continue-on-error: true

      - name: Generate summary
        run: |
          echo "## Secure AI Pipeline Results" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          echo "### Job Results" >> $GITHUB_STEP_SUMMARY
          echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
          echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| Lint & Test | ${{ needs.lint-and-test.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| AI Checks (Staging) | ${{ needs.ai-checks-staging.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| AI Checks (Production) | ${{ needs.ai-checks-production.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Security Audit | ${{ needs.security-audit.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          echo "### Security" >> $GITHUB_STEP_SUMMARY
          if [ -f all-results/security-audit/security-audit.json ]; then
            python3 -c "
          import json
          with open('all-results/security-audit/security-audit.json') as f:
              data = json.load(f)
          status = 'PASSED' if data['passed'] else 'FAILED'
          print(f'- Audit: {status}')
          print(f'- Issues found: {data[\"total_issues\"]}')
          print(f'- .gitignore OK: {data[\"gitignore_ok\"]}')
          " >> $GITHUB_STEP_SUMMARY
          else
            echo "- Security audit results not available" >> $GITHUB_STEP_SUMMARY
          fi

Step 6: Verify .gitignore

# .gitignore — Guarantees that .env and sensitive results are excluded

# Environment files
.env
.env.local
.env.staging
.env.production
.env.*.local

# Results (they can contain sensitive information)
results/

# Python
__pycache__/
*.pyc
.mypy_cache/
.pytest_cache/
.ruff_cache/
htmlcov/
.coverage

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

Step 7: Test the pipeline

Test 1: Verify that staging works

# Make sure the secrets are configured
gh secret list --env staging

# Trigger the workflow
gh workflow run secure-ai-pipeline.yml

# Watch the run
gh run watch

Expected output:

✓ Lint & Test         passed
✓ AI Checks (Staging) passed
✓ Security Audit      passed
- AI Checks (Production)  skipped (not on main or pending approval)

Test 2: Verify the protection rules on production

# Push to main to activate the production job
git push origin main

The "AI Checks (Production)" job pauses waiting for approval:

GitHub Actions → Workflow Run
  ✓ Lint & Test         passed
  ✓ AI Checks (Staging) passed
  ⏳ AI Checks (Production)  Waiting for review
  ✓ Security Audit      passed

Approve the deployment:

Click "Review deployments" → Select "production" → "Approve and deploy"

Test 3: Verify that the secrets don't get exposed

Review the workflow's logs:

gh run view --log

Search the logs: you should never see a value that starts with sk-. If you see ***, the masking is working correctly.

Test 4: Verify the security audit

Download the audit report:

gh run download --name security-audit
cat security-audit/security-audit.json

Expected output:

{
  "timestamp": "2026-03-08T15:30:00+00:00",
  "secret_scan_findings": [],
  "env_file_findings": [],
  "gitignore_ok": true,
  "total_issues": 0,
  "passed": true
}

Zero findings = a secure pipeline.


Step 8: Simulate a leak (and verify the detection)

To test that your audit detects problems:

# Create a test branch
git checkout -b test/security-audit

# Add a fake "secret" to a results file
mkdir -p results
echo '{"api_key": "sk-proj-test1234567890abcdef"}' > results/debug.json

# Run the audit locally
python scripts/security_audit.py

Expected output:

Security Audit
==================================================

1. Scanning results/ for secret patterns...
   Found 1 potential secrets in results/

2. Checking for .env files that should not exist...
   Found 0 .env issues

3. Verifying .gitignore includes .env...
   .env in .gitignore: YES

==================================================
Total issues found: 1

SECURITY AUDIT FAILED — review findings above
  - results/debug.json: sk-proj-...

The audit detected the secret pattern in the file. Clean up and go back to main:

rm -rf results/
git checkout main
git branch -D test/security-audit

Implementation checklist

Verify that you have everything:

Secrets configured

  • Repository secret: OPENAI_API_KEY
  • The "staging" environment created with its OPENAI_API_KEY
  • The "production" environment created with its OPENAI_API_KEY
  • Production has required reviewers configured
  • Production has deployment branches limited to main

Project files

  • .github/workflows/secure-ai-pipeline.yml — The workflow with 5 jobs
  • .gitignore — It includes .env and results/
  • .env.example — A template with no real values
  • scripts/verify_secrets.py — Secret verification
  • scripts/generate_env.py — The .env generator
  • scripts/security_audit.py — The security audit
  • scripts/evaluate_prompts.py — From Module 3
  • scripts/estimate_cost.py — From Module 3

Verifications

  • The lint and test workflow passes without secrets
  • Staging runs the AI checks with the environment's secrets
  • Production requires approval before running
  • .env gets generated and cleaned up on every run
  • The security audit passes (zero findings)
  • The logs contain no secret values (only ***)
  • The artifacts contain no .env files

Troubleshooting

1. The staging job fails with "secret not found"

Symptom:

Verifying secrets for environment: staging
  [FAIL] OPENAI_API_KEY: Not set

Cause: The "staging" environment has no secrets configured, or the environment's name in the workflow doesn't match the one in Settings.

Solution:

# Verify that the environment exists and has secrets
gh secret list --env staging

# If it's empty, create the secret
gh secret set OPENAI_API_KEY --env staging

2. The production job gets skipped with no explanation

Symptom: The job appears as "skipped" in the UI.

Cause: The condition if: github.ref == 'refs/heads/main' isn't met (you're on a PR or another branch).

Solution: The production job only runs on pushes to main. On PRs, skipping it is intentional — you don't want to run production checks on every PR.

3. The security audit finds false positives

Symptom: The script reports secrets in files that contain no real secrets.

Cause: The detection patterns are regexes that can match strings that aren't secrets (like IDs starting with sk-).

Solution: Adjust the patterns in security_audit.py or add exceptions:

IGNORE_FILES = [
    "results/security-audit.json",  # The audit itself
]

4. The .env cleanup doesn't run

Symptom: The .env persists after the workflow (visible if you add a debug step).

Cause: The cleanup step has a condition that isn't met, or the workflow got cancelled before reaching the cleanup.

Solution: Verify that the cleanup uses if: always():

      - name: Cleanup .env
        if: always()
        run: rm -f .env

5. The staging and production artifacts get mixed up

Symptom: The evaluation files have the same names and overwrite each other.

Cause: Both jobs generate files with the same name.

Solution: Use different names in each job's --output (already implemented in the workflow: staging-evaluation.json vs production-evaluation.json).


Success criteria

Your project is complete when:

  1. The workflow has 5 jobs: lint-and-test, ai-checks-staging, ai-checks-production, security-audit, summary
  2. Lint and tests run without secrets — they don't need API keys
  3. Staging uses its own API key — staging's environment secret
  4. Production uses its own API key — production's environment secret
  5. Production requires approval — required reviewers configured
  6. .env gets generated and cleaned up on every staging and production run
  7. The security audit passes — zero findings of exposed secrets
  8. The logs contain no secrets — only *** where values would appear
  9. The artifacts are safe — they contain no .env and no secret values
  10. The Pipeline Summary shows the status of every job, including the audit

Connection with Module 5

Your pipeline is now secure: secrets protected, environments separated, an automated audit. But one component is still missing: Docker.

In Module 3, you had a docker-build job that verified the Dockerfile built correctly. Now that you handle secrets professionally, you can go further: build the Docker image AND push it to a container registry (GitHub Container Registry, Docker Hub, or ECR).

Pushing to a registry requires authentication — and now you know exactly how to handle those credentials. Module 5 — Docker in CI/CD — teaches you to build images automatically, use layer caching for fast builds, and push to registries with secure authentication.


Summary

  • A secure pipeline with 5 jobs: lint-and-test, AI checks staging, AI checks production, security audit, summary
  • GitHub Secrets configured at the repository level and per environment
  • Two environments: staging (no restrictions, low budget) and production (with approval, the real budget)
  • A dynamic .env: generated from the secrets in CI, cleaned up afterward with if: always()
  • Secret verification: a script that validates existence and format before using them
  • A security audit: scanning the artifacts and results to detect leaks
  • Protection rules: production requires manual approval and only accepts main
  • Masking verified: the logs contain no secret values
  • Pipeline Summary: it consolidates the results of every job, including the audit
  • The connection to Module 5: professional secrets management → Docker push with registry auth

Additional resources

  1. GitHub Actions — Using Environments — Environments with protection rules
  2. GitHub Actions — Encrypted Secrets — Complete reference on secrets
  3. GitHub Actions — Security Hardening — Security best practices
  4. OpenAI API Keys — Best Practices — Security for OpenAI API keys
  5. GitHub Actions — Job Summaries — How to generate Job Summaries
  6. OWASP — Secrets Management — OWASP's cheat sheet for secrets