ci: add quality-gate workflow
This commit is contained in:
@@ -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-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"]
|
||||
|
||||
Reference in New Issue
Block a user