From 5dedaeb4b29f76f504f1f03833fdd410b9a51bfb Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Fri, 14 Aug 2026 16:59:45 -0500 Subject: [PATCH] ci: add GHCR release workflow using the TES/RAG BuildKit-secret pattern HIPAA PR3b GHCR follow-up: closes the API Gateway release-mechanism gap identified in the prior read-only investigation. This repo's Dockerfile previously required the *parent* directory as its build context (COPY omnibioai-iam-client /tmp/...), which no GitHub Actions workflow in this org has ever been able to do for a private sibling repository -- exactly why this repo had zero working publish automation despite one image having been pushed manually, once, over a month ago. Dockerfile: adopts the exact pattern already proven in production by omnibioai-tes and omnibioai-rag instead of inventing a new one. omnibioai-iam-client is now a pinned git+https dependency declared in pyproject.toml, installed via `pip install .` inside a `RUN --mount=type=secret,id=github_token` layer -- never ARG/ENV, which would leak the token into BuildKit's build-log output for that layer; a secret mount is never printed and never persists in any image layer, confirmed directly (docker history + an in-container filesystem search for both the secret mount path and any leftover git-config rewrite found neither). Build context is now `.` alone. git added to apt-get (required for pip's own git+https clone; the old raw-COPY approach never needed it). pyproject.toml: omnibioai-iam-client @ git+https://.../omnibioai-iam-client.git@v0.1.3, same pinned-tag convention and version as TES/RAG's own entries for this exact dependency. Additionally required: [tool.hatch.metadata] allow-direct-references = true -- hatchling's own metadata validation otherwise rejects any `name @ ` dependency outright ("cannot be a direct reference unless..."). TES and RAG never needed this because both are setuptools-backed, not hatchling; this repo's own [build-system] already was, so this is a required fix, not a style choice -- discovered by an actual failing local build before it was added, not assumed. requirements.txt is intentionally left untouched (unrelated to this build's dependency resolution now, but not removed without being asked). .github/workflows/ci.yml (new): single docker-publish job, modeled on omnibioai-security-audit's own tag-gated `docker` job. Triggers on push to main, v*.*.* tags, and workflow_dispatch; the job itself only runs for a real tag or an explicit manual dispatch (matches that repo's own tag-gating, so an ordinary push to main never publishes). Least-privilege permissions (contents: read, packages: write) -- nothing else. Publishes ghcr.io/omnibioai/omnibioai-api-gateway with three tags: latest, the semver from the pushed tag, and an immutable short commit SHA (sha-) -- this org's convention has never included the SHA tag before; added because every prior GHCR-content-verification step in this whole workstream had to fall back to manual docker-cp archaeology for lack of one. IAM_CLIENT_READ_TOKEN is referenced as a BuildKit secret (never a build arg/env var) but is NOT created, assumed present, or guessed at here -- if missing, the workflow will fail cleanly at the Dockerfile's private-dependency install step, which is an intentional, visible configuration gap, not something this change works around. Validation performed (local only, nothing pushed): - YAML parses cleanly. - Full local BuildKit build using GHCR_PULL_TOKEN, already present in omnibioai-studio's .env for this exact purpose (the same credential omnibioai-tes/omnibioai-rag's own local dev builds already use) -- passed via --secret id=github_token,env=GHCR_PULL_TOKEN (never on the command line, never in argv, never printed). First attempt failed on the hatchling allow-direct-references gap above; fixed; second and third builds (after a later comment-only Dockerfile edit) both succeeded, producing a working image. Container boot-tested: GET /health -> 200 {"status":"ok"}. Local image and container removed after validation. - git diff --check clean; docker history and an in-image filesystem search both confirm no credential value anywhere in the built image. - Full existing test suite: 199 passed, 4 pre-existing failures (test_iam_client.py cache-key-prefix bug, unrelated, reproduced identically on main before this branch). Not done here, deliberately: no tag pushed, no image published, no secret created, no other repository touched. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 66 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 48 ++++++++++++++++++----------- pyproject.toml | 23 ++++++++++++++ 3 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f618f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + push: + branches: [main] + tags: ['v*.*.*'] + workflow_dispatch: + +jobs: + docker: + name: Build & Push Docker image + runs-on: ubuntu-latest + # Only publishes for a real version tag or an explicit manual run -- + # never for an ordinary push to main. Matches omnibioai-security-audit's + # own tag-gated convention; workflow_dispatch is additionally allowed + # here so this can be validated once before the first real tag push, + # the same way that repo's own pipeline was validated with -ci-test + # tags before trusting it for a real release. + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + type=sha,prefix=sha-,format=short + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Build context is this repo alone (`.`) -- see the Dockerfile's own + # comment for why the previous parent-directory/sibling-COPY + # requirement no longer applies. IAM_CLIENT_READ_TOKEN must be a + # fine-grained PAT scoped to Contents:Read on omnibioai-iam-client + # only; it is NOT created or assumed present by this workflow. If + # the secret is missing, this step fails at the private-dependency + # install inside the Dockerfile's RUN --mount=type=secret layer -- + # a configuration gap to fix by adding the repo secret, not by + # working around it here. + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + secrets: | + github_token=${{ secrets.IAM_CLIENT_READ_TOKEN }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 03b245b..72f08c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,40 @@ +# syntax=docker/dockerfile:1 FROM python:3.11-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential curl \ + && apt-get install -y --no-install-recommends build-essential curl git \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -# Build context is the parent directory (/home/manish/Desktop/machine) -# so sibling repos are accessible during the build. - -# Install IAM client SDK from source, then discard the source tree. -# Re-added: removed as dead weight in a88f6ac when nothing imported it; -# app/services/iam_client.py now uses it for RS256/JWKS/HS256 token -# verification (IAM Foundation gateway integration). -COPY omnibioai-iam-client /tmp/omnibioai-iam-client -RUN pip install --no-cache-dir /tmp/omnibioai-iam-client \ - && rm -rf /tmp/omnibioai-iam-client - -# Install gateway Python dependencies. -COPY omnibioai-api-gateway/requirements.txt requirements.txt -RUN pip install --no-cache-dir -r requirements.txt - -# Copy the gateway service source. -COPY omnibioai-api-gateway . +# GHCR release follow-up: build context is now this repo alone (`.`), not +# the parent directory -- the previous COPY omnibioai-iam-client /tmp/... +# step required a sibling checkout, which no GitHub Actions workflow in +# this org has ever been able to do for a *private* sibling repo. Adopts +# the same pattern already proven in production by omnibioai-tes and +# omnibioai-rag instead: pyproject.toml declares omnibioai-iam-client as a +# pinned git+https dependency (private repo -- GitHub Packages has no +# PyPI-format registry), and `pip install .` re-resolves ALL declared +# deps including direct-URL ones (it does not trust a same-named package +# already being installed the way it does for plain version-range +# requirements), so the token must be available for pip's own git clone +# here, not just a separate pre-install step. +COPY pyproject.toml . +COPY app/ ./app/ + +# Uses a BuildKit secret mount (not ARG/ENV) -- ARG/ENV values get echoed +# into BuildKit's progress output for the RUN instruction that uses them, +# leaking the token into build logs. A secret mount is never printed and +# never persists in any image layer. hatchling is pre-installed explicitly +# (unlike omnibioai-tes/omnibioai-rag, both setuptools-backed) because this +# repo's own [build-system] uses hatchling.build -- confirmed necessary by +# an actual local build; see pyproject.toml's [tool.hatch.metadata] for the +# one other hatchling-specific allowance the pinned dependency below needs. +RUN --mount=type=secret,id=github_token \ + git config --global url."https://$(cat /run/secrets/github_token)@github.com/".insteadOf "https://github.com/" \ + && pip install --no-cache-dir hatchling \ + && pip install --no-cache-dir --upgrade-strategy only-if-needed . \ + && git config --global --unset url."https://$(cat /run/secrets/github_token)@github.com/".insteadOf EXPOSE 8080 diff --git a/pyproject.toml b/pyproject.toml index 0cf3a20..847809a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,18 @@ dependencies = [ "httpx", "redis[asyncio]", "pyjwt", + + # IAM Foundation gateway integration: app/services/iam_client.py uses + # AsyncIAMClient for RS256/JWKS/HS256 token verification. Pinned direct + # git reference -- omnibioai-iam-client is a private repo distributed + # via git+https, not a package index (GitHub Packages has no + # PyPI-format registry). Same pinned-tag distribution convention and + # version (latest, additive/backward-compatible UserContext fields) as + # omnibioai-tes/omnibioai-rag's own pyproject.toml entries for this + # same dependency -- see [tool.hatch.metadata] below for the one + # allowance this repo specifically needs that those two don't (they're + # both setuptools-backed, not hatchling). + "omnibioai-iam-client @ git+https://github.com/OmniBioAI/omnibioai-iam-client.git@v0.1.3", ] [project.optional-dependencies] @@ -25,6 +37,17 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["app"] +# Required for the pinned git+https omnibioai-iam-client dependency above: +# hatchling's own metadata validation otherwise rejects any PEP 508 "direct +# reference" (a `name @ ` dependency) with +# "Dependency ... cannot be a direct reference unless field +# tool.hatch.metadata.allow-direct-references is set to true" -- confirmed +# by an actual local `pip install .` build failing with exactly that error +# before this was added. Scoped to direct references only; does not +# otherwise relax dependency resolution. +[tool.hatch.metadata] +allow-direct-references = true + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"]