diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 716f86d..894c127 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,11 +5,19 @@ on: branches: [main] pull_request: types: [opened, synchronize] + issue_comment: + types: [created, edited] + +env: + SELF_REVIEW_TTL_MIN: '45' jobs: test: runs-on: ubuntu-24.04 + if: github.event_name == 'pull_request' steps: + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: @@ -18,14 +26,58 @@ jobs: - run: go vet ./... - run: go build -o review-bot ./cmd/review-bot - # Self-review using native SAP AI Core provider - # Models must match SAP AI Core deployments - # Available models: gpt-5, anthropic--claude-4.6-sonnet, anthropic--claude-4.6-opus - # Removed gpt-4.1, gpt-5-mini, gpt-4.1-mini - not deployed on AI Core + review-gate: + runs-on: ubuntu-24.04 + if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.issue.pull_request) + outputs: + allow_review: ${{ steps.gate.outputs.allow_review }} + reason: ${{ steps.gate.outputs.reason }} + steps: + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + - name: Check self-review gate + id: gate + env: + GITEA_TOKEN: ${{ secrets.RODIN_TOKEN }} + run: | + set -e + REPO=${{ github.repository }} + API="${{ github.server_url }}/api/v1" + if [ "${GITHUB_EVENT_NAME}" = "issue_comment" ]; then + PR=${{ github.event.issue.number }} + else + PR=${{ github.event.pull_request.number }} + fi + # Get head SHA from PR JSON (works for both events) + PR_JSON=$(curl -sS -H "Authorization: token $GITEA_TOKEN" "$API/repos/$REPO/pulls/$PR") + SHA=$(echo "$PR_JSON" | jq -r .head.sha) + UPDATED_AT=$(echo "$PR_JSON" | jq -r .updated_at) + NOW=$(date -u +%s) + PR_TS=$(date -u -d "$UPDATED_AT" +%s) + AGE_MIN=$(( (NOW - PR_TS) / 60 )) + TTL_MIN=${SELF_REVIEW_TTL_MIN} + + COMMENTS=$(curl -sS -H "Authorization: token $GITEA_TOKEN" "$API/repos/$REPO/issues/$PR/comments?limit=200") + HAS_SR=$(echo "$COMMENTS" | jq -r --arg sha "$SHA" '[.[] | select(.user.login=="rodin") | select(.body|contains("Self-review against "+$sha)) | select(.body|test("(?im)^###\\s+Doc consistency\\b"))] | length') + + if [ "$HAS_SR" -gt 0 ]; then + ALLOW=true + REASON=self-review + elif [ "$AGE_MIN" -ge "$TTL_MIN" ]; then + ALLOW=true + REASON=ttl + else + ALLOW=false + REASON=missing + fi + + echo "allow_review=$ALLOW" >> $GITHUB_OUTPUT + echo "reason=$REASON" >> $GITHUB_OUTPUT + review: runs-on: ubuntu-24.04 - if: github.event_name == 'pull_request' - needs: test + if: needs.review-gate.outputs.reason == 'self-review' && (github.event_name == 'pull_request' || github.event_name == 'issue_comment') + needs: [review-gate] strategy: matrix: include: @@ -39,19 +91,28 @@ jobs: token_secret: SECURITY_REVIEW_TOKEN model: gpt-5 patterns_repo: rodin/security-patterns - patterns_files: "." + patterns_files: '.' system_prompt_file: SECURITY_REVIEW.md steps: + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.26' - run: go build -o review-bot ./cmd/review-bot + - name: Set PR_NUMBER for event type + run: | + if [ "${GITHUB_EVENT_NAME}" = "issue_comment" ]; then + echo "PR_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV + else + echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + fi - name: Run ${{ matrix.name }} review env: VCS_URL: ${{ github.server_url }} GITEA_REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ env.PR_NUMBER }} REVIEWER_TOKEN: ${{ secrets[matrix.token_secret] }} REVIEWER_NAME: ${{ matrix.name }} LLM_PROVIDER: aicore @@ -61,9 +122,9 @@ jobs: AICORE_AUTH_URL: ${{ secrets.AICORE_AUTH_URL }} AICORE_API_URL: ${{ secrets.AICORE_API_URL }} AICORE_RESOURCE_GROUP: ${{ secrets.AICORE_RESOURCE_GROUP }} - CONVENTIONS_FILE: "CONVENTIONS.md" + CONVENTIONS_FILE: 'CONVENTIONS.md' PATTERNS_REPO: ${{ matrix.patterns_repo || 'rodin/go-patterns' }} PATTERNS_FILES: ${{ matrix.patterns_files || 'README.md,patterns/' }} - LLM_TIMEOUT: "600" + LLM_TIMEOUT: '600' SYSTEM_PROMPT_FILE: ${{ matrix.system_prompt_file }} run: ./review-bot diff --git a/.gitea/workflows/workflow-lint.yml b/.gitea/workflows/workflow-lint.yml new file mode 100644 index 0000000..2e9a38d --- /dev/null +++ b/.gitea/workflows/workflow-lint.yml @@ -0,0 +1,42 @@ +name: Workflow Lint + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize] + +jobs: + workflow-sanity: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Sanity check ci.yml triggers and gates + run: | + set -euo pipefail + python3 - <<'PY' +import sys, yaml, re +from pathlib import Path +p = Path('.gitea/workflows/ci.yml') +w = yaml.safe_load(p.read_text()) +# 1) Top-level 'on' must exist and include pull_request + issue_comment +on = w.get('on') +assert isinstance(on, dict), "ci.yml: top-level 'on' must be a mapping" +assert 'pull_request' in on, "ci.yml: missing on.pull_request" +assert 'issue_comment' in on, "ci.yml: missing on.issue_comment (self-review trigger)" +pr_types = on['pull_request'].get('types', []) if isinstance(on['pull_request'], dict) else [] +ic_types = on['issue_comment'].get('types', []) if isinstance(on['issue_comment'], dict) else [] +for t in ['opened','synchronize']: + assert t in pr_types, f"ci.yml: pull_request.types must include '{t}'" +for t in ['created','edited']: + assert t in ic_types, f"ci.yml: issue_comment.types must include '{t}'" +# 2) review-gate must run on both PR and issue_comment (if condition string) +rg_if = w['jobs']['review-gate'].get('if','') +assert 'github.event_name == ' in rg_if and 'issue_comment' in rg_if and 'pull_request' in rg_if, \ + "ci.yml: review-gate.if must include both pull_request and issue_comment" +# 3) review job must require self-review reason +rev_if = w['jobs']['review'].get('if','') +assert "needs.review-gate.outputs.reason == 'self-review'" in rev_if, \ + "ci.yml: review.if must require reason=='self-review'" +print('OK: ci.yml triggers and gates look sane') +PY