nigig-org/.forgejo/workflows/pay-domain.yml
nigig-ci bda0124992
Some checks failed
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
ci(pay): pass --config to cargo-deny's check subcommand, not the binary
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.
2026-08-02 09:42:46 +00:00

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