Skip to content

Commit 528c58e

Browse files
fix: build on the multi-arch python companion, pinned by its index digest (#71)
The default runtime image was docker.io/codeflydev/python:0.0.1, a single linux/amd64 manifest. A recipe rendered for linux/amd64,linux/arm64 pulled the amd64 image for both platforms, so the arm64 half ran amd64 binaries under QEMU and died on its first RUN ("Invalid ELF image for this architecture"). The default is now ghcr.io/codefly-dev/python:0.0.5, the companion of the core this agent links, pinned by the digest of its amd64+arm64 image index. A test holds the tag to core's companion version so a core bump cannot leave the pin behind. Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 472bb78 commit 528c58e

3 files changed

Lines changed: 63 additions & 10 deletions

File tree

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ into `templates/factory/code/pyproject.toml.tmpl` yourself.
146146
(`RECIPE_INVENTORY_SCOPE_TREE`), the Dockerfile-only one claims just what it
147147
emitted. Never rewrite `pyproject.toml` or `uv.lock` to make a path resolve.
148148
- **The runtime image is pinned.** `docker-image` rejects a bare name and
149-
`:latest`. The default `codeflydev/python` companion is built by core and
150-
repinned on release — prefer it.
149+
`:latest`. The default `ghcr.io/codefly-dev/python` companion is built from
150+
core and pinned by the digest of its multi-platform index — prefer it. Its tag
151+
must equal the linked core's companion version (test-enforced).
151152
- **A user's `flake.nix` is never overwritten.** The embedded flake is written
152153
only when the source dir has none.
153154
- **Release needs cgo.** The binary links core's tree-sitter grammars, so

‎main.go‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,39 @@ type Settings struct {
7979

8080
// RuntimeImage overrides the default codefly-built runtime image.
8181
// Format: "name:tag". Plain "name" and ":latest" are rejected —
82-
// pinning is enforced. Leave empty to use codeflydev/python:<ver>
82+
// pinning is enforced. Leave empty to use ghcr.io/codefly-dev/python:<ver>
8383
// (recommended; companion is rebuilt + pinned on every codefly
8484
// release). Field named RuntimeImage (not DockerImage) to avoid
8585
// colliding with services.Base.DockerImage(req).
8686
RuntimeImage string `yaml:"docker-image"`
8787
}
8888

89-
// runtimeImage is the codefly-built Python runtime companion —
90-
// python:3.13.1-alpine3.21 + codefly CLI + uv 0.5.29. Built from
91-
// core/companions/python/. Users can override via the Python settings
92-
// (DockerImage field) but NOT recommended — the companion image is
93-
// the mode-consistent default and gets the same tool set as every
94-
// other codefly-built image.
95-
var runtimeImage = &resources.DockerImage{Name: "codeflydev/python", Tag: "0.0.1"}
89+
// runtimeImage is the codefly-built Python runtime companion, built from
90+
// core/companions/python/ and published by the CLI to ghcr.io/codefly-dev as
91+
// a linux/amd64 + linux/arm64 image index. Users can override it through the
92+
// Python settings (docker-image) but should not: the companion is the
93+
// mode-consistent default and carries the same tool set as every other
94+
// codefly-built image.
95+
//
96+
// It is pinned by the digest of the multi-platform INDEX, never of one
97+
// platform's manifest: the build recipe is rendered for every target platform
98+
// and each resolves its own entry from the index. The legacy
99+
// docker.io/codeflydev/python:0.0.1 this replaces was a single amd64 manifest,
100+
// so every linux/arm64 image build ran amd64 binaries under emulation and died
101+
// on its first RUN ("Invalid ELF image for this architecture").
102+
//
103+
// Tag is the companion version of the core this agent links
104+
// (core/companions/python/info.codefly.yaml); a test holds the two together,
105+
// so bumping core without re-pinning here fails the build. Re-pin the digest
106+
// from the registry when the tag moves:
107+
//
108+
// docker buildx imagetools inspect ghcr.io/codefly-dev/python:<tag>
109+
var runtimeImage = &resources.DockerImage{
110+
Repository: resources.ImageRegistry,
111+
Name: "python",
112+
Tag: "0.0.5",
113+
Digest: "sha256:67d6fe825765fc30f94131ee551e3800f5b6fa4e07094b47d036904387cb75a7",
114+
}
96115

97116
// uvVersion is the uv the emitted build recipe resolves its lockfile with.
98117
//

‎runtime_image_test.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/codefly-dev/core/companions/python"
9+
"github.com/codefly-dev/core/resources"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// The default runtime image is the Python companion of the core this agent
14+
// links, from the registry codefly publishes to. A core bump that moves the
15+
// companion version fails here until the pin, and its digest, move with it.
16+
func TestRuntimeImageIsTheLinkedCoreCompanion(t *testing.T) {
17+
companion, err := python.CompanionImage(context.Background())
18+
require.NoError(t, err)
19+
require.Equal(t, companion.Repository, runtimeImage.Repository)
20+
require.Equal(t, companion.Name, runtimeImage.Name)
21+
require.Equal(t, companion.Tag, runtimeImage.Tag,
22+
"re-pin runtimeImage (tag and index digest) to core's python companion")
23+
}
24+
25+
// A tag alone lets the registry move the image under a built recipe, and a
26+
// platform manifest digest would pin every target platform to one
27+
// architecture. The pin is a digest; that it names the multi-platform index is
28+
// what the comment on runtimeImage records and a reviewer checks.
29+
func TestRuntimeImageIsDigestPinned(t *testing.T) {
30+
require.Regexp(t, regexp.MustCompile(`^sha256:[0-9a-f]{64}$`), runtimeImage.Digest)
31+
require.Equal(t, resources.ImageRegistry+"/python@"+runtimeImage.Digest, runtimeImage.FullName(),
32+
"the recipe's FROM must resolve by digest")
33+
}

0 commit comments

Comments
 (0)