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
.envgenerated 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.envandresults/ -
.env.example— A template with no real values -
scripts/verify_secrets.py— Secret verification -
scripts/generate_env.py— The.envgenerator -
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
-
.envgets generated and cleaned up on every run - The security audit passes (zero findings)
- The logs contain no secret values (only
***) - The artifacts contain no
.envfiles
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:
- The workflow has 5 jobs: lint-and-test, ai-checks-staging, ai-checks-production, security-audit, summary
- Lint and tests run without secrets — they don't need API keys
- Staging uses its own API key — staging's environment secret
- Production uses its own API key — production's environment secret
- Production requires approval — required reviewers configured
.envgets generated and cleaned up on every staging and production run- The security audit passes — zero findings of exposed secrets
- The logs contain no secrets — only
***where values would appear - The artifacts are safe — they contain no
.envand no secret values - 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 withif: 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
- GitHub Actions — Using Environments — Environments with protection rules
- GitHub Actions — Encrypted Secrets — Complete reference on secrets
- GitHub Actions — Security Hardening — Security best practices
- OpenAI API Keys — Best Practices — Security for OpenAI API keys
- GitHub Actions — Job Summaries — How to generate Job Summaries
- OWASP — Secrets Management — OWASP's cheat sheet for secrets