Some checks failed
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo production-dependency-containment (push) Has been cancelled
nigig-site / Cargo site02-crypto (push) Has been cancelled
nigig-site / Cargo site02-repository (push) Has been cancelled
nigig-site / Cargo site02-store (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (ubuntu-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (windows-latest) (push) Has been cancelled
nigig-site / SITE-02 desktop runtime and normal shutdown (push) Has been cancelled
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (macos-latest) (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
867 lines
40 KiB
YAML
867 lines
40 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/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: '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 \
|
|
-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/src/repository.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')
|
|
lib = (root / '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())
|
|
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', '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',
|
|
'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
|
|
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
|
|
crypto = (root / '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 = (root / 'src/repository.rs').read_text()
|
|
manifest_targets = manifest.get('target', {})
|
|
assert 'keyring' not in production_deps, 'all-in-one keyring facade restored'
|
|
assert 'keyring-core' in production_deps and 'zeroize' in production_deps
|
|
aes_gcm = 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 = production_deps.get(dependency, {})
|
|
assert 'zeroize' in configured.get('features', []), (
|
|
f'crypto backend zeroization feature missing: {dependency}'
|
|
)
|
|
target_text = (root / '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 lib
|
|
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 --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
|
|
- label: site02-crypto
|
|
artifact: site02-crypto
|
|
command: cargo test --locked -p nigig-site --lib crypto::tests -- --test-threads=1
|
|
- label: site02-repository
|
|
artifact: site02-repository
|
|
command: cargo test --locked -p nigig-site --lib repository::tests -- --test-threads=1
|
|
- label: site02-store
|
|
artifact: site02-store
|
|
command: 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@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')
|
|
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 = (root / 'src/crypto.rs').read_text()
|
|
repository = (root / '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'
|