name: Image on: workflow_dispatch: inputs: ref: description: "Branch, tag, or commit to build" required: false default: "main" type: string tag: description: "Image tag (defaults to sanitized ref)" required: false type: string platforms: description: "Target platforms to build" required: false default: "both" type: choice options: - amd64-only - arm64-only - both env: REGISTRY: ghcr.io IMAGE_NAME: primeintellect-ai/prime-rl BUILD_REF: ${{ inputs.ref || github.ref_name }} BUILD_TAG: ${{ inputs.tag || '' }} BUILD_PLATFORMS: ${{ inputs.platforms || 'both' }} concurrency: group: build-image-${{ inputs.tag || inputs.ref || github.ref_name }} cancel-in-progress: false jobs: authorize: name: Authorize runs-on: ubuntu-latest permissions: contents: read steps: - name: Require maintain or admin # Gate MANUAL dispatches only; other events (e.g. release) are # already privileged and pass through. Guard the step, not the job — # a skipped job would skip `prepare` via `needs: authorize`. if: github.event_name == 'workflow_dispatch' env: GH_TOKEN: ${{ github.token }} ACTOR: ${{ github.triggering_actor }} REPO: ${{ github.repository }} run: | # Restricted to repo members with maintain or admin. Automated # in-repo dispatches (devx_tag dev-image builds, the release build) # run as github-actions[bot] via GITHUB_TOKEN — allow those through. if [ "$ACTOR" = "github-actions[bot]" ]; then echo "Dispatcher is repo automation ($ACTOR); allowed." exit 0 fi role=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.role_name' 2>/dev/null || true) echo "Dispatcher $ACTOR has repo role: ${role:-}" case "$role" in admin|maintain) echo "Authorized." ;; *) echo "::error::$ACTOR (role: ${role:-none}) is not allowed to trigger image builds; requires maintain or admin." >&2 exit 1 ;; esac prepare: name: Prepare needs: authorize runs-on: ubuntu-latest permissions: contents: read outputs: matrix: ${{ steps.matrix.outputs.matrix }} ref: ${{ steps.vars.outputs.ref }} tag: ${{ steps.vars.outputs.tag }} sha: ${{ steps.vars.outputs.sha }} full_sha: ${{ steps.vars.outputs.full_sha }} platforms: ${{ steps.vars.outputs.platforms }} steps: - name: Set build matrix id: matrix env: PLATFORMS: ${{ env.BUILD_PLATFORMS }} run: | case "$PLATFORMS" in arm64-only) echo 'matrix={"include":[{"runner":"image-builder-arm-2204","platform":"linux/arm64","suffix":"arm64"}]}' >> "$GITHUB_OUTPUT" ;; both) echo 'matrix={"include":[{"runner":"image-builder","platform":"linux/amd64","suffix":"amd64"},{"runner":"image-builder-arm-2204","platform":"linux/arm64","suffix":"arm64"}]}' >> "$GITHUB_OUTPUT" ;; amd64-only) echo 'matrix={"include":[{"runner":"image-builder","platform":"linux/amd64","suffix":"amd64"}]}' >> "$GITHUB_OUTPUT" ;; *) echo "Unsupported platforms value: ${PLATFORMS}" >&2 exit 1 ;; esac - name: Checkout uses: actions/checkout@v7 with: ref: ${{ env.BUILD_REF }} fetch-depth: 0 - name: Resolve image vars id: vars env: INPUT_REF: ${{ env.BUILD_REF }} INPUT_TAG: ${{ env.BUILD_TAG }} PLATFORMS: ${{ env.BUILD_PLATFORMS }} run: | if [ -n "$INPUT_TAG" ] && [ "$INPUT_TAG" != "$INPUT_REF" ] && [ "$INPUT_REF" != "main" ]; then echo "custom 'tag' that differs from 'ref' is only allowed when ref=main; got ref='${INPUT_REF}' tag='${INPUT_TAG}'" >&2 exit 1 fi sanitize() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-50 } # A ref that is exactly a git tag builds under that tag verbatim; # any other ref (branch / arbitrary commit) is prefixed with # `git describe` so the tag carries the nearest release, commit # distance and short sha (e.g. v0.7.0-42-g1a2b3c4d5-fix-my-branch) # and can't collide with a clean release tag. if [ -n "$INPUT_TAG" ]; then tag=$(sanitize "$INPUT_TAG") elif tag=$(git describe --tags --exact-match 2>/dev/null); then : else tag="$(git describe --tags --always)-$(sanitize "$INPUT_REF")" fi if [ -z "$tag" ]; then echo "Could not derive a valid image tag from ref='${INPUT_REF}' tag='${INPUT_TAG}'." >&2 exit 1 fi echo "ref=${INPUT_REF}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" echo "full_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" echo "platforms=${PLATFORMS}" >> "$GITHUB_OUTPUT" build: name: Build (${{ matrix.suffix }}) needs: prepare strategy: fail-fast: true matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} runs-on: ${{ matrix.runner }} permissions: contents: read packages: write steps: - name: Remove unnecessary packages run: | # Deleting the preinstalled toolchains costs ~70s; skip unless disk is tight. avail_gb=$(df -BG --output=avail / | tail -1 | tr -dc '0-9') if [ "${avail_gb:-0}" -lt 80 ]; then sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc else echo "Skipping cleanup: ${avail_gb}G available on /" fi - name: Checkout uses: actions/checkout@v7 with: ref: ${{ needs.prepare.outputs.full_sha }} fetch-depth: 0 submodules: false - name: Init public submodules run: | if [ -f .gitmodules ]; then git config --global url."https://github.com/".insteadOf "git@github.com:" git submodule update --init --recursive -- deps/pydantic-config deps/verifiers deps/renderers deps/prime-envs fi - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: # CVE-2026-93318: pin the BuildKit builder to the patched release so # the version is deterministic on the self-hosted runners (otherwise # the rolling moby/buildkit:buildx-stable-1 tag is used, and a # persistent runner may keep a stale local copy of it). driver-opts: image=moby/buildkit:v0.33.1@sha256:cec9f139f45e93c5c69c60f8b07cfad9f43f4ef6b6a6cd917527fea5ff2e3dea - name: Set up uv uses: astral-sh/setup-uv@v7 with: version: "0.11.9" enable-cache: false - name: Resolve dependency image and package versions id: dependency env: GH_TOKEN: ${{ github.token }} SUFFIX: ${{ matrix.suffix }} run: | set -euo pipefail key=$(uv run --no-project --python 3.12 scripts/docker_dependencies.py fingerprint .) # deps-v2: remediation epoch for CVE-2026-93318 — the fingerprint # hashes only Dockerfile/lock inputs, not the workflow or BuildKit # version, so pre-fix deps-${key} images would otherwise be reused # as the runtime base instead of being rebuilt by the pinned v0.33.1 # builder. image="${REGISTRY}/${IMAGE_NAME}:deps-${key}-v2-${SUFFIX}" digest=$(uv run --no-project --python 3.12 scripts/docker_dependencies.py resolve "$image") echo "image=${image}" >> "$GITHUB_OUTPUT" echo "digest=${digest}" >> "$GITHUB_OUTPUT" bash scripts/docker-editable-pretend-versions.sh --github-output . >> "$GITHUB_OUTPUT" - name: Build and push dependencies id: dependencies if: steps.dependency.outputs.digest == '' uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.cuda target: dependencies outputs: type=image,push=true,compression=zstd,compression-level=3 # buildcache-v2: rotated 2026-10-05 for CVE-2026-93318 — the pre-v0.33.1 # builders (cache writers since 2026-08-16) must not be re-imported. cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dependency-buildcache-v2-${{ matrix.suffix }} cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dependency-buildcache-v2-${{ matrix.suffix }},mode=max,image-manifest=true,oci-mediatypes=true,compression=zstd,compression-level=3 platforms: ${{ matrix.platform }} tags: ${{ steps.dependency.outputs.image }} - name: Build and push id: build uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.cuda target: runtime build-args: | DEPENDENCY_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.dependencies.outputs.digest || steps.dependency.outputs.digest }} VERIFIERS_PRETEND_VERSION=${{ steps.dependency.outputs.verifiers }} RENDERERS_PRETEND_VERSION=${{ steps.dependency.outputs.renderers }} PYDANTIC_CONFIG_PRETEND_VERSION=${{ steps.dependency.outputs.pydantic_config }} outputs: type=image,push=true,compression=zstd,compression-level=3 # buildcache-v2: rotated 2026-10-05 for CVE-2026-93318 — the pre-v0.33.1 # builders (cache writers since 2026-08-16) must not be re-imported. cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:application-buildcache-v2-${{ matrix.suffix }} cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:application-buildcache-v2-${{ matrix.suffix }},mode=max,image-manifest=true,oci-mediatypes=true,compression=zstd,compression-level=3 platforms: ${{ matrix.platform }} tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag }}-${{ matrix.suffix }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:commit-${{ needs.prepare.outputs.sha }}-${{ matrix.suffix }} labels: | org.opencontainers.image.revision=${{ needs.prepare.outputs.full_sha }} org.opencontainers.image.version=${{ needs.prepare.outputs.tag }} org.opencontainers.image.source=https://github.com/${{ github.repository }} - name: Summary env: INPUT_REF: ${{ needs.prepare.outputs.ref }} IMAGE_TAG: ${{ needs.prepare.outputs.tag }} IMAGE_SHA: ${{ needs.prepare.outputs.sha }} IMAGE_DIGEST: ${{ steps.build.outputs.digest }} DEPENDENCY_IMAGE: ${{ steps.dependency.outputs.image }} DEPENDENCY_DIGEST: ${{ steps.dependencies.outputs.digest || steps.dependency.outputs.digest }} PLATFORM: ${{ matrix.platform }} SUFFIX: ${{ matrix.suffix }} run: | { echo "## Build Summary (${SUFFIX})" echo "" echo "| Property | Value |" echo "|----------|-------|" echo "| **Ref** | \`${INPUT_REF}\` |" echo "| **Platform** | \`${PLATFORM}\` |" echo "| **Image** | \`${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}-${SUFFIX}\` |" echo "| **Commit** | \`${IMAGE_SHA}\` |" echo "| **Digest** | \`${IMAGE_DIGEST}\` |" echo "| **Dependencies** | \`${DEPENDENCY_IMAGE}@${DEPENDENCY_DIGEST}\` |" } >> "$GITHUB_STEP_SUMMARY" manifest: name: Manifest needs: [prepare, build] if: needs.prepare.outputs.platforms == 'both' runs-on: ubuntu-latest permissions: packages: write steps: - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push manifests env: IMAGE_TAG: ${{ needs.prepare.outputs.tag }} IMAGE_SHA: ${{ needs.prepare.outputs.sha }} IMAGE_FULL_SHA: ${{ needs.prepare.outputs.full_sha }} PLATFORMS: ${{ needs.prepare.outputs.platforms }} SOURCE_URL: https://github.com/${{ github.repository }} run: | set -euo pipefail image="${REGISTRY}/${IMAGE_NAME}" tag_sources=() sha_sources=() if [ "$PLATFORMS" != "arm64-only" ]; then tag_sources+=("${image}:${IMAGE_TAG}-amd64") sha_sources+=("${image}:commit-${IMAGE_SHA}-amd64") fi if [ "$PLATFORMS" != "amd64-only" ]; then tag_sources+=("${image}:${IMAGE_TAG}-arm64") sha_sources+=("${image}:commit-${IMAGE_SHA}-arm64") fi docker buildx imagetools create \ --annotation "index:org.opencontainers.image.revision=${IMAGE_FULL_SHA}" \ --annotation "index:org.opencontainers.image.version=${IMAGE_TAG}" \ --annotation "index:org.opencontainers.image.source=${SOURCE_URL}" \ -t "${image}:${IMAGE_TAG}" \ "${tag_sources[@]}" docker buildx imagetools create \ --annotation "index:org.opencontainers.image.revision=${IMAGE_FULL_SHA}" \ --annotation "index:org.opencontainers.image.version=${IMAGE_TAG}" \ --annotation "index:org.opencontainers.image.source=${SOURCE_URL}" \ -t "${image}:commit-${IMAGE_SHA}" \ "${sha_sources[@]}" - name: Summary env: INPUT_REF: ${{ needs.prepare.outputs.ref }} IMAGE_TAG: ${{ needs.prepare.outputs.tag }} IMAGE_SHA: ${{ needs.prepare.outputs.sha }} PLATFORMS: ${{ needs.prepare.outputs.platforms }} run: | { echo "## Manifest Summary" echo "" echo "| Property | Value |" echo "|----------|-------|" echo "| **Ref** | \`${INPUT_REF}\` |" echo "| **Platforms** | \`${PLATFORMS}\` |" echo "| **Image** | \`${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}\` |" echo "| **Commit Image** | \`${REGISTRY}/${IMAGE_NAME}:commit-${IMAGE_SHA}\` |" } >> "$GITHUB_STEP_SUMMARY"