Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ build/
*~
.DS_Store

# Documentation (keep LICENSE and README)
# Documentation (keep runtime docs copied by Dockerfile)
docs/
*.md
!README.md
!README-reqif-ingest-cli.md
!LICENSE

# CI/CD
Expand All @@ -52,7 +53,7 @@ evidence_store/
!evidence_store/.gitkeep

# Development tools
justfile
!justfile
Dockerfile
.dockerignore
docker-compose.yml
77 changes: 77 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ on:
pull_request:
branches:
- main # Test builds on PRs
schedule:
- cron: '0 6 * * 1'
workflow_dispatch: # Allow manual triggers
inputs:
build_ingest_full:
description: "Build ingest-full image"
required: false
default: false
type: boolean

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
Expand Down Expand Up @@ -67,9 +75,12 @@ jobs:

- name: Build and push Docker image
id: build
env:
BUILD_START: ${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
uses: docker/build-push-action@v6
with:
context: .
target: runtime-lite
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
Expand All @@ -79,6 +90,24 @@ jobs:
build-args: |
PYTHON_VERSION=3.10
OPA_VERSION=0.71.0

- name: Build runtime-lite image locally for size metrics
if: always()
run: |
set -euo pipefail
START_SECONDS=$(date +%s)
docker buildx build \
--platform linux/amd64 \
--target runtime-lite \
--load \
-t local/reqif-opa-mcp:runtime-lite \
.
END_SECONDS=$(date +%s)
SIZE_BYTES=$(docker image inspect local/reqif-opa-mcp:runtime-lite --format '{{.Size}}')
DURATION_SECONDS=$((END_SECONDS - START_SECONDS))
echo "## Runtime-lite build metrics" >> "$GITHUB_STEP_SUMMARY"
echo "- Duration (s): ${DURATION_SECONDS}" >> "$GITHUB_STEP_SUMMARY"
echo "- Size (bytes): ${SIZE_BYTES}" >> "$GITHUB_STEP_SUMMARY"

- name: Generate artifact attestation
if: github.event_name != 'pull_request' && steps.build.outputs.digest != ''
Expand All @@ -104,3 +133,51 @@ jobs:
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

build-ingest-full:
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.build_ingest_full)
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push ingest-full image
uses: docker/build-push-action@v6
with:
context: .
target: ingest-full
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ingest-full
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-ingest-full
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
PYTHON_VERSION=3.10
OPA_VERSION=0.71.0

- name: Smoke test ingest-full docling import
run: |
set -euo pipefail
docker buildx build \
--platform linux/amd64 \
--target ingest-full \
--load \
-t local/reqif-opa-mcp:ingest-full \
.
docker run --rm local/reqif-opa-mcp:ingest-full \
python -c "import docling; print(docling.__version__)"
55 changes: 48 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
ARG PYTHON_VERSION=3.10
ARG OPA_VERSION=0.71.0

# Builder: Install Python deps
FROM python:${PYTHON_VERSION}-slim AS builder
# Builder: install runtime-lite dependencies only.
FROM python:${PYTHON_VERSION}-slim AS deps-lite
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /build
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY reqif_mcp/ reqif_mcp/
COPY reqif_ingest_cli/ reqif_ingest_cli/
COPY README.md LICENSE ./
RUN uv sync --frozen --no-dev
RUN uv sync --frozen --no-dev --extra ingest-lite

# OPA binary
FROM alpine:latest AS opa-downloader
Expand All @@ -23,16 +23,16 @@ RUN apk add --no-cache curl && \
curl -L -o /opa/opa "https://openpolicyagent.org/downloads/v${OPA_VERSION}/opa_linux_${ARCH}_static" && \
chmod +x /opa/opa

# Runtime
FROM python:${PYTHON_VERSION}-slim
# Runtime-lite image: default CI/runtime target.
FROM python:${PYTHON_VERSION}-slim AS runtime-lite
LABEL org.opencontainers.image.title="ReqIF-OPA-MCP" \
org.opencontainers.image.description="ReqIF compliance gate with OPA and SARIF" \
org.opencontainers.image.description="ReqIF compliance gate with OPA and SARIF (runtime-lite)" \
org.opencontainers.image.vendor="PromptExecution" \
org.opencontainers.image.source="https://github.com/PromptExecution/reqif-opa-mcp"

RUN groupadd -r reqif && useradd -r -g reqif -u 1000 reqif
WORKDIR /app
COPY --from=builder --chown=reqif:reqif /build/.venv /app/.venv
COPY --from=deps-lite --chown=reqif:reqif /build/.venv /app/.venv
COPY --from=opa-downloader /opa/opa /usr/local/bin/opa
COPY --chown=reqif:reqif reqif_mcp/ /app/reqif_mcp/
COPY --chown=reqif:reqif reqif_ingest_cli/ /app/reqif_ingest_cli/
Expand All @@ -53,3 +53,44 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import sys, urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5).read(); sys.exit(0)"
EXPOSE 8000
CMD ["python", "-m", "reqif_mcp", "--http", "--host", "0.0.0.0", "--port", "8000"]

# Builder: install full ingestion plus optional LLM review dependencies.
FROM deps-lite AS deps-full
RUN uv sync --frozen --no-dev --extra ingest-full --extra llm-review

# Ingest-full image for richer docling extraction.
FROM python:${PYTHON_VERSION}-slim AS ingest-full
LABEL org.opencontainers.image.title="ReqIF-OPA-MCP" \
org.opencontainers.image.description="ReqIF compliance gate with OPA and SARIF (ingest-full)" \
org.opencontainers.image.vendor="PromptExecution" \
org.opencontainers.image.source="https://github.com/PromptExecution/reqif-opa-mcp"

RUN groupadd -r reqif && useradd -r -g reqif -u 1000 reqif
WORKDIR /app
COPY --from=deps-full --chown=reqif:reqif /build/.venv /app/.venv
COPY --from=opa-downloader /opa/opa /usr/local/bin/opa
COPY --chown=reqif:reqif reqif_mcp/ /app/reqif_mcp/
COPY --chown=reqif:reqif reqif_ingest_cli/ /app/reqif_ingest_cli/
COPY --chown=reqif:reqif agents/ /app/agents/
COPY --chown=reqif:reqif schemas/ /app/schemas/
COPY --chown=reqif:reqif samples/ /app/samples/
COPY --chown=reqif:reqif opa-bundles/ /app/opa-bundles/
COPY --chown=reqif:reqif justfile pyproject.toml README.md README-reqif-ingest-cli.md LICENSE /app/
RUN mkdir -p /app/evidence_store/{events,sarif,decision_logs} /app/artifacts/{tests,selftest,demo} && \
chown -R reqif:reqif /app

ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

USER reqif
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import sys, urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5).read(); sys.exit(0)"
EXPOSE 8000
CMD ["python", "-m", "reqif_mcp", "--http", "--host", "0.0.0.0", "--port", "8000"]

# Demo-full can carry sample/selftest assets while sharing ingest-full runtime.
FROM ingest-full AS demo-full

# Default build target stays lean.
FROM runtime-lite AS default
9 changes: 9 additions & 0 deletions README-reqif-ingest-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ Engineer view:
- use this surface when the starting point is an artifact such as XLSX, PDF, DOCX, or Markdown
- use `reqif_mcp` when the starting point is already ReqIF

Install profiles:

- `uv sync --extra ingest-lite`
- XLSX and text-layer PDF extraction (`openpyxl`, `pypdf`)
- `uv sync --extra ingest-full`
- richer DOCX/Markdown/PDF extraction (`docling` stack)
- `uv sync --extra llm-review`
- optional Foundry/OpenAI-compatible review adapter

```mermaid
flowchart LR
ART[Source artifact] --> EXT[Deterministic extraction]
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,11 @@ Current implementation notes:
- PDF prefers `pypdf` for offline text-layer extraction
- `docling` remains the richer path for DOCX and Markdown
- Azure Foundry integration is optional and not part of the deterministic first pass
- Install `ingest-lite` with `uv sync --extra ingest-lite` for XLSX plus text-layer PDF support
- Install `ingest-full` with `uv sync --extra ingest-full` for docling-backed extraction
- Install `llm-review` with `uv sync --extra llm-review` for optional Foundry quality-eval hooks

See `README-reqif-ingest-cli.md` for command details.
See [README-reqif-ingest-cli.md](README-reqif-ingest-cli.md) for command details.

## Samples and Fixtures

Expand Down
2 changes: 1 addition & 1 deletion ado/templates/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
mkdir -p ${{ parameters.artifactRoot }}/tests
export PATH="$HOME/.local/bin:$PATH"
export UV_CACHE_DIR=.uv-cache
uv sync
uv sync --extra ingest-lite
just ci-check "${{ parameters.artifactRoot }}/tests/junit.xml"
uv run ruff check . > "${{ parameters.artifactRoot }}/tests/ruff.txt"
uv run mypy reqif_mcp reqif_ingest_cli > "${{ parameters.artifactRoot }}/tests/mypy.txt"
Expand Down
2 changes: 1 addition & 1 deletion ado/templates/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
export UV_CACHE_DIR=.uv-cache
uv sync
uv sync --extra ingest-lite
just demo-artifacts "${{ parameters.artifactRoot }}/demo" "${{ parameters.artifactRoot }}/selftest" "${{ parameters.enforceGateFailures }}"
continueOnError: ${{ eq(parameters.enforceGateFailures, 'false') }}
displayName: Build self-test and demo artifacts
Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ variables:
containerAppEnvironmentName: 'reqif-opa-demo-env'
containerAppName: 'reqif-opa-mcp-demo'
containerPort: '8000'
imageTarget: 'runtime-lite'

stages:
- stage: ci
Expand Down Expand Up @@ -61,6 +62,7 @@ stages:
set -euo pipefail
az acr login --name $(acrName)
docker build \
--target $(imageTarget) \
-t $(acrName).azurecr.io/$(imageRepository):$(Build.SourceVersion) \
-t $(acrName).azurecr.io/$(imageRepository):latest \
.
Expand Down
19 changes: 16 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ default:

# Dev
install:
uv sync
uv sync --extra ingest-lite

dev:
uv run python -m reqif_mcp
Expand Down Expand Up @@ -216,15 +216,28 @@ demo-artifacts out="artifacts/demo" selftest_out="artifacts/selftest" enforce="f
exit 0

# Docker
docker-build tag="latest":
docker build -t ghcr.io/promptexecution/reqif-opa-mcp:{{tag}} .
docker-build tag="latest" target="runtime-lite":
docker build --target {{target}} -t ghcr.io/promptexecution/reqif-opa-mcp:{{tag}} .

build-runtime-lite tag="runtime-lite":
just docker-build {{tag}} runtime-lite

build-ingest-full tag="ingest-full":
just docker-build {{tag}} ingest-full

docker-run tag="latest" port="8000":
docker run --rm -p {{port}}:8000 \
-v $(pwd)/evidence_store:/app/evidence_store \
-v $(pwd)/opa-bundles:/app/opa-bundles \
ghcr.io/promptexecution/reqif-opa-mcp:{{tag}}

smoke-runtime-lite tag="runtime-lite":
docker run --rm -d --name reqif-runtime-lite -p 18000:8000 \
ghcr.io/promptexecution/reqif-opa-mcp:{{tag}}
sleep 3
curl -fsSL http://127.0.0.1:18000/health
docker rm -f reqif-runtime-lite

docker-push tag="latest":
docker push ghcr.io/promptexecution/reqif-opa-mcp:{{tag}}

Expand Down
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ dependencies = [
"python-ulid>=3.1.0",
"returns>=0.26.0",
"fastmcp==3.0.0b1",
"docling>=2.78.0",
"reqif>=0.0.48",
"azure-ai-inference>=1.0.0b9",
]

[project.optional-dependencies]
ingest-lite = [
"openpyxl>=3.1.5",
"pypdf>=6.8.0",
]
ingest-full = [
"docling>=2.79.0",
"openpyxl>=3.1.5",
"pypdf>=6.8.0",
]
llm-review = [
"azure-ai-inference>=1.0.0b9",
]

[dependency-groups]
dev = [
Expand Down
Loading
Loading