Skip to content

Run kuiraDoctor before each release

Outcome: the kuiraDoctor Gradle task runs five preflight checks against your app's configuration and surfaces every misconfiguration that the SDK would otherwise convert into a runtime crash on a user's device. Run it before each release; wire it into CI to gate builds.


What it catches

Check What goes wrong without it (on a user's device)
minSdk floor Manifest merger surfaces minSdkVersion N cannot be smaller than version 30 mid-build — but in the wrong place, deep in the AGP error stack.
Debug-cleartext manifest Localnet indexer connection fails with a generic IOException; user sees an unrecoverable "Loading…" state with no diagnostic.
assetlinks.json reachability + applicationId match Forge fails with RP_ID_MISMATCH / PRF authentication failed / silent biometric dismissal. Causes include: file not hosted, wrong hostname, Jekyll-stripped the .well-known path on GitHub Pages, applicationId not in the targets array.
Compact runtime pin QuickJS contract runtime crashes with Unsupported bytecode version the first time the user calls a circuit.
SDK-bundled-runtime layer The @midnight-ntwrk/compact-runtime bundled inside the SDK doesn't match the version your contract was compiled against — proofs fail or the runtime rejects the bytecode at call time.

The runtime-pin check is also enforced by validateKuiraContractSource at build time independently; kuiraDoctor rolls it into a unified report so the consumer sees one pass/fail surface, not two.


Quick start

Add to your app/build.gradle.kts:

plugins {
    id("io.github.kuiralabs.contract") version "0.1.0-alpha03"
}

kuiraContract {
    source.set("contract/src/managed/<your-contract>")

    // For the assetlinks-reachability check:
    rpId.set("kuiralabs.github.io")   // YOUR passkey hostname

    // Optional: convert FAIL severities into build failures.
    // Default false (warn-only). Set true on CI / release builds.
    // requireDoctorPass.set(true)
}

Then run:

./gradlew :app:kuiraDoctor

You'll see a unified report:

─── kuiraDoctor ─────────────────────────────────────────
✓ PASS  minSdk
       minSdk = 30 (Kuira SDK requires ≥ 30)
✓ PASS  debug-cleartext
       Debug manifest permits cleartext (localnet builds work).
✗ FAIL  assetlinks-reachability
       https://kuiralabs.github.io/.well-known/assetlinks.json returned HTTP 404.
       Common causes:
         1. The file is not yet hosted at this path — see the
            'Bind your app to a passkey domain' recipe.
         2. The file IS in the repo but GitHub Pages stripped it via Jekyll:
            commit an empty .nojekyll at the repo root + redeploy.
         3. The hostname in kuiraContract.rpId is wrong — verify
            https://kuiralabs.github.io/ loads at all.
✓ PASS  compact-runtime-pin
       Contract emits + consumer pins @midnight-ntwrk/compact-runtime 0.16.0.
✓ PASS  sdk-bundled-runtime
       SDK-bundled @midnight-ntwrk/compact-runtime matches the contract (0.16.0).
─────────────────────────────────────────────────────────
4 passed, 0 warning, 1 error, 0 skipped

The task does not fail by default — it logs the report and returns success. Wire requireDoctorPass to true to gate builds on the report (see "Gating CI" below).


Severities

Symbol Severity When
PASS Check ran and passed.
WARN Check ran but couldn't be fully verified (network unreachable, ambiguous config). Doesn't fail the build regardless of requireDoctorPass.
FAIL Check ran and found a real misconfiguration. Fails the build when requireDoctorPass = true.
SKIP Check couldn't run (missing prerequisite, e.g. rpId not set). Doesn't fail the build.

Wiring details

Auto-discovery

kuiraDoctor reads two values from your app/build.gradle.kts via simple regex:

  • applicationId from applicationId = "com.example.app"
  • minSdk from minSdk = 30

If your build script uses computed values or property delegates that the regex doesn't match, the corresponding checks downgrade to SKIP with a log line. Override explicitly via task properties if needed:

tasks.named<com.midnight.kuira.contract.KuiraDoctorTask>("kuiraDoctor") {
    applicationId.set("com.example.app")
    minSdk.set(30)
}

Gating CI

For a CI workflow that blocks merges on kuiraDoctor issues:

.github/workflows/release.yml
- name: Run preflight checks
  run: ./gradlew :app:kuiraDoctor -PkuiraDoctorRequirePass=true

And in app/build.gradle.kts:

kuiraContract {
    // …
    requireDoctorPass.set(
        providers.gradleProperty("kuiraDoctorRequirePass")
            .map { it.toBoolean() }
            .orElse(false)
    )
}

This keeps requireDoctorPass = false (warn-only) for local dev builds and flips it to true only when the CI job explicitly sets the property — devs are never blocked, the release lane is.

When kuiraDoctor doesn't run

The task is not wired into preBuild. Reasoning:

  • The assetlinks check needs network I/O. Adding a network call to every local build is hostile.
  • Most developers run assembleDebug many times per day. Re-running preflight every time is wasteful.

If you want to opt into preBuild-coupling (sacrificing the above for tighter local feedback), add to your app/build.gradle.kts:

tasks.named("preBuild") {
    dependsOn("kuiraDoctor")
}

What it does NOT check

These are out of scope:

  • <queries> declarations for cross-app sigil enrollment.
  • Hilt wiring of PasskeyConfig (Dagger already catches this at build time, just with a verbose error).
  • Signing-cert fingerprint comparison against the hosted assetlinks.json (today it checks the file is reachable + lists your applicationId; doesn't verify the listed SHA-256 matches ./gradlew signingReport).

For these classes today, see the Bind your app to a passkey domain recipe's troubleshooting table.


Troubleshooting

Symptom What it means
Task succeeds but report shows ✗ FAIL rows requireDoctorPass is false (the default). Set it to true to convert FAIL into a build failure.
All checks report — SKIP Plugin extension is unset / not configured. Set source.set(...) and rpId.set(...) to give checks something to verify.
assetlinks-reachability always WARNs with "Could not reach" Network unreachable or https://<rpId>/ doesn't resolve. The check uses 5-second timeouts and treats network errors as WARN, not FAIL, so a CI environment without internet still ships.
minSdk reports SKIP Auto-discovery didn't find minSdk = N in your app/build.gradle.kts. Set tasks.named<KuiraDoctorTask>("kuiraDoctor") { minSdk.set(...) } explicitly.

What's next