Skip to content

fix(build): a build under an agent override is tagged apart from the release - #1146

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/override-build-not-the-release
Oct 24, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/override-build-not-the-release

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Defect

Rendering the obin-blue cell with CLI 0.1.210 (2026-10-24), codefly deploy gitops render <module> --env blue refused:

<registry>/compute/excel:0.3.2 is already published from other inputs: its version was not moved with its code; release a new version (codefly publish) instead of rebuilding a published one

It refused the same way for model/model:0.1.0 and records/records:0.4.2. The module releases had not changed. The workspace's agent-overrides had (codefly.dev/python-fastapi: 0.0.119, codefly.dev/go-grpc: 0.1.58). An override replaces the agent each module's service.codefly.yaml pins. The agent emits the build recipe, so the image identity (imageBuildIdentity: RecipeTree, the Dockerfile, the buildx argv…) changed. The registry already held <image>:<version> built by the previous agent, so the released-version check in decideImageReuse refused. The message was also wrong: the version had moved with its code, and it was the build that changed.

Design: a released version names one build

The rule this applies: a released version names one build, the one made with the agent the release itself pins. A build made under a workspace override is a different build. It must never be pushed under the release's tag or adopted from it.

Chosen: option (a), a build-qualified tag. When a pushed build of a released version (x.y.z, not 0.0.0) runs on an agent the workspace overrides away from the module's own pin, the image is tagged <version>-build.<first 16 hex of its input identity>, for example excel:0.3.2-build.3f9c0a1b2d4e5f60. That is a valid docker tag, and semver reads it as a prerelease, so releasedVersionTag never treats it as the release.

  • The tag holds one build. The registry probe that adopts a matching published image applies to the qualified tag too, so another checkout composing the same override adopts it. It does not push a second build over it.
  • Option (b) was rejected: it treated overridden builds like 0.0.0 (identity reuse, no release check), but they would still be pushed to <image>:<version>. That overwrites the release's tag where tags are mutable and fails where they are immutable, which breaks the rule above.
  • The deployment pins the digest. pkg/gitops/workloads.go splitPinnedImage drops the tag and keeps repository + digest, and that is all the cell file records (cellContainers). The agent's DockerImage names <image>:<version>@<captured digest>. For a qualified build the CLI now always captures the digest, even in DeployMode without CaptureImageDigest, so the deployed image pins the overridden build's digest and never falls back to the release tag.
  • Detecting the override: core applies agent-overrides in Module.LoadServiceFromReference (applyAgentOverride) and keeps no record of what it replaced. Builder.overriddenAgent checks first whether the workspace's AgentOverrides() match the service's agent at all, so a workspace without overrides reads no file. When one matches, it reads the service's own file with resources.LoadServiceFromDir, the same record composition.ResolveAgentOverrides uses. An override to the version the module already pins moves nothing, and the build is the release's.
  • The remaining refusal names the input class. Every build now also records org.codefly.build.agent=<publisher>/<name>:<version>, added after the identity like the inputs label, so it is never an input. When a release tag holds other inputs, the refusal now says which of three cases applies:
    • another agent built it: the tag holds a build the release does not name, for example one pushed under an override by an earlier CLI;
    • the same agent built it: the code or a base image changed without the version moving;
    • the image records no agent: the refusal says the published image does not record which input differs.

Classification: fix, at the owner. The CLI owns the image build and the reuse decision (pkg/orchestration). Nothing changes in core or in any agent.

Reused: imageBuildIdentity (the qualified tag is derived from it), the existing registry adopt-or-refuse probe, withBuildInputsLabel (now built on withBuildFlags), core's Workspace.AgentOverrides, AgentOverride.Matches and LoadServiceFromDir, and the splitPinnedImage cell path, which is unchanged.

Overlap with open branches

feat/ghcr-push-pull-through touches the same code in pkg/orchestration/build_plan.go:

  • It edits the released-version probe in decideImageReuse. This PR changes the condition line just above its edit and the refusal case.
  • It introduces withBuildFlags. This PR adds the identical helper (same body and doc comment) because goconst refused a second "build" literal.
  • It changes recipeAsBuilt. This PR does not touch it: the qualified tag is applied from the reuse decision (imageReuse.image) in buildRecipe, after recipeAsBuilt.

Expect a small textual conflict in decideImageReuse and around withBuildFlags. Both are mechanical to resolve. One composition point: with that branch, recipeAsBuilt rewrites the image to the push reference first, and this change then qualifies that reference's tag. Both orders yield <pushed repo>:<version>-build.<id>.

Tests

  • TestAnOverriddenAgentBuildOfAPublishedVersionIsTaggedApart: the release tag is published from other inputs by agent 0.1.0, and the workspace overrides the agent to 0.2.0. The build is not refused. It is pushed as -t example.test/app:1.2.3-build.<16 hex> and never as -t example.test/app:1.2.3, carries the agent label, and captures its digest without CaptureImageDigest. A second checkout (cache wiped) with the qualified tag published from the same identity adopts it and builds nothing. Verified red with the qualification disabled: it fails with "already published from other inputs".
  • TestAnOverrideToTheModulesOwnPinIsTheReleaseBuild: an override to the pinned version builds under the release tag.
  • TestARefusedReleaseNamesWhatDiffers: without an override, a changed build still refuses, with one message for each of the three cases (another agent, same agent, no agent recorded).
  • Existing TestAPublishedVersionFromTheSameInputsIsAdoptedNotRebuilt / …FromOtherInputsRefusesTheBuild / …WithNoInputRecordIsBuiltAgain still pass. Without an override, identical inputs still adopt the published image, and changed ones still refuse.
  • TestOverrideBuildImageIsAValidDockerTag: tag grammar, releasedVersionTag is false for it, and the agent label argv.
  • TestOnlyASha256DigestPinsAnImage now also covers the cell file: repo:1.2.0@d, repo:1.2.0-build.<id>@d and repo@d all record repo + d.

go test ./pkg/orchestration/ ./pkg/gitops/ pass; make lint reports 0 issues.

Docs

docs/commands.md: "Keeping an image whose inputs did not change" now describes the agent label, how the refusal names what differs, and the new rule that a released version names the build its release pins (the qualified tag). "Moving the agent version of composed services" links to it.

Not verified

  • Not run against a real registry or the obin-blue cell. The registry probes are test seams, and docker is a stub.
  • The obin-blue registry tags already hold builds from earlier overrides. Once an override is dropped, a build with the module's own pin will still refuse there. The message now says so (another agent built it) and suggests a new release or removing that tag. Nothing here rewrites existing tags.
  • An agent that sets a non-default image (SetDockerImage) does not take the captured digest in core's DockerImage. For such an agent, a qualified build in DeployMode would deploy by its tag. No current agent was checked for this.
  • Builds that are not pushed (local load) keep the agent's tag, since nothing is published.

🤖 Generated with Claude Code

…release

`codefly deploy gitops render` refused a release whose module had not moved
(compute/excel:0.3.2, model/model:0.1.0, records/records:0.4.2) as "already
published from other inputs: its version was not moved with its code" once the
workspace's agent-overrides moved the agent its services build with. The agent
emits the build recipe, so the image identity moved with the agent while the
release tag already held the build of the previous agent.

A released version names the build its release pins. A pushed build of a
released version whose agent the workspace overrides away from the module's
own pin is now pushed as `<version>-build.<16 hex of its input identity>`,
never under the release tag; the release check does not apply to it, another
checkout adopts it from the registry by that tag, and its digest is always
captured so the deployment pins it. Every build also records the agent that
emitted its recipe (org.codefly.build.agent), and the refusal that remains
names which input class differs: another agent, or the same agent with changed
code or base image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte force-pushed the fix/override-build-not-the-release branch from ce8dbc8 to 0b807a9 Compare October 24, 2026 19:01
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 07e5ebc 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