name: Docker Release on: push: tags: - "*" env: IMAGE_NAME: ihorleleka/project-rag-wiki CANDIDATE_IMAGE: local-rag-wiki:release-candidate jobs: test-and-release: runs-on: ubuntu-latest permissions: contents: read steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate release tag id: version shell: bash run: | RAW="${GITHUB_REF_NAME}" VERSION="${RAW#v}" if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Release tag must be X.Y.Z or vX.Y.Z; received: ${RAW}" >&2 exit 1 fi echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" - name: Capture candidate metadata id: metadata shell: bash run: echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}" - name: Resolve contract versions id: contract shell: bash run: | CONTRACT="kb-service/src/kb_service/contract.py" SCHEMA="$(python -c "import re,pathlib;print(re.search(r'INDEX_SCHEMA_VERSION\s*=\s*(\d+)', pathlib.Path('${CONTRACT}').read_text()).group(1))")" TOOL="$(python -c "import re,pathlib;print(re.search(r'MCP_TOOL_CONTRACT_VERSION\s*=\s*(\d+)', pathlib.Path('${CONTRACT}').read_text()).group(1))")" echo "index_schema_version=${SCHEMA}" >> "${GITHUB_OUTPUT}" echo "mcp_tool_contract_version=${TOOL}" >> "${GITHUB_OUTPUT}" - name: Build the release candidate run: | docker build \ --build-arg VERSION="${{ steps.version.outputs.version }}" \ --build-arg VCS_REF="${GITHUB_SHA}" \ --build-arg BUILD_DATE="${{ steps.metadata.outputs.build_date }}" \ --build-arg INDEX_SCHEMA_VERSION="${{ steps.contract.outputs.index_schema_version }}" \ --build-arg MCP_TOOL_CONTRACT_VERSION="${{ steps.contract.outputs.mcp_tool_contract_version }}" \ --tag "${CANDIDATE_IMAGE}" \ ./kb-service - name: Verify candidate metadata run: | test "$(docker image inspect "${CANDIDATE_IMAGE}" --format '{{ index .Config.Labels "org.opencontainers.image.version" }}')" = "${{ steps.version.outputs.version }}" test "$(docker run --rm "${CANDIDATE_IMAGE}" python -c 'import importlib.metadata; print(importlib.metadata.version("kb-service"))')" = "${{ steps.version.outputs.version }}" # Image labels must match the contract constants baked into the image. test "$(docker image inspect "${CANDIDATE_IMAGE}" --format '{{ index .Config.Labels "io.github.ihorleleka.project-rag-wiki.index-schema-version" }}')" = "$(docker run --rm "${CANDIDATE_IMAGE}" python -c 'from kb_service.contract import INDEX_SCHEMA_VERSION; print(INDEX_SCHEMA_VERSION)')" test "$(docker image inspect "${CANDIDATE_IMAGE}" --format '{{ index .Config.Labels "io.github.ihorleleka.project-rag-wiki.mcp-tool-contract-version" }}')" = "$(docker run --rm "${CANDIDATE_IMAGE}" python -c 'from kb_service.contract import MCP_TOOL_CONTRACT_VERSION; print(MCP_TOOL_CONTRACT_VERSION)')" - name: Start release-candidate container run: | docker volume create local-rag-wiki-ci-hf docker volume create local-rag-wiki-ci-kb docker run -d --name local-rag-wiki-ci-smoke \ -p 127.0.0.1:18111:1111 \ -v "${{ github.workspace }}/kb-service/tests/fixtures/retrieval-wiki:/workspace/wiki:ro" \ -v "${{ github.workspace }}:/repository:ro" \ -v "local-rag-wiki-ci-kb:/workspace/.kb" \ -v "local-rag-wiki-ci-hf:/root/.cache/chroma" \ "${CANDIDATE_IMAGE}" for attempt in $(seq 1 120); do if curl --fail --silent http://127.0.0.1:18111/health >/dev/null; then exit 0; fi sleep 1 done docker logs local-rag-wiki-ci-smoke exit 1 - name: Probe HTTP and MCP surface run: python kb-service/tests/container_smoke.py 18111 - name: Clean smoke resources if: always() run: | docker rm -f local-rag-wiki-ci-smoke 2>/dev/null || true docker volume rm local-rag-wiki-ci-kb 2>/dev/null || true docker volume rm local-rag-wiki-ci-hf 2>/dev/null || true - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push tested candidate without rebuilding run: | docker tag "${CANDIDATE_IMAGE}" "${IMAGE_NAME}:${{ steps.version.outputs.version }}" docker tag "${CANDIDATE_IMAGE}" "${IMAGE_NAME}:latest" docker push "${IMAGE_NAME}:${{ steps.version.outputs.version }}" docker push "${IMAGE_NAME}:latest"