name: CI on: pull_request: branches: [develop, master] permissions: contents: read pull-requests: read env: CARGO_TERM_COLOR: always jobs: # ─── Fast gates (fail early, save CI minutes) ─── check-test-presence: name: test presence runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 50 - name: Check filter modules have tests run: | git fetch origin "${{ github.base_ref }}" --depth=1 || true bash scripts/check-test-presence.sh "origin/${{ github.base_ref }}" fmt: name: fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all -- --check clippy: name: clippy needs: fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets # ─── Parallel gates (all need code to compile) ─── test: name: test (${{ matrix.os }}) needs: clippy runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test --all security: name: Security Scan needs: clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Install cargo-audit run: cargo install cargo-audit - name: Cargo Audit (CVE check) run: | echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Dependency Vulnerabilities" >> $GITHUB_STEP_SUMMARY if cargo audit 2>&1 | tee audit.log; then echo "No known vulnerabilities detected" >> $GITHUB_STEP_SUMMARY else echo "Vulnerabilities found:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY cat audit.log >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "::warning::Dependency vulnerabilities detected - review required" fi echo "" >> $GITHUB_STEP_SUMMARY - name: Critical files check run: | echo "### Critical Files Modified" >> $GITHUB_STEP_SUMMARY CRITICAL=$(git diff --name-only origin/master...HEAD | grep -E "(runner|summary|tracking|init|pnpm_cmd|container)\.rs|Cargo\.toml|workflows/.*\.yml" || true) if [ -n "$CRITICAL" ]; then echo "**HIGH RISK**: The following critical files were modified:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "$CRITICAL" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Required Actions:**" >> $GITHUB_STEP_SUMMARY echo "- [ ] Manual security review by 2 maintainers" >> $GITHUB_STEP_SUMMARY echo "- [ ] Verify no shell injection vectors" >> $GITHUB_STEP_SUMMARY echo "- [ ] Check input validation remains intact" >> $GITHUB_STEP_SUMMARY echo "::warning::Critical RTK files modified - enhanced review required" else echo "No critical files modified" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY - name: Dangerous patterns scan run: | echo "### Dangerous Code Patterns" >> $GITHUB_STEP_SUMMARY PATTERNS=$(git diff origin/master...HEAD | grep -E "Command::new\(\"sh\"|Command::new\(\"bash\"|\.env\(\"LD_PRELOAD|\.env\(\"PATH|reqwest::|std::net::|TcpStream|UdpSocket|unsafe \{|\.unwrap\(\) |panic!\(|todo!\(|unimplemented!\(" || true) if [ -n "$PATTERNS" ]; then echo "**Potentially dangerous patterns detected:**" >> $GITHUB_STEP_SUMMARY echo '```diff' >> $GITHUB_STEP_SUMMARY echo "$PATTERNS" | head -30 >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Security Concerns:**" >> $GITHUB_STEP_SUMMARY echo "$PATTERNS" | grep -q "Command::new" && echo "- Shell command execution detected" >> $GITHUB_STEP_SUMMARY || true echo "$PATTERNS" | grep -q "\.env\(\"" && echo "- Environment variable manipulation" >> $GITHUB_STEP_SUMMARY || true echo "$PATTERNS" | grep -q "reqwest::\|std::net::\|TcpStream\|UdpSocket" && echo "- Network operations added" >> $GITHUB_STEP_SUMMARY || true echo "$PATTERNS" | grep -q "unsafe" && echo "- Unsafe code blocks" >> $GITHUB_STEP_SUMMARY || true echo "$PATTERNS" | grep -q "\.unwrap\(\)\|panic!\(" && echo "- Panic-inducing code" >> $GITHUB_STEP_SUMMARY || true echo "::warning::Dangerous code patterns detected - manual review required" else echo "No dangerous patterns detected" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY - name: New dependencies check run: | echo "### Dependencies Changes" >> $GITHUB_STEP_SUMMARY if git diff origin/master...HEAD Cargo.toml | grep -E "^\+.*=" | grep -v "^\+\+\+" > new_deps.txt; then echo "**New dependencies added:**" >> $GITHUB_STEP_SUMMARY echo '```toml' >> $GITHUB_STEP_SUMMARY cat new_deps.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Required Actions:**" >> $GITHUB_STEP_SUMMARY echo "- [ ] Audit each new dependency on crates.io" >> $GITHUB_STEP_SUMMARY echo "- [ ] Check maintainer reputation and download counts" >> $GITHUB_STEP_SUMMARY echo "- [ ] Verify no typosquatting (e.g., 'reqwest' vs 'request')" >> $GITHUB_STEP_SUMMARY echo "::warning::New dependencies require supply chain audit" else echo "No new dependencies added" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY - name: Clippy security lints run: | echo "### Clippy Security Lints" >> $GITHUB_STEP_SUMMARY if cargo clippy --all-targets -- -W clippy::unwrap_used -W clippy::panic -W clippy::expect_used 2>&1 | tee clippy.log | grep -E "warning:|error:"; then echo "Security-related lints triggered:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY grep -E "warning:|error:" clippy.log | head -20 >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "::warning::Clippy security lints failed" else echo "All security lints passed" >> $GITHUB_STEP_SUMMARY fi - name: Summary verdict run: | echo "---" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Security Review Verdict" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**This is an automated security scan. A human maintainer must:**" >> $GITHUB_STEP_SUMMARY echo "1. Review all warnings above" >> $GITHUB_STEP_SUMMARY echo "2. Verify PR intent matches actual code changes" >> $GITHUB_STEP_SUMMARY echo "3. Check for subtle backdoors or logic bombs" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**For high-risk PRs (critical files modified):**" >> $GITHUB_STEP_SUMMARY echo "- Require approval from 2 maintainers" >> $GITHUB_STEP_SUMMARY echo "- Test in isolated environment before merge" >> $GITHUB_STEP_SUMMARY semgrep: name: semgrep security scan needs: clippy runs-on: ubuntu-latest container: image: semgrep/semgrep steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - run: semgrep scan --config .semgrep.yml --baseline-commit ${{ github.event.pull_request.base.sha }} --error benchmark: name: benchmark needs: clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Build rtk run: cargo build --release - name: Install system tools run: sudo apt-get install -y tree - name: Install Python tools run: pip install ruff pytest mypy - name: Install Go uses: actions/setup-go@v5 with: go-version: "stable" - name: Install Go tools run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - name: Run benchmark run: ./scripts/benchmark.sh # ─── AI Doc Review: develop PRs only ─── doc-review: name: doc review if: github.base_ref == 'develop' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Gather PR context env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUM=${{ github.event.pull_request.number }} gh pr diff "$PR_NUM" --name-only > changed_files.txt gh pr diff "$PR_NUM" | head -c 12000 > diff.txt gh pr view "$PR_NUM" --json title,body --jq '"PR Title: \(.title)\nPR Description: \(.body)"' > pr_info.txt - name: Build prompt files run: | # System prompt cat <<'EOF' > system_prompt.txt You are a documentation reviewer for the RTK project. You will receive the project's CONTRIBUTING.md (which contains the documentation rules), the PR info, changed files, and diff. Your job: based ONLY on the documentation rules in CONTRIBUTING.md, decide if the PR includes the required documentation updates. IMPORTANT: - CI/CD changes, test-only changes, and refactors with no user-facing impact do NOT require doc updates. - Be practical, not pedantic. Small obvious fixes don't need CHANGELOG entries. - Only flag missing docs when there is a clear user-facing change. EOF # User prompt: concatenate files (no printf, no variable expansion issues) { cat pr_info.txt echo "" echo "---" echo "CONTRIBUTING.md:" cat CONTRIBUTING.md echo "" echo "---" echo "Changed files:" cat changed_files.txt echo "" echo "---" echo "Diff (may be truncated):" cat diff.txt } > user_prompt.txt - name: AI documentation review env: ANTHROPIC_API_KEY: ${{ secrets.RTK_DOCS_ANTHROPIC_KEY }} run: | echo "## Documentation Review (AI)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ -z "$ANTHROPIC_API_KEY" ]; then echo "::warning::ANTHROPIC_API_KEY not configured — skipping AI doc review" echo "Skipped: ANTHROPIC_API_KEY secret not configured." >> $GITHUB_STEP_SUMMARY exit 0 fi echo "::group::Preparing API request" echo "System prompt: $(wc -c < system_prompt.txt) bytes" echo "User prompt: $(wc -c < user_prompt.txt) bytes" SYSTEM_JSON=$(jq -Rs . < system_prompt.txt) USER_JSON=$(jq -Rs . < user_prompt.txt) echo "::endgroup::" echo "::group::Calling Claude API (claude-sonnet-4-6)" RESPONSE=$(curl -s -w "\n%{http_code}" https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d "{ \"model\": \"claude-sonnet-4-6\", \"max_tokens\": 1024, \"messages\": [{\"role\": \"user\", \"content\": $USER_JSON}], \"system\": $SYSTEM_JSON, \"output_config\": { \"format\": { \"type\": \"json_schema\", \"schema\": { \"type\": \"object\", \"properties\": { \"status\": {\"type\": \"string\", \"enum\": [\"PASS\", \"FAIL\"]}, \"reasoning\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}, \"files_to_update\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}} }, \"required\": [\"status\", \"reasoning\", \"files_to_update\"], \"additionalProperties\": false } } } }") HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | sed '$d') echo "HTTP status: $HTTP_CODE" echo "::endgroup::" if [ "$HTTP_CODE" != "200" ]; then echo "::warning::Claude API returned HTTP $HTTP_CODE — skipping doc review" echo "Skipped: API error (HTTP $HTTP_CODE)" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "$BODY" | head -10 >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY exit 0 fi # Parse structured JSON response REVIEW_JSON=$(echo "$BODY" | jq -r '.content[0].text // empty') if [ -z "$REVIEW_JSON" ]; then echo "::warning::Empty response from Claude API — skipping doc review" echo "Skipped: empty API response" >> $GITHUB_STEP_SUMMARY echo "Raw response:" echo "$BODY" | head -20 exit 0 fi echo "::group::AI Review Result" echo "$REVIEW_JSON" | jq . echo "::endgroup::" STATUS=$(echo "$REVIEW_JSON" | jq -r '.status') REASONING=$(echo "$REVIEW_JSON" | jq -r '.reasoning[]' 2>/dev/null) FILES=$(echo "$REVIEW_JSON" | jq -r '.files_to_update[]' 2>/dev/null) echo "### Verdict: ${STATUS}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ -n "$REASONING" ]; then echo "**Reasoning:**" >> $GITHUB_STEP_SUMMARY echo "$REASONING" | while IFS= read -r line; do echo "- $line" >> $GITHUB_STEP_SUMMARY done echo "" >> $GITHUB_STEP_SUMMARY fi if [ "$STATUS" = "FAIL" ] && [ -n "$FILES" ]; then echo "**Files to update:**" >> $GITHUB_STEP_SUMMARY echo "$FILES" | while IFS= read -r f; do echo "- \`$f\`" >> $GITHUB_STEP_SUMMARY done echo "" >> $GITHUB_STEP_SUMMARY fi if [ "$STATUS" = "PASS" ]; then echo "Documentation review passed." elif [ "$STATUS" = "FAIL" ]; then echo "::error::Documentation review failed — see summary for details" exit 1 else echo "::warning::Unexpected status '${STATUS}' — skipping" echo "Unexpected AI response status: ${STATUS}" >> $GITHUB_STEP_SUMMARY fi