Skip to content

Live Relearn eval image: implement the harvest contract so cortex can… #14

Live Relearn eval image: implement the harvest contract so cortex can…

Live Relearn eval image: implement the harvest contract so cortex can… #14

name: publish-eval-image
# Publishes ghcr.io/cortexlm/relearn-eval and prints the pushed sha256 digest.
# That digest is what the control plane pins in `config/relearn-pin.toml` as
# `eval_image_digest`; a tag is never the pin.
#
# Two images, two Dockerfiles:
#
# :<sha> scoring — CUDA base + torch. Pin this, and only after
# this job has pulled THAT digest and run the harvest-PATH
# check against it.
# :<sha>-contract slim contract only. Fast, cannot score, not the pin.
#
# The contract image publishes first and independently, so a runtime build
# that runs out of runner disk cannot take the whole publish down with it.
on:
push:
branches: ["main", "cursor/**"]
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
jobs:
contract:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: resolve the image name
# A registry repository must be lowercase; the owner is CortexLM.
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/relearn-eval" >> "${GITHUB_ENV}"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build and push the contract image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: eval/Dockerfile
push: true
provenance: false
platforms: linux/amd64
tags: |
${{ env.IMAGE }}:${{ github.sha }}-contract
build-args: |
WITH_RUNTIME=0
RELEARN_GIT_SHA=${{ github.sha }}
cache-from: type=gha,scope=contract
cache-to: type=gha,mode=max,scope=contract
- name: report
run: |
{
echo "### relearn-eval (contract only)"
echo
echo '```'
echo "image = \"${IMAGE}\""
echo "digest = \"${{ steps.push.outputs.digest }}\""
echo '```'
echo
echo "Contract layer only: slim, no model runtime, so it refuses to score."
echo "Do not pin this digest."
} >> "${GITHUB_STEP_SUMMARY}"
runtime:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: resolve the image name
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/relearn-eval" >> "${GITHUB_ENV}"
- name: free disk for the CUDA base and runtime wheels
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache /usr/local/share/boost \
/opt/hostedtoolcache/CodeQL /usr/local/lib/node_modules \
/usr/share/swift /opt/az || true
df -h /
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build and push the scoring image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: eval/Dockerfile.scoring
push: true
provenance: false
platforms: linux/amd64
tags: |
${{ env.IMAGE }}:${{ github.sha }}
build-args: |
WITH_RUNTIME=1
RELEARN_GIT_SHA=${{ github.sha }}
# No layer cache: the CUDA base plus wheels are several GiB, which
# is past what the Actions cache will hold, and a failed cache
# export would fail a build that otherwise produced a pinnable
# digest.
- name: pull the published digest and prove it can score
# Two live failures, both of them shapes the build could not see.
#
# sha256:86240d76 — the scoring image harvest booted had no runnable
# /usr/bin/relearn-eval (exit 127). CI's harvest-PATH check had only
# ever run against the slim contract image.
#
# sha256:cbc4bbb8 — the pod ran, skipped vLLM because it was not
# installed, and died loading the native VLM base on
# `Qwen3VLVideoProcessor requires Torchvision`. Nothing checked the
# runtime the pod would actually import.
#
# So this pulls the digest just pushed — the bytes a pod will get —
# and refuses to report a pin unless that image both runs on the
# harvest's PATH and imports the scoring runtime.
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
set -eu
case "${DIGEST}" in
sha256:*) ;;
*) echo "no sha256 digest was published"; exit 1 ;;
esac
ref="${IMAGE}@${DIGEST}"
docker pull "${ref}"
docker run --rm --entrypoint /bin/sh "${ref}" -c \
'test -f /usr/bin/relearn-eval && test -x /usr/bin/relearn-eval && env -i PATH=/usr/bin:/bin /usr/bin/relearn-eval --help'
docker run --rm --entrypoint /bin/sh "${ref}" -c \
'test ! -L /usr/bin/relearn-eval'
docker run --rm --entrypoint /bin/sh "${ref}" -c \
'env -i PATH=/usr/bin:/bin /usr/bin/relearn-eval score --help'
# The plainest possible statement of what cbc4bbb8 was missing.
docker run --rm --entrypoint /opt/relearn-venv/bin/python "${ref}" \
-c 'import vllm, torchvision'
# And the same question asked the way the harvest asks it: through
# /usr/bin/relearn-eval on the PATH an SSH login gets, so this also
# covers the launcher picking an interpreter that cannot import them.
docker run --rm --entrypoint /bin/sh "${ref}" -c \
'env -i PATH=/usr/bin:/bin HOME=/root /usr/bin/relearn-eval selftest'
- name: report the digest to pin
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
set -eu
case "${DIGEST}" in
sha256:*) ;;
*) echo "no sha256 digest was published"; exit 1 ;;
esac
{
echo "### relearn-eval (scoring image, CUDA base)"
echo
echo "Pulled this digest after push, ran the harvest-PATH check,"
echo "and proved \`import vllm, torchvision\` on these bytes."
echo "Paste into the control plane's \`config/relearn-pin.toml\`:"
echo
echo '```toml'
echo "eval_image = \"${IMAGE}\""
echo "eval_image_digest = \"${DIGEST}\""
echo "relearn_git_sha = \"${GITHUB_SHA}\""
echo '```'
} >> "${GITHUB_STEP_SUMMARY}"
echo "eval_image_digest = \"${DIGEST}\""