fix: use unsafe-best-match index strategy in Docker build #235
Workflow file for this run
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: 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. |