From f74bccb5b6deec31ae425d6440562a2a42cc57e4 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Thu, 24 Sep 2026 12:47:35 +0530 Subject: [PATCH 1/5] chore: one CI workflow, run on pull requests, main and release tags - run-tests.yml becomes ci.yml, shaped like the web client's, ready for an image job. - The check job runs every gate: lint, format, types, dead code, tests. --- .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 41 ------------------------ 2 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a9a0662 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +# One workflow, one job graph: +# +# check ──→ image (main and release tags only) +# +# A pull request runs check alone. The image is built and published only once +# the code on main, or a release tag, has passed every check. +on: + pull_request: + push: + branches: [main] + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # Every gate pre-commit runs, again, since hooks can be skipped. + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + # --locked fails if uv.lock disagrees with pyproject.toml, so a dependency + # added without its lockfile fails here rather than resolving afresh. + - name: Install + run: uv sync --locked + + - name: Lint + run: uv run ruff check . + + - name: Check formatting + run: uv run ruff format --check . + + - name: Type check + run: uv run pyright + + - name: Dead code + 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. + - name: Test + run: uv run pytest diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index d099ff7..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Run Tests - -on: - push: - branches: [main] - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --locked - - - name: Lint - run: uv run ruff check . - - - name: Check formatting - run: uv run ruff format --check . - - - name: Type check - run: uv run pyright - - - name: Dead code - run: uv run vulture - - # The e2e tier needs a running DSS; the `e2e` marker keeps it out of the - # default run (pyproject.toml). - - name: Test - run: uv run pytest From eebbcea66bdebe9d13f8413b6381c02e838f9437 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Thu, 24 Sep 2026 12:48:12 +0530 Subject: [PATCH 2/5] chore: publish the image to GHCR from main and release tags - Built only after every check passes; never on a pull request. - Tagged with the ref, the commit and latest; labelled to link it to this repo. - Pushed with the workflow's own token, so no secret needs setting up. --- .github/workflows/ci.yml | 51 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9a0662..e9bd401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI # One workflow, one job graph: # -# check ──→ image (main and release tags only) +# check ──→ image (main and release tags only: build, then publish to GHCR) # # A pull request runs check alone. The image is built and published only once # the code on main, or a release tag, has passed every check. @@ -20,6 +20,8 @@ concurrency: permissions: contents: read + # To publish the image to this repository's GHCR package. + packages: write jobs: # Every gate pre-commit runs, again, since hooks can be skipped. @@ -54,3 +56,50 @@ jobs: # default run (pyproject.toml). Coverage is printed, not gated. - name: Test run: uv run pytest + + # Only on main and release tags. A pull request has already had every check, + # and an image it will never publish would cost a build on every push. + image: + if: github.event_name != 'pull_request' + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # GHCR names must be lowercase; the owner is "OpenAgriNet". + - name: Set image name + run: echo "IMAGE=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + # `main` or the release tag, with any slash made safe for a tag. + - name: Set image tag + run: echo "TAG=$(echo '${{ github.ref_name }}' | tr '/' '-')" >> "$GITHUB_ENV" + + # Three tags: the ref (main, v1.2.0), the exact commit, and latest. + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.IMAGE }}:${{ env.TAG }} + ${{ env.IMAGE }}:${{ github.sha }} + ${{ env.IMAGE }}:latest + # Links the package to this repository on GitHub, so it appears on the + # repo page and inherits its access. + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + # Reuse layers from earlier runs, so only what changed is rebuilt. + cache-from: type=gha + cache-to: type=gha,mode=max From 5fb45302db78a10464861b927461256803b9ac6b Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Thu, 24 Sep 2026 12:48:17 +0530 Subject: [PATCH 3/5] docs: where CI publishes the image --- CHANGELOG.md | 1 + README.md | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad6e94..202e16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project are recorded here, in the format of ## [Unreleased] ### Added +- CI publishes the image to `ghcr.io/openagrinet/experience-api` from main and release tags. - `EXPERIENCE_API_DSS_MODE=http` calls the real DSS: the §7 request, its event stream read leniently. - A Dockerfile and a compose file that run the API with a healthcheck. - `POST /v1/chat`: SSE or JSON by `Accept`, answered by a fake DSS for now. diff --git a/README.md b/README.md index 36550d7..d8d9c20 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,15 @@ Same port, same fake DSS. `EXPERIENCE_API_DSS_MODE=http docker compose up too. Set `EXPERIENCE_API_HOST_PORT` to publish on another port. The container reports healthy once `/healthz` answers. +### The published image + +CI publishes an image on every push to `main` and every release tag, once +every check passes: + +```bash +docker pull ghcr.io/openagrinet/experience-api:latest # or :main, :v1.2.0, : +``` + ### Try a chat turn ```bash From cb96ac5f88f87ce25ae715ac1d3baf46f0a1286f Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Thu, 24 Sep 2026 12:49:31 +0530 Subject: [PATCH 4/5] chore: use actions/checkout v7, its latest release --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9bd401..0019206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v7 @@ -64,7 +64,7 @@ jobs: needs: check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: docker/setup-buildx-action@v3 From 676779c93823ef45d1bebac914bd7a91e5bbcf0f Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Thu, 24 Sep 2026 12:53:22 +0530 Subject: [PATCH 5/5] chore: let CI be run by hand, without moving latest from a branch - workflow_dispatch runs check and publishes the chosen ref's image. - Only main tags latest, so a manual branch run cannot replace what deploys pull. --- .github/workflows/ci.yml | 17 +++++++++++------ README.md | 3 ++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0019206..589d2b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,15 @@ name: CI # One workflow, one job graph: # -# check ──→ image (main and release tags only: build, then publish to GHCR) +# check ──→ image (main, release tags and manual runs: publish to GHCR) # # A pull request runs check alone. The image is built and published only once -# the code on main, or a release tag, has passed every check. +# the code on main, or a release tag, has passed every check. A manual run +# publishes the chosen branch's image too, but only main moves `latest`. on: pull_request: + # Run by hand from the Actions tab, on any branch or tag. + workflow_dispatch: push: branches: [main] tags: @@ -57,8 +60,8 @@ jobs: - name: Test run: uv run pytest - # Only on main and release tags. A pull request has already had every check, - # and an image it will never publish would cost a build on every push. + # Not on pull requests: one has already had every check, and an image it will + # never publish would cost a build on every push. image: if: github.event_name != 'pull_request' needs: check @@ -83,7 +86,9 @@ jobs: - name: Set image tag run: echo "TAG=$(echo '${{ github.ref_name }}' | tr '/' '-')" >> "$GITHUB_ENV" - # Three tags: the ref (main, v1.2.0), the exact commit, and latest. + # The ref (main, v1.2.0, a branch run by hand) and the exact commit. Only + # main also moves `latest`, so a manual run from a branch cannot replace + # what a deployment pulls. - name: Build and push uses: docker/build-push-action@v6 with: @@ -94,7 +99,7 @@ jobs: tags: | ${{ env.IMAGE }}:${{ env.TAG }} ${{ env.IMAGE }}:${{ github.sha }} - ${{ env.IMAGE }}:latest + ${{ github.ref == 'refs/heads/main' && format('{0}:latest', env.IMAGE) || '' }} # Links the package to this repository on GitHub, so it appears on the # repo page and inherits its access. labels: | diff --git a/README.md b/README.md index d8d9c20..67f71c3 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ reports healthy once `/healthz` answers. ### The published image CI publishes an image on every push to `main` and every release tag, once -every check passes: +every check passes. It can also be run by hand from the Actions tab, for any +branch; only `main` moves `latest`. ```bash docker pull ghcr.io/openagrinet/experience-api:latest # or :main, :v1.2.0, :