diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..24db88a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,92 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: '0 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + security: + runs-on: bongbetic-ci + timeout-minutes: 15 + container: + image: docker.io/semgrep/semgrep:1.178.0 + steps: + - name: Checkout + env: + GIT_TOKEN: ${{ gitea.token }} + run: | + set -euo pipefail + git init -q . + git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git" + git -c http.extraheader="Authorization: token ${GIT_TOKEN}" fetch -q --depth=1 origin "${{ gitea.sha }}" + git checkout -q FETCH_HEAD + + - name: Semgrep + run: semgrep scan --config p/default --config p/owasp-top-ten --metrics off --error + + lint: + if: gitea.event_name != 'schedule' + runs-on: bongbetic-ci + timeout-minutes: 20 + steps: + - name: Checkout + env: + GIT_TOKEN: ${{ gitea.token }} + run: | + set -euo pipefail + git init -q . + git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git" + git -c http.extraheader="Authorization: token ${GIT_TOKEN}" fetch -q --depth=1 origin "${{ gitea.sha }}" + git checkout -q FETCH_HEAD + + # node:24-bookworm ships python3 without ensurepip, so python3-venv comes from apt. + - name: Install ruff + run: | + set -euo pipefail + apt-get update -qq + apt-get install -y -qq --no-install-recommends python3-venv + python3 -m venv /tmp/lint-venv + /tmp/lint-venv/bin/pip install -q ruff==0.16.10 + + - name: Ruff + run: /tmp/lint-venv/bin/ruff check src/ tests/ + + - name: Duplicate code (jscpd) + run: npx --yes jscpd@4.3.0 --config .jscpd.json . + + ai-review: + if: gitea.event_name == 'pull_request' + runs-on: bongbetic-ci + timeout-minutes: 10 + continue-on-error: true + container: + image: docker.io/pragent/pr-agent:0.47.0 + env: + config__git_provider: gitea + gitea__url: https://git.bongbetic.com + gitea__personal_access_token: ${{ secrets.PR_AGENT_GITEA_TOKEN }} + openrouter__key: ${{ secrets.OPENROUTER_API_KEY }} + config__model: ${{ vars.PR_AGENT_MODEL || 'openrouter/anthropic/claude-sonnet-5' }} + config__fallback_models: "[\"${{ vars.PR_AGENT_MODEL || 'openrouter/anthropic/claude-sonnet-5' }}\"]" + config__custom_model_max_tokens: '200000' + steps: + # Advisory only: posts a review comment, never pushes code. Skips when secrets are absent + # (for example PRs from forks, where Gitea withholds secrets). + - name: PR-Agent review + env: + PR_URL: ${{ gitea.event.pull_request.html_url }} + run: | + set -euo pipefail + if [ -z "${gitea__personal_access_token}" ] || [ -z "${openrouter__key}" ]; then + echo "::notice::PR_AGENT_GITEA_TOKEN or OPENROUTER_API_KEY not set; skipping AI review" + exit 0 + fi + cd /app + pr-agent --pr_url="${PR_URL}" review diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000..9b532aa --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,18 @@ +{ + "threshold": 8, + "minLines": 5, + "minTokens": 50, + "reporters": ["console"], + "gitignore": true, + "ignore": [ + "**/node_modules/**", + "**/.git/**", + "**/dist/**", + "**/docs/**", + "**/packaging/**", + "**/*.lock", + "**/package-lock.json", + "**/requirements.txt", + "**/*.md" + ] +} diff --git a/CI.md b/CI.md new file mode 100644 index 0000000..92fdcb3 --- /dev/null +++ b/CI.md @@ -0,0 +1,48 @@ +# CI quality gates + +Workflow: `.gitea/workflows/ci.yml` (runner label `bongbetic-ci`, no third-party `uses:` actions; code is checked out with shell git). +Status contexts: `CI / security (pull_request)`, `CI / lint (pull_request)`, `CI / ai-review (pull_request)`. + +| Job | Runs on | Blocks merge? | What it does | +|-----|---------|---------------|--------------| +| `security` | PR, push to main, weekly schedule, manual | Yes | Semgrep 1.178.0, `p/default` + `p/owasp-top-ten`, `--error` | +| `lint` | PR, push to main, manual | Yes | `ruff check src/ tests/` (ruff 0.16.10, rules E4/E7/E9/F) and jscpd 4.3.0 duplicate-code threshold 8% | +| `ai-review` | PR only | No (advisory) | PR-Agent `review`, comment-only, `continue-on-error: true` | + +There is no `e2e` (no web UI) and no `deploy` job (not a Coolify app). The weekly schedule (`0 3 * * 1`) runs only `security`. + +## Run locally + +```sh +# security (same command as CI) +podman run --rm -v "$PWD:/src:Z" docker.io/semgrep/semgrep:1.178.0 \ + semgrep scan --config p/default --config p/owasp-top-ten --metrics off --error + +# lint (needs `pip install -e '.[dev]'` or `pip install ruff==0.16.10`) +make lint + +# duplicate code +npx --yes jscpd@4.3.0 --config .jscpd.json . +``` + +Notes: +- The semgrep container needs no extra flags. `:Z` is only for SELinux hosts; its working directory is `/src`. +- The `lint` job installs `python3-venv` from apt because `node:24-bookworm` has no `ensurepip`; ruff itself is pinned. +- `.jscpd.json` threshold is 8%, just above the 7.15% baseline, so duplication cannot grow. Lower it as duplication is removed. +- Semgrep prints some non-fatal `PartialParsing` errors; they do not fail the job. + +## PR-Agent (advisory) + +`ai-review` posts a review comment on the PR through the Gitea API. It never pushes code and never blocks. It exits 0 with a notice when its secrets are empty (for example on fork PRs). + +Required repository secrets: `OPENROUTER_API_KEY`, `PR_AGENT_GITEA_TOKEN` (Gitea token of the bot account, scopes to comment on PRs). +Optional repository variable: `PR_AGENT_MODEL` (default `openrouter/anthropic/claude-sonnet-5`). The workflow sets `config__custom_model_max_tokens` to 200000, so adjust it if you pick a model with a different context window. + +## Caveats + +- Semgrep registry rules (`p/default`, `p/owasp-top-ten`) are fetched at runtime and are not pinned, so a new rule can turn a green main red. The weekly scheduled `security` run catches this early. +- Intentional findings are suppressed with a narrow `# nosemgrep: -- ` on the line. Do not use `.semgrepignore` for source files. + +## Rollback + +Revert the PR that added `ci.yml` (and its follow-up commits). If branch protection requires `CI / security`, `CI / lint` or `CI / ai-review`, relax it first, otherwise merges stay blocked on checks that no longer run. diff --git a/pyproject.toml b/pyproject.toml index fc99b32..3b85586 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,12 @@ dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "pytest-asyncio>=0.20.0", + "ruff==0.16.10", ] +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"]