Skip to content

[CI] Close eight merge-gate holes: required loop, path filters, pre-push parity, protoc, release tests, Kotlin smoke - #419

Open
justin13888 wants to merge 14 commits into
chore/merge-v1-head-397from
chore/close-ci-gate-holes-416
Open

[CI] Close eight merge-gate holes: required loop, path filters, pre-push parity, protoc, release tests, Kotlin smoke#419
justin13888 wants to merge 14 commits into
chore/merge-v1-head-397from
chore/close-ci-gate-holes-416

Conversation

@justin13888

@justin13888 justin13888 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Description

Eight holes in the merge gate, closed — the six #416 names, plus two the run found while this
branch was open. Each was a check that existed and could not fail, or a document that described
a check nobody was running.

  1. required could not see a red test job. ci.yml's aggregated status check listed
    rust-test in needs and omitted it from the verification loop, so a failing Rust test
    suite reported through to a mergeable pull request. The job's own comment has said it is
    blocking since S-C59; the aggregate disagreed. Fixed, and the loop is now asserted to
    cover every job in needs.
  2. The Rust path filter skipped three trees check-rust reads. i18n-check and
    i18n-guard run xtask over locales/; build-check-wasm is cargo check -p capsule-wasm;
    build-rust is a workspace build, so capsule-core-ffi is in it. A pull request touching
    only those paths skipped the whole Rust job. locales/**, capsule-wasm/** and
    capsule-core-ffi/** added.
  3. Three jobs installed protoc for a build step that retired with S-D28. No .proto,
    no prost/tonic, no build script that shells out to it — capsule-sdk/build.rs says so
    in its own header. Removed from ci.yml's rust-cross matrix and from both build-ios.yml
    jobs, one of which is a self-hosted macOS runner.
  4. test-web passed with no tests. bun test --pass-with-no-tests turned a discovery
    regression into a green gate. Flag dropped.
  5. Pre-push claimed parity it did not have. CONTRIBUTING.md said "the same checks run on
    pre-push and on every PR in CI" while hk.pkl covered two of check-rust's fourteen
    steps. Four cheap boundary gates added to pre-push; CONTRIBUTING.md rewritten to describe
    the subset honestly and point at the per-toolchain entrypoints for the full gate.
  6. SoftwareSignerSmokeTest did not compile. FfiWorkspace.create and
    createWithHardwareSigner each grew an FfiClientBuild parameter with S-D15; the test
    still passed three arguments to a four-parameter constructor and six to a seven-parameter
    one. Both call sites fixed against the regenerated binding.

Two further gate holes were added to this lane after it started, both the same defect class:

  1. docs-truth did not fire for the manifests it reads. The roadmap check resolves every
    ROADMAP.md row against the root Cargo.toml, settings.gradle.kts,
    capsule-swift/Project.swift, each Package.swift / package.json / pyproject.toml,
    .gitmodules and the legacy-review/ subtrees. The filter listed none of them, so a pull
    request adding a package without touching Markdown skipped the job. Eight globs added.
  2. Nothing built tests under the release profile, so every #[cfg(not(debug_assertions))]
    test is dead — never compiled, never run (ci: no job runs cargo test --release, so every cfg(not(debug_assertions)) test is dead #428). [tasks.test-rust-release] added. It is
    deliberately not wired into test-rust yet: see decision 9.

Validation

Run inside the worktree, on b3c2c754git status clean, HEAD == origin/chore/close-ci-gate-holes-416.

Command Outcome
python3 -c 'import yaml; yaml.safe_load(...)' on ci.yml and build-ios.yml pass — both parse; ci.yml still yields its 13 jobs, build-ios.yml its 2
python3 assertion: required.needs minus changes == the set of needs.*.result in the loop pass — 11 jobs on both sides, no unverified job and no verified non-dependency
python3 -c 'import tomllib; tomllib.load(open("mise.toml","rb"))' pass — tasks.test-web.run == "bun test"
hk validate pass — hk.pkl is valid
python3 + PyYAML dump of the docs-truth filter pass — 21 globs, the eight new manifest paths present and each resolving to a real path in the tree
cargo nextest run -p capsule-i18n --cargo-profile release fail, pre-existing and deliberately surfaced — 20 tests run, 19 passed, 1 failed: format::tests::an_unrenderable_icu_construct_is_refused_in_debug_builds. Mechanism and fix in #429; this is exactly the dead-test class #428 describes, and it is why the new task is unwired
mise run architecture-check pass, exit 0 — "Rust architecture boundaries are intact" (re-run after the last two commits)
cd capsule-web && bun install --frozen-lockfile failed on the original base f433d918; passes after merging the current base (99dd4bc8, "build(web): regenerate bun.lock…") into this branch — 385 installs, no changes. Not caused, and no longer outstanding
mise run test-web pass — 59 tests across 8 files, 209 assertions
cd capsule-web && bun test src/generated exit 1 (no suites discovered); the same invocation with --pass-with-no-tests exits 0. This is the regression the flag was hiding
mise run gen-bindings pass — create(root, passphrase, tier, client: FfiClientBuild) at capsule_core.kt:2002, createWithHardwareSigner(…, client: FfiClientBuild) at :2021; both match the edited call sites
mise run check-commits pass — no errors in 13 commits
mise run format-check-kotlin / lint-check-kotlin unavailableJAVA_HOME is not set and no 'java' command could be found; there is no java pin in [tools], and capsule-core-kotlin/build.gradle.kts applies com.android.library (compileSdk 36), which needs an Android SDK at configuration time. build-android.yml in CI is the authority for item 6
mise run check-rust pass, exit 0 — all 14 steps, including the four now on pre-push (i18n-check, i18n-guard, architecture-check, license-check) and openapi-check-kynos
mise run test-rust pass, exit 0 — 1703 + 734 + 160 tests across the three nextest partitions, 0 failures
mise run check-docs-truth pass, exit 0 — 473 cross-links, 84 endpoint citations, 119 module paths (re-run after the last two commits). The roadmap check the new filter globs serve arrives with PR #422; the globs are harmless before it and correct after
mise run check-md pass, exit 0 — 168 files, 0 issues (re-run after the last two commits)
mise run check-web pass, exit 0 — format, lint, the 59 tests, and the bundle
hk run pre-push --plan --from-ref chore/merge-v1-head-397 --to-ref HEAD pass — on this branch's own files the plan selects check-commits, architecture-check, format-check-kotlin, lint-check-kotlin, lint-check-md, check-docs-truth; i18n-check, i18n-guard and license-check correctly skip
hk run pre-push --plan --all --glob <g> for five representative pushes pass — see the table below
hk run pre-push --from-ref origin/master --to-ref HEAD -S i18n-check -S i18n-guard -S architecture-check -S license-check pass, exit 0 — 692 files selected; all four new steps green through hk itself, not just through mise

Glob selection proof for the four new pre-push steps (hk run pre-push --plan --all --glob <pattern>, which simulates a push whose changed files are exactly that set):

Simulated push Steps selected
locales/** i18n-check, i18n-guard, architecture-check, lint-check-md, check-docs-truth
capsule-web/src/routes/** i18n-guard, architecture-check, the three web steps, check-docs-truthi18n-check correctly skips
capsule-android/src/androidMain/res/** i18n-check (13 generated strings.xml), check-docs-truth — nothing Rust
Cargo.lock license-check only
capsule-core/src/** the three Rust steps + architecture-check — both i18n gates correctly skip

The third row is why decision 7 below departs from the plan's coarser glob: a hand-edit to a
generated strings.xml is precisely the drift i18n-check exists to catch, and
capsule-i18n/** alone would have skipped the step on it.

CI is fully green on the final head 1518c082 — run 33580398701, all 16 jobs success,
including required, whose log reads All required jobs passed or were skipped. The
per-item evidence below is drawn from that run plus the earlier c8157c16 run
(33577788190) and the Android runs 33577788151 / 33580398714:

  • Rust (tests) pass — item 1's newly blocking job is green, so making it blocking does not
    block this branch.

  • Rust (fmt + clippy + build), Web, Docs, Docs truth, Markdown, Kotlin (ktlint + detekt), Vision and Commit lint all pass. Web passing is the lockfile point below.

  • All four Rust cross legs (android, linux-arm64, windows, apple) run without any
    protoc install step
    — item 3 verified on real runners.

  • Item 6 has a clean before/after against this PR's own base. On f433d918 the Android run
    33574327356 fails at :core:compileDebugUnitTestKotlin with exactly
    SoftwareSignerSmokeTest.kt:49:82 No value passed for parameter 'client' and
    :66:17 No value passed for parameter 'client'. On c8157c16 (run 33577788151) and again on 1518c082 (run 33580398714),
    :core:compileDebugUnitTestKotlin, :core:testDebugUnitTest and :core:testReleaseUnitTest
    all clean. ./gradlew :core:test -Pcapsule.wireFfi compiles and passes, which is the issue's
    own success criterion for this item.

  • Build & test Capsule.app against the Rust core (build-ios.yml) pass — the workflow whose
    two protoc steps this change deletes builds the FFI xcframework and the app without them.

  • Swift (… iPhone UI sweep) passed on 1518c082 after failing on c8157c16 with no
    Swift-affecting change between the two commits — which settles the classification as flaky.
    The earlier failure was
    MockBridgeTests.swift:245: Caught error: BridgeTestTimeout(). Mechanism:
    waitForSubscriber (MockBridgeTests.swift:319-324) spins up to 1000 Task.yield()s waiting
    for an actor registration and throws if the cooperative pool has not scheduled it by then —
    a load-dependent scheduling race with no wall-clock deadline. This branch changes no Swift, no
    capsule-swift/**, and nothing in that job's steps; the same job passed on both base commits
    (runs 33574327583 and 33576515009). Worth its own issue, not filed here to avoid
    duplicating a lane that may already own it.

  • required failed on c8157c16 and passed on 1518c082 — the failing run is itself a
    demonstration of item 1's mechanism working: the loop printed
    A required job did not pass (result: failure) off a job result it verifies, and stopped
    being mergeable. The gate is no longer decorative.

The Android workflow is still red, and not because of this change. Its only FAILED task is
:android:compileDebugKotlinAndroid, with all 48 errors in capsule-android/** — files this
branch does not touch (Unresolved reference 'di' / 'initKoin' / 'ListViewModel' / 'DetailViewModel' / 'museumObject': #389's app half, over Kotlin-Multiplatform-template
leftovers). It is red on origin/master too. build-android.yml is not in CI / required, and
this change deliberately does not add it — see "Will not" in the record.

No gate was substituted for another. Every failure above is classified inline.

Review repair — six findings closed on b3c2c754

Four commits on top of 1518c082, one concern each. Every mechanism was reproduced against
the tree before the edit.

# Finding Fix Evidence
M1 required had changes in needs and in neither check. If dorny/paths-filter or its checkout dies, its outputs are empty, all eleven gates evaluate false and report skipped, if: always() runs the loop, and the aggregate exits 0 having executed zero toolchain gates. My comment claiming "every job in needs is verified" was false for exactly that job. 8736edb9 — assert needs.changes.result == 'success' before the loop. Strict equality, not "not failure", because changes carries no if: (verified: changes.if is None, unlike commit-lint's github.event_name == 'pull_request'), so a skip there is only ever the symptom. Comment rewritten to say which rule covers which job. YAML parses; 13 jobs
M2 The rust filter had locales/** but none of the three generated i18n targets. i18n-check runs in exactly one task (mise.toml:89, inside check-rust) which exactly one job invokes, so the filter is the whole of its CI reachability — leaving hk.pkl's pre-push step catching a generated-catalog edit that CI would skip, the inverse of the intended relationship. 9dd69f98 — added capsule-web/src/i18n/messages/**, capsule-swift/Generated/**, capsule-android/src/androidMain/res/values*/strings.xml. Chose the filter addition over a separate output + job, as suggested. git ls-files -- ':(glob)…' (git's :(glob) has picomatch's no-slash-crossing *) matches 13 / 2 / 13 tracked files respectively. For capsule-android/…/values-ja/strings.xml: the hk plan selects i18n-check, and the same path is matched by the rust filter — both sides now fire
L3 hk.pkl's check-docs-truth says it "Mirrors the docs-truth paths filter in ci.yml"; the eight manifest globs went into ci.yml only. 278fc157 — all eight mirrored into the hk step; comment notes the mirroring claim is load-bearing. Asserted programmatically: the eight-glob set is a subset of both lists, and hk-not-in-ci is empty
L4 "pre-push runs every toolchain's format/lint checks" was false — lint-check-ffi is in check-rust and in no hook. b3c2c754 — reworded, and lint-check-ffi named in the CI-only list. grep -c lint-check-ffi hk.pkl → 0
L5 The entrypoint list offered five commands as what CI runs; ten CI jobs have a local equivalent. test-rust — which this PR made blocking, and which is not part of check-rust — was missing, along with check-docs-truth, check-vision, check-md. b3c2c754 — all ten listed as a block with what each covers; rust-cross called out as the one job with no local entrypoint. Each of the ten resolves in mise tasks; mise run lint-check-md exit 0
L6 The docs-truth comment described the roadmap check in the present tense; neither it nor ROADMAP.md exists at this SHA. 278fc157 — comment is forward-looking and cites #422. ls ROADMAP.md → absent; grep -c roadmap capsule-docs/scripts/docs-truth.mjs → 0

Validation commands for the repair, as requested:

Command Outcome
python3 -c 'import yaml; yaml.safe_load(...)' on ci.yml and build-ios.yml pass — 13 and 2 jobs
actionlint unavailable — not on PATH and not a [tools] pin; the PyYAML parse plus the required-loop parity assertion stand in, and neither is claimed as equivalent
hk validate pass — hk.pkl is valid
hk run pre-push --plan --all --glob 'capsule-android/src/androidMain/res/values-ja/strings.xml' pass — selects check-commits, i18n-check, check-docs-truth; all eighteen other steps report "no files matched filters"
mise run lint-check-md pass, exit 0 — 168 files, 0 issues
mise run check-commits pass — no errors in 13 commits
CI on b3c2c754 pass — run 33583780821, all 16 jobs success, required included. Its log shows the new guard evaluating on real data (if [ "success" != "success" ]) before the eleven-gate loop, then All required jobs passed or were skipped.

One self-caught defect: the L4/L5 commit message initially contained a stray non-ASCII
character. Amended before the push, while the commit was still unpublished — no published
history was rewritten.

Risks and rollout

CI configuration and one test source file. No persisted data, no public interface, no
migration and nothing to observe at runtime — the backout for each item is named in
"Decisions taken" below.

The real risk is item 1 doing its job: rust-test now blocks. If the Rust test suite is red
on a branch, that branch stops being mergeable, which is the intent but is a behaviour change
for anyone who had learned to ignore the job.

Item 2 makes a locale-only or wasm-only pull request pay the full Rust gate. That is the point for
rust and rust-test, and it has a side effect worth naming: rust, rust-test and rust-cross
all key off the same changes.outputs.rust, so a locale-only pull request now also runs the
four-leg rust-cross matrix, which i18n-check does not need. Giving rust-cross its own,
narrower filter is the obvious follow-up; it is a filter-topology change rather than one of the six
holes, so it is left out of this change rather than smuggled in. Cost today is roughly ten runner-
minutes on catalog-only pull requests.

One correction to make before merging. While this branch was in flight the base branch
gained 99dd4bc8 ("build(web): regenerate bun.lock for the package.json the v1 head changed"),
which fixes a stale capsule-web/bun.lock that was breaking bun install --frozen-lockfile in
the web and docs jobs. Not knowing that fix had landed, this lane filed #427 for the
same defect. #427 is redundant and should be closed — it was verified fixed by merging the
current base into this branch, after which bun install --frozen-lockfile reports "385 installs
across 438 packages (no changes)". This lane's write permissions do not extend to closing
issues, so it is flagged here rather than closed.

That base merge (6343dd93) is the only content on this branch that is not one of the six
items; it touches capsule-web/bun.lock only, and it is there so the branch's own CI run
reflects the state this will actually merge into.

Related Issues

Refs #416
Refs #428 (the release-profile gate hole; the task lands here, the wiring waits on #429)

Filed by this lane: #429 (blocks #428's wiring — see decision 9) and #427
(redundant, already fixed on the base — see "Risks and rollout"; it should be closed).

This also lands the harness half of #389: capsule-core-kotlin's two stale FfiWorkspace
call sites are the only Kotlin ones in the tree (grep -rn 'FfiWorkspace\.create' --include='*.kt'
finds no others outside target/), so #389's remaining scope is whatever the Android app itself
needs, and #389 stays open.

Decisions taken

Issue 416 - ci: close the gate holes — test job outside required, missing path filters, pre-push parity, and the Kotlin smoke test
Plan:     B/rev1 (against f433d918, the head of PR #418)
Branch:   chore/close-ci-gate-holes-416
Base:     chore/merge-v1-head-397 (head of PR #418); the PR targets that branch
Worktree: /var/mnt/scratch/golem/dev/Capsulsaurus/Capsule.worktrees/Capsule-chore-close-ci-gate-holes-416
Cause:    -
Touches:  .github/workflows/{ci,build-ios}.yml, mise.toml ([tasks.test-web] only), hk.pkl (pre-push steps only), CONTRIBUTING.md (:25-28), capsule-core-kotlin/src/test/kotlin/com/justin13888/capsule/hardware/SoftwareSignerSmokeTest.kt
Will not: change the Android app's own FfiWorkspace.create call sites (#389's app half); add openapi-check-kynos to pre-push; add build-android.yml to required; touch SLICES.md or ROADMAP.md
Lane:     parallel (stacked on #418). Forecast collision: lane #400 deletes ci.yml's capsule-wire path-filter line adjacent to the three lines this lane adds; whichever merges second resolves a one-line adjacent hunk.
Settled:  Base branch = head of PR #418 (run decision, recorded in #418's body)

Decisions taken.

1. Deliverable boundary — all six items in one change.
   Taken:    All six; item 6 (Kotlin smoke test) is verified in CI by build-android.yml rather than locally, and locally by grepping the regenerated binding for the four-parameter create() after mise run gen-bindings.
   Rejected: Five now, the Kotlin smoke fix filed separately because this host cannot run it - java is absent, ANDROID_HOME is unset and build.gradle.kts:4 applies com.android.library (compileSdk 36); but the fix is a three-line mirror of SoftwareP256SignerSmokeTest.kt:27,65 in the same directory, and build-android.yml is red on master right now (run 33572237154), so filing it leaves a lane red for a change nobody would review differently.
   Reverses: git revert the SoftwareSignerSmokeTest.kt hunk and reopen the item as its own issue.
   Filed:    -

2. Pre-push parity — four gates added, not five, and CONTRIBUTING reworded.
   Taken:    Add i18n-check, i18n-guard, architecture-check, license-check to hk.pkl pre-push and rewrite CONTRIBUTING.md:25-28 to say pre-push runs format/lint/tests plus the cheap boundary gates while the full gate is CI. The three xtask gates share one binary (mise.toml:191,:199,:180); license-check is cargo deny with cargo-deny already a [tools] pin (mise.toml:33). Seconds each on a tree clippy has just warmed.
   Rejected: Add all five - openapi-check-kynos is cargo run -p capsule-server --bin gen_openapi (mise.toml:222), a codegen link of the server and the whole Kynos tree; clippy leaves .rmeta, not linkable rlibs, so it is a fresh multi-minute build on every push. A hook people disable is worse than one honest about its scope.
   Reverses: add ["openapi-check-kynos"] { glob = List("capsule-server/**", "capsule-sdk/**"); check = "mise run openapi-check-kynos"; depends = "lint-check-rust" } to hk.pkl.

3. Scope of the protoc removal.
   Taken:    Remove all three steps - ci.yml:243-249 plus build-ios.yml:56-60 and :126-130. No .proto, no prost/tonic, no protoc-invoking build script exists; capsule-sdk/build.rs records the gRPC stub is gone.
   Rejected: ci.yml only - the same dead comment sits verbatim in build-ios.yml and keeps a macOS runner installing a toolchain for a step that retired with S-D28.
   Reverses: restore the two build-ios.yml step blocks.

4. How test-web stops passing vacuously.
   Taken:    Drop --pass-with-no-tests from mise.toml:355 - eight suites exist under capsule-web/src/**, so bare bun test is green today and red the moment discovery breaks.
   Rejected: Keep the flag and add a discovered-file-count assertion - a new wrapper plus a hardcoded count that goes stale, the same silent drift the issue is closing.
   Reverses: restore the flag at mise.toml:355.

5. (Orchestrator re-plan, appended after lane #398 reported) The `docs-truth` path filter in .github/workflows/ci.yml lists the docs tree, `capsule-server/openapi.json` and `capsule-*/src/**` but not the manifests the new `roadmap` check (PR #422) resolves rows against — `Cargo.toml`, `settings.gradle.kts`, `capsule-swift/Project.swift`, `**/Package.swift`, `**/package.json`, `**/pyproject.toml`, `.gitmodules`, `legacy-review/*` — so a PR that adds a package without touching Markdown never fires the check.
   Taken:    Add those globs to the `docs-truth` filter in this lane (same defect class as item 2 — a gate that does not fire for the files it checks; same file already in the manifest).
   Rejected: Filing it for a later lane - the edit is six lines in a file this lane already owns, and leaving it means ROADMAP.md's gate is advisory for exactly the change it exists to catch.
   Reverses: delete the six filter lines.

6. (Orchestrator re-plan, appended after lane #414 reported and filed #428) No CI job or mise task runs tests under the release profile, so every `#[cfg(not(debug_assertions))]` test (three in capsule-i18n after #420, pinning release-build behaviour of the formatter's refusal path) is dead in CI.
   Taken:    Add a narrow task `test-rust-release` = `cargo nextest run -p capsule-i18n --cargo-profile release` to mise.toml and append it to `[tasks.test-rust]`'s run list (the crate that carries cfg(not(debug_assertions)) tests today; widen the -p list when another crate adds one). mise.toml is already in this lane's manifest.
   Rejected: Running the whole workspace under release in CI - doubles the compile cost of the test job for three tests. Rejected: leaving #428 for a later lane - the gate hole is the same class as items 1 and 2 of this issue.
   Reverses: delete the task and its entry in test-rust.

Decisions taken inside the manifest during delivery, in the same shape:

7. Glob precision for the four new pre-push steps.
   Taken:    Each glob is the exact set of files its task reads, derived from the source rather
             than from the crate name. i18n-check gets locales/** plus every generated target it
             compares against — capsule-i18n/src/**, capsule-web/src/i18n/messages/**,
             capsule-swift/Generated/**, capsule-android/src/androidMain/res/values*/strings.xml
             (xtask/src/i18n.rs:120,:129,:133,:137,:791) — plus the generator itself. i18n-guard
             gets locales/** (the catalog and the allowlist at locales/i18n-guard-allowlist.txt)
             plus the four scan surfaces at xtask/src/i18n_guard.rs:126,:135,:143,:151.
             architecture-check gets the nine extensions is_architecture_text accepts
             (xtask/src/architecture.rs:383-390).
   Rejected: The plan's coarser globs — "locales/**, capsule-i18n/**, xtask/**" for i18n-check
             and "capsule-web/**/*.{ts,tsx}, …/**/*.swift, …/**/*.kt" for i18n-guard. The first
             is unsound in the direction that matters: a hand-edit to a generated
             capsule-android strings.xml or a capsule-swift .xcstrings is exactly the drift
             i18n-check exists to catch, and that glob would skip the step on it.
   Reverses: replace the six-entry i18n-check List with List("locales/**", "capsule-i18n/**",
             "xtask/**") and the six-entry i18n-guard List with the plan's four.

8. No `depends` on the four new pre-push steps.
   Taken:    They run unchained. hk.pkl's own pre-commit note says steps are chained only when
             their globs are identical, so `depends` never skip-mismatches; none of these four
             globs matches the Rust steps' "**/*.rs". Cargo's target-directory lock serialises
             whatever actually overlaps, so concurrency costs latency, never correctness.
   Rejected: depends = "lint-check-rust" (the plan's shape for the openapi reverse-clause), which
             would make four steps' ordering depend on a step that is skipped whenever the push
             carries no .rs file — the exact skip-mismatch the file warns against.
   Reverses: add depends = "lint-check-rust" to the i18n-check step and chain the other three
             behind it.

9. `test-rust-release` ships unwired, against the letter of decision 6.
   Taken:    Add `[tasks.test-rust-release]` as decision 6 specifies, but do NOT append it to
             `[tasks.test-rust]`'s run list. Decision 6 assumed the release run would pass;
             it does not, and the reason is a latent defect the run itself exposed.
             `capsule-i18n/src/format.rs:74` refuses an unrenderable ICU construct with
             `debug_assert!` — deliberately, so a release build gains no new crash. The test
             pinning it, `format.rs:144`
             `an_unrenderable_icu_construct_is_refused_in_debug_builds`, is `#[should_panic]`
             and is *named* for debug builds but carries no `#[cfg(debug_assertions)]`. Under
             `[profile.release]` (no `debug-assertions = true` in the root Cargo.toml) the
             assertion compiles out, the expected panic never happens, and the test fails:
             `20 tests run: 19 passed, 1 failed`. Wiring the task in as written would have made
             `mise run test-rust` — and pre-push, and the `rust-test` job this same PR just made
             blocking — red on every branch in the repository.
   Rejected: Add the one-line `#[cfg(debug_assertions)]` and wire it in. That is the correct
             fix and it is one attribute, but `capsule-i18n/src/format.rs` is outside this
             lane's manifest and is being edited concurrently by #414 / PR #420, which is adding
             `cfg(not(debug_assertions))` tests to that same module. Hard gate 1 says stop
             rather than widen. Rejected: `-E 'not test(...)'` to filter the failing test out —
             a hardcoded exclusion that goes stale is the same masking as the
             `--pass-with-no-tests` this PR removes four commits earlier.
   Reverses: apply #429's one-line fix, then append `"mise run test-rust-release"` to
             `[tasks.test-rust]`'s run list and delete the "NOT yet wired" paragraph above the
             task.
   Filed:    #429 — "i18n: an_unrenderable_icu_construct_is_refused_in_debug_builds needs
             #[cfg(debug_assertions)] before the release-profile gate can be wired in".

10. Where CONTRIBUTING points for "the full gate".
   Taken:    The per-toolchain entrypoints — mise run check-rust, check-web, check-docs,
             check-kotlin, check-swift — because those are what the CI jobs invoke.
   Rejected: `mise run check`, which the plan's record implies. It is
             format-check + lint-check + test (mise.toml:47-63), and lint-check does not include
             i18n-check, openapi-check-kynos, architecture-check, license-check,
             translate-readme-check, the build-* steps, gen-bindings or verify-examples. Naming
             it as "the full gate" would replace one false parity claim with another.
   Reverses: swap the entrypoint list in CONTRIBUTING.md for `mise run check`.

11. `test-rust-release` ships unwired (answer 1a).
   Taken:    Accept unwired; #429 carries the wiring, which becomes possible once PR #420's
             `#[cfg(debug_assertions)]` guard on `format.rs:144` is on the same head — that
             guard already exists on #420's branch.
   Rejected: Wiring here with the one-line guard (widens into a file #414/PR #420 edits
             concurrently — the manifest gate); holding the task out entirely (loses the
             runnable task and its comment, which is what lets #429 be a one-line change).
   Reverses: append `"mise run test-rust-release"` to `[tasks.test-rust]` once #420 is in the
             base.

Unresolved review notes

Contributor Checklist

  • I agree to the Contributor License Agreement for this and future contributions.
  • My code follows the project's style guidelines according to CONTRIBUTING.md.
  • Tests pass — with the exclusions classified in Validation above.
  • No sensitive info / secrets
  • Docs updated if needed — CONTRIBUTING.md.

`required` is the single aggregated status check branch protection guards, and
it listed `rust-test` in `needs` while its verification loop skipped it — so a
red Rust test job reported through to a mergeable pull request. The job's own
comment has said it is blocking since `S-C59`; the aggregate never agreed.

Add `needs.rust-test.result` to the loop and rewrite the header comment, which
claimed the opposite, to state the rule the loop now enforces: every job in
`needs` is verified.
The `rust` paths filter listed nine crate trees, but `check-rust` reads three
more: `i18n-check` and `i18n-guard` run `xtask` over `locales/`,
`build-check-wasm` runs `cargo check -p capsule-wasm`, and `build-rust` is a
workspace build, so `capsule-core-ffi` is in it. A pull request touching only
those paths skipped the whole Rust job, and the gates written to catch a
catalog or FFI regression never ran on the change that caused it.

Add `capsule-core-ffi/**`, `capsule-wasm/**` and `locales/**`. The `swift`
filter has carried `locales/**` for the same reason.
Three jobs still installed `protoc` for a `prost-build` step that went with the
`capsule.sync.v1` service in `S-D28` — the workspace has no `.proto`, no
`prost`/`tonic` dependency and no build script that shells out to it, and
`capsule-sdk/build.rs` says so in its own header. The `rust-cross` matrix in
`ci.yml` and both `build-ios.yml` jobs (one of them on a self-hosted macOS
runner) were paying a toolchain install, and a GitHub token, for nothing.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploying capsule with  Cloudflare Pages  Cloudflare Pages

Latest commit: b3c2c75
Status: ✅  Deploy successful!
Preview URL: https://e8f8a4c5.capsule-22k.pages.dev
Branch Preview URL: https://chore-close-ci-gate-holes-41.capsule-22k.pages.dev

View logs

`test-web` ran `bun test --pass-with-no-tests`, so any change that stopped bun
finding the suites — a moved directory, a changed default glob, a wrong `dir` —
reported a green gate over zero assertions. Eight suites live under
`capsule-web/src/**` and 59 tests pass today, so the flag protects nothing that
exists and hides the one failure it was masking.

Verified both ways: bare `bun test` over a directory with no suites exits 1,
the same invocation with `--pass-with-no-tests` exits 0.
…overclaiming

`CONTRIBUTING.md` said "the same checks run on `pre-push` and on every PR in CI",
while `hk.pkl`'s pre-push covered two of `check-rust`'s fourteen steps. Eleven
gates — among them the i18n catalog sync, the hardcoded-string guard, the
dependency-boundary check and the licence allowlist — were reachable only by
pushing and waiting for CI.

Add the four that are seconds on a tree `lint-check-rust` has just warmed:
`i18n-check`, `i18n-guard` and `architecture-check` share one `xtask` binary,
and `license-check` is `cargo deny` with `cargo-deny` already a `[tools]` pin.
Each glob is the set of files the task actually reads, so a push that touches
none of them pays nothing.

`openapi-check-kynos` stays out: it is `cargo run -p capsule-server --bin
gen_openapi`, a codegen link of the server and the whole Kynos tree, and clippy
leaves `.rmeta` rather than linkable rlibs — so it is a fresh multi-minute build
on every push. A hook people disable is worse than one honest about its scope,
which is what `CONTRIBUTING.md` now is: it names the subset pre-push runs, names
the gates that stay in CI, and points at `mise run check-rust` and its siblings
for the full local gate.
…onger exists

`FfiWorkspace.create` and `createWithHardwareSigner` each grew an `FfiClientBuild`
parameter with `S-D15`, so every manifest a foreign app authors reports that app's
own `client_id/semver+commit`. `SoftwareSignerSmokeTest` was never updated: it
passed three arguments to a four-parameter constructor and six to a seven-parameter
one, which is a Kotlin compile error, so `./gradlew :core:test -Pcapsule.wireFfi`
could not build the test source set at all.

Mirror `SoftwareP256SignerSmokeTest` in the same directory — a `client` member
identifying `capsule-core-kotlin` — and pass it at both call sites. The parameter
lists were read off the regenerated binding (`mise run gen-bindings`,
`target/bindings/kotlin/uniffi/capsule_core/capsule_core.kt:2002,:2021`); the
Gradle/Android toolchain does not run on the reference dev host, so `build-android`
in CI is the authority.
The previous wording said pre-push runs "the test suites", which is the same
shape of overclaim the sentence above it replaced: `hk.pkl`'s pre-push carries
`test-rust` and `test-web`, not `test-kotlin`, `test-swift` or `test-docs`.
Say which two, and put the other three in the list of what CI is left to do.
The `roadmap` check resolves every `ROADMAP.md` row against the manifests that
declare what actually exists — the root `Cargo.toml`, `settings.gradle.kts`,
`capsule-swift/Project.swift`, each `Package.swift`, `package.json` and
`pyproject.toml`, `.gitmodules`, and the `legacy-review/` subtrees. The
`docs-truth` paths filter listed none of them, so a pull request that adds or
removes a package without touching Markdown skipped the job entirely, and the
gate was advisory for precisely the change it exists to catch.

Same defect class as the `rust` filter two commits back: a gate that does not
run on the files it reads.
No task or CI job has ever built the workspace's tests under the release
profile, so every `#[cfg(not(debug_assertions))]` test is dead: never compiled,
never run, and free to rot. `capsule-i18n` carries the only one today
(`format.rs:157`, pinning that a release build passes an unrenderable ICU
construct through rather than tripping the `debug_assert!` a debug build trips),
so `test-rust-release` is scoped to that crate — building the whole workspace a
second time would double the test job for a handful of tests.

Left out of `test-rust`'s run list on purpose, with the reason in the file. The
first release run this task made possible immediately found why it cannot be
wired in yet: `format.rs:144`'s `#[should_panic]` test is named for debug builds
but is not `#[cfg(debug_assertions)]`, so under release the `debug_assert!`
compiles out, the expected panic never happens, and the test fails. That one
attribute is in a file this lane does not own and that #414 is editing
concurrently, so it is filed as #429 rather than reached for here.

Refs #428
@justin13888 justin13888 changed the title [CI] Close the merge-gate holes: required loop, path filters, pre-push parity, protoc, Kotlin smoke [CI] Close eight merge-gate holes: required loop, path filters, pre-push parity, protoc, release tests, Kotlin smoke Sep 2, 2026
`changes` was in `required`'s `needs` and in neither of its checks. Every
filtered job's `if:` reads `needs.changes.outputs.*`, so when that job dies —
a checkout hiccup, a `dorny/paths-filter` error — those outputs are empty, all
eleven gates evaluate false and report `skipped`, and `if: always()` runs the
loop anyway. A loop that only rejects `failure` and `cancelled` then passes,
and `CI / required` certifies a merge having executed zero toolchain gates.

Assert `needs.changes.result == 'success'` before the loop, not merely "not
failure": `changes` carries no `if:`, so unlike `commit-lint` on push events it
can never be legitimately skipped, and a skip there is exactly the symptom.
The header comment claimed every `needs` entry was verified; now it is, and the
comment says which rule covers which.
…hine

`i18n-check` compares `locales/` against four generated trees, and it fails on
drift whichever side moved — a hand-edited generated catalog is precisely what
it exists to catch. It runs in exactly one place, `check-rust`, which exactly
one job invokes, so the `rust` paths filter is the whole of its CI reachability.
That filter gained `locales/**` two commits ago but none of the generated
targets, which left the perverse arrangement of `hk.pkl`'s pre-push step
catching a generated-catalog edit that CI would skip.

Add the web messages, the Swift `.xcstrings` and the Android `strings.xml` set,
so the CI gate is at least as wide as the local one.
…omment

Two loose ends from the `docs-truth` filter widening.

`hk.pkl`'s `check-docs-truth` step says it "Mirrors the `docs-truth` paths
filter in ci.yml", which stopped being true the moment eight manifest globs
went into ci.yml alone — so the same package-addition that now fires the CI job
still slipped past pre-push. Both lists carry the eight.

The ci.yml comment also described the `roadmap` check in the present tense, but
neither that check nor the ROADMAP.md it reads exists at this commit; both
arrive with `#422`. Say so, and say why the globs are correct to land first.
… ten entries

Two remaining overclaims in the paragraph this branch rewrote.

"Every toolchain's format/lint checks" was not true: `lint-check-ffi` runs in
`check-rust` and in no hook, so it is the one lint pre-push never performs. Said
plainly, and named in the list of what CI is left to do.

The entrypoint list offered five commands as "exactly what CI runs" while CI has
ten jobs with a local equivalent. `test-rust` was the glaring omission — this
same branch made that job blocking, and it is not part of `check-rust` — along
with `check-docs-truth`, `check-vision` and `check-md`. Listed as a block with
what each covers, and `rust-cross` called out as the one job with no local
entrypoint rather than silently absent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant