diff --git a/.gitea/workflows/image.yaml b/.gitea/workflows/image.yaml new file mode 100644 index 0000000..9807ef2 --- /dev/null +++ b/.gitea/workflows/image.yaml @@ -0,0 +1,62 @@ +name: Check and publish image + +on: + pull_request: + push: + branches: [main] + tags: ["v*"] + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - name: Install locked dependencies + run: uv sync --locked + - name: Test + run: uv run pytest + - name: Lint + run: uv run ruff check . + - name: Type check + run: uv run mypy src tests + - name: Build image + run: docker build --platform linux/amd64 -t recoll-modern:ci . + + publish: + needs: checks + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Resolve publish tags + id: tags + run: | + python3 scripts/image_tags.py "$GITHUB_REF" > image-tags.txt + if [ -s image-tags.txt ]; then + echo "valid=true" >> "$GITHUB_OUTPUT" + fi + - name: Log in to Gitea registry + if: ${{ steps.tags.outputs.valid == 'true' }} + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + printf '%s' "$REGISTRY_TOKEN" | docker login gitea.lucaswinther.info --username "$REGISTRY_USERNAME" --password-stdin + - name: Build and push + if: ${{ steps.tags.outputs.valid == 'true' }} + run: | + set -eu + docker build --platform linux/amd64 \ + --label "org.opencontainers.image.source=https://gitea.lucaswinther.info/lucasw89/recoll-modern" \ + --label "org.opencontainers.image.description=Self-hosted Recoll search API" \ + --label "org.opencontainers.image.revision=$GITHUB_SHA" \ + --label "org.opencontainers.image.version=$GITHUB_REF_NAME" \ + -t recoll-modern:publish . + while IFS= read -r image; do + docker tag recoll-modern:publish "$image" + docker push "$image" + done < image-tags.txt diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d3b2c94..52bf4ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -31,6 +31,7 @@ web UI and (b) an MCP server, running in Docker, with several independent collec | `src/recoll_modern/indexing/map_export.py` | Writes a durable atomic map from canonical paths to opaque collection IDs | | `helper/` | C reader run by Recoll's `metadatacmds` for each indexed file | | `Dockerfile`, `compose.yaml`, `docker/` | Non-root Recoll runtime, read-only document mount, persistent state/index volumes and config templates | +| `.gitea/workflows/image.yaml`, `scripts/image_tags.py` | CI checks and Gitea registry publication; Git refs select published tags | | `src/recoll_modern/engine/` | Shared Protocol, deterministic fake, and isolated Recoll Python/indexer adapter | | `src/recoll_modern/domain/auth.py`, `storage/auth.py` | Argon2 password checks, seeded administrator, hashed server-side sessions and CSRF verification | | `src/recoll_modern/api/` | HTTP auth, health, file browsing, collection/membership/rule/job resources, search, document retrieval, prefix/proxy handling and JSON errors | @@ -131,6 +132,12 @@ absent; Recoll may generate additional support files there. Compose mounts docum keeps the SQLite/config and Xapian index volumes separate. Initial health can be 503 until an indexing job creates the first index. +Gitea Actions tests and builds on pull requests, main pushes and version tags. A dependent job publishes +`linux/amd64` images only for main and full `vX.Y.Z` tags. `main` follows the branch; `latest` follows +the newest pushed version tag. The job uses package-write repository Actions secrets. The local +Compose deployment builds from source; `compose.published.yaml` pulls the registry image with the same +runtime mounts. + The host Recoll contract tests skip when no container is running. Required-container mode executes the adapter inside Compose, avoiding a host Recoll dependency; the generated-corpus test proves that a read-only file can be indexed, searched in two collections, and retrieved as extracted text. The HTTP diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1c6ddb7..6fa0bf5 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -3,6 +3,22 @@ Append-only, newest first. Never edit or delete an entry — supersede it with a new one that links back. One entry per decision a future maintainer would otherwise have to reverse-engineer. +## 2026-09-28 — Publish one amd64 image through Gitea Actions +**Status:** accepted +**Context:** The Docker deployment exists, but operators must build it locally. [Gitea's documented job +token limitation](https://docs.gitea.com/usage/actions/comparison/) means the workflow needs separate +package credentials. +**Decision:** run checks and build on pull requests, main and version tags. After checks, publish +`gitea.lucaswinther.info/lucasw89/recoll-modern:main` for main and `:vX.Y.Z` plus `:latest` for a full +version tag. Use a package-write personal access token stored as repository Actions secrets. The first +published architecture is `linux/amd64`; the existing local build remains available. +**Alternatives:** tag every main commit as `latest` (makes releases ambiguous); use Buildx for multiple +architectures now (requires more runner setup); use `GITEA_TOKEN` (currently lacks package publishing). +**Consequences:** operators can pin a version, while `main` is a development image. Publishing requires +a Docker-capable Gitea runner and configured secrets; no external deploy occurs automatically. + +--- + ## 2026-09-28 — Reject non-regular explicit sources before opening **Status:** accepted **Context:** Directory discovery filters non-regular entries, but an explicit file source could name diff --git a/docs/plans/006-gitea-image-publishing.md b/docs/plans/006-gitea-image-publishing.md index 14f4086..f8dee83 100644 --- a/docs/plans/006-gitea-image-publishing.md +++ b/docs/plans/006-gitea-image-publishing.md @@ -62,11 +62,11 @@ spec: docs/specs/006-gitea-image-publishing.md - Produces `:main` on main, and `:vX.Y.Z` plus `:latest` on a full release tag after all gates pass. - Requires a Docker-capable Gitea runner with `ubuntu-latest` label and Gitea Actions secrets `REGISTRY_USERNAME` and `REGISTRY_TOKEN` for the publish job. -- [ ] **Step 1: Run a failing structure check** (`test -f .gitea/workflows/image.yaml`) and record that no workflow exists. -- [ ] **Step 2: Add** one workflow triggered by pull requests, main pushes and `v*` tag pushes. Use `actions/checkout`, `astral-sh/setup-uv`, `uv sync --locked`, `uv run pytest`, `uv run ruff check .`, `uv run mypy src tests`, and `docker build --platform linux/amd64`. Publishing is a separate job after checks, gated to main or validated release tags; log in through `docker login --password-stdin`, then tag and push each output of Task 1's script. Grant the built-in token read-only repository access; expose PAT secrets only in the publishing job. Add OCI labels for source, description, revision and ref/version. -- [ ] **Step 3: Validate** workflow syntax with `docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint:1.7.12 .gitea/workflows/image.yaml`, inspect that no pull request path reaches login/push, then run project tests, lint, type checking and `docker build --platform linux/amd64 -t recoll-modern:ci .` locally. If the validator or Docker daemon is unavailable, record the exact unverified gate rather than claiming success. -- [ ] **Step 4: Document** the new delivery boundary in `docs/ARCHITECTURE.md` and the tag/PAT/platform choice in an append-only `docs/DECISIONS.md` entry. -- [ ] **Step 5: Commit** workflow, docs and checked plan boxes as `ci: publish Gitea container image`. +- [x] **Step 1: Run a failing structure check** (`test -f .gitea/workflows/image.yaml`) and record that no workflow exists. +- [x] **Step 2: Add** one workflow triggered by pull requests, main pushes and `v*` tag pushes. Use `actions/checkout`, `astral-sh/setup-uv`, `uv sync --locked`, `uv run pytest`, `uv run ruff check .`, `uv run mypy src tests`, and `docker build --platform linux/amd64`. Publishing is a separate job after checks, gated to main or validated release tags; log in through `docker login --password-stdin`, then tag and push each output of Task 1's script. Grant the built-in token read-only repository access; expose PAT secrets only in the publishing job. Add OCI labels for source, description, revision and ref/version. +- [x] **Step 3: Validate** workflow syntax with `docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint:1.7.12 .gitea/workflows/image.yaml`, inspect that no pull request path reaches login/push, then run project tests, lint, type checking and `docker build --platform linux/amd64 -t recoll-modern:ci .` locally. If the validator or Docker daemon is unavailable, record the exact unverified gate rather than claiming success. +- [x] **Step 4: Document** the new delivery boundary in `docs/ARCHITECTURE.md` and the tag/PAT/platform choice in an append-only `docs/DECISIONS.md` entry. +- [x] **Step 5: Commit** workflow, docs and checked plan boxes as `ci: publish Gitea container image`. ### Task 3: Pull-based deployment and operator guide