Live Relearn eval image: implement the harvest contract so cortex can… #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}\"" |