Skip to content

Fix broken release→Docker publish pipeline (stale :latest at v1.15.0)#130

Merged
jpr5 merged 1 commit into
mainfrom
fix/release-docker-publish-pipeline
Jun 25, 2026
Merged

Fix broken release→Docker publish pipeline (stale :latest at v1.15.0)#130
jpr5 merged 1 commit into
mainfrom
fix/release-docker-publish-pipeline

Conversation

@jpr5

@jpr5 jpr5 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Root cause

publish-release.yml pushes the v* git tag with secrets.GITHUB_TOKEN (Ensure git tag exists step, git push origin "${TAG}"). GitHub does not trigger downstream workflows from events made with GITHUB_TOKEN, so publish-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:latest has 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 → only ANTHROPIC_API_KEY, NOTION_TOKEN, PATHFINDER_ANALYTICS_TOKEN, RAILWAY_API_TOKEN; no variables), so the app-token alternative is not viable. Chosen approach: make publish-docker.yml reusable (workflow_call) and invoke it as a job from publish-release.yml — Docker builds in the same run right after npm publish + tag succeed, sidestepping the GITHUB_TOKEN-trigger limitation entirely with no new secret.

Changes

.github/workflows/publish-docker.yml

  • Add workflow_call and workflow_dispatch triggers, each with a required tag input, while keeping push: tags: ['v*'].
  • New "Resolve image ref" step computes ref = inputs.tag (call/dispatch) or github.ref_name (push); checkout uses that ref; image is always tagged :latest + :<ref>.
  • Declares optional SLACK_WEBHOOK_OSS_ALERTS workflow_call secret. ghcr login + Slack-notify steps preserved; permissions contents: read + packages: write.

.github/workflows/publish-release.yml

  • New docker job: needs: [build, publish], if: published == 'false', uses: ./.github/workflows/publish-docker.yml with tag: v${{ needs.build.outputs.version }}. Passes the Slack secret explicitly (not secrets: inherit) to satisfy zizmor's secrets-inherit audit.

Local Docker build proof (mandatory gate)

docker buildx build -f Dockerfile -t pathfinder:test --load .success (real docker driver, not depot):

#12 [build 2/2] RUN npm run build  → @copilotkit/pathfinder@1.16.0 build / tsc
#20 exporting to image
#20 exporting manifest list sha256:0f9be1f328201f3b953b2102b121b8db58fb1d34c354dfa2fe2e95ab36acb37e done
#20 naming to docker.io/library/pathfinder:test done
#20 DONE 3.5s

Static gates

  • prettier --check on both workflows: clean
  • actionlint: clean (exit 0)
  • zizmor --min-severity medium: No findings to report (the secrets-inherit medium 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.
  • The v1.16.0 release publish ran successfully (Publish Release run 28195343560, 2026-06-25) but no docker run followed.
  • docker manifest inspect ghcr.io/copilotkit/pathfinder:v1.16.0manifest unknown (image does not exist).
  • :latest digest sha256:80b0393ce19505520583e0190b979217f6c0bda60f1060082e760fb3e385485b == :v1.15.0 digest (GHCR version tags ['v1.15.0','latest']). So :latest IS v1.15.0.

GREEN (to be confirmed after merge — see backfill): a publish-docker run for v1.16.0 completes success and :latest digest changes to the v1.16.0 build. Backfill plan below.

Backfill v1.16.0 (after merge)

The backfill needs the workflow_dispatch trigger on main, so it runs after this PR merges:

gh workflow run publish-docker.yml --ref main -f tag=v1.16.0

Then verify GREEN:

gh run list --workflow publish-docker.yml --limit 1            # new run for v1.16.0
gh run view <id> --json status,conclusion,jobs                 # conclusion: success
docker manifest inspect ghcr.io/copilotkit/pathfinder:v1.16.0  # now exists
docker buildx imagetools inspect ghcr.io/copilotkit/pathfinder:latest    # digest != 80b0393c...
docker buildx imagetools inspect ghcr.io/copilotkit/pathfinder:v1.16.0   # == :latest digest

Going forward, every release auto-publishes Docker in the same run as the npm publish.

🤖 Generated with Claude Code

…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
jpr5 merged commit e838300 into main Jun 25, 2026
10 checks passed
@jpr5
jpr5 deleted the fix/release-docker-publish-pipeline branch June 25, 2026 22:41
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant