Skip to content

fix(build): a published release is adopted unless its code changed - #1148

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/release-image-adopt-unless-code-changed
Oct 24, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/release-image-adopt-unless-code-changed

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Defect

On 2026-10-24, with CLI 0.1.211 and 0.1.210 alike, codefly deploy gitops render robin --env staging (obin-ai/platform-obin) is refused:

us-east1-docker.pkg.dev/obin-platform-staging-data/obin-staging-registry/documents/documents:0.1.14 is already published from other inputs: the service's code, its agent's build recipe or a base image differ, and the published image does not record which; release a new version (codefly publish) instead of rebuilding a published one

Neither documents' release (0.1.14) nor its agent (go-grpc 0.1.58, not overridden) had changed. Another operator's checkout had built the published image earlier that day. Rendering one module rebuilds or reuses all 25 services, because the cell file needs every workload's digest. So one image built anywhere with different non-code inputs blocks every later render of every module on that cell. The image carried only the aggregate org.codefly.build.inputs, a hash over schema, recipe, Dockerfile, context, dockerignore, buildx command, recipe tree, context tree, Go modules and bases, so the render could not tell a code change from a recipe-invocation or base-image difference.

Per build_plan.go: "A released version is built once … when the registry already holds this service at this version, it is that image." The refusal exists only to catch a version that did not move with its code.

Design (fix, at the owner of the decision)

  • Per-class labels. Every build records, beside the unchanged aggregate:

    • org.codefly.build.inputs.code: build context, dockerignore, context tree, Go module manifests
    • org.codefly.build.inputs.recipe: recipe name, Dockerfile, recipe tree, normalized buildx command
    • org.codefly.build.inputs.bases: resolved base image digests

    They are computed in imageBuildIdentity from the same imageInputs (identityClasses). Like the aggregate, they are added to argv after the identity is computed, so they never feed it. The aggregate key is byte-identical, so existing labelled images still match their own inputs.

  • Released-version decision (judgePublishedRelease / publishedReleaseVerdict):

    Published image Result
    aggregate equals ours adopt (unchanged)
    code class equal, recipe/bases differ adopt, warning names the differing class
    code class differs refuse: "already published from other code … its code changed without its version moving"
    agent label ≠ the release's pinned agent refuse (unchanged, fix(build): a build under an agent override is tagged apart from the release #1146: the tag holds an override build)
    aggregate only (pre-class image) adopt with a warning that code was not compared; --rebuild builds it again
    no inputs label built again (unchanged)
  • Why adopt pre-class images by default. Refusing blocks every cell that holds one such image, which is today's defect. Rebuilding would overwrite a release wherever tags are mutable. Adopting follows the principle quoted above. The existing --rebuild flag is the explicit opt-out, so no new flag was added.

  • Override builds (fix(build): a build under an agent override is tagged apart from the release #1146). A -build.<identity> tag keeps the exact-aggregate check and is refused on mismatch (publishedFromOtherInputs, unchanged).

Reused: the existing label seam (withBuildFlags, resolveImageBuildLabels/buildLabelsFromImageConfig), imageInputs, the refusal helper from #1146, and --rebuild. Not added: a new flag, a second identity, or any change to the aggregate key or the local cache.

Tests

  • TestAPublishedVersionFromTheSameInputsIsAdoptedNotRebuilt: aggregate equal → adopt (unchanged)
  • TestAPublishedReleaseIsJudgedByItsCode: labels read off a real harness build. Same code with other recipe and bases → adopt. Pre-class aggregate only → adopt. Other code → refuse. Other agent → refuse. A published release is never rebuilt in any case.
  • TestPublishedReleaseVerdictNamesTheClass: the warning names recipe vs bases, the legacy warning, and a code refusal that names the code
  • TestAPublishedVersionLabelledBeforeClassesIsAdopted: replaces the old "other inputs refuses" test
  • TestBuildInputsAreReadFromEveryPlatform: class labels are written to argv and read back
  • Override path: TestAnOverriddenAgentBuildOfAPublishedVersionIsTaggedApart and TestAnOverriddenBuildIsPushedQualifiedAtTheRegistryItPullsThrough pass unchanged
  • Mutation check: disabling the code-class comparison makes the "other code" cases fail

go test ./pkg/orchestration/ ./pkg/gitops/ passes, and make lint reports 0 issues.

Docs

The image-reuse section of docs/commands.md now describes the class labels and the decision table, and explains why adopt is the default.

Not verified

  • I did not run it against the staging registry or the platform-obin render. The currently published documents:0.1.14 carries only the aggregate, so with this change the render adopts it with the pre-class warning. Nobody has observed that on the cell yet.
  • imagetools inspect reading the new labels is covered only by the buildLabelsFromImageConfig unit test, not against a real registry.
  • I did not establish which class differed between the two checkouts. The suspect is the buildx command (for example cache flags), but that is unconfirmed. The next build under this CLI will record the classes, so the next mismatch will say which one.

🤖 Generated with Claude Code

A released tag whose image records other build inputs than this build's
refused every render: one image built from another checkout (another
recipe invocation, another base resolution) blocked every later render of
every module on the cell, because a render needs every workload's digest.

Every build now records its input classes beside the aggregate label:
org.codefly.build.inputs.code (build context + Go module manifests),
.recipe (recipe tree, Dockerfile, build invocation) and .bases (resolved
base images). The aggregate key is unchanged. A published release with
other inputs is adopted when its code class equals this build's, with a
warning naming the class that differs; other code refuses, naming the
code; another agent than the release pins still refuses (#1146). An
image labelled before the classes existed is adopted with a warning that
its code was not compared (--rebuild builds it again). Build-qualified
override tags keep the exact-identity check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit aa1b94a Oct 24, 2026
14 checks passed
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