name: nigig-site # Truthful Site CI. Optional capabilities are visibly skipped until their # dedicated harness/server exists; a release-gate invocation fails instead of # treating missing infrastructure as a pass. on: push: paths: - 'crates/apps/nigig-site/**' - 'crates/nimanyatta/**' - 'crates/nigig-core/**' - 'crates/nigig-uikit/**' - 'crates/matrix_client/**' - 'crates/robius-notification/**' - 'crates/apps/doc/**' - 'crates/apps/pdf/**' - 'crates/apps/nigig_doc_scanner/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' - '.forgejo/workflows/nigig-site.yml' pull_request: paths: - 'crates/apps/nigig-site/**' - 'crates/nimanyatta/**' - 'crates/nigig-core/**' - 'crates/nigig-uikit/**' - 'crates/matrix_client/**' - 'crates/robius-notification/**' - 'crates/apps/doc/**' - 'crates/apps/pdf/**' - 'crates/apps/nigig_doc_scanner/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' - '.forgejo/workflows/nigig-site.yml' workflow_dispatch: inputs: enforce_release_gates: description: 'Fail unless runtime, migration, media, and real-server gates are enabled' required: true type: boolean default: false permissions: contents: read env: CARGO_BUILD_JOBS: '1' CARGO_INCREMENTAL: '0' CARGO_PROFILE_DEV_DEBUG: '0' CARGO_PROFILE_TEST_DEBUG: '0' CARGO_TERM_COLOR: always RUST_BACKTRACE: '1' NIGIG_SITE_CI_TIMEOUT_SECONDS: '3300' jobs: ownership-and-contracts: name: Owned paths and honest test contracts runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Every declared path filter must match repository files run: | set -euo pipefail python3 - <<'PY' from collections import Counter from pathlib import Path import glob, re workflow = Path('.forgejo/workflows/nigig-site.yml').read_text() trigger_block = workflow.split('\npermissions:', 1)[0] filters = re.findall(r"^ - '([^']+)'$", trigger_block, re.MULTILINE) counts = Counter(filters) assert filters, 'workflow path-filter scan matched nothing' assert all(count == 2 for count in counts.values()), ( 'push and pull_request path filters must be identical', counts ) for pattern in sorted(counts): matches = [Path(item) for item in glob.glob(pattern, recursive=True)] files = [item for item in matches if item.is_file()] assert files, f'path filter matches no repository files: {pattern}' print(f'{pattern}: {len(files)} file(s)') PY - name: Prove Site-owned source paths were scanned run: | set -euo pipefail mapfile -d '' files < <(find crates/apps/nigig-site \ -type f \( -name '*.rs' -o -name 'Cargo.toml' -o -name '*.sh' \) \ -print0) if [ "${#files[@]}" -lt 10 ]; then echo "ERROR: Site source scan matched only ${#files[@]} files." >&2 exit 1 fi printf 'matched %d Site-owned source/manifest/tool files\n' "${#files[@]}" test -f crates/apps/nigig-site/src/store.rs test -f crates/apps/nigig-site/tests/sync_e2e.rs test -f crates/apps/nigig-site/tools/ci-cargo.sh test -x crates/apps/nigig-site/tools/check-production-deps.sh test -f .forgejo/workflows/nigig-site.yml - name: SITE-01 production containment is structural and fail-closed run: | set -euo pipefail python3 - <<'PY' from pathlib import Path import re, tomllib root = Path('crates/apps/nigig-site') lib = (root / 'src/lib.rs').read_text() for module in ('doc_export', 'gif', 'ocr', 'report_pdf', 'video'): pattern = rf'#\[cfg\(test\)\]\s+pub mod {module};' assert re.search(pattern, lib), f'{module} must remain test-only' manifest = tomllib.loads((root / 'Cargo.toml').read_text()) features = set(manifest.get('features', {})) assert not features, ( 'feature configuration could bypass SITE-01 containment', features ) production_deps = manifest.get('dependencies', {}) for dependency in ( 'nigig-core', 'nigig-uikit', 'doc-ui', 'reqwest', 'makepad-ai-hub', 'makepad-system-speech', 'robius-location', 'robius-notification', 'nigig-pdf-cos', 'nigig-pdf-document', 'nigig-pdf-graphics', 'nigig-pdf-makepad', 'nigig_doc_scanner', 'doc-engine', 'image', 'weezl', 'zip', 'quick-xml', ): assert dependency not in production_deps, ( f'contained dependency leaked into production: {dependency}' ) for path in (root / 'src/nimanyatta_client.rs', root / 'src/sync.rs'): assert not path.exists(), f'test-only transport/codec leaked into src: {path}' for module in ('nimanyatta_client', 'sync'): assert not re.search(rf'pub mod {module};', lib), ( f'production transport/codec module exported: {module}' ) assert (root / 'tests/support/nimanyatta_fixture.rs').is_file() assert (root / 'tests/support/sync_fixture.rs').is_file() assert 'nigig_uikit::script_mod' not in lib assert 'pub use nigig_uikit::shared::*' not in lib assert 'doc_ui::script_mod' not in lib for module in ('persistence', 'location', 'tile_service'): assert not re.search(rf'pub mod {module}\s*\{{', lib), ( f'unsafe compatibility re-export restored: {module}' ) active = '\n'.join( path.read_text() for path in ( root / 'src/main.rs', root / 'src/scheduler.rs', root / 'src/site_frame/screens/chat.rs', root / 'src/site_frame/screens/meetings.rs', root / 'src/site_frame/screens/more_hub.rs', root / 'src/site_frame/screens/report_editor.rs', root / 'src/site_frame/screens/reports.rs', root / 'src/site_frame/screens/sites.rs', root / 'src/site_frame/screens/workers.rs', ) ) for token in ( 'CameraWidget', 'get_latest_location', 'makepad_system_speech', 'robius_notification::', 'photos_to_gif_file', 'photos_to_clip_file', 'request_send_text', 'demo-site', ): assert token not in active, f'reachable contained capability found: {token}' main = (root / 'src/main.rs').read_text() assert re.search( r'app_shell\s*:=\s*View\s*\{.*?visible:\s*false', main, re.DOTALL, ) assert re.search( r'recovery_page\s*:=\s*View\s*\{.*?visible:\s*true', main, re.DOTALL, ) assert 'schedule_daily_eod' not in main and 'check_and_fire_due' not in main scheduler = (root / 'src/scheduler.rs').read_text() assert 'register_os_schedule' not in scheduler assert 'check_and_fire_due' not in scheduler chat = (root / 'src/site_frame/screens/chat.rs').read_text() assert 'TextInput' not in chat, 'confidential chat input restored' editor = (root / 'src/site_frame/screens/report_editor.rs').read_text() assert 'submit_btn' not in editor, 'unreviewed report submission restored' reports = (root / 'src/site_frame/screens/reports.rs').read_text() assert '.approve(' not in reports and '.reject(' not in reports daily_report = (root / 'src/domain/daily_report.rs').read_text() assert 'refine_with_ai' not in daily_report store = (root / 'src/store.rs').read_text() assert 'pub fn save(' not in store and 'pub fn save_async(' not in store assert 'pub fn load()' not in store assert 'flush_writer_queue' not in store crypto = (root / 'src/crypto.rs').read_text() assert 'set_password' not in crypto and 'load_or_create' not in crypto print('SITE-01 containment contract passed') PY - name: Live E2E must be explicitly ignored, never early-return green run: | set -euo pipefail python3 - <<'PY' from pathlib import Path p = Path('crates/apps/nigig-site/tests/sync_e2e.rs') s = p.read_text() assert '#[ignore = ' in s, 'live test must be an explicit ignored test' assert 'NIMANYATTA_E2E_URL' in s, 'live test must name required config' assert 'fn live_round_trip()' in s, 'live test entry point missing' body = s[s.index('fn live_round_trip()'):] assert 'return;' not in body, 'live test may not early-return as a pass' print('live E2E is explicit: normal CI reports ignored; dedicated CI runs --ignored') PY - name: Third-party actions must be pinned to full commit SHAs run: | set -euo pipefail python3 - <<'PY' from pathlib import Path import re p = Path('.forgejo/workflows/nigig-site.yml') bad = [] for number, line in enumerate(p.read_text().splitlines(), 1): m = re.search(r'\buses:\s*([^\s#]+)', line) if not m: continue ref = m.group(1) if ref.startswith('./'): continue if not re.search(r'@[0-9a-f]{40}$', ref): bad.append((number, ref)) assert not bad, f'unpinned action references: {bad}' print('all third-party action references are immutable full SHAs') PY cargo-gates: name: Cargo ${{ matrix.label }} runs-on: ubuntu-latest timeout-minutes: 65 strategy: fail-fast: false matrix: include: - label: check-all-targets artifact: check command: cargo check --locked -p nigig-site --all-targets - label: production-dependency-containment artifact: production-dependencies command: crates/apps/nigig-site/tools/check-production-deps.sh /tmp/nigig-site-ci/production-tree.txt - label: unit artifact: unit command: cargo test --locked -p nigig-site --lib -- --test-threads=1 - label: integration-non-live artifact: integration command: cargo test --locked -p nigig-site --tests -- --test-threads=1 - label: containment-storage-crypto artifact: containment-storage command: cargo test --locked -p nigig-site --lib containment::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib ai_refine::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib crypto::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib store::tests -- --test-threads=1 - label: contained-media-export-fixtures artifact: contained-media command: cargo test --locked -p nigig-site --lib gif::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib video::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib ocr::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib report_pdf::tests -- --test-threads=1 && cargo test --locked -p nigig-site --lib doc_export::tests -- --test-threads=1 - label: clippy-site-owned artifact: clippy command: cargo clippy --locked -p nigig-site --all-targets --no-deps -- -D warnings steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install pinned toolchain and native packages run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh - name: Run ${{ matrix.label }} with timeout and RSS evidence env: SITE_GATE_COMMAND: ${{ matrix.command }} run: | crates/apps/nigig-site/tools/ci-cargo.sh \ "${{ matrix.artifact }}" bash -lc "$SITE_GATE_COMMAND" - name: Upload full command log and resource evidence if: always() uses: forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4 with: name: nigig-site-${{ matrix.artifact }}-${{ github.sha }} path: /tmp/nigig-site-ci/ if-no-files-found: error retention-days: 14 runtime-ui: name: Runtime UI (explicitly skipped until enabled) if: ${{ vars.NIGIG_SITE_RUNTIME_UI_ENABLED == 'true' }} runs-on: ubuntu-latest timeout-minutes: 65 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh - name: Execute real runtime assertions run: | set -euo pipefail test -f crates/apps/nigig-site/tests/runtime_ui.rs crates/apps/nigig-site/tools/ci-cargo.sh runtime-ui \ cargo test --locked -p nigig-site --test runtime_ui -- --test-threads=1 - if: always() uses: forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4 with: name: nigig-site-runtime-ui-${{ github.sha }} path: /tmp/nigig-site-ci/ if-no-files-found: error retention-days: 14 migration-recovery: name: Migration and recovery (explicitly skipped until enabled) if: ${{ vars.NIGIG_SITE_MIGRATION_GATE_ENABLED == 'true' }} runs-on: ubuntu-latest timeout-minutes: 65 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh - name: Execute recovery and fault corpus run: | set -euo pipefail test -f crates/apps/nigig-site/tests/storage_recovery.rs crates/apps/nigig-site/tools/ci-cargo.sh migration-recovery \ cargo test --locked -p nigig-site --test storage_recovery -- --test-threads=1 - if: always() uses: forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4 with: name: nigig-site-migration-${{ github.sha }} path: /tmp/nigig-site-ci/ if-no-files-found: error retention-days: 14 media-limits: name: Media limits (explicitly skipped until enabled) if: ${{ vars.NIGIG_SITE_MEDIA_LIMITS_ENABLED == 'true' }} runs-on: ubuntu-latest timeout-minutes: 65 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh - name: Execute adversarial media limits run: | set -euo pipefail test -f crates/apps/nigig-site/tests/media_limits.rs crates/apps/nigig-site/tools/ci-cargo.sh media-limits \ cargo test --locked -p nigig-site --test media_limits -- --test-threads=1 - if: always() uses: forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4 with: name: nigig-site-media-limits-${{ github.sha }} path: /tmp/nigig-site-ci/ if-no-files-found: error retention-days: 14 sync-interoperability: name: Real server interoperability (explicitly skipped until enabled) if: ${{ vars.NIGIG_SITE_SYNC_E2E_ENABLED == 'true' }} runs-on: ubuntu-latest timeout-minutes: 65 env: NIMANYATTA_E2E_URL: ${{ secrets.NIMANYATTA_E2E_URL }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh - name: Require pinned server source and execute ignored live test run: | set -euo pipefail test -n "$NIMANYATTA_E2E_URL" test -d crates/nimanyatta/src crates/apps/nigig-site/tools/ci-cargo.sh sync-e2e \ cargo test --locked -p nigig-site --test sync_e2e -- \ --ignored --exact live_round_trip --test-threads=1 - if: always() uses: forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4 with: name: nigig-site-sync-e2e-${{ github.sha }} path: /tmp/nigig-site-ci/ if-no-files-found: error retention-days: 14 security-supply-chain: name: Security and supply-chain baseline runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Lockfile resolves without mutation run: | set -euo pipefail test -f Cargo.lock cargo metadata --locked --format-version 1 >/tmp/nigig-site-metadata.json git diff --exit-code -- Cargo.lock - name: Every live git dependency has a full immutable revision run: | set -euo pipefail python3 - <<'PY' from pathlib import Path import json, re, tomllib failures = [] def walk(value, where): if isinstance(value, dict): if 'git' in value: rev = value.get('rev') if not isinstance(rev, str) or not re.fullmatch(r'[0-9a-f]{40}', rev): failures.append((where, value.get('git'), rev)) for key, child in value.items(): walk(child, f'{where}.{key}') elif isinstance(value, list): for index, child in enumerate(value): walk(child, f'{where}[{index}]') metadata = json.loads(Path('/tmp/nigig-site-metadata.json').read_text()) packages = {package['id']: package for package in metadata['packages']} manifests = {Path('Cargo.toml').resolve()} manifests.update( Path(packages[member]['manifest_path']) for member in metadata['workspace_members'] ) assert manifests, 'live workspace manifest scan matched nothing' for manifest in sorted(manifests): with manifest.open('rb') as fh: data = tomllib.load(fh) walk(data, str(manifest)) assert not failures, f'unpinned git dependencies: {failures}' print(f'checked {len(manifests)} live manifests; all git dependencies use full revs') PY - name: No plaintext sync/store exception may be hidden in workflow shell run: | set -euo pipefail if grep -nE 'cargo (check|test|clippy).*(\|\| true|; true)' \ .forgejo/workflows/nigig-site.yml; then echo 'ERROR: Cargo failure suppression found in Site workflow.' >&2 exit 1 fi echo 'no Cargo failure suppression found' release-capability-gate: name: Release capability gate runs-on: ubuntu-latest timeout-minutes: 10 env: ENFORCE_RELEASE_GATES: ${{ inputs.enforce_release_gates }} RUNTIME_ENABLED: ${{ vars.NIGIG_SITE_RUNTIME_UI_ENABLED }} MIGRATION_ENABLED: ${{ vars.NIGIG_SITE_MIGRATION_GATE_ENABLED }} MEDIA_ENABLED: ${{ vars.NIGIG_SITE_MEDIA_LIMITS_ENABLED }} SYNC_ENABLED: ${{ vars.NIGIG_SITE_SYNC_E2E_ENABLED }} NIMANYATTA_E2E_URL: ${{ secrets.NIMANYATTA_E2E_URL }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Development status or hard release gate run: | set -euo pipefail release=false case "${GITHUB_REF:-}" in refs/tags/*) release=true ;; esac if [ "${ENFORCE_RELEASE_GATES:-false}" = true ]; then release=true; fi if [ "$release" != true ]; then echo 'Development CI capability status:' echo " runtime-ui=${RUNTIME_ENABLED:-false} (disabled jobs are shown as skipped)" echo " migration=${MIGRATION_ENABLED:-false} (disabled jobs are shown as skipped)" echo " media-limits=${MEDIA_ENABLED:-false} (disabled jobs are shown as skipped)" echo " sync-e2e=${SYNC_ENABLED:-false} (disabled jobs are shown as skipped)" exit 0 fi test "${RUNTIME_ENABLED:-false}" = true test "${MIGRATION_ENABLED:-false}" = true test "${MEDIA_ENABLED:-false}" = true test "${SYNC_ENABLED:-false}" = true test -n "$NIMANYATTA_E2E_URL" test -f crates/apps/nigig-site/tests/runtime_ui.rs test -f crates/apps/nigig-site/tests/storage_recovery.rs test -f crates/apps/nigig-site/tests/media_limits.rs test -d crates/nimanyatta/src echo 'all required release capabilities are configured; their jobs still decide pass/fail'