---
id: 12150
title: 'Cloud deploy must source neo from a pinned clone, not a local folder'
state: CLOSED
labels:
  - bug
  - ai
  - architecture
  - build
assignees:
  - neo-opus-ada
createdAt: '2026-05-28T20:27:50Z'
updatedAt: '2026-05-28T21:33:24Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12150'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-28T21:33:24Z'
---
# Cloud deploy must source neo from a pinned clone, not a local folder

## Context

Surfaced during a real-time dogfood of #12146 on a cloud Agent OS deployment. The deployment's images build neo from a **co-located local checkout** (`NEO_SOURCE_DIR`, default `../../../../github/neomjs/neo`). Two failure modes fell out immediately:

1. **A second operator cannot deploy.** Anyone without that exact co-located checkout has no build context — the relative path resolves to nothing, and `docker compose build` fails. The deployment is only buildable on the original author's machine.
2. **The deployed neo version is "whatever the local checkout happens to be."** During the dogfood the co-located checkout was on an unrelated feature branch with uncommitted WIP — so a rebuild would have baked another lane's untested working tree into the deployment image, *and* still lacked the merged fix being dogfooded.

## The Problem

The canonical `ai/deploy` build sources neo via Docker `COPY . .` of a **local build context** — there is no `git clone`, no pinned ref. Consequences:

- **Non-portable** — requires a co-located neo checkout at a hardcoded relative path on the build host. The Day-0 docs list "clone + co-locate a neo checkout" as a manual prerequisite, which is the smell.
- **Non-reproducible** — the image contents equal the local working tree at build time, including uncommitted changes and whatever branch is checked out. Two builders produce different images.
- **Version-coupled to local branch state** — the deployment's neo version is implicit and mutable, not declared.

neo is MIT-licensed, so cloning + baking the source into the deploy image has no license barrier.

## The Architectural Reality

- `ai/deploy/Dockerfile:15` — `COPY . .` from the build context (no source acquisition step).
- `ai/deploy/docker-compose.yml` — `context: ../..` (canonical); deployment overlays set `context: ${NEO_SOURCE_DIR:-<local path>}`.
- `git` is available in alpine via `apk`, so a build-time clone is feasible without changing the base image family.

## The Fix

For cloud / portable deployments, acquire neo from a **pinned GitHub ref at build time** instead of `COPY . .` from a local folder:

- Add a source-acquisition step (build `ARG NEO_REF` + optional `ARG NEO_REPO_URL=https://github.com/neomjs/neo`) that `git clone`s neo at the pinned ref into the build stage. A shallow clone (`--depth 1 --branch <ref>`) also sidesteps the #12017 local-context-bloat problem by construction.
- Prefer a **tag or commit SHA** for reproducibility; a branch ref (e.g. `dev`) is acceptable only as an explicit interim for branch-tracking deployments, with the mutability tradeoff documented.
- Keep a **local-source mode** for fast dev iteration (explicit `NEO_SOURCE=git-ref|local`), so the convenience of the co-located checkout is preserved without being the only path.
- Update the Day-0 / deployment docs: the co-located checkout becomes a dev convenience, not a deployment prerequisite.

## Acceptance Criteria

- [ ] AC1: the deploy build produces a working image on a host with **no co-located neo checkout** (portability).
- [ ] AC2: the build pins neo to an explicit ref (tag/SHA/branch ARG); the same ref yields the same source (reproducibility).
- [ ] AC3: a local-source dev mode is still available (fast iteration preserved).
- [ ] AC4: deployment docs updated — co-located checkout reframed as dev-only convenience.
- [ ] AC5: capability-framing audit pass (no downstream-deployment-partner names in any artifact).

## Out of Scope

- Sourcing neo from a published npm package or a prebuilt neo base image — a heavier, longer-term evolution; this ticket is the immediate portability fix (pinned clone).
- Choosing the specific release tag for any given deployment — operator config, not a code change.

## Avoided Traps

- ❌ Pinning to a mutable **branch** as the default — undermines reproducibility; default to tag/SHA, branch only as a documented interim.
- ❌ Removing the local-`COPY` path entirely — it's the right tool for dev iteration; make source-mode explicit instead of replacing one coupling with another.

## Decision Record impact

`aligned-with ADR 0014` (cloud deployment topology) — refines the neo-sourcing model the deployment topology depends on. If ADR 0014 codified the co-located-checkout model explicitly, this amends that detail.

## Related

- #12017 (missing root `.dockerignore` → 90 GB build context) — same local-`COPY .` root mechanism, different facet (size); a shallow pinned clone addresses both.
- ADR 0014 (cloud deployment topology + scheduler task taxonomy).
- Surfaced during the #12146 (pull-mode `tenantRepos` tiered resolver) real-time dogfood.

## Handoff Retrieval Hints

- Retrieval Hint: "cloud deploy neo source local folder COPY vs pinned github clone reproducible portable"

## Timeline

- 2026-05-28T20:27:51Z @neo-opus-ada added the `bug` label
- 2026-05-28T20:27:51Z @neo-opus-ada added the `ai` label
- 2026-05-28T20:27:51Z @neo-opus-ada added the `architecture` label
- 2026-05-28T20:27:51Z @neo-opus-ada added the `build` label
- 2026-05-28T20:34:46Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-28T20:45:59Z @neo-opus-ada cross-referenced by PR #12151
- 2026-05-28T20:52:03Z @neo-opus-ada referenced in commit `3cf84e1` - "refactor(deploy): rename clone dir /src to /neo to avoid collision with neo's src/ (#12150)

The source-stage clone directory was /src, which reads ambiguously against neo's own
top-level src/ folder (the clone surfaced it as the confusing /src/src). Rename to /neo
so the clone root is unmistakably the whole repo, not the framework src/ subtree.

No behavior change: COPY --from=source /neo ./ copies the entire cloned repo into /app,
identical to the prior /src (and to the original COPY . .). Re-verified via a
--target source build (ai/, src/, buildScripts/, package.json all present under /neo).

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-28T20:58:23Z @neo-opus-ada referenced in commit `5475d7a` - "docs(deploy): drop inline ticket-ref archaeology from deploy doc + Dockerfile comment (#12150)

Remove the inline (#12150) anchor from the Day0Tutorial note and the ai/deploy/Dockerfile
source-acquisition comment. A tutorial reader / maintainer does not need the ticket id in
the prose; that lineage belongs in git + PR history (commit subjects keep the ref per the
ticket gate). No content change beyond the ref removal.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-28T21:15:51Z @neo-opus-ada referenced in commit `e3a9c14` - "fix(deploy): real SHA-pin support + local-mode for test/dev compose (#12150)

Addresses @neo-gpt cycle-1:
- RA1: docker-compose.test.yml + docker-compose.dev.yml share ai/deploy/Dockerfile but did
  not pass NEO_SOURCE=local — with the new git default they would build remote dev instead
  of the local branch under test. Add NEO_SOURCE: local to every test/dev build-arg block
  (4 + 2), keeping context: ../.. so they build the local checkout.
- RA2: git clone --branch ${NEO_REF} rejected raw commit SHAs (the advertised pin). Replace
  with git init + fetch --depth 1 origin ${NEO_REF} + checkout --detach FETCH_HEAD, which
  accepts a branch, tag, OR full SHA — the tag/SHA contract is now real.

Verified: --target source with NEO_REF=<raw dev SHA> checks out exactly that SHA; NEO_REF=dev
still works; docker compose config confirms NEO_SOURCE: local on all test (4) + dev (2) builds.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-28T21:33:23Z @tobiu referenced in commit `e0be8b7` - "feat(deploy): source neo from a pinned git clone, not a local folder (#12150) (#12151)

* feat(deploy): source neo from a pinned git clone, not a local folder (#12150)

The ai/deploy images built neo via `COPY . .` of a local co-located checkout
(NEO_SOURCE_DIR) — non-portable (no checkout → no build context → build fails for any
second operator / CI host), non-reproducible (image = local working tree + whatever
branch is checked out), version-coupled. neo is MIT, so cloning + baking it into the
image has no license barrier.

- Dockerfile: ARG-selectable source stage. Default NEO_SOURCE=git shallow-clones neo at
  NEO_REF (default dev) — portable, correct-branch, freshly pulled. NEO_SOURCE=local keeps
  the local build context for dev iteration.
- docker-compose.yml: the 3 neo services build clone-mode by default (context: . — minimal,
  always-exists), so the reference pattern deployments mirror is now portable.
- Day0Tutorial: the deploy build sources neo independently; the local checkout is dev-only.

Verified: `docker build --target source` from an EMPTY context clones neo@dev and the
cloned source contains #12146 (listConfiguredTenantRepos) + #12148 (resolveCliProjectRoot)
— portability + correct-fresh-branch both proven. A published npm package supersedes the
clone after the v13 release (out of scope).

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

* refactor(deploy): rename clone dir /src to /neo to avoid collision with neo's src/ (#12150)

The source-stage clone directory was /src, which reads ambiguously against neo's own
top-level src/ folder (the clone surfaced it as the confusing /src/src). Rename to /neo
so the clone root is unmistakably the whole repo, not the framework src/ subtree.

No behavior change: COPY --from=source /neo ./ copies the entire cloned repo into /app,
identical to the prior /src (and to the original COPY . .). Re-verified via a
--target source build (ai/, src/, buildScripts/, package.json all present under /neo).

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

* docs(deploy): drop inline ticket-ref archaeology from deploy doc + Dockerfile comment (#12150)

Remove the inline (#12150) anchor from the Day0Tutorial note and the ai/deploy/Dockerfile
source-acquisition comment. A tutorial reader / maintainer does not need the ticket id in
the prose; that lineage belongs in git + PR history (commit subjects keep the ref per the
ticket gate). No content change beyond the ref removal.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(deploy): real SHA-pin support + local-mode for test/dev compose (#12150)

Addresses @neo-gpt cycle-1:
- RA1: docker-compose.test.yml + docker-compose.dev.yml share ai/deploy/Dockerfile but did
  not pass NEO_SOURCE=local — with the new git default they would build remote dev instead
  of the local branch under test. Add NEO_SOURCE: local to every test/dev build-arg block
  (4 + 2), keeping context: ../.. so they build the local checkout.
- RA2: git clone --branch ${NEO_REF} rejected raw commit SHAs (the advertised pin). Replace
  with git init + fetch --depth 1 origin ${NEO_REF} + checkout --detach FETCH_HEAD, which
  accepts a branch, tag, OR full SHA — the tag/SHA contract is now real.

Verified: --target source with NEO_REF=<raw dev SHA> checks out exactly that SHA; NEO_REF=dev
still works; docker compose config confirms NEO_SOURCE: local on all test (4) + dev (2) builds.

Co-Authored-By: neo-opus-ada <neo-opus-ada@neomjs.com>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-28T21:33:24Z @tobiu closed this issue

