Skip to content

feat(webapp): one-click "try the bundled sample" for the local demo #17

feat(webapp): one-click "try the bundled sample" for the local demo

feat(webapp): one-click "try the bundled sample" for the local demo #17

name: Build and push PDF-QA image (GHCR)
# Builds the public PDF Q&A extraction images and pushes them to the GitHub
# Container Registry (ghcr.io) as a build matrix:
# * :latest - CPU-only, slim (~2GB) - the default for most users
# * :latest-gpu - CUDA torch + onnxruntime-gpu (~8GB) - run with `--gpus all`
# to GPU-accelerate BOTH unstructured model families: layout
# detection (YOLOX/detectron2_onnx on onnxruntime-gpu) and the
# table-structure model (Table Transformer, PyTorch). Tesseract
# OCR stays CPU.
# The same images feed both Azure ML and SageMaker, so learners can `docker
# pull` without a cloud account.
on:
push:
branches: [main]
paths:
- "pdf_qa_extraction/**"
- ".github/workflows/build-and-push.yml"
tags: ["v*"]
pull_request:
paths:
- "pdf_qa_extraction/**"
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/pdf-qa-extractor
# A new push to the same ref cancels an in-flight build (saves CI minutes).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-push:
name: build-and-push (${{ matrix.variant }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- variant: cpu
torch_index: https://download.pytorch.org/whl/cpu
onnxruntime_package: onnxruntime
tag_suffix: ""
- variant: gpu
torch_index: https://download.pytorch.org/whl/cu124
onnxruntime_package: onnxruntime-gpu
tag_suffix: "-gpu"
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
# The build still juggles a builder stage (torch/onnxruntime + build
# tools) and the runtime image at once, plus the gha cache export and the
# smoke-test pull. GitHub-hosted runners only ship ~14GB free on /, so
# reclaim the large preinstalled toolchains we don't use up front.
- name: Free up runner disk space
run: |
echo "::group::Disk before cleanup"; df -h /; echo "::endgroup::"
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup \
/usr/local/lib/android /usr/local/share/boost \
/opt/hostedtoolcache/CodeQL "${AGENT_TOOLSDIRECTORY:-}" || true
sudo docker image prune --all --force || true
echo "::group::Disk after cleanup"; df -h /; echo "::endgroup::"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
# Skip on PRs from forks (no package write permission).
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata (tags & labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# The GPU variant gets a `-gpu` suffix on every tag (incl. latest);
# the CPU variant keeps the bare tags. e.g. :latest / :latest-gpu.
flavor: |
suffix=${{ matrix.tag_suffix }},onlatest=true
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
id: build
# Validate the CPU build on PRs; the ~8GB GPU variant only builds on
# pushes/tags to keep PR CI fast.
if: github.event_name != 'pull_request' || matrix.variant == 'cpu'
uses: docker/build-push-action@v6
with:
context: pdf_qa_extraction
file: pdf_qa_extraction/Dockerfile
# Push on branch/tag builds; PRs only validate the build.
push: ${{ github.event_name != 'pull_request' }}
# cpu -> whl/cpu + onnxruntime (CPU, ~2GB); gpu -> whl/cu124 CUDA torch
# + onnxruntime-gpu (layout & table on GPU, ~8GB).
build-args: |
TORCH_INDEX_URL=${{ matrix.torch_index }}
ONNXRUNTIME_PACKAGE=${{ matrix.onnxruntime_package }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Separate cache scope per variant so cpu/gpu don't evict each other.
cache-from: type=gha,scope=${{ matrix.variant }}
# mode=min caches only the final-stage layers. mode=max would also
# cache the fat builder stage, doubling the gha cache blobs written to
# the runner's disk during export and risking "no space left".
cache-to: type=gha,mode=min,scope=${{ matrix.variant }}
provenance: false
- name: Smoke test & image size
# Only on pushed builds (the image must exist in the registry to pull).
if: github.event_name != 'pull_request'
run: |
# env.IMAGE_NAME embeds github.repository, whose owner/name can carry
# uppercase letters; docker refs must be lowercase (metadata-action
# lowercases the pushed tags, but this ref is built by hand).
IMG="$(echo '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')"
REF="${IMG}@${{ steps.build.outputs.digest }}"
# Reclaim the BuildKit build cache (both stages' layers) before
# pulling the finished image back onto the runner.
docker buildx prune --all --force || true
echo "::group::Disk before pull"; df -h /; echo "::endgroup::"
echo "Pulling $REF"
docker pull -q "$REF"
echo "::group::Image size (${{ matrix.variant }})"
docker image inspect "$REF" --format '{{.Size}}' | \
awk '{printf "Uncompressed image size: %.2f GiB (%d bytes)\n", $1/1073741824, $1}'
echo "::endgroup::"
echo "::group::Import smoke test (${{ matrix.variant }})"
docker run --rm "$REF" python -c "import importlib.metadata as md, pdf_qa, torch, cv2, unstructured, onnxruntime as ort; names={d.metadata['Name'].lower() for d in md.distributions()}; assert '${{ matrix.onnxruntime_package }}' in names, 'expected ${{ matrix.onnxruntime_package }}, found '+str(sorted(n for n in names if 'onnx' in n)); assert pdf_qa.list_personas()[0]=='professor'; assert 'feynman' in pdf_qa.list_personas(), 'personas.yaml ledger not packaged in image'; print('imports OK - ${{ matrix.variant }} | pdf_qa', pdf_qa.__version__, '| personas', pdf_qa.list_personas(), '| torch', torch.__version__, '| onnxruntime', ort.__version__, '(${{ matrix.onnxruntime_package }}) providers:', ort.get_available_providers())"
echo "::endgroup::"
echo "::group::Device probe (${{ matrix.variant }})"
docker run --rm "$REF" python -c "import pdf_qa; print(pdf_qa.probe_device().summary())"
echo "::endgroup::"
echo "::group::Web app smoke (${{ matrix.variant }})"
# Prove the single-node demo web app is shipped & importable in-image
# (fastapi/uvicorn present + webapp package COPYed). TestClient needs
# httpx (a test-only dep not in the image), so call the payload helpers
# directly instead of issuing HTTP requests.
docker run --rm "$REF" python -c "from webapp.app import _persona_payload, _device_payload, _meta_payload; pl=_persona_payload(); keys=[p['key'] for p in pl['personas']]; assert keys[0]=='professor', keys; assert 'memoirist' in keys and 'feynman' in keys, keys; assert len(keys)==7, keys; assert all(p['method_summary'] for p in pl['personas']); dev=_device_payload(); assert 'gpu_ready' in dev and dev['summary']; meta=_meta_payload(); assert meta['sample_available'], 'sample PDF (data/fsi_data.pdf) not packaged in image'; import fastapi, uvicorn; print('webapp OK - ${{ matrix.variant }} | fastapi', fastapi.__version__, '| personas', keys, '| sample', meta['sample_name'], '| gpu_ready', dev['gpu_ready'])"
echo "::endgroup::"