diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c00560a59..630138dd8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,14 +4,21 @@ name: release # Trigger: GitHub Actions UI → "Run workflow" (workflow_dispatch). # # Flow (one click → everything below): -# 1. prepare — bump version, regen CHANGELOG, open + merge release PR, push tag vX.Y.Z -# 2. build-web — build Next.js standalone bundle -# 3. build-cli — build + esbuild-bundle CLI -# 4. build-bundles — assemble bin/ + web/ + cli/ tarballs for 5 platforms -# 5. docker — build & push multi-arch container image to GHCR -# 6. publish-release — create GH release with .tar.gz + .sha256 assets + auto-generated notes -# 7. publish-npm — publish kandev@X.Y.Z + @kdlbs/runtime-*@X.Y.Z to npmjs -# 8. update-homebrew-tap — push updated Formula/kandev.rb to kdlbs/homebrew-kandev +# 1. prepare — bump version, regen CHANGELOG, open + merge release PR, push tag vX.Y.Z +# 2. build-web — build Next.js standalone bundle (shared across arches) +# 3. build-cli — build + esbuild-bundle CLI (shared across arches) +# 4. build-bundles — assemble bin/ + web/ + cli/ tarballs for 5 platforms +# 5. docker-{amd64,arm64} — native per-arch image builds (no QEMU), consuming +# the prebuilt linux bundles from step 4 +# 6. docker-manifest — stitch the two arch digests into the user-facing +# multi-arch tags via `buildx imagetools create` +# 7. docker-universal-{amd64,arm64} — native universal builds on top of +# docker-manifest's per-arch base +# 8. docker-universal-manifest — size-gate amd64 staging, then promote both +# arches to the user-facing :universal tags +# 9. publish-release — create GH release with .tar.gz + .sha256 assets + auto-generated notes +# 10. publish-npm — publish kandev@X.Y.Z + @kdlbs/runtime-*@X.Y.Z to npmjs +# 11. update-homebrew-tap — push updated Formula/kandev.rb to kdlbs/homebrew-kandev # # npm publishing uses Trusted Publishers (OIDC) — no NPM_TOKEN required. # Each package (kandev + 5 @kdlbs/runtime-* packages) must have this workflow @@ -255,6 +262,8 @@ jobs: - uses: actions/setup-node@v6 with: node-version: "24" + cache: pnpm + cache-dependency-path: apps/pnpm-lock.yaml - name: Install dependencies run: pnpm -C apps install --frozen-lockfile - name: Build web (standalone) @@ -284,6 +293,8 @@ jobs: - uses: actions/setup-node@v6 with: node-version: "24" + cache: pnpm + cache-dependency-path: apps/pnpm-lock.yaml - name: Install dependencies run: pnpm -C apps install --frozen-lockfile - name: Build and bundle CLI @@ -304,11 +315,10 @@ jobs: platform: linux-x64 goos: linux goarch: amd64 - - os: ubuntu-latest + - os: ubuntu-24.04-arm platform: linux-arm64 goos: linux goarch: arm64 - cc: aarch64-linux-gnu-gcc - os: macos-14 platform: macos-x64 goos: darwin @@ -348,7 +358,7 @@ jobs: if: matrix.cc run: | sudo apt-get update - sudo apt-get install -y ${{ matrix.cc == 'aarch64-linux-gnu-gcc' && 'gcc-aarch64-linux-gnu' || 'gcc-mingw-w64-x86-64' }} + sudo apt-get install -y gcc-mingw-w64-x86-64 - name: Build backend binaries env: @@ -398,19 +408,151 @@ jobs: dist/kandev-${{ matrix.platform }}.tar.gz dist/kandev-${{ matrix.platform }}.tar.gz.sha256 - docker: - name: Build & push container image - needs: prepare + # --------------------------------------------------------------------------- + # Docker base image — split per arch onto native runners, then stitched into + # a multi-arch manifest list. Each arch consumes the matching linux bundle + # produced by build-bundles, so no Go/Next.js/pnpm work happens inside the + # Docker build (the Dockerfile is reduced to apt-get + COPY). + # --------------------------------------------------------------------------- + docker-amd64: + name: Build & push container image (amd64) + needs: [prepare, build-bundles] if: ${{ !inputs.dry_run }} runs-on: ubuntu-latest permissions: contents: read - packages: write # push to GHCR + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + env: + STAGING_IMAGE: ghcr.io/kdlbs/kandev:base-staging-${{ needs.prepare.outputs.tag }}-${{ github.run_id }}-amd64 steps: - uses: actions/checkout@v6 with: ref: ${{ needs.prepare.outputs.tag }} + - uses: actions/download-artifact@v4 + with: + name: bundle-linux-x64 + path: dist/bundle + + - name: Prepare Docker build context + run: | + set -euo pipefail + mkdir -p ctx/bundle + # --strip-components=1 drops the top-level `kandev/` dir inside the + # tarball so ctx/bundle/ ends up with bin/, web/, cli/ directly. + tar -xzf dist/bundle/kandev-linux-x64.tar.gz --strip-components=1 -C ctx/bundle + cp docker-entrypoint.sh ctx/ + + - uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # OCI labels are produced once here per arch. The manifest job combines + # the per-arch image manifests into a multi-arch list via `imagetools + # create`, which cannot retroactively add labels to image configs, so + # the labels MUST be stamped at build time on each arch. GHCR's package + # page link + provenance tooling rely on these annotations. + - name: Docker metadata (labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/kdlbs/kandev + + - name: Build and push (amd64 staging tag) + id: build + uses: docker/build-push-action@v6 + with: + context: ./ctx + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ env.STAGING_IMAGE }} + labels: ${{ steps.meta.outputs.labels }} + # provenance/sbom would attach an extra attestation manifest, which + # turns `imagetools inspect --raw` into an OCI index and breaks the + # size gate's `.layers[].size` query. The final multi-arch tags + # don't preserve attestations through `imagetools create` anyway, + # so disabling here doesn't change user-facing image content. + provenance: false + sbom: false + cache-from: type=gha,scope=base-amd64 + cache-to: type=gha,mode=max,scope=base-amd64 + + docker-arm64: + name: Build & push container image (arm64) + needs: [prepare, build-bundles] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + env: + STAGING_IMAGE: ghcr.io/kdlbs/kandev:base-staging-${{ needs.prepare.outputs.tag }}-${{ github.run_id }}-arm64 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.tag }} + + - uses: actions/download-artifact@v4 + with: + name: bundle-linux-arm64 + path: dist/bundle + + - name: Prepare Docker build context + run: | + set -euo pipefail + mkdir -p ctx/bundle + tar -xzf dist/bundle/kandev-linux-arm64.tar.gz --strip-components=1 -C ctx/bundle + cp docker-entrypoint.sh ctx/ + + - uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata (labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/kdlbs/kandev + + - name: Build and push (arm64 staging tag) + id: build + uses: docker/build-push-action@v6 + with: + context: ./ctx + file: ./Dockerfile + push: true + platforms: linux/arm64 + tags: ${{ env.STAGING_IMAGE }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + sbom: false + cache-from: type=gha,scope=base-arm64 + cache-to: type=gha,mode=max,scope=base-arm64 + + docker-manifest: + name: Assemble multi-arch base manifest + needs: [prepare, docker-amd64, docker-arm64] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -431,41 +573,43 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + # `imagetools create` combines two single-arch images into a multi-arch + # manifest list under each of the user-facing tags. No layers are + # rebuilt or re-uploaded; this is a registry-side metadata operation. + # The per-arch source images are referenced by digest (not by the + # mutable staging tag) so this is pinned to the exact bytes the + # per-arch jobs just pushed. + - name: Combine arch images into multi-arch tags + env: + AMD64_REF: ghcr.io/kdlbs/kandev@${{ needs.docker-amd64.outputs.digest }} + ARM64_REF: ghcr.io/kdlbs/kandev@${{ needs.docker-arm64.outputs.digest }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + tag_args=() + while IFS= read -r tag; do + [ -z "$tag" ] && continue + tag_args+=(--tag "$tag") + done <<<"$TAGS" + docker buildx imagetools create "${tag_args[@]}" "$AMD64_REF" "$ARM64_REF" - docker-universal: - name: Build & push universal container image - needs: [prepare, docker] + # --------------------------------------------------------------------------- + # Universal image — same per-arch native split, on top of the base image + # we just promoted. The size gate inspects the amd64 staging tag and aborts + # before the user-facing :universal tags are moved. + # --------------------------------------------------------------------------- + docker-universal-amd64: + name: Build & push universal image (amd64) + needs: [prepare, docker-manifest] if: ${{ !inputs.dry_run }} runs-on: ubuntu-latest permissions: contents: read - packages: write # push to GHCR - # Build flow: push to a per-run staging tag, gate on size against the - # staging tag, then `buildx imagetools create` to promote the same - # manifest to the user-facing tags. The promote step is metadata-only - - # no rebuild, no new bytes - so this costs us one extra registry call - # in exchange for never publishing an oversized image as :universal. - # - # Tradeoff: staging tags accumulate in GHCR over time (one per release - # run). Auto-cleanup isn't safe here - `imagetools create` produces a - # byte-identical manifest list when retagging a single source, so the - # staging tag and the promoted user-facing tags share the same package - # version in GHCR. Deleting the version to drop the staging tag would - # also drop :universal / :vX.Y.Z-universal. A separate cleanup workflow - # filtering for versions whose ONLY tags match `universal-staging-*` - # would be the correct way to address this if it becomes a problem. + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} env: - STAGING_IMAGE: ghcr.io/kdlbs/kandev:universal-staging-${{ needs.prepare.outputs.tag }}-${{ github.run_id }} + STAGING_IMAGE: ghcr.io/kdlbs/kandev:universal-staging-${{ needs.prepare.outputs.tag }}-${{ github.run_id }}-amd64 steps: - uses: actions/checkout@v6 with: @@ -480,59 +624,131 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Build universal on top of the vanilla image we just pushed. Passing - # the exact `:vX.Y.Z` tag (not `:latest`) keeps the pair coherent: - # universal-of-vX.Y.Z always rests on vanilla-vX.Y.Z, even if a later - # release moves `:latest`. The tag pushed here is the staging tag; - # final tags are applied below only if the size gate passes. - - name: Build and push (staging tag) + - name: Docker metadata (labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/kdlbs/kandev + + - name: Build and push (universal amd64 staging tag) + id: build uses: docker/build-push-action@v6 with: context: . file: Dockerfile.universal push: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 build-args: | BASE_IMAGE=ghcr.io/kdlbs/kandev:${{ needs.prepare.outputs.tag }} tags: ${{ env.STAGING_IMAGE }} - cache-from: type=gha,scope=universal - cache-to: type=gha,mode=max,scope=universal + labels: ${{ steps.meta.outputs.labels }} + # provenance/sbom off so `imagetools inspect --raw` on the staging + # tag returns the plain image manifest, not an OCI index — required + # by the size gate in docker-universal-manifest. + provenance: false + sbom: false + cache-from: type=gha,scope=universal-amd64 + cache-to: type=gha,mode=max,scope=universal-amd64 - # Universal size gate. Inspects the staging tag - the image is in - # GHCR but under a per-run tag no user pulls. Failing the gate aborts - # before promotion, so the user-facing :universal / :vX.Y.Z-universal - # tags never move to an oversized image. Bump MAX_COMPRESSED_BYTES - # deliberately when adding scope, with a one-sentence justification - # in the commit msg. + docker-universal-arm64: + name: Build & push universal image (arm64) + needs: [prepare, docker-manifest] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + env: + STAGING_IMAGE: ghcr.io/kdlbs/kandev:universal-staging-${{ needs.prepare.outputs.tag }}-${{ github.run_id }}-arm64 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.tag }} + + - uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata (labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/kdlbs/kandev + + - name: Build and push (universal arm64 staging tag) + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.universal + push: true + platforms: linux/arm64 + build-args: | + BASE_IMAGE=ghcr.io/kdlbs/kandev:${{ needs.prepare.outputs.tag }} + tags: ${{ env.STAGING_IMAGE }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + sbom: false + cache-from: type=gha,scope=universal-arm64 + cache-to: type=gha,mode=max,scope=universal-arm64 + + docker-universal-manifest: + name: Gate size + promote universal manifest + # docker-universal-manifest is included in publish-release's `needs` so + # its size gate (and any per-arch build failure) blocks the release. + needs: [prepare, docker-universal-amd64, docker-universal-arm64] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + AMD64_REF: ghcr.io/kdlbs/kandev@${{ needs.docker-universal-amd64.outputs.digest }} + ARM64_REF: ghcr.io/kdlbs/kandev@${{ needs.docker-universal-arm64.outputs.digest }} + steps: + - uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Universal size gate. Inspects the amd64 staging image by digest - the + # image is in GHCR but under a per-run tag no user pulls. Failing the + # gate aborts before promotion, so the user-facing :universal / + # :vX.Y.Z-universal tags never move to an oversized image. Bump + # MAX_COMPRESSED_BYTES deliberately when adding scope, with a + # one-sentence justification in the commit msg. amd64 is the bigger of + # the two arches in practice, so gating on it gives us a single-number + # signal. Per-arch staging builds set provenance: false, so the digest + # points directly at the image manifest (not an OCI index). - name: Enforce size budget env: MAX_COMPRESSED_BYTES: "2684354560" # 2.5 GiB run: | set -euo pipefail - # Inspect the multi-arch manifest and sum compressed layer sizes - # for linux/amd64. amd64 is the bigger of the two arches in - # practice, so gating on it gives us a single-number signal. - docker buildx imagetools inspect "$STAGING_IMAGE" --raw \ - > /tmp/index.json - AMD64_DIGEST="$(jq -r ' - .manifests[] - | select(.platform.architecture == "amd64" and .platform.os == "linux") - | .digest - ' /tmp/index.json)" - docker buildx imagetools inspect "ghcr.io/kdlbs/kandev@${AMD64_DIGEST}" --raw \ - > /tmp/manifest.json + docker buildx imagetools inspect "$AMD64_REF" --raw > /tmp/manifest.json TOTAL="$(jq '[.layers[].size] | add' /tmp/manifest.json)" echo "Universal image (amd64) compressed size: $TOTAL bytes (max: $MAX_COMPRESSED_BYTES)" if [ "$TOTAL" -gt "$MAX_COMPRESSED_BYTES" ]; then - echo "::error::Universal image exceeds compressed-size budget. Staging tag $STAGING_IMAGE will not be promoted. See docs/images.md inclusion policy." >&2 + echo "::error::Universal image exceeds compressed-size budget. Staging tags will not be promoted. See docs/images.md inclusion policy." >&2 exit 1 fi - # Promote: `buildx imagetools create` retargets an existing manifest - # list to new tag(s) without rebuilding or re-uploading layers. Multi- - # arch is preserved. Only runs if the size gate passed (default step - # ordering); if any earlier step failed, the user-facing tags stay at - # whatever they pointed to before this run. + # Promote: combine both arches into a single multi-arch manifest list + # under the user-facing tags. `imagetools create` is a metadata-only + # registry call - no layers are rebuilt or re-uploaded. Per-arch source + # images referenced by digest, not tag, so this is pinned to the bytes + # the previous jobs pushed. - name: Promote staging to final tags run: | set -euo pipefail @@ -540,14 +756,11 @@ jobs: --tag "ghcr.io/kdlbs/kandev:${{ needs.prepare.outputs.version }}-universal" \ --tag "ghcr.io/kdlbs/kandev:${{ needs.prepare.outputs.tag }}-universal" \ --tag "ghcr.io/kdlbs/kandev:universal" \ - "$STAGING_IMAGE" + "$AMD64_REF" "$ARM64_REF" publish-release: name: Publish GitHub release - # docker-universal is included so its size gate (and any build failure) - # blocks the release. Without this, publish-release runs in parallel with - # docker-universal and an oversized universal image still ships a release. - needs: [prepare, build-bundles, docker-universal] + needs: [prepare, build-bundles, docker-universal-manifest] if: ${{ !inputs.dry_run }} runs-on: ubuntu-latest permissions: @@ -621,6 +834,8 @@ jobs: with: node-version: "24" registry-url: "https://registry.npmjs.org" + cache: pnpm + cache-dependency-path: apps/pnpm-lock.yaml - name: Install dependencies run: pnpm -C apps install --frozen-lockfile diff --git a/Dockerfile b/Dockerfile index 34da1f699a..b59a9d60ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,70 +1,24 @@ # Kandev Server Dockerfile -# Multi-stage build: Go backend + Next.js frontend + CLI launcher # -# Build: -# docker build -t kandev:latest . +# Single-stage build that consumes prebuilt artifacts. The release workflow +# (.github/workflows/release.yml) extracts the per-arch release bundle +# (`kandev-linux-{x64,arm64}.tar.gz`) into the build context, then this file +# just COPYs the binaries + web standalone + CLI into the runtime layout. +# +# Building this file outside CI (manual `docker build .`) will fail because +# the `bundle/` directory isn't present in the build context. To build +# locally, extract a release tarball into ./ctx/bundle/ alongside +# ./ctx/docker-entrypoint.sh and run: +# docker build -f Dockerfile ./ctx # # Run: -# docker run -p 38429:38429 -v kandev-data:/data kandev:latest +# docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:latest -# --------------------------------------------------------------------------- -# Stage 1: Go builder — compile kandev + agentctl binaries -# --------------------------------------------------------------------------- -FROM golang:1.26-bookworm AS go-builder +FROM node:24-bookworm-slim -WORKDIR /build - -# Cache Go module downloads -COPY apps/backend/go.mod apps/backend/go.sum ./ -RUN go mod download - -# Copy backend source and build -COPY apps/backend/ ./ - -RUN go build -ldflags "-s -w" -o /out/kandev ./cmd/kandev && \ - go build -ldflags "-s -w" -o /out/agentctl ./cmd/agentctl && \ - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o /out/agentctl-linux-amd64 ./cmd/agentctl - -# --------------------------------------------------------------------------- -# Stage 2: Web + CLI builder — build Next.js standalone + CLI -# --------------------------------------------------------------------------- -FROM node:24-slim AS web-builder - -ARG PNPM_VERSION=9.15.9 -RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate - -WORKDIR /build/apps - -# Copy workspace config and all package.json files for dependency caching -COPY apps/package.json apps/pnpm-workspace.yaml apps/pnpm-lock.yaml ./ -COPY apps/web/package.json ./web/package.json -COPY apps/cli/package.json ./cli/package.json -COPY apps/packages/ui/package.json ./packages/ui/package.json -COPY apps/packages/theme/package.json ./packages/theme/package.json -COPY apps/packages/types/package.json ./packages/types/package.json - -RUN pnpm install --frozen-lockfile - -# Copy full source for build -COPY apps/ ./ - -# CHANGELOG.md lives at the repo root in source; the web build scripts -# (generate-changelog.mjs, generate-release-notes.mjs) resolve it via -# `${WEB_ROOT}/../../CHANGELOG.md`. Mirror that layout inside the builder. -COPY CHANGELOG.md /build/CHANGELOG.md - -# Build shared packages, web app, and CLI -RUN pnpm --filter @kandev/web build && \ - pnpm --filter kandev build - -# --------------------------------------------------------------------------- -# Stage 3: Runtime — minimal image with both services -# --------------------------------------------------------------------------- -FROM node:24-bookworm-slim AS runtime - -# Install only essential runtime dependencies, then clean up. -# gh is included because the GitHub integration (PR review, webhooks) shells out -# to it for auth fallback when GITHUB_TOKEN is not set. +# Install runtime dependencies. gh is included because the GitHub integration +# (PR review, webhooks) shells out to it for auth fallback when GITHUB_TOKEN +# is not set. apprise is installed via pipx for notification fan-out. RUN apt-get update && \ apt-get install -y --no-install-recommends \ git \ @@ -83,32 +37,42 @@ RUN apt-get update && \ # copilot, amp, ...) lives on the PV and survives pod restarts and image upgrades. RUN userdel -r node && groupadd -r kandev && useradd -r -g kandev -u 1000 -d /data/home -M kandev -# Create app directory structure matching what the CLI expects: +# Create app directory structure matching what `kandev start` expects: # /app/apps/backend/bin/kandev # /app/apps/web/.next/standalone/web/server.js -RUN mkdir -p /app/apps/backend/bin /app/apps/web/.next /data/worktrees +RUN mkdir -p /app/apps/backend/bin /app/apps/web/.next/standalone /usr/local/lib/kandev-cli /data/worktrees -# Copy Go binaries. The -linux-amd64 variant of agentctl is bind-mounted into +# Build context layout (prepared by the release workflow from the extracted +# bundle tarball): +# bundle/bin/kandev - native Go backend (per-arch) +# bundle/bin/agentctl - native agentctl (per-arch) +# bundle/bin/agentctl-linux-amd64 - amd64 agentctl helper (always amd64) +# bundle/web/... - Next.js standalone + static + public +# bundle/cli/bin/cli.js - CLI entrypoint (calls cli.bundle.js) +# bundle/cli/dist/cli.bundle.js - self-contained CLI bundle (deps inlined) +# bundle/cli/package.json - package metadata (version, etc.) +# +# The bundle's `bin/agentctl-linux-amd64` variant is bind-mounted into # Docker-executor sandboxes by the lifecycle manager; ship it next to kandev # so the AgentctlResolver finds it without manual configuration. -COPY --from=go-builder /out/kandev /app/apps/backend/bin/kandev -COPY --from=go-builder /out/agentctl-linux-amd64 /app/apps/backend/bin/agentctl-linux-amd64 -COPY --from=go-builder /out/agentctl /usr/local/bin/agentctl +COPY bundle/bin/kandev /app/apps/backend/bin/kandev +COPY bundle/bin/agentctl-linux-amd64 /app/apps/backend/bin/agentctl-linux-amd64 +COPY bundle/bin/agentctl /usr/local/bin/agentctl +COPY bundle/web/ /app/apps/web/.next/standalone/ +COPY bundle/cli/ /usr/local/lib/kandev-cli/ +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -# Copy Next.js standalone output -COPY --from=web-builder /build/apps/web/.next/standalone/ /app/apps/web/.next/standalone/ - -# Copy static assets and public directory into standalone output -COPY --from=web-builder /build/apps/web/.next/static/ /app/apps/web/.next/standalone/web/.next/static/ -COPY --from=web-builder /build/apps/web/public/ /app/apps/web/.next/standalone/web/public/ - -# Install CLI: copy built output + package.json, install prod deps, link binary -COPY --from=web-builder /build/apps/cli/dist/ /usr/local/lib/kandev-cli/dist/ -COPY --from=web-builder /build/apps/cli/bin/ /usr/local/lib/kandev-cli/bin/ -COPY --from=web-builder /build/apps/cli/package.json /usr/local/lib/kandev-cli/ -RUN cd /usr/local/lib/kandev-cli && npm install --omit=dev && \ - chmod +x bin/cli.js && \ - ln -s /usr/local/lib/kandev-cli/bin/cli.js /usr/local/bin/kandev +# Re-apply executable bits stripped by tar/COPY edge cases, then link the +# CLI launcher onto PATH. The CLI's deps are inlined into dist/cli.bundle.js +# by the release bundle step, so no `npm install` is needed here. +RUN chmod +x \ + /app/apps/backend/bin/kandev \ + /app/apps/backend/bin/agentctl-linux-amd64 \ + /usr/local/bin/agentctl \ + /usr/local/lib/kandev-cli/bin/cli.js \ + /usr/local/bin/docker-entrypoint.sh && \ + ln -s /usr/local/lib/kandev-cli/bin/cli.js /usr/local/bin/kandev && \ + chown -R kandev:kandev /app /data # Kandev home directory (DB, worktrees, sessions, repos) VOLUME ["/data"] @@ -125,13 +89,6 @@ ENV KANDEV_NO_BROWSER=1 \ HOSTNAME=0.0.0.0 \ NODE_ENV=production -# Copy entrypoint script -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -# Set build-time ownership of /app and /data -RUN chown -R kandev:kandev /app /data - WORKDIR /app # Only the backend port is exposed — it reverse-proxies the Next.js frontend