nigig-org/.forgejo/workflows/nigig-site.yml

961 lines
45 KiB
YAML

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/apps/nigig-site-core/**'
- '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/apps/nigig-site-core/**'
- '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: 'Enforce exact SITE-02 approval plus all later release capabilities'
required: true
type: boolean
default: false
permissions:
contents: read
# Gitdab run 1293 proved that the patched v4 artifact action's Twirp
# CreateArtifact request times out on this deployment and leaves the runner
# process stuck. The immutable v3-node20 pin below uses the supported legacy
# artifact protocol while retaining a current Node runtime.
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 \
crates/apps/nigig-site-core \
-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-core/src/store.rs
test -f crates/apps/nigig-site-core/src/repository.rs
test -f crates/apps/nigig-site-core/src/lib.rs
test -f crates/apps/nigig-site/SITE_02_SECURITY_REVIEW.md
test -f crates/apps/nigig-site/SITE_02_KEY_LIFECYCLE_DESIGN.md
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 -x crates/apps/nigig-site/tools/runtime-smoke.sh
test -x crates/apps/nigig-site/tools/native-keyring-smoke.sh
test -x crates/apps/nigig-site/tools/macos-native-keyring-smoke.sh
test -x crates/apps/nigig-site/tools/audit-production-deps.py
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')
core = Path('crates/apps/nigig-site-core')
lib = (root / 'src/lib.rs').read_text()
core_lib = (core / 'src/lib.rs').read_text()
for module in ('doc_export', 'gif', 'ocr', 'report_pdf', 'video'):
pattern = (
rf'#\[cfg\(all\(test, target_os = "linux"\)\)\]'
rf'\s+pub mod {module};'
)
assert re.search(pattern, lib), (
f'{module} must remain Linux-CI-only test code'
)
manifest = tomllib.loads((root / 'Cargo.toml').read_text())
core_manifest = tomllib.loads((core / 'Cargo.toml').read_text())
core_production_deps = core_manifest.get('dependencies', {})
assert not core_manifest.get('features'), (
'core feature configuration could bypass SITE-01 containment'
)
assert not core_manifest.get('dev-dependencies'), (
'core must keep broad fixtures out of its dependency graph'
)
assert 'makepad-widgets' not in core_production_deps, (
'core must stay UI-free so contracts test without Makepad'
)
assert 'nigig-site-core' in manifest.get('dependencies', {}), (
'app must consume the core crate, not a parallel copy'
)
features = set(manifest.get('features', {}))
assert not features, (
'feature configuration could bypass SITE-01 containment', features
)
production_deps = manifest.get('dependencies', {})
assert not manifest.get('dev-dependencies'), (
'broad fixture dependencies must not enter every native test target'
)
linux_dev = manifest.get('target', {}).get(
'cfg(target_os = "linux")', {}
).get('dev-dependencies', {})
for dependency in (
'makepad-test', 'nigig-core', 'nigig-pdf-cos',
'nigig-pdf-document', 'nigig-pdf-graphics',
'nigig_doc_scanner', 'image', 'weezl', 'zip', 'reqwest',
):
assert dependency in linux_dev, (
f'Linux-owned fixture dependency missing: {dependency}'
)
for dependency in (
'nigig-core', 'nigig-uikit', 'doc-ui', 'reqwest',
'makepad-ai-hub', 'makepad-system-speech',
'robius-location',
'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}'
)
# Reviewed exception (SITE-17/SITE-31, src/os_notify.rs):
# `robius-notification` holds no credentials and sends nothing
# anywhere — it hands validated local payloads to the already
# permitted OS service. Admission requires all three markers:
# availability probing, titles-and-counts bodies, and explicit
# outcomes with no silent path.
assert 'robius-notification' in production_deps, (
'reviewed notification provider missing from production'
)
notify = (root / 'src/os_notify.rs').read_text()
assert 'is_available' in notify
assert 'notification_body' in notify
assert 'NotifyOutcome' in notify
assert 'delivered_by_os' in notify
for module in (
'aggregates', 'ai_policy', 'assets', 'auth', 'commands',
'chat', 'containment', 'domain', 'e2ee', 'export_policy', 'ids',
'integrations', 'interop', 'journal', 'locales', 'media_bounds',
'net_client', 'nfr', 'ocr_policy',
'reminder_state', 'report_pack', 'site_context', 'store', 'sync_driver',
'sync_protocol', 'workflows',
):
assert f'pub mod {module};' in core_lib, (
f'core contract not compiled: {module}'
)
assert module in lib, (
f'app does not re-export the core contract: {module}'
)
for stale in ('src/store.rs', 'src/repository.rs', 'src/crypto.rs',
'src/aggregates.rs', 'src/site_context.rs', 'src/ids.rs'):
assert not (root / stale).exists(), (
f'parallel copy of a core module in the app crate: {stale}'
)
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',
'Muthaiga Villas',
):
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
assert 'submit_for_approval(' not in reports
# The SITE-21 review tab records decisions only through the
# domain review checks with a bound signature and a change log:
# these markers must be present, so a future edit cannot
# silently swap the reviewed flow for a direct transition.
assert 'check_decide' in reports
assert 'approval_signature' in reports
assert 'change_log' in reports
daily_report = (core / 'src/domain/daily_report.rs').read_text()
assert 'refine_with_ai' not in daily_report
store = (core / '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
crypto = (core / 'src/crypto.rs').read_text()
assert 'set_password' not in crypto and 'load_or_create' not in crypto
# SITE-02 candidate: strict envelope, exact native-key lookup,
# bounded/coalesced persistence, explicit health, and hard locks.
repository = (core / 'src/repository.rs').read_text()
manifest_targets = core_manifest.get('target', {})
assert 'keyring' not in core_production_deps, (
'all-in-one keyring facade restored'
)
assert 'keyring' not in production_deps, (
'all-in-one keyring facade restored in app manifest'
)
for crypto_dep in ('keyring-core', 'zeroize', 'aes-gcm'):
assert crypto_dep in core_production_deps, (
f'fail-closed crypto dependency missing from core: {crypto_dep}'
)
aes_gcm = core_production_deps.get('aes-gcm', {})
assert aes_gcm.get('default-features') is False
assert {'aes', 'alloc', 'zeroize'} <= set(aes_gcm.get('features', []))
for dependency in ('aes', 'ghash', 'polyval'):
configured = core_production_deps.get(dependency, {})
assert 'zeroize' in configured.get('features', []), (
f'crypto backend zeroization feature missing: {dependency}'
)
target_text = (core / 'Cargo.toml').read_text()
for provider in (
'zbus-secret-service-keyring-store',
'apple-native-keyring-store',
'windows-native-keyring-store',
):
assert provider in target_text, f'explicit native provider missing: {provider}'
assert 'windows-sys' in target_text and 'Win32_Storage_FileSystem' in target_text
for token in (
'NIGIG2', 'Payload {', 'aad:', 'MAX_PLAINTEXT_BYTES',
'NonceInvocationLimit', 'AuthenticationFailed', 'cipher_for',
'envelope_matches_independent_aes_gcm_known_answer',
):
assert token in crypto, f'SITE-02 crypto contract missing: {token}'
for token in (
'CredentialPersistence::UntilDelete', 'create_new(true)',
'O_NOFOLLOW', 'try_lock()', 'read_expected_current',
'flush()', 'FlushFailed', 'sync_all()', 'std::fs::rename',
'CanonicalReadbackFailed',
'rollback_publication', 'pending: Option<Pending<T>>',
'pending_depth', 'flush_and_shutdown',
'impl<T> Drop for RepositoryWriter<T>',
'abrupt_process_termination_is_fail_closed_at_every_commit_stage',
'concurrent_writers_serialize_and_exactly_one_stale_commit_fails',
'SchemaVersionProbe', 'open_versioned_json',
'linux_native_provider_real_vault_lifecycle',
'apple_windows_native_provider_real_vault_lifecycle',
'attributes.get("persistence")', 'value == "Local"',
'FILE_ATTRIBUTE_REPARSE_POINT', 'GetFileInformationByHandle',
'nNumberOfLinks', 'windows-reparse-real',
'validate_canonical_permissions', 'reject_symlink_chain',
):
assert token in repository, f'SITE-02 repository contract missing: {token}'
for token in (
'mutate_scoped', 'profile_mutation_fence', 'site_mutation_fence',
'PersistenceHealth', 'accepted_revision', 'durable_revision',
'deny_unknown_fields', 'open_versioned_json::<SiteStore>(STORE_VERSION)',
'OlderVersionMigrationRequired', 'SecurityReviewRequired',
'setup_review_locked', 'migration_review_locked',
):
assert token in store, f'SITE-02 runtime contract missing: {token}'
assert '#[cfg(test)]\n fn migrate_legacy_json' in repository, (
'migration execution harness must remain test-only while review is pending'
)
assert 'pub(crate) mod repository;' in core_lib, (
'core must keep the repository crate-private'
)
assert 'pub mod repository;' not in lib, (
'app must not re-export a public repository surface'
)
assert 'pub mod repository;' not in lib
assert 'impl Drop for SiteStandaloneApp' in main
assert 'persistence_health' in main and 'flush_and_shutdown(5_000)' in main
assert 'SITE-02-SECURITY-REVIEW-REQUIRED' in store
workflow = Path('.forgejo/workflows/nigig-site.yml').read_text()
assert 'cargo metadata --locked --format-version 1 --all-features' in workflow, (
'RustSec production-graph audit must retain conservative feature resolution'
)
# Every confidential screen mutation is explicitly scoped. The only
# broad mutations left are profile-level site create/select actions.
for screen in ('approvals.rs', 'meetings.rs', 'procurement.rs', 'report_editor.rs'):
text = (root / 'src/site_frame/screens' / screen).read_text()
assert 'SiteStore::mutate(' not in text, f'unscoped mutation in {screen}'
assert 'SiteStore::mutate_profile' not in text, f'profile mutation in {screen}'
assert 'SiteStore::mutate_scoped' in text, f'no scoped mutation in {screen}'
sites = (root / 'src/site_frame/screens/sites.rs').read_text()
assert 'SiteStore::mutate_profile' in sites
assert 'pub fn mutate(' not in store
assert 'selected_or_first' not in store
print('SITE-01 containment and SITE-02 hard-lock contracts 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 '#![cfg(target_os = "linux")]' in s, (
'live transport fixture must not enter native provider test builds'
)
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')
workflow = p.read_text()
bad = []
references = []
for number, line in enumerate(workflow.splitlines(), 1):
m = re.search(r'\buses:\s*([^\s#]+)', line)
if not m:
continue
ref = m.group(1)
references.append(ref)
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}'
artifact = (
'forgejo/upload-artifact@'
'97a0fba1372883ab732affbe8f94b823f91727db'
)
assert references.count(artifact) == 8, (
'every evidence upload plus the native transport canary must use '
'the pinned v3-node20 legacy protocol'
)
assert all(
not ref.startswith('forgejo/upload-artifact@') or ref == artifact
for ref in references
), 'mixed or obsolete artifact protocols are forbidden'
native = workflow.split('\n native-platform-contracts:', 1)[1].split(
'\n runtime-ui:', 1
)[0]
assert "github.event_name == 'workflow_dispatch'" in native
assert "github.event_name == 'push'" in native
assert "github.ref == 'refs/heads/main'" in native
compile_index = native.index('Compile the target-native provider')
assert native.index('Verify artifact transport') < compile_index
assert native.index('Verify disposable Apple Keychain') < compile_index
assert '--preflight' in native
assert 'macos-native-keyring-smoke.sh' in native
assert 'if-no-files-found: error' in native
wrapper = Path(
'crates/apps/nigig-site/tools/macos-native-keyring-smoke.sh'
).read_text()
for token in (
'security create-keychain', 'security unlock-keychain',
'security default-keychain -d user -s "$keychain_path"',
'security default-keychain -d user -s "$original_default"',
'security delete-keychain', "trap cleanup EXIT",
'refs/heads/main', 'workflow_dispatch', '--preflight',
'--ignored --exact --test-threads=1',
):
assert token in wrapper, f'macOS keychain isolation contract missing: {token}'
print('all third-party action and native-host references are immutable and fail-closed')
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-core --lib crypto::tests -- --test-threads=1 && cargo test --locked -p nigig-site-core --lib repository::tests -- --test-threads=1 && cargo test --locked -p nigig-site-core --lib store::tests -- --test-threads=1
- label: site02-crypto
artifact: site02-crypto
command: cargo test --locked -p nigig-site-core --lib crypto::tests -- --test-threads=1
- label: site02-repository
artifact: site02-repository
command: cargo test --locked -p nigig-site-core --lib repository::tests -- --test-threads=1
- label: site02-store
artifact: site02-store
command: cargo test --locked -p nigig-site-core --lib store::tests -- --test-threads=1
# Core contracts run without Makepad, so this lane is cheap enough to
# hold every SITE-03..SITE-28 unit suite and its own clippy gate.
- label: core-contracts
artifact: core-contracts
command: cargo test --locked -p nigig-site-core --lib -- --test-threads=1
- label: core-clippy
artifact: core-clippy
command: cargo clippy --locked -p nigig-site-core --all-targets --no-deps -- -D warnings
- 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: Core contract lane must collect a real test count
if: matrix.label == 'core-contracts'
run: |
set -euo pipefail
collected=$(cargo test --locked -p nigig-site-core --lib -- --list 2>/dev/null \
| grep -c ': test$' || true)
echo "core contract tests collected: ${collected}"
if [ "${collected}" -lt 100 ]; then
echo "ERROR: expected at least 100 core contract tests, found ${collected}." >&2
exit 1
fi
- 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@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
with:
name: nigig-site-${{ matrix.artifact }}-${{ github.sha }}
path: /tmp/nigig-site-ci/
if-no-files-found: error
retention-days: 14
native-platform-contracts:
name: SITE-02 native provider/filesystem (${{ matrix.os }})
# Never execute pull-request- or branch-controlled code on privileged host
# runners that can reach a native credential store. Exact main pushes and
# explicitly dispatched revisions still exercise every target vault.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ${{ matrix.os }}
# A clean macOS host-executor run proved checkout plus check/Clippy in
# 20 minutes, then spent the remainder of the former 65-minute bound doing
# single-job test-profile code generation. Keep this bounded, but allow a
# clean native build to reach the contracts, real vault test, and artifact.
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Create native evidence transport canary
shell: bash
run: |
set -euo pipefail
mkdir -p /tmp/nigig-site-native-evidence
printf 'commit=%s\nrunner_os=%s\n' "$GITHUB_SHA" "$RUNNER_OS" > \
/tmp/nigig-site-native-evidence/transport.txt
- name: Verify artifact transport before the expensive native build
uses: forgejo/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
with:
name: nigig-site-native-transport-${{ runner.os }}-${{ github.sha }}
path: /tmp/nigig-site-native-evidence/transport.txt
if-no-files-found: error
retention-days: 14
- name: Verify disposable Apple Keychain before the expensive native build
if: runner.os == 'macOS'
shell: bash
env:
NIGIG_SITE_LIVE_KEYRING_TEST: isolated-ci-native-v1
run: |
set -euo pipefail
crates/apps/nigig-site/tools/macos-native-keyring-smoke.sh \
--preflight 2>&1 | \
tee /tmp/nigig-site-native-evidence/macos-keychain-preflight.log
- name: Install Linux native build and disposable-vault dependencies
if: runner.os == 'Linux'
shell: bash
env:
NIGIG_SITE_INSTALL_NATIVE_KEYRING: '1'
run: crates/apps/nigig-site/tools/ci-setup-ubuntu.sh
- name: Compile the target-native provider and repository
shell: bash
run: |
set -euo pipefail
mkdir -p /tmp/nigig-site-native-evidence
{
rustc --version --verbose
cargo --version --verbose
cargo check --locked -p nigig-site --tests
cargo clippy --locked -p nigig-site --tests --no-deps -- -D warnings
} 2>&1 | tee /tmp/nigig-site-native-evidence/compile.log
- name: Execute target-native crypto/repository/store contracts
shell: bash
run: |
set -euo pipefail
{
cargo test --locked -p nigig-site --lib crypto::tests -- --test-threads=1
cargo test --locked -p nigig-site --lib repository::tests -- --test-threads=1
cargo test --locked -p nigig-site --lib store::tests -- --test-threads=1
} 2>&1 | tee /tmp/nigig-site-native-evidence/contracts.log
- name: Exercise a disposable real Secret Service vault
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
crates/apps/nigig-site/tools/native-keyring-smoke.sh \
/tmp/nigig-site-native-keyring 2>&1 | \
tee /tmp/nigig-site-native-evidence/linux-secret-service.log
test ! -e /tmp/nigig-site-native-keyring
- name: Exercise a disposable Apple native vault
if: runner.os == 'macOS'
shell: bash
env:
NIGIG_SITE_LIVE_KEYRING_TEST: isolated-ci-native-v1
run: |
set -euo pipefail
crates/apps/nigig-site/tools/macos-native-keyring-smoke.sh 2>&1 | \
tee /tmp/nigig-site-native-evidence/native-vault.log
- name: Exercise a disposable Windows native vault
if: runner.os == 'Windows'
shell: bash
env:
NIGIG_SITE_LIVE_KEYRING_TEST: isolated-ci-native-v1
run: |
set -euo pipefail
cargo test --locked -p nigig-site --lib \
repository::tests::apple_windows_native_provider_real_vault_lifecycle -- \
--ignored --exact --test-threads=1 2>&1 | \
tee /tmp/nigig-site-native-evidence/native-vault.log
- name: Upload target-native evidence
if: always()
uses: forgejo/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
with:
name: nigig-site-native-${{ runner.os }}-${{ github.sha }}
path: /tmp/nigig-site-native-evidence/
if-no-files-found: error
retention-days: 14
runtime-ui:
name: SITE-02 desktop runtime and normal shutdown
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: Build the real desktop binary
run: |
crates/apps/nigig-site/tools/ci-cargo.sh runtime-build \
cargo build --locked -p nigig-site --bin nigig-site
- name: Render safe mode, close normally, and prove startup wrote no repository
run: |
set -euo pipefail
crates/apps/nigig-site/tools/runtime-smoke.sh \
target/debug/nigig-site /tmp/nigig-site-ci
- if: always()
uses: forgejo/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
with:
name: nigig-site-runtime-ui-${{ github.sha }}
path: /tmp/nigig-site-ci/
if-no-files-found: error
retention-days: 14
migration-recovery:
name: SITE-02 migration, recovery, and fault corpus
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 strict envelope and locked migration/recovery tests
run: |
set -euo pipefail
crates/apps/nigig-site/tools/ci-cargo.sh migration-recovery bash -lc '
cargo test --locked -p nigig-site --lib crypto::tests -- --test-threads=1
cargo test --locked -p nigig-site --lib repository::tests -- --test-threads=1
cargo test --locked -p nigig-site --lib store::tests -- --test-threads=1
'
- if: always()
uses: forgejo/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
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@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
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@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
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: 35
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lockfile resolves without mutation
run: |
set -euo pipefail
test -f Cargo.lock
# Resolve a conservative all-feature superset as well as every target
# predicate; the Site crate itself has no feature bypass surface.
cargo metadata --locked --format-version 1 --all-features \
>/tmp/nigig-site-metadata.json
git diff --exit-code -- Cargo.lock
- name: Audit the Site production graph against RustSec
run: |
set -euo pipefail
cargo install cargo-audit --version 0.22.2 --locked
set +e
cargo audit --file Cargo.lock --json > /tmp/nigig-site-audit.json
audit_status=$?
set -e
test "$audit_status" -eq 0 || test "$audit_status" -eq 1
test -s /tmp/nigig-site-audit.json
crates/apps/nigig-site/tools/audit-production-deps.py \
/tmp/nigig-site-metadata.json \
/tmp/nigig-site-audit.json \
/tmp/nigig-site-rustsec-report.txt
echo "workspace cargo-audit exit=${audit_status}; scoped report is authoritative for this production graph"
- 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'
- name: SITE-02 review packet and plaintext policy are explicit
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path('crates/apps/nigig-site')
core = Path('crates/apps/nigig-site-core')
review = (root / 'SITE_02_SECURITY_REVIEW.md').read_text()
lifecycle = (root / 'SITE_02_KEY_LIFECYCLE_DESIGN.md').read_text()
assert '**Decision status:** `PROPOSED — NOT APPROVED`' in lifecycle
assert '**Production implementation:** `ABSENT / BLOCKED`' in lifecycle
assert '**Independent cryptography reviewer:** `UNASSIGNED`' in lifecycle
pending = '**Security decision:** `NOT APPROVED`' in review
approved = '**Security decision:** `APPROVED`' in review
assert pending != approved, 'review decision must be exactly pending or approved'
if pending:
assert '**Production activation:** `BLOCKED`' in review
else:
assert '**Production activation:** `APPROVED`' in review
assert '**Independent reviewer:** `UNASSIGNED`' not in review
assert '**Decision status:** `APPROVED`' in lifecycle
assert '**Production implementation:** `IMPLEMENTED / ENABLED`' in lifecycle
assert '**Independent cryptography reviewer:** `UNASSIGNED`' not in lifecycle
for blocker in [f'B{i}' for i in range(1, 13)]:
assert f'| {blocker} |' in review, f'missing review blocker {blocker}'
crypto = (core / 'src/crypto.rs').read_text()
repository = (core / 'src/repository.rs').read_text()
production_repository = repository.split('\n#[cfg(test)]\nmod tests {', 1)[0]
assert len(production_repository) < len(repository), 'test boundary not found'
assert 'const ALG_PLAINTEXT:' not in crypto
assert 'plaintext fallback' in crypto.lower()
assert 'write_all(envelope)' in production_repository
assert 'write_all(&plaintext)' not in production_repository
assert 'set_password' not in production_repository
assert 'set_secret' not in production_repository
assert 'fn create_key' not in production_repository
print('pending review is explicit; production has no plaintext/key-creation path')
PY
- name: Upload RustSec evidence
if: always()
uses: forgejo/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3-node20
with:
name: nigig-site-rustsec-${{ github.sha }}
path: |
/tmp/nigig-site-audit.json
/tmp/nigig-site-rustsec-report.txt
if-no-files-found: warn
retention-days: 14
release-capability-gate:
name: Release capability gate
if: always()
needs:
- ownership-and-contracts
- cargo-gates
- native-platform-contracts
- runtime-ui
- migration-recovery
- media-limits
- sync-interoperability
- security-supply-chain
runs-on: ubuntu-latest
timeout-minutes: 10
env:
OWNERSHIP_RESULT: ${{ needs.ownership-and-contracts.result }}
CARGO_RESULT: ${{ needs.cargo-gates.result }}
NATIVE_PLATFORM_RESULT: ${{ needs.native-platform-contracts.result }}
RUNTIME_RESULT: ${{ needs.runtime-ui.result }}
MIGRATION_RESULT: ${{ needs.migration-recovery.result }}
SECURITY_RESULT: ${{ needs.security-supply-chain.result }}
MEDIA_RESULT: ${{ needs.media-limits.result }}
SYNC_RESULT: ${{ needs.sync-interoperability.result }}
ENFORCE_RELEASE_GATES: ${{ inputs.enforce_release_gates }}
SITE02_APPROVED: ${{ vars.NIGIG_SITE_02_SECURITY_APPROVED }}
SITE02_APPROVED_COMMIT: ${{ vars.NIGIG_SITE_02_APPROVED_COMMIT }}
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
for status in \
"$OWNERSHIP_RESULT" "$CARGO_RESULT" "$NATIVE_PLATFORM_RESULT" \
"$RUNTIME_RESULT" "$MIGRATION_RESULT" "$SECURITY_RESULT"; do
test "$status" = success
done
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=mandatory job'
echo ' migration/recovery=mandatory locked-design job'
echo " site02-security-approved=${SITE02_APPROVED:-false}"
echo " media-limits=${MEDIA_ENABLED:-false} (later tranche; disabled job is skipped)"
echo " sync-e2e=${SYNC_ENABLED:-false} (later tranche; disabled job is skipped)"
exit 0
fi
# Two independent facts are required: a repository variable naming
# the exact reviewed commit, and a signed-off packet in that commit.
test "${SITE02_APPROVED:-false}" = true
test -n "${SITE02_APPROVED_COMMIT:-}"
test "${SITE02_APPROVED_COMMIT}" = "${GITHUB_SHA}"
grep -Fq '**Security decision:** `APPROVED`' \
crates/apps/nigig-site/SITE_02_SECURITY_REVIEW.md
! grep -Fq '**Independent reviewer:** `UNASSIGNED`' \
crates/apps/nigig-site/SITE_02_SECURITY_REVIEW.md
grep -Fq '**Decision status:** `APPROVED`' \
crates/apps/nigig-site/SITE_02_KEY_LIFECYCLE_DESIGN.md
grep -Fq '**Production implementation:** `IMPLEMENTED / ENABLED`' \
crates/apps/nigig-site/SITE_02_KEY_LIFECYCLE_DESIGN.md
! grep -Fq '**Independent cryptography reviewer:** `UNASSIGNED`' \
crates/apps/nigig-site/SITE_02_KEY_LIFECYCLE_DESIGN.md
# Full application release still requires later-tranche capabilities
# to be enabled and to have actually succeeded for this exact run.
test "$MEDIA_RESULT" = success
test "$SYNC_RESULT" = success
test "${MEDIA_ENABLED:-false}" = true
test "${SYNC_ENABLED:-false}" = true
test -n "$NIMANYATTA_E2E_URL"
test -f crates/apps/nigig-site/tests/media_limits.rs
test -d crates/nimanyatta/src
echo 'reviewed commit and all later release capabilities are configured; jobs still decide pass/fail'