The dependency audit step has never audited anything. cargo-deny 0.18.6
exits 2 immediately:
error: unexpected argument '--config' found
tip: 'check --config' exists
--config belongs to the `check` subcommand, and its path resolves
relative to the manifest rather than the working directory, so it also
has to be absolute. nigig-build.yml already gets both right; this
invocation predates that fix.
This step is the last one that runs in isolated-payment-tests, so its
failure also skipped the three gates behind it:
- Payment crates must not depend on Makepad
- Domain and storage must not reach the platform SDK
- Mock gateway must not compile into a release build
Verified with cargo-deny 0.18.6 against all three crates:
nigig-pay-domain advisories ok, bans ok, licenses ok, sources ok
nigig-pay-storage advisories ok, bans ok, licenses ok, sources ok
nigig-pay-platform advisories ok, bans ok, licenses ok, sources ok
Found by running the workflow on a real runner for the first time.
359 lines
17 KiB
YAML
359 lines
17 KiB
YAML
name: Payment domain, storage, platform and UI
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'crates/nigig-pay-domain/**'
|
|
- 'crates/nigig-pay-storage/**'
|
|
- 'crates/nigig-pay-platform/**'
|
|
- 'crates/robius-ussd/**'
|
|
- 'crates/apps/nigig-pay-ui/**'
|
|
- 'tools/makepad-native-libs.sh'
|
|
- 'tools/test-rust-clean.sh'
|
|
- 'tools/test-mpesa-store-clean.sh'
|
|
- 'crates/nigig-core/src/persistence/mpesa/**'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.forgejo/workflows/pay-domain.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'crates/nigig-pay-domain/**'
|
|
- 'crates/nigig-pay-storage/**'
|
|
- 'crates/nigig-pay-platform/**'
|
|
- 'crates/robius-ussd/**'
|
|
- 'crates/apps/nigig-pay-ui/**'
|
|
- 'tools/makepad-native-libs.sh'
|
|
- 'tools/test-rust-clean.sh'
|
|
- 'tools/test-mpesa-store-clean.sh'
|
|
- 'crates/nigig-core/src/persistence/mpesa/**'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.forgejo/workflows/pay-domain.yml'
|
|
|
|
jobs:
|
|
isolated-payment-tests:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Each step installs its own throwaway toolchain at the version declared
|
|
# in rust-toolchain.toml, runs tests with --locked against the checked-in
|
|
# lockfile, then checks formatting, Clippy (-D warnings) and benchmarks.
|
|
- name: Domain tests, fmt, clippy and benches
|
|
run: TEST_TARGET=domain ./tools/test-rust-clean.sh
|
|
|
|
# The storage target additionally exercises the opt-in SQLCipher
|
|
# encryption-at-rest path.
|
|
- name: Storage tests, fmt, clippy and SQLCipher
|
|
run: TEST_TARGET=storage ./tools/test-rust-clean.sh
|
|
|
|
# The platform crate is the seam between the domain and Android. This
|
|
# target additionally compiles the optional USSD adapter for the host
|
|
# and asserts that the mock gateway cannot be built into a release.
|
|
- name: Platform gateway tests, fmt, clippy and mock-in-release guard
|
|
run: TEST_TARGET=platform ./tools/test-rust-clean.sh
|
|
|
|
# nigig-core is not a workspace member (it needs git-hosted Makepad and
|
|
# Robius crates), but its parser/classifier/store logic is pure and can
|
|
# be verified standalone. This covers defects B1 and B4.
|
|
- name: M-Pesa parser, classifier and store tests
|
|
run: ./tools/test-mpesa-store-clean.sh
|
|
|
|
- name: Reject whitespace errors
|
|
run: git diff --check
|
|
|
|
# Build governance: lockfiles must be present and current. A lockfile
|
|
# that changes during CI means the committed one was stale.
|
|
- name: Lockfiles must be committed and unchanged
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
test -f "crates/$crate/Cargo.lock" \
|
|
|| { echo "missing crates/$crate/Cargo.lock"; exit 1; }
|
|
done
|
|
git diff --exit-code -- '**/Cargo.lock'
|
|
|
|
# Dependency audit, licence and source checks (review item 1.2).
|
|
# Scoped to the crates whose dependency graph fully resolves standalone.
|
|
- name: Dependency audit, licences, bans and sources
|
|
run: |
|
|
set -e
|
|
version=0.18.6
|
|
url="https://github.com/EmbarkStudios/cargo-deny/releases/download"
|
|
curl -sSLf -o /tmp/cargo-deny.tar.gz \
|
|
"$url/$version/cargo-deny-$version-x86_64-unknown-linux-musl.tar.gz"
|
|
tar xzf /tmp/cargo-deny.tar.gz -C /tmp
|
|
install -m 0755 \
|
|
"/tmp/cargo-deny-$version-x86_64-unknown-linux-musl/cargo-deny" \
|
|
/usr/local/bin/cargo-deny
|
|
# `--config` is a flag on the `check` SUBCOMMAND, not on
|
|
# cargo-deny itself. Putting it before `check` makes 0.18.6
|
|
# exit 2 with "unexpected argument '--config' found" before it
|
|
# audits anything. And the path is resolved relative to the
|
|
# manifest, not the working directory, so it must be absolute
|
|
# -- nigig-build.yml already gets both of these right.
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
echo "== cargo-deny: $crate =="
|
|
cargo-deny --manifest-path "crates/$crate/Cargo.toml" \
|
|
check --config "$PWD/deny.toml"
|
|
done
|
|
|
|
# Boundary enforcement (review item 1.4): the payment domain and storage
|
|
# crates must never depend on the UI framework. This checks real
|
|
# dependencies and imports, not prose in doc comments.
|
|
- name: Payment crates must not depend on Makepad
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
if grep -n -i 'makepad' "crates/$crate/Cargo.toml"; then
|
|
echo "Makepad dependency declared in $crate"
|
|
exit 1
|
|
fi
|
|
if grep -rn --include='*.rs' -E '^[[:space:]]*(use|extern crate)[[:space:]]+makepad' \
|
|
"crates/$crate"; then
|
|
echo "Makepad imported in $crate"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no Makepad dependency in payment domain, storage or platform'
|
|
|
|
# Review item 5.1: JNI and platform SDK calls belong in the platform
|
|
# crate's adapters. The domain and storage crates must stay portable.
|
|
- name: Domain and storage must not reach the platform SDK
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage; do
|
|
if grep -rn --include='*.rs' -E '^[[:space:]]*use[[:space:]]+(jni|robius_ussd|robius_sms|robius_fingerprinting)' \
|
|
"crates/$crate"; then
|
|
echo "platform SDK imported in $crate"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no platform SDK imports in payment domain or storage'
|
|
|
|
# Review item 5.4 / 0.5. The guard is a compile_error!, so the check is
|
|
# that the build genuinely fails. A passing build here means a release
|
|
# artifact could fabricate payment confirmations.
|
|
- name: Mock gateway must not compile into a release build
|
|
run: |
|
|
set -e
|
|
if cargo build --manifest-path crates/nigig-pay-platform/Cargo.toml \
|
|
--locked --release --features mock >/dev/null 2>&1; then
|
|
echo 'the mock gateway compiled into a release build'
|
|
exit 1
|
|
fi
|
|
echo 'mock gateway is correctly refused in release builds'
|
|
|
|
# Review item 7.3: the payment UI crate's tests must run in CI rather than
|
|
# being skipped. They were previously unrunnable here because the Makepad
|
|
# link step needs native libraries; `tools/makepad-native-libs.sh` now
|
|
# installs exactly the set required (including ALSA/PulseAudio/OpenSSL,
|
|
# which are needed to *link* a test binary even though `cargo check`
|
|
# succeeds without them).
|
|
payment-ui-tests:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install Makepad native libraries
|
|
run: ./tools/makepad-native-libs.sh --install
|
|
|
|
- name: Install the declared toolchain
|
|
run: |
|
|
set -e
|
|
version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
rust-toolchain.toml | head -n 1)"
|
|
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init
|
|
chmod 700 /tmp/rustup-init
|
|
/tmp/rustup-init -y --profile minimal --default-toolchain "$version" --no-modify-path
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
# The pure payment crates are gated far more strictly elsewhere in this
|
|
# workflow. Here the goal is that the widget-layer logic — fee
|
|
# breakdowns, CSV parsing, money formatting — is actually exercised.
|
|
- name: Payment UI unit tests
|
|
run: cargo test -p nigig-pay-ui --lib
|
|
|
|
# Review item 6.1 / Phase 6 exit criterion. A settlement tick may only
|
|
# follow a provider confirmation, so the payment status vocabulary is
|
|
# owned by `nigig_pay_domain::FlowStage` and must not be hand-written
|
|
# beside an untrusted signal. This catches the specific regression that
|
|
# shipped before: "✓ SMS received" and "✓ N/N sent".
|
|
- name: Payment status must not claim settlement from untrusted signals
|
|
run: |
|
|
set -e
|
|
sheet=crates/apps/nigig-pay-ui/src/shared_pay_sheet.rs
|
|
if grep -n '✓.*\(SMS\|sent\|Sent\|received\)' "$sheet"; then
|
|
echo 'a settlement tick is being claimed from an untrusted signal'
|
|
exit 1
|
|
fi
|
|
for marker in ObservedEvidence NeedsReconciliation; do
|
|
if grep -n "PayFlowEvent::$marker" -A 6 "$sheet" | grep -q '✓'; then
|
|
echo "PayFlowEvent::$marker renders a settlement tick"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no settlement claims from untrusted signals'
|
|
|
|
# Review items 6.6 / U7. Batch progress must be derived from
|
|
# `PaymentBatch`, which records one outcome per item, rather than from a
|
|
# bare counter incremented at each match arm. The unbounded version let
|
|
# a single item with two terminal USSD events report "3/2".
|
|
- name: Batch progress must not be an unbounded counter
|
|
run: |
|
|
set -e
|
|
handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs
|
|
if grep -n '^[^/]*bulk_done[[:space:]]*+=' "$handler"; then
|
|
echo 'bulk_done is being incremented directly; use settle_bulk_item()'
|
|
exit 1
|
|
fi
|
|
echo 'batch progress is derived, not counted'
|
|
|
|
# Review defect S2, the fail-open biometric. On Android
|
|
# `robius_fingerprinting::authenticate` returns Ok(()) as soon as the
|
|
# prompt is *shown*, not when the user answers. Dispatch must therefore
|
|
# be gated on an AuthorizationAttempt grant, never on that early Ok.
|
|
- name: USSD dispatch must be gated on an authorization grant
|
|
run: |
|
|
set -e
|
|
handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs
|
|
if ! grep -q 'attempt.consume()' "$handler"; then
|
|
echo 'dispatch_ussd no longer consumes an authorization grant'
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'AuthorizationSignal::Succeeded' "$handler"; then
|
|
echo 'no explicit success signal feeds the authorization attempt'
|
|
exit 1
|
|
fi
|
|
echo 'dispatch is gated on an explicit authorization grant'
|
|
|
|
# The demo gate must be reachable by the obvious command. Without a
|
|
# forwarding feature on the app crates, `cargo run -p nigig-pay
|
|
# --features demo` fails with "does not contain this feature", and the
|
|
# only way in is the non-obvious `--features nigig-pay-ui/demo`. That
|
|
# made the intended tracker-only message look like a broken build.
|
|
- name: USSD automation must be on by default and switchable off
|
|
run: |
|
|
set -e
|
|
for app in nigig-pay nigig-mpesa; do
|
|
grep -q 'default = \["demo"\]' "crates/apps/$app/Cargo.toml" \
|
|
|| { echo "$app no longer enables demo by default"; exit 1; }
|
|
done
|
|
grep -q 'default = \["native", "demo"\]' crates/pageflipnav/Cargo.toml \
|
|
|| { echo "the APK crate no longer enables demo by default"; exit 1; }
|
|
# A packaging build must still be able to turn it off. Without
|
|
# `default-features = false` on the inner deps this silently
|
|
# re-enables itself, which is how the first attempt at this leaked.
|
|
for app in nigig-pay nigig-mpesa; do
|
|
grep -q 'nigig-pay-ui = { path = "../nigig-pay-ui", default-features = false }' \
|
|
"crates/apps/$app/Cargo.toml" \
|
|
|| { echo "$app would re-enable demo under --no-default-features"; exit 1; }
|
|
done
|
|
cargo check -p nigig-pay
|
|
cargo check -p nigig-pay --no-default-features
|
|
echo 'automation on by default; packaging opt-out intact'
|
|
|
|
# A gated feature must explain the product boundary, not the binary.
|
|
# "unavailable in this build" was reported as a bug three times: it
|
|
# reads like something is broken and gives the user no next step.
|
|
# Checks displayed strings only, so the explanatory comments stay legal.
|
|
- name: Gated features must not report themselves as build errors
|
|
run: |
|
|
set -e
|
|
if grep -rn --include='*.rs' 'set_status(.*unavailable in this build' crates/; then
|
|
echo 'a user-facing string describes the build instead of the product'
|
|
exit 1
|
|
fi
|
|
echo 'gated features explain the product boundary'
|
|
|
|
# Review item 0.3: remove custom PIN capture from the default product.
|
|
# `form_pin` must not exist unless `demo` is enabled, so a stale UI
|
|
# definition, a hot reload, or a merely-hidden widget cannot populate
|
|
# it. Asserted by compiling a reference to the field and requiring the
|
|
# default build to reject it.
|
|
# USSD automation is now on by default (the app's primary function),
|
|
# so the meaningful assertion is that a *packaging* build can still
|
|
# produce a tracker with no PIN capture at all.
|
|
- name: A packaging build must not contain a PIN field
|
|
run: ./tools/check-no-pin-capture.sh
|
|
|
|
# pageflipnav is the crate that produces the shipped APK. If it does
|
|
# not forward `demo`, the flag is unreachable in a real build: the Pay
|
|
# sheet reports dispatch unavailable and there is no way to turn the
|
|
# *334# automation on. That shipped, and was only found by a user.
|
|
- name: The APK crate must be able to reach the demo flag
|
|
run: |
|
|
set -e
|
|
grep -q 'demo = \["nigig-pay/demo", "nigig-mpesa/demo"\]' \
|
|
crates/pageflipnav/Cargo.toml \
|
|
|| { echo "pageflipnav does not forward the demo feature"; exit 1; }
|
|
echo 'the APK crate can reach the demo flag'
|
|
|
|
# Review items 1.2 / Q2. The three pure payment crates already deny
|
|
# unwrap_used; nigig-pay-ui did not, and the one production `.unwrap()`
|
|
# it held sat on the payment dispatch path. The ratchet is in
|
|
# lib.rs; this makes CI enforce it.
|
|
#
|
|
# Scoped with `--no-deps`: `matrix_client` and `robius-ussd` carry
|
|
# pre-existing warnings that are not this crate's to fix, and failing on
|
|
# them would make the gate someone else's problem and get it disabled.
|
|
- name: Payment UI quality ratchet
|
|
run: cargo clippy -p nigig-pay-ui --all-targets --no-deps -- -D warnings
|
|
|
|
# Review item S7. The exchange client forged `origin`/`referer` for
|
|
# api2.bybit.com and p2p.binance.com — impersonating those exchanges'
|
|
# own web clients against internal endpoints. Both copies of the client
|
|
# are fixed; this stops either regrowing.
|
|
- name: Exchange clients must not impersonate a browser
|
|
run: |
|
|
set -e
|
|
if grep -rn --include='*.rs' 'set_header("origin"\|set_header("referer"' crates/; then
|
|
echo 'an exchange client is forging browser identity headers'
|
|
exit 1
|
|
fi
|
|
if grep -rn --include='*.rs' 'Robrix/1.0 Makepad' crates/ | grep -v '^\S*: *///'; then
|
|
echo 'a request advertises the UI framework in its User-Agent'
|
|
exit 1
|
|
fi
|
|
# S8: TLS verification must never be weakened, and every dial site
|
|
# must go through the host allowlist.
|
|
if grep -rn --include='*.rs' 'set_ignore_ssl_cert(' crates/; then
|
|
echo 'a request disables TLS certificate verification'
|
|
exit 1
|
|
fi
|
|
for f in crates/apps/nigig-pay/src/payments_frame/pages/mpesa/exchange/api.rs \
|
|
crates/apps/nigig-mpesa/src/pages/exchange/api.rs; do
|
|
grep -q 'fn check_transport' "$f" \
|
|
|| { echo "$f lost its transport allowlist"; exit 1; }
|
|
done
|
|
echo 'no browser impersonation; TLS intact; allowlist present'
|
|
|
|
# R2.1 / review items 2.7, 5.3. `robius_ussd::next_event()` is a
|
|
# process-wide queue whose entries carry no session id. Without the
|
|
# correlation gate the pump applies each event to whatever payment is
|
|
# current, so an abandoned payment's queued ResultText settles the one
|
|
# that replaced it.
|
|
- name: USSD events must be correlated before they touch an intent
|
|
run: |
|
|
set -e
|
|
handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs
|
|
grep -q 'h.sessions.admit(' "$handler" \
|
|
|| { echo "the USSD pump no longer admits events through the registry"; exit 1; }
|
|
# rustfmt wraps this call, so match the method rather than a
|
|
# receiver-prefixed literal.
|
|
grep -q '\.begin(operation' "$handler" \
|
|
|| { echo "dispatch no longer claims a session"; exit 1; }
|
|
test "$(grep -c 'h.sessions.close(' "$handler")" -ge 6 \
|
|
|| { echo "a terminal path stopped retiring its session id"; exit 1; }
|
|
echo 'USSD events are correlated, and sessions are retired'
|
|
|
|
# Phase 4's draw_walk fixes live in these two crates. Compiling them is
|
|
# what makes that work verifiable rather than merely written.
|
|
- name: Payment app crates must compile
|
|
run: |
|
|
set -e
|
|
cargo check -p nigig-pay-ui
|
|
cargo check -p nigig-pay
|
|
cargo check -p nigig-mpesa
|