diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..589d2b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +# One workflow, one job graph: +# +# 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. 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: + - "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 + # To publish the image to this repository's GHCR package. + packages: write + +jobs: + # Every gate pre-commit runs, again, since hooks can be skipped. + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - 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 + + # 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 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - 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" + + # 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: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.IMAGE }}:${{ env.TAG }} + ${{ env.IMAGE }}:${{ github.sha }} + ${{ 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: | + 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 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 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..67f71c3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,16 @@ 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. 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, : +``` + ### Try a chat turn ```bash