name: Managed validation # The gate on protected master. Before v1 this keyed off manifests at the repository root, and this # repository has none — pyproject.toml, package.json and the lock files all live in backend/, # frontend/, node-agent/ and runtime-worker/. The "full" profile therefore completed in nine seconds # having run a whitespace check, a merge-marker scan and py_compile, and no test suite at all, while # reporting success to a branch protection rule that required it. # # It now targets the component roots explicitly, and refuses to report success when a component that # should have run tests ran none. A gate that passes because it found nothing to do is worse than no # gate: it produces the paperwork of validation without the fact of it. on: # Public exports require explicit owner dispatch; fork PRs never reach private runners. workflow_dispatch: inputs: profile: description: Allowlisted validation profile required: true default: full type: choice options: [test, lint, typecheck, build, security, full] permissions: contents: read concurrency: group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }} cancel-in-progress: true jobs: full: name: full runs-on: ubuntu-latest timeout-minutes: 45 env: PROFILE: ${{ inputs.profile || 'full' }} # An explicit interpreter path rather than PATH manipulation: how a runner propagates PATH # between steps varies, and a validation gate should not depend on that detail. VENV: /tmp/modelforge-validation-venv steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Production delivery policy shell: bash run: python3 -m unittest discover -s .gitea/tests -p 'test_*.py' -v - name: Validate the requested profile shell: bash run: | set -euo pipefail case "${PROFILE}" in test|lint|typecheck|build|security|full) ;; *) echo "Profile is not allowlisted: ${PROFILE}" >&2; exit 2 ;; esac echo "profile=${PROFILE}" echo "commit=$(git rev-parse HEAD)" - name: Repository hygiene shell: bash run: | set -euo pipefail git diff --check if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then echo "Unresolved merge markers detected" >&2 exit 1 fi echo "hygiene: clean" - name: Prepare the report directory shell: bash run: mkdir -p reports # Deliberately not using setup-python/setup-node: this runs on a self-hosted runner whose # image already carries both, and the previous workflow depended on that too. What changes is # that a missing toolchain now stops the run instead of quietly reducing what gets validated. - name: Toolchain shell: bash run: | set -euo pipefail command -v python3 >/dev/null || { echo "python3 is not on PATH" >&2; exit 1; } command -v node >/dev/null || { echo "node is not on PATH" >&2; exit 1; } command -v npm >/dev/null || { echo "npm is not on PATH" >&2; exit 1; } echo " python $(python3 --version)" echo " node $(node --version)" echo " npm $(npm --version)" python3 -m venv "${VENV}" "${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet --upgrade pip if [[ "${PROFILE}" == security || "${PROFILE}" == full ]]; then command -v gitleaks >/dev/null || { echo "gitleaks is required for the security profile" >&2 exit 1 } "${VENV}/bin/python" -m pip install \ --disable-pip-version-check --quiet 'pip-audit==2.10.0' fi echo " venv $("${VENV}/bin/python" --version) at ${VENV}" - name: Backend — install, lint, typecheck, test id: backend shell: bash working-directory: backend run: | set -euo pipefail "${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]' if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m ruff check src tests fi if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m mypy src fi if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/backend.xml fi - name: Node Agent — install, lint, typecheck, test id: node_agent shell: bash working-directory: node-agent run: | set -euo pipefail "${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]' if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m ruff check src fi if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m mypy src fi if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/node-agent.xml fi - name: Runtime Worker — install, lint, typecheck, test id: runtime_worker shell: bash working-directory: runtime-worker run: | set -euo pipefail "${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]' if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m ruff check src fi if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m mypy src fi if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then "${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/runtime-worker.xml fi - name: Console — install frozen, typecheck, test, build id: console shell: bash working-directory: frontend run: | set -euo pipefail npm ci if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then npx tsc --noEmit; fi if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then npx vitest run --reporter=junit --outputFile=../reports/frontend.xml fi if [[ "${PROFILE}" == build || "${PROFILE}" == full ]]; then npm run build; fi - name: Security — secrets and vulnerable dependencies if: ${{ env.PROFILE == 'full' || env.PROFILE == 'security' }} shell: bash run: | set -euo pipefail gitleaks git . --no-banner --redact --report-format json \ --report-path reports/gitleaks.json "${VENV}/bin/python" -m pip_audit --strict --skip-editable --desc=on \ --format=json --output=reports/python-audit.json cd frontend npm audit --audit-level=high --omit=dev --json > ../reports/npm-audit.json - name: Compose projections if: ${{ env.PROFILE == 'full' || env.PROFILE == 'build' }} shell: bash run: | set -euo pipefail if ! command -v docker >/dev/null; then echo "SKIPPED: no docker CLI on this runner; compose projections are validated by the" echo "local release gate instead. This step never reports a pass it did not earn." exit 0 fi # Compose interpolation needs values, not live credentials. These fixed validation-only # strings never reach a service and ensure the fail-closed production projection is # actually parsed on every full/build run. export MODELFORGE_POSTGRES_DB=modelforge_validation export MODELFORGE_POSTGRES_ADMIN_PASSWORD=validation-admin-only export MODELFORGE_MIGRATION_DB_PASSWORD=validation-owner-only export MODELFORGE_RUNTIME_DB_PASSWORD=validation-runtime-only export MODELFORGE_MIGRATION_DATABASE_URL=postgresql+psycopg://modelforge:validation-owner-only@postgres:5432/modelforge_validation export MODELFORGE_RUNTIME_DATABASE_URL=postgresql+psycopg://modelforge_runtime:validation-runtime-only@postgres:5432/modelforge_validation export MODELFORGE_OPERATOR_API_KEY=validation-operator-key-32-characters # gitleaks:allow — synthetic Compose interpolation only export MODELFORGE_BACKUP_ENCRYPTION_KEY=dmFsaWRhdGlvbi1vbmx5LWtleS0zMi1ieXRlcw== # gitleaks:allow — base64 of a public validation-only string export MODELFORGE_CORS_ORIGINS=https://modelforge.example.test export VITE_API_BASE_URL=https://modelforge.example.test export MODELFORGE_VERSION=1.2.1 export MODELFORGE_AGENT_CONTROL_PLANE_HOST_ADDRESS=127.0.0.1 export MODELFORGE_AGENT_CA_CERT_PATH=./config/ca.crt failures=0 check() { if docker compose "$@" config -q >/dev/null 2>&1; then echo " OK $*" else echo " FAIL $*"; failures=$((failures + 1)) fi } check -f docker-compose.yml for overlay in backup dr gpu node-agent node-recovery production runtime-worker; do check -f docker-compose.yml -f "docker-compose.${overlay}.yml" done check -f docker-compose.yml -f docker-compose.node-agent.yml \ -f docker-compose.node-agent.private-ca.yml check -f docker-compose.yml -f docker-compose.runtime-worker.yml \ -f docker-compose.runtime-worker.private-ca.yml [[ "${failures}" -eq 0 ]] || { echo "${failures} projection(s) invalid" >&2; exit 1; } - name: Configuration documentation is current if: ${{ env.PROFILE == 'full' }} shell: bash run: | "${VENV}/bin/python" scripts/generate_configuration_docs.py --check # The rule that makes the rest of this meaningful. Every component above declares a manifest, # so every component must have reported a test count. Zero tests where tests were expected is # a failure, not a pass — that is exactly how the previous workflow reported success. - name: Refuse a validation that silently ran no tests if: ${{ env.PROFILE == 'full' || env.PROFILE == 'test' }} shell: bash run: | set -euo pipefail "${VENV}/bin/python" - <<'PY' import sys import xml.etree.ElementTree as ET from pathlib import Path expected = { "backend.xml": "backend", "node-agent.xml": "node-agent", "runtime-worker.xml": "runtime-worker", "frontend.xml": "frontend", } reports = Path("reports") failures = [] total = 0 for filename, component in expected.items(): path = reports / filename if not path.is_file(): failures.append(f"{component}: no test report was produced") continue root = ET.parse(path).getroot() suites = [root] if root.tag == "testsuite" else list(root.iter("testsuite")) tests = sum(int(suite.get("tests", 0)) for suite in suites) errors = sum(int(suite.get("errors", 0)) for suite in suites) failed = sum(int(suite.get("failures", 0)) for suite in suites) skipped = sum(int(suite.get("skipped", 0)) for suite in suites) executed = tests - skipped total += tests print(f" {component:16} {tests:5} tests, {skipped} skipped, " f"{failed} failed, {errors} errors") if executed <= 0: failures.append(f"{component}: {tests} tests collected, {executed} executed") if failed or errors: failures.append(f"{component}: {failed} failed, {errors} errors") print(f" {'TOTAL':16} {total:5} tests") if failures: print("\nManaged validation refused:", file=sys.stderr) for failure in failures: print(f" - {failure}", file=sys.stderr) sys.exit(1) PY - name: Validation summary if: always() shell: bash run: | echo "commit: $(git rev-parse HEAD)" echo "profile: ${PROFILE}" ls -l reports/ 2>/dev/null || echo "no reports directory" # Gitea only lists dispatchable workflow files from the default branch. Reusing the dedicated # workflow from the existing managed entry point lets an unmerged branch prove its exact public # export on the server. Ordinary PR validation and every profile except an explicit `build` # dispatch remain unchanged. public_candidate_acceptance: name: Public candidate server acceptance if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.profile == 'build' }} uses: ./.gitea/workflows/public-candidate-acceptance.yml with: source_commit: ${{ gitea.sha }} public_api_origin: https://modelforge.example.test