ci: add quality-gate workflow (semgrep, ruff, jscpd, advisory PR-Agent) #112
@@ -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
|
||||||
+18
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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: <rule-id> -- <reason>` 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.
|
||||||
@@ -13,8 +13,12 @@ dev = [
|
|||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-cov>=4.0.0",
|
||||||
"pytest-asyncio>=0.20.0",
|
"pytest-asyncio>=0.20.0",
|
||||||
|
"ruff==0.16.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E4", "E7", "E9", "F"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
python_files = ["test_*.py"]
|
python_files = ["test_*.py"]
|
||||||
|
|||||||
Reference in New Issue
Block a user