diff --git a/.github/scripts/generate_coverage_comment.py b/.github/scripts/generate_coverage_comment.py new file mode 100755 index 0000000..261d136 --- /dev/null +++ b/.github/scripts/generate_coverage_comment.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Generate coverage comment markdown from coverage data files. + +This script aggregates coverage data from multiple JSON files and generates +a markdown comment suitable for posting to GitHub PRs. + +Usage: + python generate_coverage_comment.py + +Input JSON format (per file): + { + "pr_number": 123, + "component": "Backend", + "pr_coverage": 85.5, + "main_coverage": 82.0, + "diff": 3.5 + } + +Output: + Markdown file written to stdout +""" + +import json +import re +import sys +from pathlib import Path +from typing import Any, Dict, List + + +def sanitize_component_name(name: str) -> str: + """ + Sanitize component name to prevent injection attacks. + + Allows only alphanumeric characters, spaces, hyphens, and underscores. + Truncates to 50 characters. + """ + # Remove any non-safe characters and strip whitespace + sanitized = re.sub(r'[^a-zA-Z0-9 _-]', '', name).strip() + # Truncate to 50 chars + return sanitized[:50] if sanitized else "Unknown" + + +def validate_number(value: Any, default: float = 0.0) -> float: + """ + Validate that a value is a valid number. + + Returns the float value if valid, otherwise returns default. + """ + try: + num = float(value) + # Check if it's a valid float (not NaN, not Inf) + if num != num or num == float('inf') or num == float('-inf'): + return default + return num + except (ValueError, TypeError): + return default + + +def format_diff(diff: float) -> str: + """ + Format diff value with appropriate sign. + + Args: + diff: The difference value (can be positive, negative, or zero) + + Returns: + Formatted string like "+3.5%" or "-2.1%" or "0.0%" + """ + if diff > 0: + return f"+{diff:.1f}%" + elif diff < 0: + return f"{diff:.1f}%" + else: + return "0.0%" + + +def load_coverage_data(coverage_dir: Path) -> List[Dict]: + """ + Load and validate coverage data from all JSON files in directory. + + Args: + coverage_dir: Path to directory containing coverage JSON files + + Returns: + List of validated coverage data dictionaries + """ + coverage_data = [] + + # Find all JSON files in directory + json_files = list(coverage_dir.glob("*.json")) + + if not json_files: + print("⚠️ No coverage JSON files found", file=sys.stderr) + return coverage_data + + for json_file in json_files: + try: + with open(json_file, 'r') as f: + data = json.load(f) + + # Validate required fields + component = sanitize_component_name(data.get('component', 'Unknown')) + pr_coverage = validate_number(data.get('pr_coverage', 0)) + main_coverage = validate_number(data.get('main_coverage', 0)) + diff = validate_number(data.get('diff', 0)) + + # Only include if we have valid data + if component != "Unknown" or pr_coverage > 0: + coverage_data.append({ + 'component': component, + 'pr_coverage': pr_coverage, + 'main_coverage': main_coverage, + 'diff': diff + }) + + except (json.JSONDecodeError, IOError) as e: + print(f"⚠️ Failed to read {json_file}: {e}", file=sys.stderr) + continue + + return coverage_data + + +def generate_coverage_markdown( + coverage_data: List[Dict], + workflow_run_id: str, + workflow_run_url: str +) -> str: + """ + Generate markdown table from coverage data. + + Args: + coverage_data: List of coverage data dictionaries + workflow_run_id: GitHub workflow run ID + workflow_run_url: GitHub workflow run URL + + Returns: + Markdown string + """ + lines = [ + "## πŸ“Š Test Coverage Report", + "", + "Coverage results from automated tests:", + "", + "| Component | Coverage | Change |", + "|-----------|----------|--------|" + ] + + # Sort by component name for consistent ordering + sorted_data = sorted(coverage_data, key=lambda x: x['component']) + + for item in sorted_data: + component = item['component'] + pr_cov = item['pr_coverage'] + diff = item['diff'] + diff_display = format_diff(diff) + + lines.append(f"| {component} | {pr_cov:.1f}% | {diff_display} |") + + lines.extend([ + "", + f"*Coverage generated from workflow run [#{workflow_run_id}]({workflow_run_url})*" + ]) + + return "\n".join(lines) + + +def main(): + """Main entry point.""" + if len(sys.argv) != 4: + print("Usage: generate_coverage_comment.py ", file=sys.stderr) + sys.exit(1) + + coverage_dir = Path(sys.argv[1]) + workflow_run_id = sys.argv[2] + workflow_run_url = sys.argv[3] + + # Validate coverage directory exists + if not coverage_dir.exists(): + print(f"❌ Coverage directory not found: {coverage_dir}", file=sys.stderr) + sys.exit(1) + + # Load coverage data + coverage_data = load_coverage_data(coverage_dir) + + if not coverage_data: + print("❌ No valid coverage data found", file=sys.stderr) + sys.exit(1) + + # Generate markdown + markdown = generate_coverage_markdown(coverage_data, workflow_run_id, workflow_run_url) + + # Output to stdout + print(markdown) + + # Success message to stderr (so it doesn't pollute markdown output) + print(f"βœ… Generated coverage comment for {len(coverage_data)} component(s)", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..37a77da --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,589 @@ +# GitHub Actions Workflows + +This directory contains GitHub Actions workflows demonstrating modern CI/CD patterns for AI-assisted development. All workflows follow best practices for security, performance, and maintainability. + +## Overview + +**Total Workflows**: 11 (10 functional + 1 demonstration) + +All workflows implement: +- βœ… Concurrency controls (prevent queue backlog, save CI minutes) +- βœ… GitHub step summaries (enhanced UX in Actions UI) +- βœ… Minimal necessary permissions (security best practice) +- βœ… Clear naming and documentation + +--- + +## Core Validation Workflows + +### [ci.yml](ci.yml) + +**Purpose**: Documentation structure validation and repomap currency checks + +**Triggers**: +- Push to `main` branch +- Pull requests to `main` +- Manual workflow dispatch + +**Jobs**: +1. **documentation-structure** - Verifies required docs exist +2. **repomap-validation** - Ensures `.repomap.txt` is current + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) + +**Why it exists**: Ensures codebase structure remains consistent and context maps are up-to-date for AI-assisted development. + +--- + +### [validate.yml](validate.yml) + +**Purpose**: Autonomous Quality Enforcement (AQE) checks + +**Triggers**: +- Push to `main` or `validation/*` branches +- Pull requests to `main` +- Manual workflow dispatch + +**Jobs**: +1. **aqe-validation** - Quality checks via `.github/scripts/check.sh` +2. **documentation-structure** - Documentation existence checks +3. **validation-summary** - GitHub step summary of results + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) + +**Why it exists**: Enforces quality gates before human review, demonstrates AQE pattern. + +--- + +### [docs-validation.yml](docs-validation.yml) + +**Purpose**: Documentation quality validation + +**Triggers**: +- Push to `main` with doc changes (`docs/**`, `**/*.md`) +- Pull requests to `main` with doc changes +- Runs only when documentation files change + +**Jobs**: +1. **validate-mermaid** - Validates Mermaid diagram syntax +2. **lint-markdown** - Lints markdown files with markdownlint-cli2 +3. **validation-summary** - Summary with pass/fail status + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) + +**Why it exists**: User noted "Mermaid diagrams always have errors" - this catches them in CI. + +--- + +## Security Workflows + +### [security.yml](security.yml) + +**Purpose**: Security scanning and vulnerability detection + +**Triggers**: +- Push to `main` +- Pull requests to `main` +- Weekly schedule (Sunday midnight UTC) + +**Jobs**: +1. **codeql** - Multi-language static analysis (Python, JavaScript) + - Matrix strategy for parallel execution + - Integrates with GitHub Security tab + - Uses `security-extended` query pack +2. **security-scan** - Pattern-based security checks + - Secrets in documentation detection + - Hardcoded URL credentials detection + - `.env` file commit prevention +3. **security-summary** - Consolidated results with GitHub step summary + +**Permissions**: +- `contents: read` (checkout code) +- `security-events: write` (upload CodeQL results) +- `actions: read` (access workflow metadata) + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) + +**Why it exists**: Demonstrates industry-standard security scanning, finds real issues in doc examples and workflows. + +--- + +## Testing Workflows + +### [e2e-pattern-tests.yml](e2e-pattern-tests.yml) + +**Purpose**: End-to-end validation of all 11 pattern implementations + +**Triggers**: +- Push to `main` (when patterns, workflows, or E2E test scripts change) +- Pull requests to `main` (same path filters) +- Manual workflow dispatch + +**Path Filters**: Optimized to run only when relevant files change: +- Pattern documentation (`docs/patterns/**`) +- Workflow files (`.github/workflows/**`) - patterns reference actual workflows +- E2E test scripts (`.github/scripts/e2e-tests/**`) + +⚠️ **Path Filter Limitation**: If test logic changes in ways not covered by these paths (e.g., updating test expectations in untracked files), tests won't auto-trigger. Use `workflow_dispatch` (manual trigger) for comprehensive validation in these cases. + +**Jobs**: +- **pattern-1-aqe** through **pattern-11-testing** (parallel execution) +- **summary** - Consolidated results table with pass/fail status + +**Test Patterns**: +1. AQE (Autonomous Quality Enforcement) +2. CBA (Codebase Agent) +3. Dependabot +4. GitHub Actions +5. Issue-to-PR +6. Multi-Agent +7. PR Review +8. Security +9. Self-Review +10. Stale Management +11. Testing + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) + +**Why it exists**: Ensures all pattern documentation is accurate and examples work correctly. + +--- + +## Automation Workflows + +### [issue-to-pr.yml](issue-to-pr.yml) + +**Purpose**: Converts labeled issues to draft PRs automatically + +**Triggers**: +- Issue labeled with `ready-for-pr` + +**Workflow**: +1. Analyzes issue for clarity (acceptance criteria, requirements) +2. If vague: Requests clarification via comment +3. If clear: Creates feature branch and draft PR + +**Permissions**: +- `contents: write` (create branches) +- `pull-requests: write` (create PRs) +- `issues: write` (post comments) + +**Concurrency**: Does NOT cancel in-progress (`cancel-in-progress: false`) +- Rationale: Don't interrupt PR creation mid-process + +**Why it exists**: Demonstrates Issue-to-PR automation pattern. + +--- + +### [pr-review.yml](pr-review.yml) + +**Purpose**: Automated PR security and quality reviews + +**Triggers**: +- PR opened, synchronized, or marked ready for review +- Skips draft PRs and PRs with `skip-review` label + +**Checks**: +1. **Security Review**: + - Hardcoded secrets detection + - `.env` file additions + - TODOs in security-sensitive code +2. **Code Quality Review**: + - Large file changes (>100 lines) + - Missing test coverage + +**Permissions**: +- `contents: read` (read code) +- `pull-requests: write` (post comments) + +**Concurrency**: Cancels outdated runs (`cancel-in-progress: true`) +- Rationale: Re-review on each push, cancel old reviews + +**Why it exists**: Provides fast automated feedback before human review. + +--- + +### [dependabot-auto-merge.yml](dependabot-auto-merge.yml) + +**Purpose**: Automatically merges Dependabot patch updates + +**Triggers**: +- Pull request opened, synchronized, reopened, or labeled +- Only processes PRs from `dependabot[bot]` + +**Strategy**: +- **Auto-merge**: `semver-patch` updates (e.g., 1.2.3 β†’ 1.2.4) +- **Comment only**: `semver-minor` and `semver-major` updates (requires human review) + +**Permissions**: +- `contents: write` (merge PRs) +- `pull-requests: write` (post comments) + +**Concurrency**: Does NOT cancel in-progress (`cancel-in-progress: false`) +- Rationale: Don't interrupt merge operations + +**Why it exists**: Reduces maintenance burden for low-risk dependency updates. + +--- + +### [stale.yml](stale.yml) + +**Purpose**: Automated stale issue and PR management + +**Triggers**: +- Daily at midnight UTC +- Manual workflow dispatch + +**Configuration**: +- **Issues**: 30 days inactive β†’ stale, 7 days stale β†’ close +- **PRs**: 14 days inactive β†’ stale, 7 days stale β†’ close +- **Exemptions**: Issues/PRs with `pinned`, `security`, `bug`, `help-wanted` labels +- **Assignees**: Never mark as stale if assigned + +**Permissions**: +- `issues: write` (label/close issues) +- `pull-requests: write` (label/close PRs) + +**Concurrency**: Does NOT cancel in-progress (`cancel-in-progress: false`) +- Rationale: Scheduled jobs should complete, not race with each other + +**Why it exists**: Keeps issue tracker clean without manual triage. + +--- + +## Deployment Workflows + +### [deploy-docs.yml](deploy-docs.yml) + +**Purpose**: Deploys documentation to GitHub Pages + +**Triggers**: +- Push to `main` with doc changes (`docs/**`, `mkdocs.yml`, workflow file) +- Manual workflow dispatch + +**Jobs**: +1. **build** - Builds MkDocs site with Material theme +2. **deploy** - Deploys to GitHub Pages + +**Permissions**: +- `contents: read` (read documentation) +- `pages: write` (deploy to GitHub Pages) +- `id-token: write` (OIDC for deployment) + +**Concurrency**: +- Group: `pages` +- Does NOT cancel in-progress (`cancel-in-progress: false`) +- Rationale: Complete deployment before starting new one + +**Why it exists**: Automatically publishes documentation changes to public site. + +--- + +## Demonstration Workflows + +### [coverage-comment.yml](coverage-comment.yml) ⚠️ DEMONSTRATION ONLY + +**Purpose**: Demonstrates fork PR coverage comment pattern + +**Status**: **NOT FUNCTIONAL** - Requires coverage collection to be added to test workflows + +**Triggers**: +- After `E2E Pattern Tests` workflow completes + +**Pattern Demonstrated**: +1. **Fork PR Support**: Uses `workflow_run` trigger for write permissions +2. **Security Validation**: Validates PR number to prevent cross-PR injection +3. **Update-or-Create**: Updates existing comment or creates new one +4. **Graceful Degradation**: Skips comment if no coverage data available + +**Why it exists**: Shows template users how to implement coverage comments that work with fork PRs. + +**To enable**: +1. Add coverage collection to test workflows (pytest --cov, jest --coverage) +2. Upload coverage data as artifacts with pattern `coverage-data-*` +3. Include JSON fields: `pr_number`, `component`, `pr_coverage`, `main_coverage`, `diff` + +See workflow file header for detailed implementation instructions. + +--- + +## Pattern Highlights + +### 1. Concurrency Control Pattern + +All workflows use concurrency controls to prevent queue backlog and save CI minutes: + +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true # or false, depending on workflow type +``` + +**When to cancel**: +- βœ… Validation/test workflows (ci.yml, validate.yml, e2e-pattern-tests.yml) +- βœ… Review workflows (pr-review.yml) +- ❌ Creation workflows (issue-to-pr.yml, dependabot-auto-merge.yml) +- ❌ Scheduled workflows (stale.yml) +- ❌ Deployment workflows (deploy-docs.yml) + +### 2. GitHub Step Summary Pattern + +Key workflows generate rich summaries visible in the Actions UI: + +```yaml +- name: Generate summary + if: always() + run: | + { + echo "## 🎯 Workflow Results" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| Job 1 | ${{ needs.job1.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + } >> "$GITHUB_STEP_SUMMARY" +``` + +**Workflows with summaries**: +- e2e-pattern-tests.yml (11-pattern table) +- validate.yml (AQE validation results) +- docs-validation.yml (Mermaid + markdown lint results) +- security.yml (CodeQL + security scan results) + +### 3. Fork PR Support Pattern + +**Problem**: Fork PRs have limited permissions (can't write comments/statuses) + +**Solution**: Use `workflow_run` trigger (demonstrated in coverage-comment.yml) + +```yaml +on: + workflow_run: + workflows: ["Tests"] + types: [completed] + +jobs: + post-comment: + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' +``` + +**Security**: Always validate PR number from artifact matches trusted PR number. + +### 4. Matrix Strategy Pattern + +Multi-language/version testing with parallel execution: + +```yaml +strategy: + fail-fast: false + matrix: + language: ['python', 'javascript'] +``` + +Used in: security.yml (CodeQL for Python and JavaScript) + +### 5. CodeQL Customization Pattern + +**IMPORTANT**: This is a template repository - customize CodeQL for YOUR project! + +#### Template Configuration (Current) + +```yaml +codeql: + continue-on-error: true # Allows workflow to succeed even if language has no code + strategy: + matrix: + language: ['python', 'javascript'] # Demo languages for template +``` + +**Why `continue-on-error: true` in this template**: +- This repo is documentation-only with minimal Python scripts +- No JavaScript/TypeScript code exists +- Template demonstrates pattern for projects that DO use these languages +- Without `continue-on-error`, JavaScript analysis would fail (no code to analyze) + +#### Customization for YOUR Project + +##### Step 1: Update Language Matrix + +Match the languages in YOUR codebase: + +```yaml +# Example for a Python + TypeScript project: +matrix: + language: ['python', 'typescript'] + +# Example for a Go + Java project: +matrix: + language: ['go', 'java'] +``` + +**Supported languages**: +- `python` - Python code +- `javascript` - JavaScript/Node.js code +- `typescript` - TypeScript code (includes .js and .ts files) +- `java` - Java code +- `cpp` - C/C++ code +- `csharp` - C# code +- `go` - Go code +- `ruby` - Ruby code +- `swift` - Swift code +- `kotlin` - Kotlin code + +##### Step 2: Set `continue-on-error` Appropriately + +| Use Case | Setting | Rationale | +|----------|---------|-----------| +| **Production application** | `continue-on-error: false` | Fail workflow if security analysis fails - strict enforcement | +| **Template/reference repo** | `continue-on-error: true` | Allow success even if language has no code | +| **Multi-language monorepo** | `continue-on-error: false` | All declared languages should have code | + +**Recommended for most projects**: + +```yaml +codeql: + continue-on-error: false # Strict - fail if any analysis fails +``` + +##### Step 3: Remove Unused Languages + +Only include languages you actually use: + +```yaml +# ❌ DON'T: Include languages you don't use +matrix: + language: ['python', 'javascript', 'java', 'go'] # But you only use Python! + +# βœ… DO: Only include languages in your codebase +matrix: + language: ['python'] # Just Python - faster analysis, clearer results +``` + +#### Example Configurations + +**FastAPI Backend (Python only)**: + +```yaml +codeql: + continue-on-error: false + strategy: + matrix: + language: ['python'] +``` + +**React + Node.js App (TypeScript + JavaScript)**: + +```yaml +codeql: + continue-on-error: false + strategy: + matrix: + language: ['typescript'] # Includes both .ts and .js files +``` + +**Microservices Monorepo (Go + Python)**: + +```yaml +codeql: + continue-on-error: false + strategy: + matrix: + language: ['go', 'python'] +``` + +#### Path Filters for CodeQL + +The security.yml workflow includes path filters to avoid unnecessary runs: + +```yaml +paths: + - '**/*.py' + - '**/*.js' + - '**/*.ts' + - '**/*.tsx' + - '.github/workflows/security.yml' +``` + +**Customize these** if you add other languages: + +```yaml +# Example: Add Go and Java patterns +paths: + - '**/*.py' + - '**/*.go' + - '**/*.java' + - '.github/workflows/security.yml' +``` + +#### Troubleshooting CodeQL + +**"No source code seen during build"**: +- CodeQL couldn't find code for the specified language +- **Solution**: Remove that language from the matrix OR add `continue-on-error: true` + +**"CodeQL analysis timed out"**: +- Large codebase exceeding timeout +- **Solution**: Increase `timeout-minutes` or split into separate workflows + +**"Insufficient permissions"**: +- Missing `security-events: write` permission +- **Solution**: Already configured correctly in this template + +--- + +## Best Practices Demonstrated + +1. **Minimal Permissions**: Each workflow requests only necessary permissions +2. **Concurrency Controls**: Prevent queue backlog, save CI minutes +3. **Clear Naming**: Job and step names clearly describe what they do +4. **GitHub Step Summaries**: Enhanced UX in Actions UI +5. **Security Validation**: Input sanitization, credential detection +6. **Graceful Degradation**: Workflows handle missing files/data gracefully +7. **Documentation**: Inline comments explain complex logic +8. **Semantic Triggers**: Workflows trigger only when relevant files change + +--- + +## Maintenance Guidelines + +### When to Update Workflows + +1. **Adding new patterns**: Add test job to e2e-pattern-tests.yml +2. **Changing documentation structure**: Update ci.yml and validate.yml checks +3. **Adding new languages**: Update security.yml CodeQL matrix +4. **Modifying permissions**: Review security implications carefully + +### Testing Workflow Changes + +1. **Syntax validation**: Use `actionlint` (recommended) +2. **Test in PR**: All workflows run on PRs, verify they work +3. **Check step summaries**: Ensure summaries render correctly +4. **Review permissions**: Ensure minimal necessary permissions + +### Common Issues + +- **"Resource not accessible by integration"**: Insufficient permissions +- **"Concurrency group conflicts"**: Multiple workflows with same group +- **"Required status checks not found"**: Branch protection referencing old job names +- **"Mermaid syntax errors"**: Run `./scripts/validate-mermaid.sh` locally +- **"Permission denied" when running scripts**: Ensure scripts are committed with executable permissions (`git add --chmod=+x script.sh` or `chmod +x script.sh && git add script.sh`). Workflows no longer use `chmod +x` inline, relying on git-tracked executable bits. + +--- + +## Reference Documentation + +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Workflow syntax reference](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) +- [Security hardening for Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [CodeQL documentation](https://codeql.github.com/docs/) +- [actionlint - Workflow linter](https://github.com/rhysd/actionlint) + +--- + +## Questions or Issues? + +- Review workflow file headers for detailed implementation notes +- Check [CLAUDE.md](../../CLAUDE.md) for repository standards +- See [docs/patterns/](../../docs/patterns/) for pattern documentation +- Open an issue for workflow-specific questions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02b1f3..a3ee17c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,14 @@ on: branches: [main] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true # Cancel outdated validation runs on new pushes + jobs: documentation-structure: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -24,6 +29,7 @@ jobs: repomap-validation: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -32,14 +38,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.11' + cache: 'pip' - name: Install repomap dependencies run: | pip install uv - uv pip install tree-sitter tree-sitter-python tree-sitter-javascript \ + # Use --system flag since we're not in a virtual environment + uv pip install --system tree-sitter tree-sitter-python tree-sitter-javascript \ tree-sitter-typescript tree-sitter-go tree-sitter-bash - name: Validate repomap is current - run: | - chmod +x scripts/update-repomap.sh - ./scripts/update-repomap.sh --check + run: ./scripts/update-repomap.sh --check diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml new file mode 100644 index 0000000..587bdd6 --- /dev/null +++ b/.github/workflows/coverage-comment.yml @@ -0,0 +1,212 @@ +name: Coverage Comment + +# DEMONSTRATION WORKFLOW - NOT FUNCTIONAL WITHOUT COVERAGE COLLECTION +# +# This workflow demonstrates fork PR coverage comment patterns using workflow_run trigger. +# It shows best practices for: +# - Supporting fork PRs with write permissions (workflow_run pattern) +# - Security validation to prevent cross-PR comment injection +# - Update-or-create pattern for coverage comments +# - Graceful handling of missing coverage data +# +# To enable for your repository: +# 1. Add coverage collection to your test workflow (e.g., pytest --cov, jest --coverage) +# 2. Upload coverage data as artifacts with pattern: coverage-data-* +# 3. Include these fields in JSON artifacts: +# - pr_number: PR number from the test workflow +# - component: Component name (e.g., "Backend", "Frontend") +# - pr_coverage: Coverage percentage for this PR +# - main_coverage: Coverage percentage on main branch +# - diff: Difference (pr_coverage - main_coverage) +# +# See docs/patterns/coverage-reporting.md for implementation details (when documented) + +on: + workflow_run: + workflows: ["E2E Pattern Tests"] + types: [completed] + +permissions: + pull-requests: write + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: false # Process all coverage data for this commit + +jobs: + post-coverage-comment: + name: Post Coverage Comment + runs-on: ubuntu-latest + timeout-minutes: 10 + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + + steps: + - name: Get PR number from workflow run + id: get-pr + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Two-tier PR detection approach for maximum reliability + # Tier 1: Use workflow_run.pull_requests (works for same-repo PRs) + PR_NUMBER=$(echo '${{ toJSON(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number // ""') + + # Tier 2: Fallback to branch lookup for fork PRs + if [ -z "$PR_NUMBER" ]; then + echo "PR not found in workflow_run.pull_requests, trying branch lookup..." + PR_NUMBER=$(gh pr view "${{ github.event.workflow_run.head_branch }}" \ + --repo ${{ github.repository }} \ + --json number \ + --jq '.number' 2>/dev/null || echo "") + fi + + if [ -z "$PR_NUMBER" ]; then + echo "No PR found for branch ${{ github.event.workflow_run.head_branch }}" + exit 1 + fi + + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "Found PR #$PR_NUMBER" + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + pattern: coverage-data-* + path: coverage-data + merge-multiple: true + + - name: Check for coverage data + id: check-coverage + run: | + set -euo pipefail + if [ -d "coverage-data" ] && [ "$(ls -A coverage-data/*.json 2>/dev/null)" ]; then + echo "has_coverage=true" >> "$GITHUB_OUTPUT" + echo "Coverage data found" + else + echo "has_coverage=false" >> "$GITHUB_OUTPUT" + echo "No coverage data found - skipping comment" + fi + + - name: Security validation + if: steps.check-coverage.outputs.has_coverage == 'true' + run: | + set -euo pipefail + # Validate that PR number from artifact matches the trusted PR number + # This prevents cross-PR comment injection attacks + for coverage_file in coverage-data/*.json; do + if [ -f "$coverage_file" ]; then + # Validate PR number is numeric to prevent injection + ARTIFACT_PR_NUMBER=$(jq -r '.pr_number // ""' "$coverage_file" | grep -E '^[0-9]+$' || echo "") + TRUSTED_PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}" + + if [ -z "$ARTIFACT_PR_NUMBER" ]; then + echo "Warning: Invalid or missing pr_number in $coverage_file" + continue + fi + + if [ "$ARTIFACT_PR_NUMBER" != "$TRUSTED_PR_NUMBER" ]; then + echo "Security violation: PR number mismatch" + echo "Artifact claims PR #$ARTIFACT_PR_NUMBER but workflow ran for PR #$TRUSTED_PR_NUMBER" + exit 1 + fi + fi + done + echo "Security validation passed" + + - name: Checkout repository + if: steps.check-coverage.outputs.has_coverage == 'true' + uses: actions/checkout@v4 + + - name: Setup Python + if: steps.check-coverage.outputs.has_coverage == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Generate coverage comment + if: steps.check-coverage.outputs.has_coverage == 'true' + id: generate-comment + run: | + set -euo pipefail + # Use Python script for cleaner, more maintainable coverage comment generation + COMMENT_FILE="coverage-comment.md" + + python .github/scripts/generate_coverage_comment.py \ + coverage-data \ + "${{ github.event.workflow_run.id }}" \ + "${{ github.event.workflow_run.html_url }}" \ + > "$COMMENT_FILE" + + # Store comment file path in output + echo "comment_file=$COMMENT_FILE" >> "$GITHUB_OUTPUT" + + - name: Find existing coverage comment + if: steps.check-coverage.outputs.has_coverage == 'true' + id: find-comment + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Find existing coverage comment by looking for our marker + COMMENT_ID=$(gh api \ + "repos/${{ github.repository }}/issues/${{ steps.get-pr.outputs.pr_number }}/comments" \ + --jq '.[] | select(.body | contains("πŸ“Š Test Coverage Report")) | .id' \ + | head -n 1) + + if [ -n "$COMMENT_ID" ]; then + echo "existing_comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT" + echo "Found existing coverage comment: $COMMENT_ID" + else + echo "No existing coverage comment found" + fi + + - name: Update or create coverage comment + if: steps.check-coverage.outputs.has_coverage == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + COMMENT_BODY=$(cat "${{ steps.generate-comment.outputs.comment_file }}") + + if [ -n "${{ steps.find-comment.outputs.existing_comment_id }}" ]; then + # Try to update existing comment, with fallback to create if it was deleted + echo "Attempting to update existing comment #${{ steps.find-comment.outputs.existing_comment_id }}..." + if gh api \ + --method PATCH \ + "repos/${{ github.repository }}/issues/comments/${{ steps.find-comment.outputs.existing_comment_id }}" \ + -f body="$COMMENT_BODY" 2>/dev/null; then + echo "βœ… Updated existing coverage comment" + else + # Comment may have been deleted or is inaccessible - create new one + echo "⚠️ Failed to update comment (may have been deleted), creating new comment instead..." + gh pr comment ${{ steps.get-pr.outputs.pr_number }} \ + --repo ${{ github.repository }} \ + --body "$COMMENT_BODY" + echo "βœ… Created new coverage comment" + fi + else + # Create new comment + gh pr comment ${{ steps.get-pr.outputs.pr_number }} \ + --repo ${{ github.repository }} \ + --body "$COMMENT_BODY" + echo "βœ… Created new coverage comment" + fi + + - name: Generate summary + if: always() + run: | + set -euo pipefail + { + echo "## πŸ“Š Coverage Comment Status" + echo "" + if [ "${{ steps.check-coverage.outputs.has_coverage }}" == "true" ]; then + echo "βœ… Coverage comment posted to PR #${{ steps.get-pr.outputs.pr_number }}" + else + echo "⏭️ No coverage data available - comment skipped" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 2243e4f..f538c75 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -8,9 +8,14 @@ permissions: contents: write pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false # Don't cancel merge operations mid-process + jobs: auto-merge: runs-on: ubuntu-latest + timeout-minutes: 10 if: github.actor == 'dependabot[bot]' steps: @@ -22,16 +27,44 @@ jobs: - name: Auto-merge patch updates if: steps.metadata.outputs.update-type == 'version-update:semver-patch' - run: | - echo "Auto-merging patch update: ${{ github.event.pull_request.title }}" - gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + set -euo pipefail + echo "Auto-merging patch update: $PR_TITLE" + gh pr merge --auto --squash "$PR_URL" - name: Comment on minor/major updates if: steps.metadata.outputs.update-type != 'version-update:semver-patch' run: | + set -euo pipefail echo "Minor/major update detected - requires human review" gh pr comment "${{ github.event.pull_request.html_url }}" --body "⚠️ This is a **${{ steps.metadata.outputs.update-type }}** update and requires human review before merging." env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate summary + if: always() + env: + PR_TITLE: ${{ github.event.pull_request.title }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + run: | + set -euo pipefail + { + echo "## πŸ€– Dependabot Auto-Merge" + echo "" + echo "**PR**: $PR_TITLE" + echo "**Update Type**: $UPDATE_TYPE" + echo "" + if [ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-patch" ]; then + echo "**Decision**: βœ… Auto-merged (patch update)" + echo "" + echo "Patch updates are automatically merged after CI passes." + else + echo "**Decision**: ⏸️ Requires human review" + echo "" + echo "Minor and major updates require manual approval for safety." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 3af7708..0d7ddf1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,11 +16,12 @@ permissions: concurrency: group: "pages" - cancel-in-progress: false + cancel-in-progress: false # Complete deployment before starting new one jobs: build: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 with: @@ -30,6 +31,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" + cache: 'pip' - name: Install dependencies run: | @@ -48,8 +50,32 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest + timeout-minutes: 10 needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + docs-deployment-summary: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [build, deploy] + if: always() + steps: + - name: Generate summary + run: | + { + echo "## πŸ“š Documentation Deployment" + echo "" + echo "| Step | Status |" + echo "|------|--------|" + echo "| Build | ${{ needs.build.result == 'success' && 'βœ… Success' || '❌ Failed' }} |" + echo "| Deploy | ${{ needs.deploy.result == 'success' && 'βœ… Success' || '❌ Failed' }} |" + echo "" + if [ "${{ needs.deploy.result }}" == "success" ]; then + echo "**Status**: βœ… Deployment successful" + else + echo "**Status**: ❌ Deployment failed" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml index 74d75af..5a50795 100644 --- a/.github/workflows/docs-validation.yml +++ b/.github/workflows/docs-validation.yml @@ -12,9 +12,14 @@ on: - "docs/**" - "**/*.md" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate-mermaid: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -25,7 +30,6 @@ jobs: - name: Validate Mermaid diagrams run: | if [ -f scripts/validate-mermaid.sh ]; then - chmod +x scripts/validate-mermaid.sh ./scripts/validate-mermaid.sh else echo "No Mermaid diagrams to validate" @@ -35,6 +39,7 @@ jobs: lint-markdown: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -44,3 +49,29 @@ jobs: with: globs: "**/*.md" config: ".markdownlint.json" + + docs-validation-summary: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [validate-mermaid, lint-markdown] + if: always() + + steps: + - name: Generate summary + run: | + { + echo "## πŸ“ Documentation Validation Results" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| Mermaid Diagrams | ${{ needs.validate-mermaid.result == 'success' && 'βœ… Passed' || needs.validate-mermaid.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" + echo "| Markdown Linting | ${{ needs.lint-markdown.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "" + if [ "${{ needs.validate-mermaid.result }}" == "failure" ] || [ "${{ needs.lint-markdown.result }}" == "failure" ]; then + echo "**Status**: ❌ Validation failed" + echo "" + echo "*Review the logs above for detailed error messages*" + else + echo "**Status**: βœ… All documentation validation checks passed" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/e2e-pattern-tests.yml b/.github/workflows/e2e-pattern-tests.yml index fc66a65..9387141 100644 --- a/.github/workflows/e2e-pattern-tests.yml +++ b/.github/workflows/e2e-pattern-tests.yml @@ -3,136 +3,149 @@ name: E2E Pattern Tests on: push: branches: [main] + paths: + # Path filters optimize CI by only running when relevant files change + # These filters run E2E tests when: + # - Pattern documentation changes (docs/patterns/**) + # - Workflow files change (patterns reference actual workflows) + # - Any GitHub scripts change (includes E2E tests, AQE check.sh, auto-fix.sh, etc.) + # BENEFIT: Saves CI minutes for unrelated changes (docs/adr, README updates) + # COVERAGE: Catches when referenced workflows/scripts are modified + # MANUAL OVERRIDE: workflow_dispatch available for comprehensive validation + - 'docs/patterns/**' + - '.github/workflows/**' + - '.github/scripts/**' pull_request: branches: [main] + paths: + # Same path filters as push (see comments above) + - 'docs/patterns/**' + - '.github/workflows/**' + - '.github/scripts/**' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: pattern-1-aqe: name: Pattern 1 - AQE runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run AQE tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-1-aqe.sh - .github/scripts/e2e-tests/test-pattern-1-aqe.sh + run: .github/scripts/e2e-tests/test-pattern-1-aqe.sh pattern-2-cba: name: Pattern 2 - CBA runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run CBA tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-2-cba.sh - .github/scripts/e2e-tests/test-pattern-2-cba.sh + run: .github/scripts/e2e-tests/test-pattern-2-cba.sh pattern-3-dependabot: name: Pattern 3 - Dependabot runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run Dependabot tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-3-dependabot.sh - .github/scripts/e2e-tests/test-pattern-3-dependabot.sh + run: .github/scripts/e2e-tests/test-pattern-3-dependabot.sh pattern-4-gha: name: Pattern 4 - GHA runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run GHA tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-4-gha.sh - .github/scripts/e2e-tests/test-pattern-4-gha.sh + run: .github/scripts/e2e-tests/test-pattern-4-gha.sh pattern-5-issue-to-pr: name: Pattern 5 - Issue-to-PR runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run Issue-to-PR tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-5-issue-to-pr.sh - .github/scripts/e2e-tests/test-pattern-5-issue-to-pr.sh + run: .github/scripts/e2e-tests/test-pattern-5-issue-to-pr.sh pattern-6-multi-agent: name: Pattern 6 - Multi-Agent runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run Multi-Agent tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-6-multi-agent.sh - .github/scripts/e2e-tests/test-pattern-6-multi-agent.sh + run: .github/scripts/e2e-tests/test-pattern-6-multi-agent.sh pattern-7-pr-review: name: Pattern 7 - PR Review runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run PR Review tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-7-pr-review.sh - .github/scripts/e2e-tests/test-pattern-7-pr-review.sh + run: .github/scripts/e2e-tests/test-pattern-7-pr-review.sh pattern-8-security: name: Pattern 8 - Security runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' + cache: 'pip' - name: Install dependencies run: pip install pytest - name: Run Security tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-8-security.sh - .github/scripts/e2e-tests/test-pattern-8-security.sh + run: .github/scripts/e2e-tests/test-pattern-8-security.sh pattern-9-self-review: name: Pattern 9 - Self-Review runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run Self-Review tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-9-self-review.sh - .github/scripts/e2e-tests/test-pattern-9-self-review.sh + run: .github/scripts/e2e-tests/test-pattern-9-self-review.sh pattern-10-stale: name: Pattern 10 - Stale runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Run Stale tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-10-stale.sh - .github/scripts/e2e-tests/test-pattern-10-stale.sh + run: .github/scripts/e2e-tests/test-pattern-10-stale.sh pattern-11-testing: name: Pattern 11 - Testing runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' + cache: 'pip' - name: Install dependencies run: pip install pytest - name: Run Testing tests - run: | - chmod +x .github/scripts/e2e-tests/test-pattern-11-testing.sh - .github/scripts/e2e-tests/test-pattern-11-testing.sh + run: .github/scripts/e2e-tests/test-pattern-11-testing.sh - summary: + e2e-test-summary: name: E2E Test Summary runs-on: ubuntu-latest + timeout-minutes: 5 needs: - pattern-1-aqe - pattern-2-cba @@ -149,6 +162,27 @@ jobs: steps: - name: Check results run: | + # Generate GitHub Step Summary + { + echo "## πŸ§ͺ E2E Pattern Test Results" + echo "" + echo "| Pattern | Status |" + echo "|---------|--------|" + echo "| 1 - AQE | ${{ needs.pattern-1-aqe.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 2 - CBA | ${{ needs.pattern-2-cba.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 3 - Dependabot | ${{ needs.pattern-3-dependabot.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 4 - GHA | ${{ needs.pattern-4-gha.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 5 - Issue-to-PR | ${{ needs.pattern-5-issue-to-pr.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 6 - Multi-Agent | ${{ needs.pattern-6-multi-agent.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 7 - PR Review | ${{ needs.pattern-7-pr-review.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 8 - Security | ${{ needs.pattern-8-security.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 9 - Self-Review | ${{ needs.pattern-9-self-review.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 10 - Stale | ${{ needs.pattern-10-stale.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| 11 - Testing | ${{ needs.pattern-11-testing.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + # Also print to console echo "==========================================" echo " E2E Pattern Test Results" echo "==========================================" @@ -178,8 +212,8 @@ jobs: [[ "${{ needs.pattern-9-self-review.result }}" == "failure" ]] || \ [[ "${{ needs.pattern-10-stale.result }}" == "failure" ]] || \ [[ "${{ needs.pattern-11-testing.result }}" == "failure" ]]; then - echo "❌ SOME PATTERN TESTS FAILED" + echo "❌ SOME PATTERN TESTS FAILED" | tee -a "$GITHUB_STEP_SUMMARY" exit 1 else - echo "βœ… ALL PATTERN TESTS PASSED" + echo "βœ… ALL PATTERN TESTS PASSED" | tee -a "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/issue-to-pr.yml b/.github/workflows/issue-to-pr.yml index 443db21..d462be9 100644 --- a/.github/workflows/issue-to-pr.yml +++ b/.github/workflows/issue-to-pr.yml @@ -9,9 +9,14 @@ permissions: pull-requests: write issues: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false # Don't cancel PR creation mid-process + jobs: analyze-and-create-pr: runs-on: ubuntu-latest + timeout-minutes: 10 if: github.event.label.name == 'ready-for-pr' steps: @@ -22,12 +27,13 @@ jobs: env: ISSUE_BODY: ${{ github.event.issue.body }} run: | + set -euo pipefail # Check for acceptance criteria indicators if echo "$ISSUE_BODY" | grep -qiE "(acceptance criteria|requirements|should|must|expected)"; then - echo "clear=true" >> $GITHUB_OUTPUT + echo "clear=true" >> "$GITHUB_OUTPUT" echo "Issue has clear requirements" else - echo "clear=false" >> $GITHUB_OUTPUT + echo "clear=false" >> "$GITHUB_OUTPUT" echo "Issue may need clarification" fi @@ -37,6 +43,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} run: | + set -euo pipefail gh issue comment "$ISSUE_NUMBER" --body "This issue was labeled ready-for-pr but may need more detail. Please provide: @@ -52,9 +59,10 @@ jobs: ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_NUMBER: ${{ github.event.issue.number }} run: | + set -euo pipefail BRANCH_NAME="issue-${ISSUE_NUMBER}-$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | head -c 40)" git checkout -b "$BRANCH_NAME" - echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV + echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV" - name: Create draft PR if: steps.analyze.outputs.clear == 'true' @@ -63,6 +71,7 @@ jobs: ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_NUMBER: ${{ github.event.issue.number }} run: | + set -euo pipefail # Create a placeholder commit cat > IMPLEMENTATION.md << EOF # Implementation for Issue #${ISSUE_NUMBER} @@ -94,3 +103,37 @@ jobs: --- Created automatically from issue #${ISSUE_NUMBER}" + + - name: Generate summary + if: always() + env: + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + BRANCH_NAME: ${{ env.BRANCH_NAME }} + ANALYZE_CLEAR: ${{ steps.analyze.outputs.clear }} + run: | + set -euo pipefail + { + echo "## 🎫 Issue to PR Automation" + echo "" + echo "**Issue**: #$ISSUE_NUMBER - $ISSUE_TITLE" + echo "" + if [ "$ANALYZE_CLEAR" == "true" ]; then + echo "**Action**: βœ… Draft PR created" + echo "" + echo "A draft PR has been created with branch: \`$BRANCH_NAME\`" + echo "" + echo "Next steps:" + echo "- Implement the changes" + echo "- Mark PR as ready for review" + else + echo "**Action**: πŸ’¬ Clarification requested" + echo "" + echo "The issue needs more detail before a PR can be created." + echo "" + echo "A comment has been added requesting:" + echo "- Clear acceptance criteria" + echo "- Expected behavior" + echo "- Specific implementation requirements" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 0bc49c9..a4d141d 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -1,16 +1,24 @@ name: PR Auto-Review +# IMPORTANT: Uses pull_request_target to support fork PRs +# This allows posting comments on PRs from forks while maintaining security +# by only checking out code for diff analysis (not executing PR code) on: - pull_request: + pull_request_target: types: [opened, synchronize, ready_for_review] permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true # Re-review on each push, cancel old reviews + jobs: auto-review: runs-on: ubuntu-latest + timeout-minutes: 15 # Skip draft PRs and PRs with skip-review label if: | github.event.pull_request.draft == false && @@ -19,95 +27,136 @@ jobs: steps: - uses: actions/checkout@v4 with: + # SECURITY: Check out base branch, not PR code + # We only analyze diffs via GitHub API, never execute PR code + ref: ${{ github.event.pull_request.base.sha }} fetch-depth: 0 + - name: Fetch PR branch for diff analysis + run: | + # Fetch PR branch without checking it out (security) + git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-branch + - name: Get changed files id: changed - run: | - CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path' | tr '\n' ' ') - echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + CHANGED_FILES=$(gh pr view "$PR_NUMBER" --json files -q '.files[].path' | tr '\n' ' ') + echo "files=$CHANGED_FILES" >> "$GITHUB_OUTPUT" - name: Security review id: security + env: + CHANGED_FILES: ${{ steps.changed.outputs.files }} run: | + set -euo pipefail FINDINGS="" # Check for hardcoded secrets patterns - if git diff origin/main...HEAD | grep -iE "(password|secret|api_key|token)\s*=\s*['\"][^'\"]+['\"]"; then + # Use pr-branch ref instead of HEAD for security (never checkout PR code) + if SECRETS_FOUND=$(git diff origin/${{ github.event.pull_request.base.ref }}...pr-branch | grep -iE "(password|secret|api_key|token)\s*=\s*['\"][^'\"]+['\"]" || true) && [ -n "$SECRETS_FOUND" ]; then FINDINGS="${FINDINGS}πŸ”΄ **CRITICAL**: Potential hardcoded secrets detected\n" fi # Check for .env file additions - if echo "${{ steps.changed.outputs.files }}" | grep -qE "\.env$"; then + if echo "$CHANGED_FILES" | grep -qE "\.env$" || false; then FINDINGS="${FINDINGS}πŸ”΄ **CRITICAL**: .env file should not be committed\n" fi # Check for TODO/FIXME in security-sensitive areas - if git diff origin/main...HEAD | grep -iE "(security|auth|password)" | grep -iE "(todo|fixme|hack)"; then + if TODO_FOUND=$(git diff origin/${{ github.event.pull_request.base.ref }}...pr-branch | grep -iE "(security|auth|password)" | grep -iE "(todo|fixme|hack)" || true) && [ -n "$TODO_FOUND" ]; then FINDINGS="${FINDINGS}🟑 **WARNING**: TODO/FIXME in security-sensitive code\n" fi if [ -n "$FINDINGS" ]; then - echo "has_findings=true" >> $GITHUB_OUTPUT - echo -e "findings=$FINDINGS" >> $GITHUB_OUTPUT + echo "has_findings=true" >> "$GITHUB_OUTPUT" + echo -e "findings=$FINDINGS" >> "$GITHUB_OUTPUT" else - echo "has_findings=false" >> $GITHUB_OUTPUT + echo "has_findings=false" >> "$GITHUB_OUTPUT" fi - name: Code quality review id: quality + env: + CHANGED_FILES: ${{ steps.changed.outputs.files }} run: | + set -euo pipefail FINDINGS="" # Check for large files - LARGE_FILES=$(git diff --stat origin/main...HEAD | grep -E "\+[0-9]{3,}" | head -5) + LARGE_FILES=$(git diff --stat origin/${{ github.event.pull_request.base.ref }}...pr-branch | grep -E "\+[0-9]{3,}" | head -5 || true) if [ -n "$LARGE_FILES" ]; then FINDINGS="${FINDINGS}🟑 **WARNING**: Large changes detected - consider breaking into smaller PRs\n" fi # Check for missing tests in code changes - CODE_CHANGED=$(echo "${{ steps.changed.outputs.files }}" | grep -E "\.(py|js|ts)$" | grep -v test || true) - TEST_CHANGED=$(echo "${{ steps.changed.outputs.files }}" | grep -E "test" || true) + CODE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "\.(py|js|ts)$" | grep -v test || true) + TEST_CHANGED=$(echo "$CHANGED_FILES" | grep -E "test" || true) if [ -n "$CODE_CHANGED" ] && [ -z "$TEST_CHANGED" ]; then FINDINGS="${FINDINGS}🟑 **WARNING**: Code changes without corresponding tests\n" fi if [ -n "$FINDINGS" ]; then - echo "has_findings=true" >> $GITHUB_OUTPUT - echo -e "findings=$FINDINGS" >> $GITHUB_OUTPUT + echo "has_findings=true" >> "$GITHUB_OUTPUT" + echo -e "findings=$FINDINGS" >> "$GITHUB_OUTPUT" else - echo "has_findings=false" >> $GITHUB_OUTPUT + echo "has_findings=false" >> "$GITHUB_OUTPUT" fi - name: Post review comment if: steps.security.outputs.has_findings == 'true' || steps.quality.outputs.has_findings == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SECURITY_FINDINGS: ${{ steps.security.outputs.findings }} + QUALITY_FINDINGS: ${{ steps.quality.outputs.findings }} + HAS_SECURITY: ${{ steps.security.outputs.has_findings }} + HAS_QUALITY: ${{ steps.quality.outputs.has_findings }} run: | - COMMENT="## πŸ€– Automated PR Review\n\n" + set -euo pipefail + + # Build comment body + COMMENT="## πŸ€– Automated PR Review - if [ "${{ steps.security.outputs.has_findings }}" == "true" ]; then - COMMENT="${COMMENT}### Security Findings\n${{ steps.security.outputs.findings }}\n" +" + + if [ "$HAS_SECURITY" == "true" ]; then + COMMENT="${COMMENT}### Security Findings +${SECURITY_FINDINGS} + +" fi - if [ "${{ steps.quality.outputs.has_findings }}" == "true" ]; then - COMMENT="${COMMENT}### Code Quality\n${{ steps.quality.outputs.findings }}\n" + if [ "$HAS_QUALITY" == "true" ]; then + COMMENT="${COMMENT}### Code Quality +${QUALITY_FINDINGS} + +" fi - COMMENT="${COMMENT}\n---\n*This is an automated review. Please address any πŸ”΄ CRITICAL issues before merging.*" + COMMENT="${COMMENT}--- +*This is an automated review. Please address any πŸ”΄ CRITICAL issues before merging.*" - echo -e "$COMMENT" | gh pr comment ${{ github.event.pull_request.number }} --body-file - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Post comment with explicit repository + echo "$COMMENT" | gh pr comment "$PR_NUMBER" \ + --repo "${{ github.repository }}" \ + --body-file - - name: Post success comment if: steps.security.outputs.has_findings == 'false' && steps.quality.outputs.has_findings == 'false' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - gh pr comment ${{ github.event.pull_request.number }} --body "## πŸ€– Automated PR Review + gh pr comment "$PR_NUMBER" \ + --repo "${{ github.repository }}" \ + --body "## πŸ€– Automated PR Review - βœ… No security or code quality issues detected. +βœ… No security or code quality issues detected. - --- - *This is an automated review.*" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +--- +*This is an automated review.*" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e862778..8fc87fe 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,34 +3,191 @@ name: Security on: push: branches: [main] + paths: + - '**/*.py' + - '**/*.js' + - '**/*.ts' + - '**/*.tsx' + - '.github/workflows/security.yml' pull_request: branches: [main] + paths: + - '**/*.py' + - '**/*.js' + - '**/*.ts' + - '**/*.tsx' + - '.github/workflows/security.yml' schedule: - cron: "0 0 * * 0" # Weekly on Sunday +permissions: + contents: read + security-events: write + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + # NOTE: This repository is documentation-only, so CodeQL may not find substantial code to analyze. + # This workflow demonstrates the CodeQL pattern for repositories with Python/JavaScript code. + # For production use, CodeQL is most valuable in repositories with significant application code. + # + # CUSTOMIZATION GUIDE FOR YOUR PROJECT: + # ====================================== + # 1. Language Matrix: Update 'language' array to match YOUR codebase + # - Supported: python, javascript, typescript, java, cpp, csharp, go, ruby, swift, kotlin + # - Example: language: ['python', 'typescript', 'go'] + # + # 2. continue-on-error: Set based on your requirements + # - true: Workflow succeeds even if a language has no code (good for templates/multi-language repos) + # - false: Workflow fails if any language analysis fails (strict enforcement for production apps) + # - RECOMMENDED: Set to 'false' for production applications to catch security issues + # + # 3. Remove languages you DON'T use to avoid unnecessary analysis time + # + # See .github/workflows/README.md for detailed customization instructions + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + timeout-minutes: 45 + continue-on-error: true # TEMPLATE: Set to 'false' for production apps with actual code + strategy: + fail-fast: false + matrix: + language: ['python', 'javascript'] # TEMPLATE: Customize for your languages + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + security-scan: + name: Security Checks runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Check for secrets in documentation run: | + set -euo pipefail # Check for common secret patterns in docs - if grep -r -E '(api_key|secret|password|token).*=.*["\047][A-Za-z0-9+/]{20,}' docs/ README.md || true; then - echo "Warning: Potential secrets found in documentation" + # Exit code 1 from grep means "no match found" - that's good! + FINDINGS=false + + for location in docs/ README.md; do + if [ ! -e "$location" ]; then + echo "⏭️ $location not found, skipping" + continue + fi + + echo "πŸ” Scanning ${location} for secret patterns..." + if grep -r -E '(api_key|secret|password|token).*=.*["\047][A-Za-z0-9+/]{20,}' "$location" 2>/dev/null; then + FINDINGS=true + fi + done + + if [ "$FINDINGS" = "true" ]; then + # SECURITY POLICY: Fail workflow on secret detection + # NOTE: For documentation repos with intentional examples, patterns may match code snippets. + # If you need warning-only mode, remove the 'exit 1' below and keep only the echo statements. + echo "❌ ERROR: Potential secrets detected in documentation!" + echo "Review the matches above to ensure they are not real credentials." + echo "" + echo "If these are intentional code examples, consider:" + echo " - Using placeholder values (e.g., 'your-api-key-here')" + echo " - Shortening example secrets to <20 characters" + echo " - Adding comments clarifying they are examples" + exit 1 # FAIL the workflow + else + echo "βœ… No secret patterns detected in documentation" fi - name: Check for hardcoded URLs with credentials run: | - if grep -r -E 'https?://[^:]+:[^@]+@' docs/ README.md || true; then - echo "Warning: URLs with credentials found in documentation" + set -euo pipefail + # Check for URLs with embedded credentials (e.g., https://user:pass@example.com) + # Exit code 1 from grep means "no match found" - that's good! + FINDINGS=false + + for location in docs/ README.md; do + if [ ! -e "$location" ]; then + echo "⏭️ $location not found, skipping" + continue + fi + + echo "πŸ” Scanning ${location} for URLs with credentials..." + if grep -r -E 'https?://[^:]+:[^@]+@' "$location" 2>/dev/null; then + FINDINGS=true + fi + done + + if [ "$FINDINGS" = "true" ]; then + # SECURITY POLICY: Fail workflow on credential-in-URL detection + # NOTE: For documentation repos with intentional examples, patterns may match code snippets. + # If you need warning-only mode, remove the 'exit 1' below and keep only the echo statements. + echo "❌ ERROR: URLs with embedded credentials detected!" + echo "Credentials should never be in URLs. Use secure credential management instead." + echo "" + echo "If these are intentional examples in documentation:" + echo " - Use placeholder credentials (e.g., https://user:pass@example.com β†’ https://USER:PASS@example.com)" + echo " - Add clear comments that these are NOT real credentials" + exit 1 # FAIL the workflow + else + echo "βœ… No URLs with embedded credentials detected" fi - name: Verify no .env files committed run: | + set -euo pipefail if find . -name ".env*" -not -path "./.git/*" -not -name ".env.example" | grep -q .; then echo "Error: .env files should not be committed" exit 1 fi + echo "βœ… No .env files detected" + + security-summary: + name: Security Summary + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [codeql, security-scan] + if: always() + + steps: + - name: Generate summary + run: | + set -euo pipefail + { + echo "## πŸ”’ Security Scan Results" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| CodeQL Analysis (Python + JavaScript) | ${{ needs.codeql.result == 'success' && 'βœ… Passed' || needs.codeql.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" + echo "| Secrets in Documentation | ${{ needs.security-scan.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| Hardcoded URL Credentials | ${{ needs.security-scan.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| .env Files | ${{ needs.security-scan.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "" + if [ "${{ needs.codeql.result }}" == "failure" ] || [ "${{ needs.security-scan.result }}" == "failure" ]; then + echo "**Status**: ❌ Security checks failed" + echo "" + echo "*Review detailed findings in the [Security tab](https://github.com/${{ github.repository }}/security/code-scanning)*" + else + echo "**Status**: βœ… All security checks passed" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 05f42e4..5ee5c06 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,9 +10,14 @@ permissions: issues: write pull-requests: write +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false # Scheduled jobs should complete, not race with each other + jobs: stale: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/stale@v9 @@ -61,3 +66,30 @@ jobs: # Rate limiting operations-per-run: 100 + + - name: Generate summary + if: always() + run: | + { + echo "## 🧹 Stale Issue Management" + echo "" + echo "Automated maintenance run completed." + echo "" + echo "### Configuration" + echo "| Type | Days Until Stale | Days Until Close |" + echo "|------|------------------|------------------|" + echo "| Issues | 30 | 7 |" + echo "| Pull Requests | 14 | 7 |" + echo "" + echo "### Exemptions" + echo "- **Issue labels**: \`pinned\`, \`security\`, \`bug\`, \`help-wanted\`" + echo "- **PR labels**: \`pinned\`, \`security\`" + echo "- **Assignees**: All assigned items are exempt" + echo "" + echo "### Operations" + echo "- Marked inactive items as stale" + echo "- Closed items stale for 7+ days" + echo "- Removed stale label from updated items" + echo "" + echo "*Check the workflow logs for specific items processed*" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f47629a..ba14fe8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -7,10 +7,15 @@ on: branches: [main] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true # Cancel outdated validation runs on new pushes + jobs: aqe-validation: name: AQE Quality Checks runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@v4 @@ -19,14 +24,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' + # No cache - installing global npm package, no lockfile present - name: Install markdownlint run: npm install -g markdownlint-cli - name: Run validation checks - run: | - chmod +x .github/scripts/check.sh - .github/scripts/check.sh + run: .github/scripts/check.sh - name: Markdown linting run: markdownlint docs/**/*.md README.md CLAUDE.md CONTRIBUTING.md @@ -34,6 +38,7 @@ jobs: documentation-structure: name: Documentation Structure runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -52,3 +57,29 @@ jobs: test -f .claude/agents/codebase-agent.md || { echo "Error: codebase-agent.md missing"; exit 1; } test -d .claude/context || { echo "Error: .claude/context/ missing"; exit 1; } echo "All required configuration files present" + + validation-summary: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [aqe-validation, documentation-structure] + if: always() + + steps: + - name: Generate summary + run: | + { + echo "## βœ… AQE Validation Results" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| AQE Quality Checks | ${{ needs.aqe-validation.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "| Documentation Structure | ${{ needs.documentation-structure.result == 'success' && 'βœ… Passed' || '❌ Failed' }} |" + echo "" + if [ "${{ needs.aqe-validation.result }}" == "failure" ] || [ "${{ needs.documentation-structure.result }}" == "failure" ]; then + echo "**Status**: ❌ Validation failed" + echo "" + echo "*Review the logs above for detailed error messages*" + else + echo "**Status**: βœ… All validation checks passed" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.repomap.txt b/.repomap.txt index 86cb9e4..dc5270d 100644 --- a/.repomap.txt +++ b/.repomap.txt @@ -15,6 +15,12 @@ repomap.py scripts/ record-demo.sh setup.sh + update-repomap.sh + def usage() + def check_dependencies() + def generate_repomap() + def k_repomap_current() {() + def loc() validate-mermaid.sh def validate_markdown_file() scripts/autonomous-review/ @@ -24,3 +30,56 @@ scripts/validation/ scripts/validation/tests/ run-all.sh test-autonomous-fix-loop.sh +src/ + __init__.py +src/core/ + __init__.py + security.py + def sanitize_string() + def validate_slug() + def sanitize_path() + def validate_environment_secret() +tests/ + conftest.py + def sample_string() + def dangerous_string() + def path_traversal_string() + def valid_slug() + def invalid_slugs() +tests/e2e/ + __init__.py + test_cba_workflow.py + class TestCBAWorkflow + def test_issue_to_pr_workflow() + def test_pr_auto_review_workflow() + def test_dependabot_auto_merge_workflow() +tests/integration/ + __init__.py + test_validation_api.py + class TestAPIValidation + def test_valid_input_accepted() + def test_invalid_slug_rejected() + def test_missing_required_field_rejected() + def test_xss_attempt_sanitized() +tests/unit/ + __init__.py + test_security.py + class TestSanitizeString + def test_removes_control_characters() + def test_trims_whitespace() + def test_enforces_max_length() + def test_handles_none() + def test_removes_html_tags() + class TestValidateSlug + def test_valid_slug_passes() + def test_empty_slug_raises() + def test_leading_hyphen_raises() + def test_trailing_hyphen_raises() + def test_consecutive_hyphens_raises() + def test_uppercase_raises() + def test_special_characters_raises() + class TestSanitizePath + def test_blocks_path_traversal() + def test_allows_valid_path() + def test_normalizes_slashes() + def test_removes_null_bytes()