From fa30153c0d4d9e04722ffe00fffda32a8564297c Mon Sep 17 00:00:00 2001 From: latte Date: Fri, 27 Feb 2026 11:02:48 +0100 Subject: [PATCH 1/5] Enhance Docker workflow with gated publish Expand workflow triggers to push/pull_request on main and dev and to PR reviews. Run lint/test only for non-review events or when a review is approved. Add a docker-test job that smoke-tests the built image. Add a docker-publish job that resolves SHA and stable tags (latest/dev), builds the releasable image, and optionally pushes when PUSH_IMAGE=true. Update docs/deployment.md --- .gitea/workflows/docker.yml | 183 +++++++++++++++++++++++------------- docs/deployment.md | 2 +- 2 files changed, 120 insertions(+), 65 deletions(-) diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 5ad3c4f..9c4f154 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -1,74 +1,129 @@ name: docker on: - push: - pull_request: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + pull_request_review: + types: + - submitted jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt - - name: Run lint - run: | - ruff check src tests - ruff format --check src tests - black --check src tests - mypy src + lint: + if: ${{ github.event_name != 'pull_request_review' || github.event.review.state == 'approved' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Run lint + run: | + ruff check src tests + ruff format --check src tests + black --check src tests + mypy src - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt - - name: Run tests - run: pytest --cov=aegis_gitea_mcp --cov-report=term-missing --cov-fail-under=80 + test: + if: ${{ github.event_name != 'pull_request_review' || github.event.review.state == 'approved' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Run tests + run: pytest --cov=aegis_gitea_mcp --cov-report=term-missing --cov-fail-under=80 - docker-build: - runs-on: ubuntu-latest - needs: [lint, test] - env: - IMAGE_NAME: aegis-gitea-mcp - steps: - - name: Checkout - uses: actions/checkout@v4 + docker-test: + if: ${{ github.event_name != 'pull_request_review' || github.event.review.state == 'approved' }} + runs-on: ubuntu-latest + needs: [lint, test] + env: + IMAGE_NAME: aegis-gitea-mcp + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Build image tagged with commit SHA - run: | - SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" - docker build -f docker/Dockerfile -t ${IMAGE_NAME}:${SHA_TAG} . + - name: Build candidate image + run: | + SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" + docker build -f docker/Dockerfile -t ${IMAGE_NAME}:${SHA_TAG} . - - name: Tag latest on main - run: | - REF_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME:-}}" - SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" - if [ "${REF_NAME}" = "main" ]; then - docker tag ${IMAGE_NAME}:${SHA_TAG} ${IMAGE_NAME}:latest - fi + - name: Smoke-test image + run: | + SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" + docker run --rm --entrypoint python ${IMAGE_NAME}:${SHA_TAG} -c "import aegis_gitea_mcp" - - name: Optional registry push - if: ${{ vars.PUSH_IMAGE == 'true' }} - run: | - SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" - docker push ${IMAGE_NAME}:${SHA_TAG} - REF_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME:-}}" - if [ "${REF_NAME}" = "main" ]; then - docker push ${IMAGE_NAME}:latest - fi + docker-publish: + runs-on: ubuntu-latest + needs: [lint, test, docker-test] + if: >- + (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')) || + (github.event_name == 'pull_request_review' && + github.event.review.state == 'approved' && + (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'dev')) + env: + IMAGE_NAME: aegis-gitea-mcp + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Resolve tags + id: tags + run: | + EVENT_NAME="${GITHUB_EVENT_NAME:-${CI_EVENT_NAME:-}}" + REF_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME:-}}" + BASE_REF="${PR_BASE_REF:-${GITHUB_BASE_REF:-${CI_BASE_REF:-}}}" + SHA_TAG="${GITHUB_SHA:-${CI_COMMIT_SHA:-local}}" + + if [ "${EVENT_NAME}" = "pull_request_review" ]; then + TARGET_BRANCH="${BASE_REF}" + SHA_TAG="${PR_HEAD_SHA:-$SHA_TAG}" + else + TARGET_BRANCH="${REF_NAME}" + fi + + if [ "${TARGET_BRANCH}" = "main" ]; then + STABLE_TAG="latest" + elif [ "${TARGET_BRANCH}" = "dev" ]; then + STABLE_TAG="dev" + else + echo "Unsupported target branch '${TARGET_BRANCH}'" + exit 1 + fi + + echo "sha_tag=${SHA_TAG}" >> "${GITHUB_OUTPUT}" + echo "stable_tag=${STABLE_TAG}" >> "${GITHUB_OUTPUT}" + + - name: Build releasable image + run: | + docker build -f docker/Dockerfile -t ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} . + docker tag ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} ${IMAGE_NAME}:${{ steps.tags.outputs.stable_tag }} + + - name: Optional registry push + if: ${{ vars.PUSH_IMAGE == 'true' }} + run: | + docker push ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} + docker push ${IMAGE_NAME}:${{ steps.tags.outputs.stable_tag }} diff --git a/docs/deployment.md b/docs/deployment.md index 22214ca..b1e69c2 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -39,7 +39,7 @@ Workflows live in `.gitea/workflows/`: - `lint.yml`: ruff + format checks + mypy. - `test.yml`: lint + tests + coverage fail-under `80`. -- `docker.yml`: gated Docker build (depends on lint+test), SHA tag, `latest` on `main`. +- `docker.yml`: lint + test + docker smoke-test gating; image publish on push to `main`/`dev` and on approved PR review targeting `main`/`dev`; tags include commit SHA plus `latest` (`main`) or `dev` (`dev`). ## Production Recommendations From c0357ceb698e43ed9cd07cd05226a170f7676147 Mon Sep 17 00:00:00 2001 From: latte Date: Fri, 27 Feb 2026 15:50:12 +0100 Subject: [PATCH 2/5] Add configurable registry push to Docker workflow --- .gitea/workflows/docker.yml | 36 ++++++++++++++++++++++++++++++++---- docs/deployment.md | 6 ++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 9c4f154..a5a0990 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -82,8 +82,12 @@ jobs: (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'dev')) env: IMAGE_NAME: aegis-gitea-mcp + REGISTRY_IMAGE: ${{ vars.REGISTRY_IMAGE }} + REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} PR_BASE_REF: ${{ github.event.pull_request.base.ref }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} steps: - name: Checkout uses: actions/checkout@v4 @@ -118,12 +122,36 @@ jobs: echo "stable_tag=${STABLE_TAG}" >> "${GITHUB_OUTPUT}" - name: Build releasable image + id: image run: | - docker build -f docker/Dockerfile -t ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} . - docker tag ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} ${IMAGE_NAME}:${{ steps.tags.outputs.stable_tag }} + IMAGE_REF="${REGISTRY_IMAGE:-${IMAGE_NAME}}" + echo "image_ref=${IMAGE_REF}" >> "${GITHUB_OUTPUT}" + docker build -f docker/Dockerfile -t ${IMAGE_REF}:${{ steps.tags.outputs.sha_tag }} . + docker tag ${IMAGE_REF}:${{ steps.tags.outputs.sha_tag }} ${IMAGE_REF}:${{ steps.tags.outputs.stable_tag }} + + - name: Login to registry + if: ${{ vars.PUSH_IMAGE == 'true' }} + run: | + if [ -z "${REGISTRY_USER}" ] || [ -z "${REGISTRY_TOKEN}" ]; then + echo "REGISTRY_USER and REGISTRY_TOKEN secrets are required when PUSH_IMAGE=true" + exit 1 + fi + + IMAGE_REF="${{ steps.image.outputs.image_ref }}" + LOGIN_HOST="${REGISTRY_HOST}" + if [ -z "${LOGIN_HOST}" ]; then + FIRST_PART="${IMAGE_REF%%/*}" + case "${FIRST_PART}" in + *.*|*:*|localhost) LOGIN_HOST="${FIRST_PART}" ;; + *) LOGIN_HOST="docker.io" ;; + esac + fi + + printf "%s" "${REGISTRY_TOKEN}" | docker login "${LOGIN_HOST}" --username "${REGISTRY_USER}" --password-stdin - name: Optional registry push if: ${{ vars.PUSH_IMAGE == 'true' }} run: | - docker push ${IMAGE_NAME}:${{ steps.tags.outputs.sha_tag }} - docker push ${IMAGE_NAME}:${{ steps.tags.outputs.stable_tag }} + IMAGE_REF="${{ steps.image.outputs.image_ref }}" + docker push ${IMAGE_REF}:${{ steps.tags.outputs.sha_tag }} + docker push ${IMAGE_REF}:${{ steps.tags.outputs.stable_tag }} diff --git a/docs/deployment.md b/docs/deployment.md index b1e69c2..e674dbc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -41,6 +41,12 @@ Workflows live in `.gitea/workflows/`: - `test.yml`: lint + tests + coverage fail-under `80`. - `docker.yml`: lint + test + docker smoke-test gating; image publish on push to `main`/`dev` and on approved PR review targeting `main`/`dev`; tags include commit SHA plus `latest` (`main`) or `dev` (`dev`). +Docker publish settings: +- `vars.PUSH_IMAGE=true` enables registry push. +- `vars.REGISTRY_IMAGE` sets the target image name (for example `registry.example.com/org/aegis-gitea-mcp`). +- `vars.REGISTRY_HOST` is optional and overrides the login host detection. +- `secrets.REGISTRY_USER` and `secrets.REGISTRY_TOKEN` are required when push is enabled. + ## Production Recommendations - Place MCP behind TLS reverse proxy. From fc93b8d29e3be6382a2c413c17cf8bbc1acbc5fb Mon Sep 17 00:00:00 2001 From: latte Date: Fri, 27 Feb 2026 16:08:17 +0100 Subject: [PATCH 3/5] Fix Prometheus metric f-string and add YAML helper --- src/aegis_gitea_mcp/observability.py | 2 +- tests/test_automation.py | 16 ++++++++++------ tests/test_policy.py | 12 ++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/aegis_gitea_mcp/observability.py b/src/aegis_gitea_mcp/observability.py index b49676d..fde4f64 100644 --- a/src/aegis_gitea_mcp/observability.py +++ b/src/aegis_gitea_mcp/observability.py @@ -56,7 +56,7 @@ class MetricsRegistry: lines.append("# TYPE aegis_tool_calls_total counter") for (tool_name, status), count in sorted(self._tool_calls_total.items()): lines.append( - "aegis_tool_calls_total" f'{{tool="{tool_name}",status="{status}"}} {count}' + f'aegis_tool_calls_total{{tool="{tool_name}",status="{status}"}} {count}' ) lines.append( diff --git a/tests/test_automation.py b/tests/test_automation.py index 97623d0..f6c908f 100644 --- a/tests/test_automation.py +++ b/tests/test_automation.py @@ -34,6 +34,10 @@ def _set_base_env( monkeypatch.setenv("POLICY_FILE_PATH", str(policy_path)) +def _yaml_with_trailing_newline(content: str) -> str: + return content.strip() + "\n" + + def test_automation_job_denied_when_disabled( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, allow_oauth: None ) -> None: @@ -61,7 +65,7 @@ def test_automation_job_executes_when_enabled( """Dependency scan job should execute when automation is enabled and policy allows it.""" policy_path = tmp_path / "policy.yaml" policy_path.write_text( - """ + _yaml_with_trailing_newline(""" defaults: read: allow write: deny @@ -69,7 +73,7 @@ tools: allow: - automation_dependency_hygiene_scan - automation_webhook_ingest -""".strip() + "\n", +"""), encoding="utf-8", ) _set_base_env(monkeypatch, automation_enabled=True, policy_path=policy_path) @@ -95,14 +99,14 @@ def test_automation_webhook_policy_denied( """Webhook ingestion must respect policy deny rules.""" policy_path = tmp_path / "policy.yaml" policy_path.write_text( - """ + _yaml_with_trailing_newline(""" defaults: read: allow write: deny tools: deny: - automation_webhook_ingest -""".strip() + "\n", +"""), encoding="utf-8", ) _set_base_env(monkeypatch, automation_enabled=True, policy_path=policy_path) @@ -126,14 +130,14 @@ def test_auto_issue_creation_denied_without_write_mode( """Auto issue creation job should be denied unless write mode is enabled.""" policy_path = tmp_path / "policy.yaml" policy_path.write_text( - """ + _yaml_with_trailing_newline(""" defaults: read: allow write: allow tools: allow: - automation_auto_issue_creation -""".strip() + "\n", +"""), encoding="utf-8", ) _set_base_env(monkeypatch, automation_enabled=True, policy_path=policy_path) diff --git a/tests/test_policy.py b/tests/test_policy.py index 1cae024..ce7b71e 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -14,6 +14,10 @@ def _set_base_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("MCP_API_KEYS", "a" * 64) +def _yaml_with_trailing_newline(content: str) -> str: + return content.strip() + "\n" + + def test_default_policy_allows_read_and_denies_write( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -35,14 +39,14 @@ def test_policy_global_deny(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> _set_base_env(monkeypatch) policy_path = tmp_path / "policy.yaml" policy_path.write_text( - """ + _yaml_with_trailing_newline(""" defaults: read: allow write: deny tools: deny: - list_repositories -""".strip() + "\n", +"""), encoding="utf-8", ) @@ -60,7 +64,7 @@ def test_repository_path_restriction(monkeypatch: pytest.MonkeyPatch, tmp_path: _set_base_env(monkeypatch) policy_path = tmp_path / "policy.yaml" policy_path.write_text( - """ + _yaml_with_trailing_newline(""" repositories: acme/app: tools: @@ -69,7 +73,7 @@ repositories: paths: allow: - src/* -""".strip() + "\n", +"""), encoding="utf-8", ) From 5b4495a0a9b7cd1438cdd7065de015a0ba830047 Mon Sep 17 00:00:00 2001 From: latte Date: Fri, 27 Feb 2026 19:47:54 +0100 Subject: [PATCH 4/5] updaAdd AI review workflowste --- .gitea/workflows/ai-chat.yml | 61 ++++++++++++++ .gitea/workflows/ai-codebase-review.yml | 58 ++++++++++++++ .gitea/workflows/ai-comment-reply.yml | 98 +++++++++++++++++++++++ .gitea/workflows/ai-issue-triage.yml | 44 ++++++++++ .gitea/workflows/enterprise-ai-review.yml | 53 ++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 .gitea/workflows/ai-chat.yml create mode 100644 .gitea/workflows/ai-codebase-review.yml create mode 100644 .gitea/workflows/ai-comment-reply.yml create mode 100644 .gitea/workflows/ai-issue-triage.yml create mode 100644 .gitea/workflows/enterprise-ai-review.yml diff --git a/.gitea/workflows/ai-chat.yml b/.gitea/workflows/ai-chat.yml new file mode 100644 index 0000000..db861c2 --- /dev/null +++ b/.gitea/workflows/ai-chat.yml @@ -0,0 +1,61 @@ +name: AI Chat ({{BOT_USERNAME}}) + +# WORKFLOW ROUTING: +# This workflow handles FREE-FORM questions/chat (no specific command) +# Other workflows: ai-issue-triage.yml (@{{BOT_NAME}} triage), ai-comment-reply.yml (specific commands) +# This is the FALLBACK for any @{{BOT_NAME}} mention that isn't a known command + +on: + issue_comment: + types: [created] + +# CUSTOMIZE YOUR BOT NAME: +# Change '@{{BOT_NAME}}' in all conditions below to match your config.yml mention_prefix +# Examples: '@bartender', '@uni', '@joey', '@codebot' + +jobs: + ai-chat: + # Only run if comment mentions the bot but NOT a specific command + # This prevents duplicate runs with ai-comment-reply.yml and ai-issue-triage.yml + # CRITICAL: Ignore bot's own comments to prevent infinite loops (bot username: {{BOT_USERNAME}}) + if: | + {{PLATFORM}}.event.comment.user.login != '{{BOT_USERNAME}}' && + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}}') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} triage') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} help') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} explain') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} suggest') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} security') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} summarize') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} changelog') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} explain-diff') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} review-again') && + !contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} setup-labels') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: {{OPENRABBIT_REPO}} + path: .ai-review + token: ${{ secrets.AI_REVIEW_TOKEN }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - run: pip install requests pyyaml + + - name: Run AI Chat + env: + AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }} + AI_REVIEW_REPO: ${{ {{PLATFORM}}.repository }} + AI_REVIEW_API_URL: {{API_URL}} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }} + SEARXNG_URL: ${{ secrets.SEARXNG_URL }} + run: | + cd .ai-review/tools/ai-review + python main.py comment ${{ {{PLATFORM}}.repository }} ${{ {{PLATFORM}}.event.issue.number }} "${{ {{PLATFORM}}.event.comment.body }}" diff --git a/.gitea/workflows/ai-codebase-review.yml b/.gitea/workflows/ai-codebase-review.yml new file mode 100644 index 0000000..1e30283 --- /dev/null +++ b/.gitea/workflows/ai-codebase-review.yml @@ -0,0 +1,58 @@ +name: AI Codebase Quality Review + +on: + # Weekly scheduled run + # schedule: + # - cron: "0 0 * * 0" # Every Sunday at midnight + + # Manual trigger + workflow_dispatch: + inputs: + report_type: + description: "Type of report to generate" + required: false + default: "full" + type: choice + options: + - full + - security + - quick + +jobs: + ai-codebase-review: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for analysis + + # Checkout central AI tooling + - uses: actions/checkout@v4 + with: + repository: { { OPENRABBIT_REPO } } + path: .ai-review + token: ${{ secrets.AI_REVIEW_TOKEN }} + + # Setup Python + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # Install dependencies + - run: pip install requests pyyaml + + # Run AI codebase analysis + - name: Run AI Codebase Analysis + env: + AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }} + AI_REVIEW_REPO: ${{ {{PLATFORM}}.repository }} + AI_REVIEW_API_URL: { { API_URL } } + + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }} + run: | + cd .ai-review/tools/ai-review + python main.py codebase ${{ {{PLATFORM}}.repository }} diff --git a/.gitea/workflows/ai-comment-reply.yml b/.gitea/workflows/ai-comment-reply.yml new file mode 100644 index 0000000..f6fff57 --- /dev/null +++ b/.gitea/workflows/ai-comment-reply.yml @@ -0,0 +1,98 @@ +name: AI Comment Reply + +# WORKFLOW ROUTING: +# This workflow handles SPECIFIC commands: help, explain, suggest, security, summarize, changelog, explain-diff, review-again, setup-labels +# Other workflows: ai-issue-triage.yml (@{{BOT_NAME}} triage), ai-chat.yml (free-form questions) + +on: + issue_comment: + types: [created] + +# CUSTOMIZE YOUR BOT NAME: +# Change '@{{BOT_NAME}}' in the 'if' condition below to match your config.yml mention_prefix +# Examples: '@bartender', '@uni', '@joey', '@codebot' + +jobs: + ai-reply: + runs-on: ubuntu-latest + # Only run for specific commands (not free-form chat or triage) + # This prevents duplicate runs with ai-chat.yml and ai-issue-triage.yml + # CRITICAL: Ignore bot's own comments to prevent infinite loops (bot username: {{BOT_USERNAME}}) + if: | + {{PLATFORM}}.event.comment.user.login != '{{BOT_USERNAME}}' && + (contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} help') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} explain') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} suggest') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} security') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} summarize') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} changelog') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} explain-diff') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} review-again') || + contains({{PLATFORM}}.event.comment.body, '@{{BOT_NAME}} setup-labels')) + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: {{OPENRABBIT_REPO}} + path: .ai-review + token: ${{ secrets.AI_REVIEW_TOKEN }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - run: pip install requests pyyaml + + - name: Run AI Comment Response + env: + AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }} + AI_REVIEW_API_URL: {{API_URL}} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }} + run: | + cd .ai-review/tools/ai-review + + # Determine if this is a PR or issue comment + IS_PR="${{ {{PLATFORM}}.event.issue.pull_request != null }}" + REPO="${{ {{PLATFORM}}.repository }}" + ISSUE_NUMBER="${{ {{PLATFORM}}.event.issue.number }}" + + # Validate inputs + if [ -z "$REPO" ] || [ -z "$ISSUE_NUMBER" ]; then + echo "Error: Missing required parameters" + exit 1 + fi + + # Validate repository format (owner/repo) + if ! echo "$REPO" | grep -qE '^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$'; then + echo "Error: Invalid repository format: $REPO" + exit 1 + fi + + if [ "$IS_PR" = "true" ]; then + # This is a PR comment - use safe dispatch with minimal event data + # Build minimal event payload (does not include sensitive user data) + EVENT_DATA=$(cat < Date: Fri, 27 Feb 2026 19:55:01 +0100 Subject: [PATCH 5/5] Return explicit error for tokens lacking scopes --- src/aegis_gitea_mcp/server.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/aegis_gitea_mcp/server.py b/src/aegis_gitea_mcp/server.py index 5fc9ec2..b10374a 100644 --- a/src/aegis_gitea_mcp/server.py +++ b/src/aegis_gitea_mcp/server.py @@ -371,6 +371,24 @@ async def authenticate_and_rate_limit( "scopes_observed": observed_scopes, }, ) + message = ( + "OAuth token is valid but lacks required Gitea API access. " + "Re-authorize this OAuth app in Gitea and try again." + ) + if request.url.path.startswith("/mcp/"): + return _oauth_unauthorized_response( + request, + message, + scope=READ_SCOPE, + ) + return JSONResponse( + status_code=401, + content={ + "error": "Authentication failed", + "message": message, + "request_id": getattr(request.state, "request_id", "-"), + }, + ) else: probe_result = "pass" _api_scope_cache[token_hash] = now + _API_SCOPE_CACHE_TTL