Fix broken release→Docker publish pipeline (stale :latest at v1.15.0)#130
Merged
Conversation
…est) Releases never published Docker images. publish-release.yml pushes the v* git tag using secrets.GITHUB_TOKEN, and GitHub does not trigger downstream workflows from events made with GITHUB_TOKEN. So publish-docker.yml (on: push: tags: ['v*']) never fired on a release — the last Docker publish was the manually-tagged v1.15.0, and v1.15.1..v1.16.0 all skipped Docker. ghcr.io/copilotkit/pathfinder:latest has been stale at 1.15.0, missing the issue-#88 fix shipped as 1.16.0. Changes: - publish-docker.yml: add workflow_call + workflow_dispatch triggers (each with a required `tag` input) while KEEPING push: tags: ['v*']. A "Resolve image ref" step picks the input tag (call/dispatch) or github.ref_name (push), checks out that ref, and tags the image :latest + :<ref> on every path. Declares an optional SLACK_WEBHOOK_OSS_ALERTS workflow_call secret. - publish-release.yml: add a `docker` job that needs [build, publish] and invokes publish-docker.yml via workflow_call with tag=v<version>, so Docker builds in the SAME run right after npm publish + tag succeed — sidestepping the GITHUB_TOKEN trigger limitation with no new secret. Passes the Slack secret explicitly (not `secrets: inherit`) to satisfy zizmor's secrets-inherit audit. Gates: prettier clean, actionlint clean, zizmor (min-severity medium) no findings, local `docker buildx build -f Dockerfile` succeeds.
jpr5
added a commit
that referenced
this pull request
Jun 25, 2026
…age (#88) (#131) Closes the local-embeddings half of #88. (The schema half shipped in v1.16.0.) `@xenova/transformers` is an optional peer dep absent from the published Docker image, so `provider: local` failed — and failed *silently* in the common path. This PR makes it fail loudly and ships a supported Docker path. Three independent units; the authoritative design spec is included in the branch (`docs/superpowers/specs/2026-06-25-local-embeddings-88-closeout-design.md`). ## Unit 1 — fail loud + fix validate semantics - **Startup guard:** `serve` now eagerly resolves `@xenova/transformers` at boot when `embedding.provider === "local"`. If absent, the process exits non-zero immediately with an actionable message (install the peer **or** use `ghcr.io/copilotkit/pathfinder:latest-local`). Previously it booted fine and threw lazily at first embed. - **validate semantics:** the optional-dep check now routes to a new `result.warnings[]` field instead of `result.errors[]`, so `validate` exits **0** when only warnings exist (it's a static linter; the startup guard is the real enforcement). The "Optional Dependencies" display block and the schema early-return are unchanged. - A single shared `resolveLocalEmbeddingDep()` / `assertLocalEmbeddingDepForProvider()` helper (with one `LOCAL_EMBEDDING_DEP_MESSAGE`) backs both the startup guard and validate. - Unit tests added: startup guard (local+absent→fail, local+present→ok, non-local+absent→ok) and validate exit-0-with-warning. ## Unit 2 — `-local` Docker image variant - **Dockerfile:** added `ARG INCLUDE_LOCAL_EMBEDDINGS=false` to the prod stage; after `npm ci --omit=dev`, conditionally `npm install @xenova/transformers` only when the arg is `true`. Default build stays slim. - **publish-docker.yml:** the single build is now two variants in the same run from the same Dockerfile with **disjoint** tags — default (slim) → `:latest`, `:<ref>`; local (`INCLUDE_LOCAL_EMBEDDINGS=true`) → `:latest-local`, `:<ref>-local`. Both multi-arch, shared `type=gha` cache, one Slack notify summarizing both. The `workflow_call`/`workflow_dispatch`/`push:tags` triggers and tag-resolution step from #130 are preserved. ## Unit 3 — docs - `docs/deploy`: notes `:latest-local` ships `@xenova/transformers` preinstalled and that the default image now fails loudly at startup for `provider: local`. - `docs/config`: cross-references the `-local` image vs `npm install` for the local provider. - `README.md`: one-line pointer to the `-local` image. --- ## Local RED→GREEN proof (real surfaces) All runs used a schema-valid `provider: local` config with `@xenova/transformers` genuinely **ABSENT** from `node_modules`. ### Unit 1 — startup guard (`serve`) - **RED** (origin/main code, rebuilt to dist): `serve` boots past the embedding config with no mention of `@xenova/transformers` (grep count `0`) and only dies later on the DB connection (`ECONNREFUSED`) — i.e. with a reachable DB it would boot a healthy-looking server that explodes at first embed. - **GREEN** (this branch): `serve` exits **1** at boot: ``` [startup] fatal: Error: embedding.provider is "local" but the optional peer dependency @xenova/transformers is not installed. Either install it (npm install @xenova/transformers) or use the prebuilt image ghcr.io/copilotkit/pathfinder:latest-local, which ships it preinstalled. ``` ### Unit 1 — validate exit code - **RED** (origin/main): `validate` on the schema-valid local config prints `Result: 1 optional dependency warning(s), no hard errors.` yet exits **1** (the self-contradiction). - **GREEN** (this branch): same config, same printed line, exits **0**. ### Unit 2 — Docker variants (built locally before CI) - `docker buildx build --build-arg INCLUDE_LOCAL_EMBEDDINGS=true --target prod -t pf:local .` → `docker run --rm pf:local node -e "require.resolve('@xenova/transformers')…"` → **PRESENT** - `docker buildx build --target prod -t pf:slim .` → same probe → **ABSENT** ### Test suite Full `npx vitest run` (excluding `**/.claude/**`): **171 files / 3192 tests passed, 0 failures.** Prettier `--check` clean, `tsc` build clean, `tsc -p tsconfig.scripts.json` clean, version-sync in sync. No `as any`. --- A **follow-up release** ships the `-local` image and the startup guard (the matrix docker build only runs on release/dispatch, not on this PR — so PR CI here is Static Quality only). Do not merge as a release commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
publish-release.ymlpushes thev*git tag withsecrets.GITHUB_TOKEN(Ensure git tag exists step,git push origin "${TAG}"). GitHub does not trigger downstream workflows from events made withGITHUB_TOKEN, sopublish-docker.yml(on: push: tags: ['v*']) never fires on a release.Result: the last Docker publish was the manually-tagged v1.15.0; v1.15.1–v1.15.4 and v1.16.0 all skipped Docker.
ghcr.io/copilotkit/pathfinder:latesthas been stale at 1.15.0 — missing the issue-#88 fix that shipped to npm as 1.16.0.No app-token secrets exist in the repo (
gh secret list→ onlyANTHROPIC_API_KEY,NOTION_TOKEN,PATHFINDER_ANALYTICS_TOKEN,RAILWAY_API_TOKEN; no variables), so the app-token alternative is not viable. Chosen approach: makepublish-docker.ymlreusable (workflow_call) and invoke it as a job frompublish-release.yml— Docker builds in the same run right after npm publish + tag succeed, sidestepping theGITHUB_TOKEN-trigger limitation entirely with no new secret.Changes
.github/workflows/publish-docker.ymlworkflow_callandworkflow_dispatchtriggers, each with a requiredtaginput, while keepingpush: tags: ['v*'].ref=inputs.tag(call/dispatch) orgithub.ref_name(push); checkout uses that ref; image is always tagged:latest+:<ref>.SLACK_WEBHOOK_OSS_ALERTSworkflow_callsecret. ghcr login + Slack-notify steps preserved; permissionscontents: read+packages: write..github/workflows/publish-release.ymldockerjob:needs: [build, publish],if: published == 'false',uses: ./.github/workflows/publish-docker.ymlwithtag: v${{ needs.build.outputs.version }}. Passes the Slack secret explicitly (notsecrets: inherit) to satisfy zizmor'ssecrets-inheritaudit.Local Docker build proof (mandatory gate)
docker buildx build -f Dockerfile -t pathfinder:test --load .→ success (real docker driver, not depot):Static gates
--checkon both workflows: clean--min-severity medium: No findings to report (thesecrets-inheritmedium was resolved by switching to explicit secret pass-through)RED → GREEN evidence
RED (current state, pre-fix):
gh run list --workflow publish-docker.yml→ last run is v1.15.0 (2026-06-15). No docker run exists for v1.15.1..v1.16.0.Publish Releaserun 28195343560, 2026-06-25) but no docker run followed.docker manifest inspect ghcr.io/copilotkit/pathfinder:v1.16.0→ manifest unknown (image does not exist).:latestdigestsha256:80b0393ce19505520583e0190b979217f6c0bda60f1060082e760fb3e385485b==:v1.15.0digest (GHCR version tags['v1.15.0','latest']). So:latestIS v1.15.0.GREEN (to be confirmed after merge — see backfill): a
publish-dockerrun for v1.16.0 completessuccessand:latestdigest changes to the v1.16.0 build. Backfill plan below.Backfill v1.16.0 (after merge)
The backfill needs the
workflow_dispatchtrigger onmain, so it runs after this PR merges:Then verify GREEN:
Going forward, every release auto-publishes Docker in the same run as the npm publish.
🤖 Generated with Claude Code