name: ENGIN CI on: push: branches: - main - prod-hardening pull_request: branches: - main jobs: production-check: name: Build, test and smoke runs-on: engin-ci timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Clean previous CI resources run: | docker rm -f engin-ci-app 2>/dev/null || true docker image rm -f engin-console:ci 2>/dev/null || true docker image rm -f engin-console:test 2>/dev/null || true - name: Build test image run: docker build --target test --tag engin-console:test . - name: Run Ruff run: | docker run --rm \ --entrypoint sh \ engin-console:test \ -c "python -m pip install --quiet \ --disable-pip-version-check \ --root-user-action=ignore \ ruff==0.16.4 && ruff check --no-cache \ app.py engin tests final_pipeline.py ml_polish_benchmark.py scripts" - name: Check installed dependencies run: | docker run --rm \ --entrypoint python \ engin-console:test \ -m pip check - name: Run test suite run: | docker run --rm \ --entrypoint python \ engin-console:test \ -m unittest discover -v - name: Verify frozen model artifact run: | docker run --rm \ --entrypoint python \ engin-console:test \ -m scripts.build_model_artifact --verify-only - name: Validate Python syntax run: | docker run --rm \ --entrypoint python \ engin-console:test \ -m compileall -q app.py engin tests final_pipeline.py scripts - name: Build production image run: docker build --target runtime --tag engin-console:ci . - name: Assert production image contract run: | docker run --rm \ --entrypoint sh \ engin-console:ci \ -c "test \"\$(id -u)\" = 10001 && test \"\$(id -g)\" = 10001 && test ! -w /app && test ! -e val.csv && test ! -e train.csv && test ! -e final_pipeline.py && test ! -e severity_benchmark.py && test -s artifacts/engin-2026.08.25-1/model.pkl && test -s artifacts/engin-2026.08.25-1/manifest.json" - name: Start application run: | docker run -d \ --name engin-ci-app \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --cap-drop ALL \ --security-opt no-new-privileges:true \ --pids-limit 256 \ --memory 1g \ --cpus 2 \ engin-console:ci - name: Verify runtime isolation run: | test "$(docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' engin-ci-app)" = true test "$(docker inspect --format '{{.HostConfig.PidsLimit}}' engin-ci-app)" = 256 test "$(docker inspect --format '{{.HostConfig.Memory}}' engin-ci-app)" = 1073741824 test "$(docker inspect --format '{{.HostConfig.NanoCpus}}' engin-ci-app)" = 2000000000 - name: Verify Streamlit health run: | set -eu attempt=1 while [ "$attempt" -le 30 ]; do if docker exec engin-ci-app python -c \ "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8501/_stcore/health', timeout=3).read()"; then exit 0 fi sleep 2 attempt=$((attempt + 1)) done docker logs engin-ci-app exit 1 - name: Cleanup if: always() run: | docker logs engin-ci-app 2>/dev/null || true docker rm -f engin-ci-app 2>/dev/null || true docker image rm -f engin-console:ci 2>/dev/null || true docker image rm -f engin-console:test 2>/dev/null || true