diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 589d2b3..a08a57c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,23 @@ jobs: run: uv run vulture # The e2e tier needs a running DSS; the `e2e` marker keeps it out of the - # default run (pyproject.toml). Coverage is printed, not gated. + # default run (pyproject.toml). Coverage is printed and sent to Codecov, + # not gated. The XML report is written here only, not on local runs. - name: Test - run: uv run pytest + run: uv run pytest --cov-report=xml + + # `!cancelled()`: a failing run is when the line-by-line report helps most. + # The job still fails; this only keeps the report. + - name: Upload coverage to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + # Named explicitly: without it Codecov resolves the repo from the token + # alone, which fails with an unclear "Repository not found". + slug: ${{ github.repository }} + files: ./coverage.xml + fail_ci_if_error: true # Not on pull requests: one has already had every check, and an image it will # never publish would cost a build on every push. diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..f37c928 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,51 @@ +name: Security + +# Scans uv.lock for known CVEs. +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + dependency-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Trivy (if not cached) + run: | + if [ ! -x bin/trivy ]; then + mkdir -p bin + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin + fi + + # Gates the build: the README carries a Security badge, and a green badge + # has to mean "no fixable HIGH/CRITICAL CVEs", not merely "the scan ran". + # --ignore-unfixed keeps the gate actionable — it only fires when an + # upgrade is actually available. + - name: Trivy dependency scan + run: | + ./bin/trivy fs \ + --scanners vuln \ + --exit-code 1 \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + . diff --git a/README.md b/README.md index 67f71c3..0e7ec50 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # experience-api +[![codecov](https://codecov.io/gh/OpenAgriNet/experience-api/graph/badge.svg?token=f0PYgnH6iO)](https://codecov.io/gh/OpenAgriNet/experience-api) +[![Security](https://github.com/OpenAgriNet/experience-api/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/OpenAgriNet/experience-api/actions/workflows/security.yml) + The Experience API sits between the OpenAgriNet web client and the Decision Support System (DSS). The client sends a chat turn; the API builds a DSS request, calls the DSS, and streams the answer back. diff --git a/uv.lock b/uv.lock index c474423..777bf0d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.13" [[package]] @@ -47,11 +47,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.7.22" +version = "2026.10.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" }, ] [[package]]