Skip to content

Commit f2a642f

Browse files
Merge pull request #76 from magnificentlycursed/fix/75-container-build-and-publish
fix(container): make container build work + fork-publishable agent image (gh#75)
2 parents a153c3d + 9315102 commit f2a642f

3 files changed

Lines changed: 59 additions & 3 deletions

File tree

.github/workflows/container-image.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ on:
2727
- '.github/workflows/container-image.yml'
2828
- 'crosslink/Cargo.toml'
2929
- 'crosslink/Cargo.lock'
30+
# On-demand publish from any branch (e.g. a fork validating the image before
31+
# develop lands), tagged :nightly + :manual-<sha>. Lets a fork produce a
32+
# working agent image without a develop push.
33+
workflow_dispatch:
3034

3135
permissions:
3236
contents: read
@@ -38,7 +42,10 @@ concurrency:
3842

3943
env:
4044
REGISTRY: ghcr.io
41-
IMAGE_NAME: dollspace-gay/crosslink-agent
45+
# Repo-owner-derived so a fork publishes to its own GHCR namespace instead of
46+
# failing to push to the upstream org. On dollspace-gay/crosslink this still
47+
# resolves to dollspace-gay/crosslink-agent.
48+
IMAGE_NAME: ${{ github.repository_owner }}/crosslink-agent
4249

4350
jobs:
4451
# ===========================================
@@ -179,6 +186,11 @@ jobs:
179186
TAGS=("${BASE}:nightly" "${BASE}:nightly-${SHORT_SHA}")
180187
PRIMARY="${BASE}:nightly"
181188
PUSH=true
189+
elif [ "$EVENT" = "workflow_dispatch" ]; then
190+
VERSION="manual-${SHORT_SHA}"
191+
TAGS=("${BASE}:nightly" "${BASE}:manual-${SHORT_SHA}")
192+
PRIMARY="${BASE}:nightly"
193+
PUSH=true
182194
else
183195
echo "::error::Unexpected ref/event combination: ref=${REF} event=${EVENT}"
184196
exit 1

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2525

2626
### Fixed
2727

28+
- `crosslink container build` no longer fails with a cryptic
29+
`COPY crosslink-<arch>: not found` (gh#75). It staged the binary as
30+
`crosslink` while the Dockerfile expects `crosslink-${TARGETARCH}`; it now
31+
stages `crosslink-<arch>` and passes `--build-arg TARGETARCH=<arch>` so a
32+
plain `docker build` resolves the COPY. Since it packages the *installed*
33+
binary (no source tree to cross-compile from), it now fails fast with a
34+
clear message on non-Linux hosts — where that binary can't run in the
35+
Linux image — pointing at the CI workflow / `just build-image` instead.
36+
- The container-image workflow's `IMAGE_NAME` is derived from
37+
`${{ github.repository_owner }}` (gh#75), so a fork publishes to its own
38+
GHCR namespace instead of failing to push to the upstream org (on
39+
dollspace-gay it still resolves to `dollspace-gay/crosslink-agent`), and a
40+
`workflow_dispatch` trigger lets a fork publish a working agent image
41+
on-demand without a `develop` push. Addresses the unpublished
42+
`DEFAULT_AGENT_IMAGE` (forecast-bio/crosslink#576).
2843
- `kickoff run --container` now passes claude's permission flag into the
2944
container agent (gh#59). The local (tmux) path emitted
3045
`--dangerously-skip-permissions` / `--permission-mode`, and `container start`

crosslink/src/commands/container.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,35 @@ pub fn build(force: bool, tag: Option<&str>, dockerfile: Option<&str>) -> Result
249249
// Write entrypoint
250250
std::fs::write(build_path.join("entrypoint.sh"), ENTRYPOINT)?;
251251

252-
// Copy crosslink binary
252+
// The Dockerfile COPYs `crosslink-${TARGETARCH}`, and the staged binary
253+
// must be a Linux binary to run in the agent image. `crosslink container
254+
// build` packages the *installed* binary (it has no source tree to
255+
// cross-compile from), so it can only produce a runnable image on a Linux
256+
// host of a supported arch. For cross-arch or non-Linux hosts, the CI
257+
// workflow (.github/workflows/container-image.yml) and `just build-image`
258+
// cross-compile a static musl binary instead.
259+
let docker_arch = match std::env::consts::ARCH {
260+
"x86_64" => "amd64",
261+
"aarch64" => "arm64",
262+
other => bail!(
263+
"unsupported host architecture `{other}` for `crosslink container build`; \
264+
build the image via CI (.github/workflows/container-image.yml) or `just build-image`"
265+
),
266+
};
267+
if !cfg!(target_os = "linux") {
268+
bail!(
269+
"`crosslink container build` packages the installed crosslink binary, which must \
270+
be a Linux binary to run in the agent image — but this host is `{}`. Build on a \
271+
Linux host, or use the CI workflow (.github/workflows/container-image.yml) or \
272+
`just build-image`, which cross-compile a static musl binary.",
273+
std::env::consts::OS
274+
);
275+
}
276+
277+
// Copy crosslink binary under the arch-suffixed name the Dockerfile expects.
253278
let binary = find_crosslink_binary()?;
254-
std::fs::copy(&binary, build_path.join("crosslink"))
279+
let staged_binary = format!("crosslink-{docker_arch}");
280+
std::fs::copy(&binary, build_path.join(&staged_binary))
255281
.context("Failed to copy crosslink binary to build context")?;
256282

257283
// Compute binary hash for staleness detection
@@ -261,6 +287,9 @@ pub fn build(force: bool, tag: Option<&str>, dockerfile: Option<&str>) -> Result
261287

262288
let mut cmd = Command::new("docker");
263289
cmd.args(["build", "-t", &image]);
290+
// Pin TARGETARCH so plain `docker build` (not buildx) resolves the COPY to
291+
// the arch-suffixed binary we staged, instead of the Dockerfile default.
292+
cmd.args(["--build-arg", &format!("TARGETARCH={docker_arch}")]);
264293
cmd.args(["--label", LABEL_AGENT]);
265294
cmd.args(["--label", &format!("crosslink-binary-hash={binary_hash}")]);
266295
if force {

0 commit comments

Comments
 (0)