9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
1335 lines
50 KiB
YAML
1335 lines
50 KiB
YAML
name: Autonomous Code Scanner
|
|
|
|
# Daily multi-scanner: formatting (P0), security (P1), type hints (P2), docs (P3).
|
|
# Creates separate draft PRs per scan category. Budget-gated at $100/run.
|
|
# Estimated cost: $5-60 per run depending on findings.
|
|
|
|
on:
|
|
# schedule:
|
|
# - cron: '0 2 * * *' # 2 AM UTC daily — enable after cost validation
|
|
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: 'Dry run (skip PR creation)'
|
|
type: boolean
|
|
default: false
|
|
skip_expensive:
|
|
description: 'Skip expensive scanners (P2/P3)'
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: autonomous-code-scanner
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Job 1: Pre-Flight Security Check (10 min)
|
|
security-check:
|
|
name: Pre-Flight Security Check
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
outputs:
|
|
python_files_count: ${{ steps.generate-lists.outputs.python_count }}
|
|
typescript_files_count: ${{ steps.generate-lists.outputs.typescript_count }}
|
|
shell_files_count: ${{ steps.generate-lists.outputs.shell_count }}
|
|
total_files: ${{ steps.generate-lists.outputs.total }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Validate required secrets
|
|
env:
|
|
HAS_API_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }}
|
|
run: |
|
|
if [ "$HAS_API_KEY" != "true" ]; then
|
|
echo "::error::ANTHROPIC_API_KEY not configured"
|
|
exit 1
|
|
fi
|
|
echo "Required secrets present"
|
|
|
|
- name: Generate scannable file lists
|
|
id: generate-lists
|
|
run: |
|
|
# Protected file patterns (ABSOLUTE BLOCK)
|
|
PROTECTED_PATTERNS=(
|
|
".github/workflows/*"
|
|
".env*"
|
|
"ods/installers/*"
|
|
"ods/ods-cli"
|
|
"ods/config/*"
|
|
)
|
|
|
|
# Generate Python file list
|
|
git ls-files 'ods/**/*.py' '.github/scripts/*.py' | while read -r file; do
|
|
PROTECTED=false
|
|
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
if [[ "$file" == $pattern ]]; then
|
|
PROTECTED=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$PROTECTED" = false ]; then
|
|
echo "$file"
|
|
fi
|
|
done > /tmp/scannable_python.txt
|
|
|
|
# Generate TypeScript file list (dashboard)
|
|
git ls-files 'ods/extensions/services/dashboard/src/**/*.ts' \
|
|
'ods/extensions/services/dashboard/src/**/*.tsx' | while read -r file; do
|
|
echo "$file"
|
|
done > /tmp/scannable_typescript.txt
|
|
|
|
# Generate Shell file list
|
|
git ls-files 'ods/scripts/*.sh' 'ods/tests/**/*.sh' | while read -r file; do
|
|
PROTECTED=false
|
|
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
if [[ "$file" == $pattern ]]; then
|
|
PROTECTED=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$PROTECTED" = false ]; then
|
|
echo "$file"
|
|
fi
|
|
done > /tmp/scannable_shell.txt
|
|
|
|
# Count files
|
|
PYTHON_COUNT=$(wc -l < /tmp/scannable_python.txt | tr -d ' ')
|
|
TS_COUNT=$(wc -l < /tmp/scannable_typescript.txt | tr -d ' ')
|
|
SHELL_COUNT=$(wc -l < /tmp/scannable_shell.txt | tr -d ' ')
|
|
TOTAL=$((PYTHON_COUNT + TS_COUNT + SHELL_COUNT))
|
|
|
|
echo "python_count=$PYTHON_COUNT" >> $GITHUB_OUTPUT
|
|
echo "typescript_count=$TS_COUNT" >> $GITHUB_OUTPUT
|
|
echo "shell_count=$SHELL_COUNT" >> $GITHUB_OUTPUT
|
|
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
|
|
|
echo "## Scannable Files" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Python**: $PYTHON_COUNT files" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **TypeScript**: $TS_COUNT files" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Shell**: $SHELL_COUNT files" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Total**: $TOTAL files" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload scannable file lists
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: scannable-files
|
|
path: |
|
|
/tmp/scannable_python.txt
|
|
/tmp/scannable_typescript.txt
|
|
/tmp/scannable_shell.txt
|
|
retention-days: 7
|
|
|
|
# Job 2: Formatting Scanner (15 min, P0)
|
|
scan-formatting:
|
|
name: Formatting Scanner (P0)
|
|
runs-on: ubuntu-latest
|
|
needs: security-check
|
|
timeout-minutes: 15
|
|
outputs:
|
|
has_changes: ${{ steps.detect-changes.outputs.has_changes }}
|
|
files_changed: ${{ steps.detect-changes.outputs.files_changed }}
|
|
cost: ${{ steps.calculate-cost.outputs.cost }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install Ruff
|
|
run: pip install ruff
|
|
|
|
- name: Download scannable files
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: scannable-files
|
|
path: /tmp
|
|
|
|
- name: Run Ruff format and check
|
|
run: |
|
|
echo "## Formatting Scan Results" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# Run ruff format on all scannable Python files
|
|
TOTAL=0
|
|
FORMATTED=0
|
|
|
|
while IFS= read -r file; do
|
|
if [ -f "$file" ]; then
|
|
TOTAL=$((TOTAL + 1))
|
|
|
|
# Format file
|
|
if ruff format "$file" 2>/dev/null; then
|
|
if ! git diff --quiet "$file"; then
|
|
FORMATTED=$((FORMATTED + 1))
|
|
fi
|
|
fi
|
|
|
|
# Fix linting issues
|
|
ruff check "$file" --fix --quiet 2>/dev/null || true
|
|
fi
|
|
done < /tmp/scannable_python.txt
|
|
|
|
echo "- **Files scanned**: $TOTAL" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Files formatted**: $FORMATTED" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Detect changes
|
|
id: detect-changes
|
|
run: |
|
|
if git diff --quiet; then
|
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
echo "files_changed=0" >> $GITHUB_OUTPUT
|
|
echo "No formatting changes needed" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
FILES_CHANGED=$(git diff --name-only | wc -l | tr -d ' ')
|
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT
|
|
|
|
echo "### Changed Files" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
|
|
git diff --stat > /tmp/formatting-summary.txt
|
|
fi
|
|
|
|
- name: Create git patch
|
|
if: steps.detect-changes.outputs.has_changes == 'true'
|
|
run: |
|
|
git diff > /tmp/formatting-changes.patch
|
|
echo "Created patch file with $(wc -l < /tmp/formatting-changes.patch) lines"
|
|
|
|
- name: Calculate cost
|
|
id: calculate-cost
|
|
run: |
|
|
# Formatting is $0.50 (CI overhead only, zero API cost)
|
|
echo "cost=0.50" >> $GITHUB_OUTPUT
|
|
echo "**Cost**: \$0.50" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload formatting summary and patch
|
|
if: steps.detect-changes.outputs.has_changes == 'true'
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: formatting-changes
|
|
path: |
|
|
/tmp/formatting-summary.txt
|
|
/tmp/formatting-changes.patch
|
|
retention-days: 7
|
|
|
|
# Job 3: Security Scanner (30 min, P1)
|
|
scan-security:
|
|
name: Security Scanner (P1)
|
|
runs-on: ubuntu-latest
|
|
needs: security-check
|
|
timeout-minutes: 30
|
|
outputs:
|
|
has_findings: ${{ steps.analyze-findings.outputs.has_findings }}
|
|
finding_count: ${{ steps.analyze-findings.outputs.finding_count }}
|
|
high_severity_count: ${{ steps.analyze-findings.outputs.high_severity_count }}
|
|
cost: ${{ steps.calculate-cost.outputs.cost }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install Bandit
|
|
run: pip install bandit[toml]
|
|
|
|
- name: Download scannable files
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: scannable-files
|
|
path: /tmp
|
|
|
|
- name: Run Bandit security scan
|
|
run: |
|
|
echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# Run Bandit on ODS Python files
|
|
bandit -r ods/extensions/services/dashboard-api \
|
|
ods/scripts \
|
|
.github/scripts \
|
|
--format json \
|
|
--output /tmp/bandit-results.json \
|
|
--severity-level medium \
|
|
--confidence-level medium \
|
|
--exclude __pycache__ \
|
|
|| true # Don't fail on findings
|
|
|
|
# Count findings by severity
|
|
if [ -f /tmp/bandit-results.json ]; then
|
|
HIGH=$(jq '[.results[] | select(.issue_severity=="HIGH")] | length' /tmp/bandit-results.json)
|
|
MEDIUM=$(jq '[.results[] | select(.issue_severity=="MEDIUM")] | length' /tmp/bandit-results.json)
|
|
LOW=$(jq '[.results[] | select(.issue_severity=="LOW")] | length' /tmp/bandit-results.json)
|
|
|
|
echo "- **High severity**: $HIGH" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Medium severity**: $MEDIUM" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Low severity**: $LOW" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Filter high severity findings
|
|
id: filter-findings
|
|
run: |
|
|
if [ -f /tmp/bandit-results.json ]; then
|
|
# Extract top 20 high severity findings
|
|
jq '[.results[] | select(.issue_severity=="HIGH")] | .[0:20]' \
|
|
/tmp/bandit-results.json > /tmp/high-severity.json
|
|
|
|
HIGH_COUNT=$(jq '. | length' /tmp/high-severity.json)
|
|
echo "high_count=$HIGH_COUNT" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "high_count=0" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Analyze findings
|
|
id: analyze-findings
|
|
run: |
|
|
if [ -f /tmp/bandit-results.json ]; then
|
|
TOTAL=$(jq '.results | length' /tmp/bandit-results.json)
|
|
HIGH=$(jq '[.results[] | select(.issue_severity=="HIGH")] | length' /tmp/bandit-results.json)
|
|
|
|
if [ "$TOTAL" -gt 0 ]; then
|
|
echo "has_findings=true" >> $GITHUB_OUTPUT
|
|
echo "finding_count=$TOTAL" >> $GITHUB_OUTPUT
|
|
echo "high_severity_count=$HIGH" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "has_findings=false" >> $GITHUB_OUTPUT
|
|
echo "finding_count=0" >> $GITHUB_OUTPUT
|
|
echo "high_severity_count=0" >> $GITHUB_OUTPUT
|
|
fi
|
|
else
|
|
echo "has_findings=false" >> $GITHUB_OUTPUT
|
|
echo "finding_count=0" >> $GITHUB_OUTPUT
|
|
echo "high_severity_count=0" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Calculate cost
|
|
id: calculate-cost
|
|
run: |
|
|
# Bandit only — no API cost
|
|
echo "cost=2.00" >> $GITHUB_OUTPUT
|
|
echo "**Cost**: \$2.00" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload security findings
|
|
if: steps.analyze-findings.outputs.has_findings == 'true'
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: security-findings
|
|
path: |
|
|
/tmp/bandit-results.json
|
|
/tmp/high-severity.json
|
|
retention-days: 7
|
|
|
|
# Job 4: Cost Tracker (5 min)
|
|
cost-tracker:
|
|
name: Cost Tracker & Budget Gates
|
|
runs-on: ubuntu-latest
|
|
needs: [scan-formatting, scan-security]
|
|
timeout-minutes: 5
|
|
outputs:
|
|
total_cost: ${{ steps.calculate.outputs.total }}
|
|
budget_remaining: ${{ steps.calculate.outputs.remaining }}
|
|
budget_exceeded: ${{ steps.calculate.outputs.exceeded }}
|
|
run_p2: ${{ steps.gates.outputs.run_p2 }}
|
|
run_p3: ${{ steps.gates.outputs.run_p3 }}
|
|
|
|
steps:
|
|
- name: Calculate total cost
|
|
id: calculate
|
|
env:
|
|
FORMATTING_COST: ${{ needs.scan-formatting.outputs.cost }}
|
|
SECURITY_COST: ${{ needs.scan-security.outputs.cost }}
|
|
run: |
|
|
FORMATTING="${FORMATTING_COST:-0}"
|
|
SECURITY="${SECURITY_COST:-0}"
|
|
TOTAL=$(echo "$FORMATTING + $SECURITY" | bc)
|
|
REMAINING=$(echo "100 - $TOTAL" | bc)
|
|
|
|
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
|
echo "remaining=$REMAINING" >> $GITHUB_OUTPUT
|
|
|
|
# Check if budget exceeded
|
|
if (( $(echo "$TOTAL > 90" | bc -l) )); then
|
|
echo "exceeded=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "exceeded=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
echo "## Cost Summary" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Formatting**: \$$FORMATTING" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Security**: \$$SECURITY" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Total so far**: \$$TOTAL" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Budget remaining**: \$$REMAINING" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Determine budget gates
|
|
id: gates
|
|
env:
|
|
BUDGET_REMAINING: ${{ steps.calculate.outputs.remaining }}
|
|
SKIP_EXPENSIVE_INPUT: ${{ github.event.inputs.skip_expensive || 'false' }}
|
|
run: |
|
|
REMAINING="${BUDGET_REMAINING:-0}"
|
|
SKIP_EXPENSIVE="$SKIP_EXPENSIVE_INPUT"
|
|
|
|
# P2 (Type Hints) runs if budget >$40 and not manually skipped
|
|
if (( $(echo "$REMAINING > 40" | bc -l) )) && [ "$SKIP_EXPENSIVE" != "true" ]; then
|
|
echo "run_p2=true" >> $GITHUB_OUTPUT
|
|
echo "**P2 (Type Hints)**: Will run" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "run_p2=false" >> $GITHUB_OUTPUT
|
|
echo "**P2 (Type Hints)**: Skipped (budget: \$$REMAINING)" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
# P3 (Documentation) runs if budget >$30 and not manually skipped
|
|
if (( $(echo "$REMAINING > 30" | bc -l) )) && [ "$SKIP_EXPENSIVE" != "true" ]; then
|
|
echo "run_p3=true" >> $GITHUB_OUTPUT
|
|
echo "**P3 (Documentation)**: Will run" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "run_p3=false" >> $GITHUB_OUTPUT
|
|
echo "**P3 (Documentation)**: Skipped (budget: \$$REMAINING)" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
# Job 5: Type Hints Scanner (25 min, P2, Conditional)
|
|
scan-type-hints:
|
|
name: Type Hints Scanner (P2)
|
|
runs-on: ubuntu-latest
|
|
needs: [security-check, cost-tracker]
|
|
if: needs.cost-tracker.outputs.run_p2 == 'true'
|
|
timeout-minutes: 25
|
|
outputs:
|
|
has_suggestions: ${{ steps.analyze.outputs.has_suggestions }}
|
|
has_changes: ${{ steps.detect-changes.outputs.has_changes }}
|
|
functions_annotated: ${{ steps.analyze.outputs.functions_annotated }}
|
|
cost: ${{ steps.calculate-cost.outputs.cost }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Download scannable files
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: scannable-files
|
|
path: /tmp
|
|
|
|
- name: Find functions without type hints
|
|
id: find-functions
|
|
run: |
|
|
echo "## Type Hints Scan" >> $GITHUB_STEP_SUMMARY
|
|
|
|
cat > /tmp/find_missing_hints.py << 'PYTHON'
|
|
import ast
|
|
import json
|
|
from pathlib import Path
|
|
|
|
def find_missing_hints(file_path):
|
|
"""Find functions without type hints."""
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
tree = ast.parse(f.read(), filename=file_path)
|
|
except:
|
|
return []
|
|
|
|
missing = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef):
|
|
if node.returns:
|
|
continue
|
|
if node.name.startswith('_'):
|
|
continue
|
|
missing.append({
|
|
'file': str(file_path),
|
|
'function': node.name,
|
|
'line': node.lineno
|
|
})
|
|
|
|
return missing
|
|
|
|
results = []
|
|
with open('/tmp/scannable_python.txt', 'r') as f:
|
|
files = [line.strip() for line in f if line.strip()]
|
|
|
|
for file_path in files[:100]:
|
|
missing = find_missing_hints(file_path)
|
|
results.extend(missing[:50])
|
|
|
|
if len(results) >= 50:
|
|
break
|
|
|
|
with open('/tmp/missing-hints.json', 'w') as f:
|
|
json.dump(results[:50], f, indent=2)
|
|
|
|
print(f"Found {len(results)} functions without type hints")
|
|
PYTHON
|
|
|
|
python /tmp/find_missing_hints.py
|
|
|
|
MISSING_COUNT=$(jq '. | length' /tmp/missing-hints.json)
|
|
echo "missing_count=$MISSING_COUNT" >> $GITHUB_OUTPUT
|
|
echo "- **Functions without hints**: $MISSING_COUNT" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Install dependencies for type hints generation
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
run: pip install anthropic httpx
|
|
|
|
- name: Generate type hints
|
|
id: generate
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
run: |
|
|
echo "### Type Hints Generation" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "Generating type hints for ${{ steps.find-functions.outputs.missing_count }} functions..." >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
python .github/scripts/generate-type-hints.py \
|
|
/tmp/missing-hints.json \
|
|
/tmp/type-hints-suggestions.json | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
# Cost is estimated from batch count; default if script doesn't report
|
|
echo "type_hints_cost=35.00" >> $GITHUB_OUTPUT
|
|
|
|
- name: Apply type hints to source files
|
|
id: apply
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
run: |
|
|
echo "### Applying Type Hints" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ -f /tmp/type-hints-suggestions.json ]; then
|
|
python .github/scripts/apply-type-hints.py \
|
|
/tmp/type-hints-suggestions.json >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "No suggestions file found, skipping apply step" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Detect type hint changes
|
|
id: detect-changes
|
|
run: |
|
|
if git diff --quiet; then
|
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
echo "files_changed=0" >> $GITHUB_OUTPUT
|
|
echo "No type hint changes applied" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
FILES_CHANGED=$(git diff --name-only | wc -l | tr -d ' ')
|
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT
|
|
|
|
echo "### Changed Files" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create type hints patch
|
|
if: steps.detect-changes.outputs.has_changes == 'true'
|
|
run: |
|
|
git diff > /tmp/type-hints-changes.patch
|
|
echo "Created patch file with $(wc -l < /tmp/type-hints-changes.patch) lines"
|
|
git checkout .
|
|
|
|
- name: Analyze results
|
|
id: analyze
|
|
run: |
|
|
MISSING=${{ steps.find-functions.outputs.missing_count }}
|
|
|
|
if [ "$MISSING" -gt 0 ]; then
|
|
echo "has_suggestions=true" >> $GITHUB_OUTPUT
|
|
echo "functions_annotated=$MISSING" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "has_suggestions=false" >> $GITHUB_OUTPUT
|
|
echo "functions_annotated=0" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Calculate cost
|
|
id: calculate-cost
|
|
run: |
|
|
COST=${{ steps.generate.outputs.type_hints_cost || '35.00' }}
|
|
|
|
echo "cost=$COST" >> $GITHUB_OUTPUT
|
|
echo "**Actual cost**: \$$COST" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload type hints suggestions
|
|
if: steps.analyze.outputs.has_suggestions == 'true'
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: type-hints-suggestions
|
|
path: |
|
|
/tmp/missing-hints.json
|
|
/tmp/type-hints-suggestions.json
|
|
/tmp/type-hints-changes.patch
|
|
retention-days: 7
|
|
|
|
# Job 6: Documentation Scanner (20 min, P3, Conditional)
|
|
scan-documentation:
|
|
name: Documentation Scanner (P3)
|
|
runs-on: ubuntu-latest
|
|
needs: [security-check, cost-tracker]
|
|
if: needs.cost-tracker.outputs.run_p3 == 'true'
|
|
timeout-minutes: 20
|
|
outputs:
|
|
has_suggestions: ${{ steps.analyze.outputs.has_suggestions }}
|
|
has_changes: ${{ steps.detect-changes.outputs.has_changes }}
|
|
functions_documented: ${{ steps.analyze.outputs.functions_documented }}
|
|
cost: ${{ steps.calculate-cost.outputs.cost }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Download scannable files
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: scannable-files
|
|
path: /tmp
|
|
|
|
- name: Find functions without docstrings
|
|
id: find-functions
|
|
run: |
|
|
echo "## Documentation Scan" >> $GITHUB_STEP_SUMMARY
|
|
|
|
cat > /tmp/find_missing_docs.py << 'PYTHON'
|
|
import ast
|
|
import json
|
|
from pathlib import Path
|
|
|
|
def find_missing_docstrings(file_path):
|
|
"""Find functions without docstrings."""
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
tree = ast.parse(f.read(), filename=file_path)
|
|
except:
|
|
return []
|
|
|
|
missing = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef):
|
|
docstring = ast.get_docstring(node)
|
|
if docstring:
|
|
continue
|
|
if node.name.startswith('_') and not node.name.startswith('__'):
|
|
continue
|
|
missing.append({
|
|
'file': str(file_path),
|
|
'function': node.name,
|
|
'line': node.lineno
|
|
})
|
|
|
|
return missing
|
|
|
|
results = []
|
|
with open('/tmp/scannable_python.txt', 'r') as f:
|
|
files = [line.strip() for line in f if line.strip()]
|
|
|
|
for file_path in files[:80]:
|
|
missing = find_missing_docstrings(file_path)
|
|
results.extend(missing[:40])
|
|
|
|
if len(results) >= 40:
|
|
break
|
|
|
|
with open('/tmp/missing-docs.json', 'w') as f:
|
|
json.dump(results[:40], f, indent=2)
|
|
|
|
print(f"Found {len(results)} functions without docstrings")
|
|
PYTHON
|
|
|
|
python /tmp/find_missing_docs.py
|
|
|
|
MISSING_COUNT=$(jq '. | length' /tmp/missing-docs.json)
|
|
echo "missing_count=$MISSING_COUNT" >> $GITHUB_OUTPUT
|
|
echo "- **Functions without docstrings**: $MISSING_COUNT" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Install dependencies for docstring generation
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
run: pip install anthropic httpx
|
|
|
|
- name: Generate docstrings
|
|
id: generate
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
run: |
|
|
echo "### Docstring Generation" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "Generating docstrings for ${{ steps.find-functions.outputs.missing_count }} functions..." >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
python .github/scripts/generate-docstrings.py \
|
|
/tmp/missing-docs.json \
|
|
/tmp/documentation-suggestions.json | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
# Cost is estimated from batch count; default if script doesn't report
|
|
echo "docstring_cost=25.00" >> $GITHUB_OUTPUT
|
|
|
|
- name: Apply docstrings to source files
|
|
id: apply
|
|
if: steps.find-functions.outputs.missing_count > 0
|
|
run: |
|
|
echo "### Applying Docstrings" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ -f /tmp/documentation-suggestions.json ]; then
|
|
python .github/scripts/apply-docstrings.py \
|
|
/tmp/documentation-suggestions.json >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "No suggestions file found, skipping apply step" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Detect documentation changes
|
|
id: detect-changes
|
|
run: |
|
|
if git diff --quiet; then
|
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
echo "files_changed=0" >> $GITHUB_OUTPUT
|
|
echo "No documentation changes applied" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
FILES_CHANGED=$(git diff --name-only | wc -l | tr -d ' ')
|
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT
|
|
|
|
echo "### Changed Files" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create documentation patch
|
|
if: steps.detect-changes.outputs.has_changes == 'true'
|
|
run: |
|
|
git diff > /tmp/documentation-changes.patch
|
|
echo "Created patch file with $(wc -l < /tmp/documentation-changes.patch) lines"
|
|
git checkout .
|
|
|
|
- name: Analyze results
|
|
id: analyze
|
|
run: |
|
|
MISSING=${{ steps.find-functions.outputs.missing_count }}
|
|
|
|
if [ "$MISSING" -gt 0 ]; then
|
|
echo "has_suggestions=true" >> $GITHUB_OUTPUT
|
|
echo "functions_documented=$MISSING" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "has_suggestions=false" >> $GITHUB_OUTPUT
|
|
echo "functions_documented=0" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Calculate cost
|
|
id: calculate-cost
|
|
run: |
|
|
COST=${{ steps.generate.outputs.docstring_cost || '25.00' }}
|
|
|
|
echo "cost=$COST" >> $GITHUB_OUTPUT
|
|
echo "**Actual cost**: \$$COST" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload documentation suggestions
|
|
if: steps.analyze.outputs.has_suggestions == 'true'
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: documentation-suggestions
|
|
path: |
|
|
/tmp/missing-docs.json
|
|
/tmp/documentation-suggestions.json
|
|
/tmp/documentation-changes.patch
|
|
retention-days: 7
|
|
|
|
# Job 7: PR Creation (15 min)
|
|
create-prs:
|
|
name: Create Pull Requests
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
needs:
|
|
- security-check
|
|
- scan-formatting
|
|
- scan-security
|
|
- cost-tracker
|
|
- scan-type-hints
|
|
- scan-documentation
|
|
if: |
|
|
always() &&
|
|
!contains(needs.*.result, 'failure') &&
|
|
!contains(needs.*.result, 'cancelled')
|
|
timeout-minutes: 15
|
|
outputs:
|
|
pr_count: ${{ steps.final-summary.outputs.pr_count }}
|
|
total_cost: ${{ steps.final-summary.outputs.total_cost }}
|
|
remaining_budget: ${{ steps.final-summary.outputs.remaining_budget }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
fetch-depth: 0
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: /tmp/artifacts
|
|
|
|
- name: Configure git
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Apply formatting patch
|
|
if: needs.scan-formatting.outputs.has_changes == 'true'
|
|
run: |
|
|
echo "## Applying Formatting Patch" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ -f /tmp/artifacts/formatting-changes/formatting-changes.patch ]; then
|
|
if git apply --check /tmp/artifacts/formatting-changes/formatting-changes.patch 2>/dev/null; then
|
|
git apply /tmp/artifacts/formatting-changes/formatting-changes.patch
|
|
echo "Formatting patch applied successfully" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "::error::Failed to apply formatting patch"
|
|
echo "Formatting patch application failed" >> $GITHUB_STEP_SUMMARY
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "::warning::Formatting patch file not found"
|
|
echo "Formatting patch file not found" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Verify changes before PR
|
|
if: |
|
|
needs.scan-formatting.outputs.has_changes == 'true' ||
|
|
needs.scan-security.outputs.has_findings == 'true' ||
|
|
(needs.scan-type-hints.result == 'success' && needs.scan-type-hints.outputs.has_changes == 'true') ||
|
|
(needs.scan-documentation.result == 'success' && needs.scan-documentation.outputs.has_changes == 'true')
|
|
env:
|
|
HAS_FORMATTING_CHANGES: ${{ needs.scan-formatting.outputs.has_changes }}
|
|
run: |
|
|
echo "## Verifying Changes" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ "$HAS_FORMATTING_CHANGES" == "true" ]; then
|
|
if git diff --quiet; then
|
|
echo "::error::Expected formatting changes but working tree is clean"
|
|
echo "No changes detected after patch apply" >> $GITHUB_STEP_SUMMARY
|
|
exit 1
|
|
fi
|
|
echo "Formatting changes detected, ready to create PR" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Changed files:**" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "Validation passed (no formatting changes)" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Pre-PR validation
|
|
id: validate
|
|
run: |
|
|
echo "## Pre-PR Validation" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# 1. Secret scanning
|
|
SECRETS_FOUND=$(git diff | grep '^+' | grep -v '^+++' | grep -iE \
|
|
'(sk-[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{12,}|(api[_-]?key|password|secret|token)\s*[:=]\s*["\x27][a-zA-Z0-9].{8,}["\x27])' || true)
|
|
if [ -n "$SECRETS_FOUND" ]; then
|
|
echo "::error::Potential secret detected in diff"
|
|
echo "$SECRETS_FOUND" | head -5 >> $GITHUB_STEP_SUMMARY
|
|
echo "**Secret scan**: FAILED" >> $GITHUB_STEP_SUMMARY
|
|
exit 1
|
|
fi
|
|
echo "**Secret scan**: Passed" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# 2. Python syntax check
|
|
if git diff --name-only | grep -q '\.py$'; then
|
|
find . -name "*.py" -type f | while read -r file; do
|
|
if ! python3 -m py_compile "$file" 2>/dev/null; then
|
|
echo "::error::Syntax error in $file"
|
|
echo "**Syntax check**: FAILED ($file)" >> $GITHUB_STEP_SUMMARY
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "**Syntax check**: Passed" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
# 3. Diff size check
|
|
if git diff --quiet; then
|
|
LINES=0
|
|
else
|
|
LINES=$(git diff --stat | tail -1 | awk '{print $4+$6}')
|
|
fi
|
|
|
|
echo "diff_lines=$LINES" >> $GITHUB_OUTPUT
|
|
|
|
if [ "$LINES" -gt 5000 ]; then
|
|
echo "**Diff size**: Large ($LINES lines - requires extra review)" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "**Diff size**: $LINES lines" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create PR - Formatting
|
|
id: pr-formatting
|
|
if: |
|
|
needs.scan-formatting.outputs.has_changes == 'true' &&
|
|
github.event.inputs.dry_run != 'true'
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
branch: scanner/formatting-${{ github.run_id }}
|
|
draft: true
|
|
delete-branch: true
|
|
title: "[Auto-Scan] Formatting & Linting Fixes"
|
|
body: |
|
|
## Automated Formatting Fixes
|
|
|
|
**Run ID**: ${{ github.run_id }}
|
|
**Files changed**: ${{ needs.scan-formatting.outputs.files_changed }}
|
|
**Tool**: Ruff (format + check --fix)
|
|
**Cost**: $0.50
|
|
|
|
### Changes
|
|
|
|
This PR applies automated formatting and linting fixes using Ruff:
|
|
- Code formatting (line length, indentation, etc.)
|
|
- Auto-fixable linting issues (unused imports, etc.)
|
|
|
|
### Review Instructions
|
|
|
|
1. Review changed files for correctness
|
|
2. Run tests: `cd ods && make test`
|
|
3. Approve and merge if tests pass
|
|
|
|
### Safety
|
|
|
|
- Protected files excluded from scanning
|
|
- All changes are formatting-only (no logic changes)
|
|
- Draft PR requires manual approval
|
|
|
|
---
|
|
*Generated by Autonomous Code Scanner*
|
|
|
|
- name: Label PR - Formatting
|
|
if: steps.pr-formatting.outputs.pull-request-number
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ steps.pr-formatting.outputs.pull-request-number }}
|
|
run: gh pr edit "$PR_NUMBER" --add-label "auto-formatting,ai-generated,needs-human-review"
|
|
|
|
- name: Create PR - Security
|
|
id: pr-security
|
|
if: |
|
|
needs.scan-security.outputs.has_findings == 'true' &&
|
|
github.event.inputs.dry_run != 'true'
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
branch: scanner/security-${{ github.run_id }}
|
|
draft: true
|
|
delete-branch: true
|
|
title: "[Auto-Scan] Security Findings - REQUIRES REVIEW"
|
|
body: |
|
|
## Security Vulnerabilities Found
|
|
|
|
**Run ID**: ${{ github.run_id }}
|
|
**Findings**: ${{ needs.scan-security.outputs.finding_count }}
|
|
**High severity**: ${{ needs.scan-security.outputs.high_severity_count }}
|
|
**Cost**: ${{ needs.scan-security.outputs.cost }}
|
|
|
|
### CRITICAL: Manual Security Review Required
|
|
|
|
This PR contains potential security vulnerabilities detected by Bandit.
|
|
|
|
**DO NOT merge without:**
|
|
1. Manual review of each finding
|
|
2. Validation that fixes don't break functionality
|
|
3. Security testing of affected code paths
|
|
4. Second pair of eyes review
|
|
|
|
### Findings Summary
|
|
|
|
See attached artifacts for detailed findings:
|
|
- `bandit-results.json` - Full Bandit scan results
|
|
- `high-severity.json` - High severity findings only
|
|
|
|
---
|
|
*Generated by Autonomous Code Scanner*
|
|
|
|
- name: Label PR - Security
|
|
if: steps.pr-security.outputs.pull-request-number
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ steps.pr-security.outputs.pull-request-number }}
|
|
run: gh pr edit "$PR_NUMBER" --add-label "auto-security,needs-human-review"
|
|
|
|
- name: Reset checkout before type hints
|
|
if: |
|
|
needs.scan-type-hints.result == 'success' &&
|
|
needs.scan-type-hints.outputs.has_changes == 'true'
|
|
run: git checkout -- .
|
|
|
|
- name: Apply type hints patch
|
|
if: |
|
|
needs.scan-type-hints.result == 'success' &&
|
|
needs.scan-type-hints.outputs.has_changes == 'true'
|
|
run: |
|
|
echo "## Applying Type Hints Patch" >> $GITHUB_STEP_SUMMARY
|
|
|
|
PATCH_FILE="/tmp/artifacts/type-hints-suggestions/type-hints-changes.patch"
|
|
if [ -f "$PATCH_FILE" ]; then
|
|
if git apply --check "$PATCH_FILE" 2>/dev/null; then
|
|
git apply "$PATCH_FILE"
|
|
echo "Type hints patch applied successfully" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "::warning::Type hints patch could not be applied cleanly"
|
|
echo "Type hints patch application failed" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
else
|
|
echo "::warning::Type hints patch file not found"
|
|
echo "Type hints patch file not found" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create PR - Type Hints
|
|
id: pr-type-hints
|
|
if: |
|
|
needs.scan-type-hints.result == 'success' &&
|
|
needs.scan-type-hints.outputs.has_changes == 'true' &&
|
|
github.event.inputs.dry_run != 'true'
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
branch: scanner/type-hints-${{ github.run_id }}
|
|
draft: true
|
|
delete-branch: true
|
|
title: "[Auto-Scan] Type Hints Improvements"
|
|
body: |
|
|
## Type Hints Improvements
|
|
|
|
**Run ID**: ${{ github.run_id }}
|
|
**Functions annotated**: ${{ needs.scan-type-hints.outputs.functions_annotated }}
|
|
**Cost**: ${{ needs.scan-type-hints.outputs.cost }}
|
|
|
|
### Changes
|
|
|
|
This PR adds type annotations to function signatures that were missing them.
|
|
Types were generated by Claude Haiku 4.5 and applied via AST-based function lookup.
|
|
|
|
- Parameter type annotations added
|
|
- Return type annotations added
|
|
- Required imports inserted (deduplicated against existing imports)
|
|
- All modified files validated with `py_compile`
|
|
|
|
### Review Instructions
|
|
|
|
1. Verify type hints are accurate for each function
|
|
2. Run tests: `cd ods && make test`
|
|
3. Approve and merge if checks pass
|
|
|
|
### Safety
|
|
|
|
- Protected files excluded from scanning
|
|
- AST-based lookup ensures correct function targeting
|
|
- Files that fail `py_compile` after modification are automatically reverted
|
|
- Draft PR requires manual approval
|
|
|
|
---
|
|
*Generated by Autonomous Code Scanner*
|
|
|
|
- name: Label PR - Type Hints
|
|
if: steps.pr-type-hints.outputs.pull-request-number
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ steps.pr-type-hints.outputs.pull-request-number }}
|
|
run: gh pr edit "$PR_NUMBER" --add-label "auto-type-hints,ai-generated,needs-human-review"
|
|
|
|
- name: Reset checkout before documentation
|
|
if: |
|
|
needs.scan-documentation.result == 'success' &&
|
|
needs.scan-documentation.outputs.has_changes == 'true'
|
|
run: git checkout -- .
|
|
|
|
- name: Apply documentation patch
|
|
if: |
|
|
needs.scan-documentation.result == 'success' &&
|
|
needs.scan-documentation.outputs.has_changes == 'true'
|
|
run: |
|
|
echo "## Applying Documentation Patch" >> $GITHUB_STEP_SUMMARY
|
|
|
|
PATCH_FILE="/tmp/artifacts/documentation-suggestions/documentation-changes.patch"
|
|
if [ -f "$PATCH_FILE" ]; then
|
|
if git apply --check "$PATCH_FILE" 2>/dev/null; then
|
|
git apply "$PATCH_FILE"
|
|
echo "Documentation patch applied successfully" >> $GITHUB_STEP_SUMMARY
|
|
git diff --stat >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "::warning::Documentation patch could not be applied cleanly"
|
|
echo "Documentation patch application failed" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
else
|
|
echo "::warning::Documentation patch file not found"
|
|
echo "Documentation patch file not found" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create PR - Documentation
|
|
id: pr-documentation
|
|
if: |
|
|
needs.scan-documentation.result == 'success' &&
|
|
needs.scan-documentation.outputs.has_changes == 'true' &&
|
|
github.event.inputs.dry_run != 'true'
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
branch: scanner/documentation-${{ github.run_id }}
|
|
draft: true
|
|
delete-branch: true
|
|
title: "[Auto-Scan] Documentation Improvements"
|
|
body: |
|
|
## Documentation Improvements
|
|
|
|
**Run ID**: ${{ github.run_id }}
|
|
**Functions documented**: ${{ needs.scan-documentation.outputs.functions_documented }}
|
|
**Cost**: ${{ needs.scan-documentation.outputs.cost }}
|
|
|
|
### Changes
|
|
|
|
This PR adds Google-style docstrings to functions that were missing documentation.
|
|
Docstrings were generated by Claude Haiku 4.5 and inserted via AST-based function lookup.
|
|
|
|
- Summary lines in imperative mood
|
|
- Args/Returns/Raises sections where applicable
|
|
- Functions with existing docstrings are skipped
|
|
- All modified files validated with `py_compile`
|
|
|
|
### Review Instructions
|
|
|
|
1. Verify docstrings are accurate and helpful
|
|
2. Check formatting follows Google style
|
|
3. Run tests: `cd ods && make test`
|
|
4. Approve and merge if documentation is clear
|
|
|
|
### Safety
|
|
|
|
- Protected files excluded from scanning
|
|
- Functions with existing docstrings are never overwritten
|
|
- Files that fail `py_compile` after modification are automatically reverted
|
|
- Draft PR requires manual approval
|
|
|
|
---
|
|
*Generated by Autonomous Code Scanner*
|
|
|
|
- name: Label PR - Documentation
|
|
if: steps.pr-documentation.outputs.pull-request-number
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ steps.pr-documentation.outputs.pull-request-number }}
|
|
run: gh pr edit "$PR_NUMBER" --add-label "auto-documentation,ai-generated,needs-human-review"
|
|
|
|
- name: Generate final summary
|
|
id: final-summary
|
|
if: always()
|
|
env:
|
|
RUN_ID: ${{ github.run_id }}
|
|
HAS_FORMATTING: ${{ needs.scan-formatting.outputs.has_changes }}
|
|
HAS_SECURITY: ${{ needs.scan-security.outputs.has_findings }}
|
|
HAS_TYPE_HINTS: ${{ needs.scan-type-hints.outputs.has_changes }}
|
|
HAS_DOCS: ${{ needs.scan-documentation.outputs.has_changes }}
|
|
FORMATTING_COST: ${{ needs.scan-formatting.outputs.cost }}
|
|
SECURITY_COST: ${{ needs.scan-security.outputs.cost }}
|
|
TYPE_HINTS_COST: ${{ needs.scan-type-hints.outputs.cost }}
|
|
DOCS_COST: ${{ needs.scan-documentation.outputs.cost }}
|
|
IS_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
|
run: |
|
|
echo "# Autonomous Code Scanner Results" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Scan date**: $(date -u '+%Y-%m-%d %H:%M UTC')" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Run ID**: $RUN_ID" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
echo "## PRs Created" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
PR_COUNT=0
|
|
|
|
if [ "$HAS_FORMATTING" = "true" ]; then
|
|
echo "- **Formatting**: Created (Branch: scanner/formatting-$RUN_ID)" >> $GITHUB_STEP_SUMMARY
|
|
PR_COUNT=$((PR_COUNT + 1))
|
|
fi
|
|
|
|
if [ "$HAS_SECURITY" = "true" ]; then
|
|
echo "- **Security**: Created - NEEDS REVIEW (Branch: scanner/security-$RUN_ID)" >> $GITHUB_STEP_SUMMARY
|
|
PR_COUNT=$((PR_COUNT + 1))
|
|
fi
|
|
|
|
if [ "$HAS_TYPE_HINTS" = "true" ]; then
|
|
echo "- **Type Hints**: Created (Branch: scanner/type-hints-$RUN_ID)" >> $GITHUB_STEP_SUMMARY
|
|
PR_COUNT=$((PR_COUNT + 1))
|
|
fi
|
|
|
|
if [ "$HAS_DOCS" = "true" ]; then
|
|
echo "- **Documentation**: Created (Branch: scanner/documentation-$RUN_ID)" >> $GITHUB_STEP_SUMMARY
|
|
PR_COUNT=$((PR_COUNT + 1))
|
|
fi
|
|
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Total PRs**: $PR_COUNT" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
echo "pr_count=$PR_COUNT" >> $GITHUB_OUTPUT
|
|
|
|
echo "## Cost Summary" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
FORMATTING="${FORMATTING_COST:-0}"
|
|
SECURITY="${SECURITY_COST:-0}"
|
|
TYPES="${TYPE_HINTS_COST:-0}"
|
|
DOCS="${DOCS_COST:-0}"
|
|
|
|
TOTAL=$(echo "$FORMATTING + $SECURITY + $TYPES + $DOCS" | bc)
|
|
REMAINING=$(echo "100 - $TOTAL" | bc)
|
|
|
|
echo "total_cost=$TOTAL" >> $GITHUB_OUTPUT
|
|
echo "remaining_budget=$REMAINING" >> $GITHUB_OUTPUT
|
|
|
|
echo "| Scanner | Cost |" >> $GITHUB_STEP_SUMMARY
|
|
echo "|---------|------|" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Formatting | \$$FORMATTING |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Security | \$$SECURITY |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Type Hints | \$$TYPES |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Docs | \$$DOCS |" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Total spend**: \$$TOTAL" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Budget**: \$100" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Remaining**: \$$REMAINING" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ "$IS_DRY_RUN" = "true" ]; then
|
|
echo "**Dry run mode**: No PRs were created" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
- name: Create issue on workflow failure
|
|
if: failure()
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
script: |
|
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: 'Autonomous Code Scanner Failed',
|
|
labels: ['bug'],
|
|
body: `## Workflow Failure Alert
|
|
|
|
The Autonomous Code Scanner workflow failed during execution.
|
|
|
|
**Run ID**: ${context.runId}
|
|
**Triggered by**: ${context.eventName}
|
|
**Timestamp**: ${new Date().toISOString()}
|
|
|
|
### Action Required
|
|
|
|
1. Review the [workflow logs](${runUrl}) for error details
|
|
2. Check if API keys are valid
|
|
3. Verify budget limits weren't exceeded
|
|
|
|
[View Full Logs](${runUrl})
|
|
|
|
---
|
|
*This issue was automatically created by the Autonomous Code Scanner workflow.*`
|
|
});
|
|
|
|
- name: Create issue for consecutive no-change runs
|
|
if: |
|
|
always() &&
|
|
steps.final-summary.outputs.pr_count == '0'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
script: |
|
|
// Check if we've had 7+ consecutive no-change runs
|
|
const runs = await github.rest.actions.listWorkflowRuns({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
workflow_id: 'autonomous-code-scanner.yml',
|
|
per_page: 7,
|
|
status: 'completed'
|
|
});
|
|
|
|
let consecutiveNoChange = 0;
|
|
for (const run of runs.data.workflow_runs) {
|
|
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: run.id
|
|
});
|
|
|
|
const createPrJob = jobs.data.jobs.find(j => j.name === 'Create Pull Requests');
|
|
if (createPrJob && createPrJob.conclusion === 'success') {
|
|
const hadChanges = createPrJob.steps.some(s =>
|
|
s.name.includes('Create PR') && s.conclusion === 'success'
|
|
);
|
|
|
|
if (!hadChanges) {
|
|
consecutiveNoChange++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (consecutiveNoChange >= 7) {
|
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
|
|
const existingIssues = await github.rest.issues.listForRepo({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
labels: 'scanner-no-changes',
|
|
state: 'open'
|
|
});
|
|
|
|
if (existingIssues.data.length === 0) {
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: 'Autonomous Code Scanner: 7+ Consecutive No-Change Runs',
|
|
labels: ['bug', 'scanner-no-changes'],
|
|
body: `## Configuration Alert
|
|
|
|
The Autonomous Code Scanner has completed **${consecutiveNoChange} consecutive runs** without creating any PRs.
|
|
|
|
### Possible Causes
|
|
|
|
1. **Codebase is clean**: All code meets quality standards
|
|
2. **Scanner configuration issue**: Scanners may not be detecting issues
|
|
3. **Protected files**: Too many files excluded from scanning
|
|
4. **Threshold too high**: Quality bars set too strict
|
|
|
|
### Recommended Actions
|
|
|
|
1. Manually run formatters/linters to verify scanner is working
|
|
2. Review protected file patterns in workflow
|
|
3. Check if scanners are running (not skipped due to budget)
|
|
|
|
[View latest run](${runUrl})
|
|
|
|
---
|
|
*This issue was automatically created by the Autonomous Code Scanner workflow.*`
|
|
});
|
|
}
|
|
}
|