diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..0e14d70 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,134 @@ +name: Verify and publish ARM64 image + +on: + pull_request: + paths: + - Dockerfile + - .dockerignore + - pyproject.toml + - uv.lock + - jarca_ledger_mcp/** + - dashboard/** + - .github/workflows/publish-image.yml + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: image-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify-arm64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - uses: docker/setup-buildx-action@v3 + + - name: Build ARM64 verification image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/arm64 + load: true + push: false + tags: jarca-ledger-mcp:arm64-verify + cache-from: type=gha,scope=jarca-ledger-arm64 + cache-to: type=gha,mode=max,scope=jarca-ledger-arm64 + + - name: Verify image platform, imports, and SQLite + run: | + set -euo pipefail + test "$(docker image inspect jarca-ledger-mcp:arm64-verify --format '{{.Architecture}}')" = arm64 + docker run --rm --platform linux/arm64 \ + -e JARCA_LEDGER_DB_PATH=/tmp/ledger.sqlite3 \ + jarca-ledger-mcp:arm64-verify \ + python -c 'import platform, cryptography, numpy, pandas, pyarrow, pymupdf; from jarca_ledger_mcp import service; result=service.health_check(); assert platform.machine() == "aarch64"; assert result["success"], result; print(result["schema_version"])' + + - name: Verify MCP and Dashboard startup + run: | + set -euo pipefail + trap 'docker rm -f jarca-mcp-smoke jarca-dashboard-smoke >/dev/null 2>&1 || true' EXIT + + docker run -d --name jarca-mcp-smoke --platform linux/arm64 \ + jarca-ledger-mcp:arm64-verify + for _ in $(seq 1 60); do + status=$(docker inspect --format '{{.State.Health.Status}}' jarca-mcp-smoke) + test "$status" = healthy && break + test "$status" = unhealthy && { docker logs jarca-mcp-smoke; exit 1; } + sleep 1 + done + test "$(docker inspect --format '{{.State.Health.Status}}' jarca-mcp-smoke)" = healthy + + docker run -d --name jarca-dashboard-smoke --platform linux/arm64 \ + -p 127.0.0.1::8501 jarca-ledger-mcp:arm64-verify \ + python -m streamlit run dashboard/app.py \ + --server.address 0.0.0.0 --server.port 8501 \ + --server.headless true --browser.gatherUsageStats false + port=$(docker port jarca-dashboard-smoke 8501/tcp | sed 's/.*://') + code=000 + for _ in $(seq 1 90); do + code=$(curl -sS -o /tmp/dashboard-health -w '%{http_code}' \ + "http://127.0.0.1:${port}/_stcore/health" || true) + test "$code" = 200 && break + sleep 1 + done + test "$code" = 200 + test "$(tr -d '\n' < /tmp/dashboard-health)" = ok + + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: verify-arm64 + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - 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 }} + + - name: Build and publish immutable ARM64 image + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/arm64 + push: true + tags: | + ghcr.io/genichin/jarca-ledger-mcp:${{ github.ref_name }} + ghcr.io/genichin/jarca-ledger-mcp:sha-${{ github.sha }} + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ github.ref_name }} + cache-from: type=gha,scope=jarca-ledger-arm64 + cache-to: type=gha,mode=max,scope=jarca-ledger-arm64 + provenance: mode=max + sbom: true + + - name: Report published digest + run: | + echo "ghcr.io/genichin/jarca-ledger-mcp:${GITHUB_REF_NAME}@${{ steps.build.outputs.digest }}" diff --git a/Dockerfile b/Dockerfile index bd1d846..020e9b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,28 @@ +FROM ghcr.io/astral-sh/uv:0.11.14 AS uv + FROM python:3.12-slim ENV PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ JARCA_LEDGER_DB_PATH=/data/ledger.sqlite3 \ JARCA_MCP_TRANSPORT=streamable-http \ JARCA_MCP_HOST=0.0.0.0 \ JARCA_MCP_PORT=8000 \ - JARCA_MCP_STREAMABLE_HTTP_PATH=/mcp + JARCA_MCP_STREAMABLE_HTTP_PATH=/mcp \ + PATH=/app/.venv/bin:$PATH + +COPY --from=uv /uv /uvx /usr/local/bin/ WORKDIR /app -COPY pyproject.toml README.md ./ +COPY pyproject.toml uv.lock README.md ./ +RUN uv sync --frozen --no-dev --extra dashboard --no-install-project + COPY jarca_ledger_mcp ./jarca_ledger_mcp COPY dashboard ./dashboard - -RUN python -m pip install --upgrade pip \ - && python -m pip install '.[dashboard]' +RUN uv sync --frozen --no-dev --extra dashboard VOLUME ["/data"] EXPOSE 8000 8501