Skip to content

fix: use unsafe-best-match index strategy in Docker build #235

fix: use unsafe-best-match index strategy in Docker build

fix: use unsafe-best-match index strategy in Docker build #235

name: Build and Push Container
on:
push:
tags:
- "v*" # Semantic version releases (v1.0.0) and release candidates (v1.0.0-rc1)
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GHCR_REGISTRY: ghcr.io
ACR_REGISTRY: crcommonallfrc.azurecr.io
IMAGE_NAME: ${{ github.repository }}
# ACR image name is always the upstream path so Azure picks up fork RC
# builds. GHCR uses IMAGE_NAME (per-fork) — each fork has its own GHCR.
ACR_IMAGE_NAME: iterorganization/imas-codex
# Graph OCI artifacts: try the repo owner's registry first (where
# `imas-codex release` pushes them), fall back to the upstream org
# registry for forks that haven't pushed their own graph yet.
GRAPH_REGISTRY: ghcr.io/${{ github.repository_owner }}
GRAPH_REGISTRY_FALLBACK: ghcr.io/iterorganization
jobs:
graph-quality:
runs-on: ubuntu-latest
# Only run on tag pushes or workflow_dispatch (same gate as build-and-push)
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
timeout-minutes: 30
outputs:
graph-tag-imas: ${{ steps.graph-tag.outputs.imas-tag }}
graph-registry: ${{ steps.graph-tag.outputs.graph-registry }}
services:
neo4j:
image: neo4j:2026.01.4-community
ports:
- 7474:7474
- 7687:7687
env:
NEO4J_AUTH: neo4j/imas-codex
NEO4J_PLUGINS: '["apoc"]'
NEO4J_server_memory_heap_initial__size: 512m
NEO4J_server_memory_heap_max__size: 1G
options: >-
--health-cmd "wget -q --spider http://localhost:7474/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 10
--health-start-period 30s
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install oras CLI
run: |
ORAS_VERSION="1.2.0"
curl -sLO "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz"
tar -xzf "oras_${ORAS_VERSION}_linux_amd64.tar.gz" oras
sudo mv oras /usr/local/bin/
oras version
- name: Login to GHCR
run: |
echo "${{ secrets.GHCR_TOKEN }}" | oras login ghcr.io -u token --password-stdin
- name: Resolve graph tag
id: graph-tag
run: |
IMAS_PACKAGE="imas-codex-graph"
OWNER_REGISTRY=$(echo "${{ env.GRAPH_REGISTRY }}" | tr '[:upper:]' '[:lower:]')
FALLBACK_REGISTRY="${{ env.GRAPH_REGISTRY_FALLBACK }}"
# Try the repo owner's registry first (where release CLI pushes)
resolve_tag() {
local REGISTRY="$1"
local TAGS
TAGS=$(oras repo tags "${REGISTRY}/${IMAS_PACKAGE}" 2>&1) || true
# Match only 'latest' or semver tags (v1.2.3, 5.2.0rc8, etc.)
local CLEAN
CLEAN=$(echo "$TAGS" | grep -E '^(latest|v?[0-9]+\.[0-9]+\.)' || true)
if echo "$CLEAN" | grep -qx "latest"; then
echo "latest"
elif [ -n "$CLEAN" ]; then
echo "$CLEAN" | sort -V | tail -1
fi
}
TAG=$(resolve_tag "$OWNER_REGISTRY")
USED_REGISTRY="$OWNER_REGISTRY"
# Fall back to upstream if fork has no graph artifacts
if [ -z "$TAG" ] && [ "$OWNER_REGISTRY" != "$FALLBACK_REGISTRY" ]; then
echo "No graph at ${OWNER_REGISTRY}/${IMAS_PACKAGE}, trying upstream fallback..."
TAG=$(resolve_tag "$FALLBACK_REGISTRY")
USED_REGISTRY="$FALLBACK_REGISTRY"
fi
if [ -n "$TAG" ]; then
echo "Resolved graph tag: ${TAG} from ${USED_REGISTRY}"
echo "imas-tag=${TAG}" >> $GITHUB_OUTPUT
echo "graph-registry=${USED_REGISTRY}" >> $GITHUB_OUTPUT
else
echo "::warning::No graph package found at either registry — skipping graph quality tests"
echo "imas-tag=none" >> $GITHUB_OUTPUT
echo "graph-registry=none" >> $GITHUB_OUTPUT
fi
- name: Skip notice
if: steps.graph-tag.outputs.imas-tag == 'none'
run: |
echo "::warning::No graph available — graph quality tests skipped. Docker build will proceed without graph validation."
- name: Pull graph dump from GHCR
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
REGISTRY="${{ steps.graph-tag.outputs.graph-registry }}"
ARTIFACT="${REGISTRY}/imas-codex-graph:${{ steps.graph-tag.outputs.imas-tag }}"
echo "Pulling: ${ARTIFACT}"
mkdir -p /tmp/graph-dump
oras pull "${ARTIFACT}" -o /tmp/graph-dump --allow-path-traversal
# Handle oras path traversal: artifact may land outside -o dir
FOUND=$(find /tmp -maxdepth 3 -name "*.tar.gz" ! -path "/tmp/graph-dump/*" 2>/dev/null | head -1)
if [ -n "$FOUND" ] && [ ! -f /tmp/graph-dump/*.tar.gz ]; then
echo "Moving artifact from traversal path: ${FOUND}"
mv "$FOUND" /tmp/graph-dump/
fi
ls -la /tmp/graph-dump/
- name: Stop Neo4j for dump load
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
docker stop $(docker ps -q --filter "ancestor=neo4j:2026.01.4-community") || true
sleep 3
- name: Load graph data into Neo4j
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
set -euo pipefail
ARCHIVE=$(ls /tmp/graph-dump/*.tar.gz | head -1)
echo "Loading archive: ${ARCHIVE}"
# Extract archive
mkdir -p /tmp/graph-extracted
tar -xzf "${ARCHIVE}" -C /tmp/graph-extracted
NEO4J_CONTAINER=$(docker ps -aq --filter "ancestor=neo4j:2026.01.4-community" | head -1)
NEO4J_DATA_VOLUME=$(docker inspect "${NEO4J_CONTAINER}" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')
DUMP_FILE=$(find /tmp/graph-extracted -name "*.dump" | head -1)
if [ -z "${DUMP_FILE}" ]; then
echo "ERROR: No .dump file found in archive"
ls -laR /tmp/graph-extracted/
exit 1
fi
echo "Found dump: ${DUMP_FILE}"
DUMP_DIR=$(dirname "${DUMP_FILE}")
if [ "$(basename ${DUMP_FILE})" != "neo4j.dump" ]; then
cp "${DUMP_FILE}" "${DUMP_DIR}/neo4j.dump"
fi
docker run --rm \
-v "${NEO4J_DATA_VOLUME}:/data" \
-v "${DUMP_DIR}:/dump" \
neo4j:2026.01.4-community \
neo4j-admin database load neo4j \
--from-path=/dump \
--overwrite-destination=true
- name: Start Neo4j with loaded data
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
NEO4J_CONTAINER=$(docker ps -aq --filter "ancestor=neo4j:2026.01.4-community" | head -1)
docker start "${NEO4J_CONTAINER}"
# Wait for Neo4j to come back up
echo "Waiting for Neo4j to restart..."
for i in $(seq 1 60); do
if curl -sf http://localhost:7474/ > /dev/null 2>&1; then
echo "Neo4j is ready with loaded graph"
break
fi
if [ $i -eq 60 ]; then
echo "ERROR: Neo4j did not restart in time"
docker logs "${NEO4J_CONTAINER}" --tail 50
exit 1
fi
sleep 2
done
- name: Reset Neo4j password
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
NEO4J_CONTAINER=$(docker ps -q --filter "ancestor=neo4j:2026.01.4-community" | head -1)
docker exec "${NEO4J_CONTAINER}" neo4j-admin dbms set-initial-password imas-codex 2>/dev/null || true
- name: Verify graph is loaded
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
NEO4J_CONTAINER=$(docker ps -q --filter "ancestor=neo4j:2026.01.4-community" | head -1)
docker exec "${NEO4J_CONTAINER}" cypher-shell \
-u neo4j -p imas-codex \
"MATCH (n) RETURN count(n) AS nodes, labels(n)[0] AS label ORDER BY nodes DESC LIMIT 10"
- name: Install uv
if: steps.graph-tag.outputs.imas-tag != 'none'
uses: astral-sh/[email protected]
with:
enable-cache: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
if: steps.graph-tag.outputs.imas-tag != 'none'
run: uv python install 3.12
- name: Install dependencies
if: steps.graph-tag.outputs.imas-tag != 'none'
run: uv sync --extra test --no-dev
env:
HATCH_BUILD_NO_HOOKS: true
- name: Build generated models
if: steps.graph-tag.outputs.imas-tag != 'none'
run: uv run build-models --force
- name: Initialize schema constraints
if: steps.graph-tag.outputs.imas-tag != 'none'
continue-on-error: true
run: |
uv run python -c "
from imas_codex.graph.client import GraphClient
with GraphClient() as gc:
gc.initialize_schema()
print('Schema initialized')
"
env:
NEO4J_URI: bolt://localhost:7687
NEO4J_USERNAME: neo4j
NEO4J_PASSWORD: imas-codex
- name: Run graph quality tests
if: steps.graph-tag.outputs.imas-tag != 'none'
run: |
uv run pytest tests/graph/ -m graph \
--tb=short \
--junit-xml=graph-quality-results.xml \
-v
env:
CI: true
NEO4J_URI: bolt://localhost:7687
NEO4J_USERNAME: neo4j
NEO4J_PASSWORD: imas-codex
IMAS_CODEX_GRAPH_LOCATION: local
- name: Upload test results
if: always() && steps.graph-tag.outputs.imas-tag != 'none'
uses: actions/upload-artifact@v7
with:
name: graph-quality-results
path: graph-quality-results.xml
retention-days: 30
smoke-test:
runs-on: ubuntu-latest
needs: graph-quality
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Free disk space
run: |
echo "Before cleanup:"
df -h /
# Aggressively free space — Docker build with Neo4j graph needs ~25 GB
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache /usr/share/swift /usr/local/share/boost \
/usr/local/graalvm /usr/local/share/chromium /usr/local/lib/node_modules
sudo docker image prune --all --force
echo "After cleanup:"
df -h /
- name: Login to GHCR
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build test image
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: imas-codex:smoke-test
build-args: |
GRAPH_TAG=${{ needs.graph-quality.outputs.graph-tag-imas }}
GRAPH_PACKAGE=imas-codex-graph-dd
GHCR_REGISTRY=${{ needs.graph-quality.outputs.graph-registry }}
secrets: |
GHCR_TOKEN=${{ secrets.GHCR_TOKEN }}
no-cache: true
- name: Start container
run: |
docker run -d \
--name smoke-test \
-p 8000:8000 \
-e OPENROUTER_API_KEY=dummy \
imas-codex:smoke-test
echo "Container started, waiting for services..."
- name: Wait for health check
timeout-minutes: 10
run: |
echo "Waiting for MCP server to be ready..."
for i in $(seq 1 90); do
if docker exec smoke-test curl -sf http://127.0.0.1:8000/health > /dev/null 2>&1; then
echo "✅ MCP server healthy after $((i * 5))s"
break
fi
if [ $i -eq 90 ]; then
echo "❌ Health check timed out after 450s"
docker logs smoke-test
exit 1
fi
sleep 5
done
- name: Test version endpoint
run: |
VERSION=$(docker exec smoke-test imas-codex --version 2>/dev/null || echo "failed")
echo "Container version: ${VERSION}"
if [ "$VERSION" = "failed" ]; then
echo "❌ Version check failed"
docker logs smoke-test
exit 1
fi
echo "✅ Version check passed"
- name: Test MCP tools listing
run: |
# Use curl to hit the MCP endpoint and verify tools are registered
RESPONSE=$(curl -sf http://localhost:8000/mcp 2>/dev/null || echo "")
if [ -z "$RESPONSE" ]; then
echo "⚠️ MCP endpoint not responding via HTTP (may need SSE/WebSocket)"
echo "Checking container logs for tool registration..."
docker logs smoke-test 2>&1 | grep -i "tool\|register\|ready" | head -10
fi
echo "✅ MCP endpoint check complete"
- name: Verify embedding model available
run: |
docker exec smoke-test python -c "
import torch
from sentence_transformers import SentenceTransformer
from huggingface_hub import try_to_load_from_cache
from pathlib import Path
import os
hf_home = os.environ.get('HF_HOME', '~/.cache/huggingface')
print(f'HF_HOME={hf_home}')
print(f'torch={torch.__version__}, cuda={torch.cuda.is_available()}')
# Use the same loading path as the runtime Encoder._load_model()
model_name = 'Qwen/Qwen3-Embedding-0.6B'
path = try_to_load_from_cache(model_name, 'config.json')
resolved = str(Path(path).parent) if path else model_name
print(f'Resolved path: {resolved}')
model = SentenceTransformer(
resolved,
device=None,
truncate_dim=256,
model_kwargs={'dtype': torch.bfloat16, 'low_cpu_mem_usage': False},
trust_remote_code=True,
)
result = model.encode(['test embedding'])
print(f'✅ Embedding model loaded, output shape: {result.shape}')
assert result.shape == (1, 256), f'Expected (1, 256) but got {result.shape}'
" || {
echo "❌ Embedding model verification failed"
docker logs smoke-test
exit 1
}
- name: Verify Neo4j graph loaded
run: |
GRAPH_TAG="${{ needs.graph-quality.outputs.graph-tag-imas }}"
if [ "$GRAPH_TAG" = "none" ]; then
echo "⚠️ No graph loaded (GRAPH_TAG=none), skipping graph verification"
exit 0
fi
# Use the health endpoint (already verified working) instead of cypher-shell
# which fails with Neo4j 2026 auth-disabled + explicit credentials
HEALTH=$(docker exec smoke-test curl -sf http://127.0.0.1:8000/health)
echo "Health response: ${HEALTH}"
NODE_COUNT=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin).get('graph',{}).get('node_count',0))")
echo "Node count: ${NODE_COUNT}"
if [ -z "$NODE_COUNT" ] || [ "$NODE_COUNT" = "0" ] || [ "$NODE_COUNT" = "None" ]; then
echo "❌ Graph is empty (node_count=${NODE_COUNT}) despite GRAPH_TAG=${GRAPH_TAG}"
docker logs smoke-test
exit 1
fi
echo "✅ Neo4j graph loaded with ${NODE_COUNT} nodes"
- name: Cleanup
if: always()
run: |
docker stop smoke-test 2>/dev/null || true
docker rm smoke-test 2>/dev/null || true
build-and-push:
runs-on: ubuntu-latest
needs: [graph-quality, smoke-test]
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
graph_variant:
- name: dd-only
package: imas-codex-graph-dd
suffix: ""
permissions:
contents: read
packages: write
id-token: write
attestations: write
actions: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # Full git history for dynamic versioning
# Ensure all refs are available
ref: ${{ github.ref }}
- name: Verify git state for versioning
run: |
echo "Current commit: $(git rev-parse HEAD)"
echo "Git describe: $(git describe --tags --always --dirty)"
echo "Git status:"
git status --porcelain
- name: Free disk space
run: |
echo "Before cleanup:"
df -h /
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
echo "After cleanup:"
df -h /
- name: Set graph tag for variant
id: variant
run: |
echo "graph-tag=${{ needs.graph-quality.outputs.graph-tag-imas }}" >> $GITHUB_OUTPUT
- name: Set lowercase image name for cache
id: image-name
run: |
echo "lowercase=$(echo '${{ env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set lowercase registry
id: registry
run: |
echo "ghcr=${{ env.GHCR_REGISTRY }}/$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
logout: false
- name: Log in to Azure Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.ACR_REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
logout: false
- name: Extract metadata for GHCR (releases only)
id: meta-ghcr
if: github.event_name != 'pull_request' && !contains(github.ref, '-rc')
uses: docker/metadata-action@v5
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern=v{{version}}${{ matrix.graph_variant.suffix }}
type=semver,pattern=v{{major}}.{{minor}}${{ matrix.graph_variant.suffix }}
type=semver,pattern=v{{major}}${{ matrix.graph_variant.suffix }}
type=raw,value=latest-stable${{ matrix.graph_variant.suffix }}
type=raw,value=latest${{ matrix.graph_variant.suffix }}
flavor: |
latest=false
- name: Extract metadata for GHCR (RC builds)
id: meta-ghcr-rc
if: github.event_name != 'pull_request' && contains(github.ref, '-rc')
uses: docker/metadata-action@v5
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern=v{{version}}${{ matrix.graph_variant.suffix }}
type=raw,value=latest-rc${{ matrix.graph_variant.suffix }}
flavor: |
latest=false
- name: Extract metadata for ACR
id: meta-acr
uses: docker/metadata-action@v5
with:
images: ${{ env.ACR_REGISTRY }}/${{ env.ACR_IMAGE_NAME }}
tags: |
type=semver,pattern=v{{version}}${{ matrix.graph_variant.suffix }}
type=semver,pattern=v{{major}}.{{minor}}${{ matrix.graph_variant.suffix }}
type=semver,pattern=v{{major}}${{ matrix.graph_variant.suffix }}
type=raw,value=latest-stable${{ matrix.graph_variant.suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc') }}
type=raw,value=latest${{ matrix.graph_variant.suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc') }}
type=raw,value=latest-rc${{ matrix.graph_variant.suffix }},enable=${{ contains(github.ref, '-rc') }}
flavor: |
latest=false
- name: Build and push Docker image (RC builds - both registries)
id: build-rc
if: github.event_name != 'pull_request' && contains(github.ref, '-rc')
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ steps.meta-acr.outputs.tags }}
${{ steps.meta-ghcr-rc.outputs.tags }}
labels: |
${{ steps.meta-acr.outputs.labels }}
${{ steps.meta-ghcr-rc.outputs.labels }}
build-args: |
IDS_FILTER=
GHCR_REGISTRY=${{ needs.graph-quality.outputs.graph-registry }}
GRAPH_TAG=${{ steps.variant.outputs.graph-tag }}
GRAPH_PACKAGE=${{ matrix.graph_variant.package }}
GIT_SHA=${{ github.sha }}
GIT_TAG=${{ github.ref_name }}
GIT_REF=${{ github.ref }}
secrets: |
GHCR_TOKEN=${{ secrets.GHCR_TOKEN }}
cache-from: type=gha,scope=build-${{ matrix.graph_variant.name }}
cache-to: type=gha,mode=min,scope=build-${{ matrix.graph_variant.name }}
- name: Build and push Docker image (releases - both registries)
id: build-release
if: github.event_name != 'pull_request' && !contains(github.ref, '-rc')
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ steps.meta-ghcr.outputs.tags }}
${{ steps.meta-acr.outputs.tags }}
labels: |
${{ steps.meta-ghcr.outputs.labels }}
${{ steps.meta-acr.outputs.labels }}
build-args: |
IDS_FILTER=
GHCR_REGISTRY=${{ needs.graph-quality.outputs.graph-registry }}
GRAPH_TAG=${{ steps.variant.outputs.graph-tag }}
GRAPH_PACKAGE=${{ matrix.graph_variant.package }}
GIT_SHA=${{ github.sha }}
GIT_TAG=${{ github.ref_name }}
GIT_REF=${{ github.ref }}
secrets: |
GHCR_TOKEN=${{ secrets.GHCR_TOKEN }}
cache-from: type=gha,scope=build-${{ matrix.graph_variant.name }}
cache-to: type=gha,mode=min,scope=build-${{ matrix.graph_variant.name }}
- name: Verify version inside image (release builds)
if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc')
run: |
set -euo pipefail
EXPECTED=$(python3 -c "from packaging.version import Version; print(Version('${GITHUB_REF_NAME#v}'))")
# Pick one tag to test (first GHCR tag)
IMAGE=$(echo "${{ steps.meta-ghcr.outputs.tags }}" | head -n1)
echo "Testing image: $IMAGE expecting version: $EXPECTED"
docker pull "$IMAGE"
# Bypass entrypoint (which starts Neo4j) and call CLI directly
ACTUAL=$(docker run --rm --entrypoint imas-codex "$IMAGE" --version || true)
echo "Version reported by image: $ACTUAL"
if [ -z "$ACTUAL" ]; then
echo "ERROR: No version output from CLI" >&2
exit 1
fi
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "ERROR: Expected $EXPECTED got $ACTUAL" >&2
exit 1
fi
echo "Version check passed"
- name: Verify version inside image (RC builds)
if: github.event_name != 'pull_request' && contains(github.ref, '-rc')
run: |
set -euo pipefail
EXPECTED=$(python3 -c "from packaging.version import Version; print(Version('${GITHUB_REF_NAME#v}'))")
# Pick one tag to test (first ACR tag)
IMAGE=$(echo "${{ steps.meta-acr.outputs.tags }}" | head -n1)
echo "Testing image: $IMAGE expecting version: $EXPECTED"
docker pull "$IMAGE"
# Bypass entrypoint (which starts Neo4j) and call CLI directly
ACTUAL=$(docker run --rm --entrypoint imas-codex "$IMAGE" --version || true)
echo "Version reported by image: $ACTUAL"
if [ -z "$ACTUAL" ]; then
echo "ERROR: No version output from CLI" >&2
exit 1
fi
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "ERROR: Expected $EXPECTED got $ACTUAL" >&2
exit 1
fi
echo "Version check passed"
- name: Generate artifact attestation for GHCR (releases only)
if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc')
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-release.outputs.digest }}
push-to-registry: true
# Azure Web App has continuous deployment enabled on ACR —
# new images are auto-deployed without a webhook trigger.