fix: use bfloat16 instead of float32 for CPU embedding to avoid OOM #225
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: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| GHCR_REGISTRY: ghcr.io | |
| ACR_REGISTRY: crcommonallfrc.azurecr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| IMAS_DD_VERSION: "4.1.0" | |
| # 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 (same gate as build-and-push) | |
| if: startsWith(github.ref, 'refs/tags/') | |
| 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@v4 | |
| - 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-dd" | |
| 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 | |
| local CLEAN | |
| CLEAN=$(echo "$TAGS" | grep -E '^[0-9v]' || 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-dd:${{ 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 dump 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 | |
| # Find the dump file (may be named graph.dump or neo4j.dump) | |
| 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}" | |
| # neo4j-admin expects the dump file named neo4j.dump | |
| DUMP_DIR=$(dirname "${DUMP_FILE}") | |
| if [ "$(basename ${DUMP_FILE})" != "neo4j.dump" ]; then | |
| cp "${DUMP_FILE}" "${DUMP_DIR}/neo4j.dump" | |
| fi | |
| # Get the Neo4j container ID for data volume | |
| 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}}') | |
| # Load into Neo4j using a fresh container | |
| 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/setup-uv@v5 | |
| 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 | |
| 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@v4 | |
| 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/') | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| 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 / | |
| # Remove packages we don't need for docker builds | |
| 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: 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: 5 | |
| run: | | |
| echo "Waiting for MCP server to be ready..." | |
| for i in $(seq 1 60); 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}s" | |
| break | |
| fi | |
| if [ $i -eq 60 ]; then | |
| echo "❌ Health check timed out" | |
| 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] | |
| # Only run on tag pushes | |
| if: startsWith(github.ref, 'refs/tags/') | |
| 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@v4 | |
| 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.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= | |
| IMAS_DD_VERSION=${{ env.IMAS_DD_VERSION }} | |
| 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= | |
| IMAS_DD_VERSION=${{ env.IMAS_DD_VERSION }} | |
| 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@v1 | |
| with: | |
| subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.build-release.outputs.digest }} | |
| push-to-registry: true | |
| - name: Trigger Azure Web App deployment | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| env: | |
| WEBHOOK_URL: ${{ secrets.AZURE_WEBAPP_WEBHOOK }} | |
| run: | | |
| if [ -z "$WEBHOOK_URL" ]; then | |
| echo "ℹ️ AZURE_WEBAPP_WEBHOOK secret not configured — skipping" | |
| exit 0 | |
| fi | |
| echo "Triggering Azure Web App deployment webhook..." | |
| STATUS=$(curl -sf -o /dev/null -w '%{http_code}' -X POST "$WEBHOOK_URL" 2>&1) || true | |
| if [ "$STATUS" = "200" ] || [ "$STATUS" = "204" ]; then | |
| echo "✅ Webhook triggered successfully (HTTP $STATUS)" | |
| else | |
| echo "⚠️ Webhook returned HTTP $STATUS — Web App may need manual restart" | |
| fi |