Skip to content

ci: gate every compile job from one change filter - #648

Merged
TarikGul merged 4 commits into
mainfrom
tg/ci-change-gate
Sep 10, 2026
Merged

ci: gate every compile job from one change filter#648
TarikGul merged 4 commits into
mainfrom
tg/ci-change-gate

Conversation

@TarikGul

@TarikGul TarikGul commented Sep 8, 2026

Copy link
Copy Markdown
Member

Closes #670.

Summary

ci.yml computes every path gate in one place. A changes job publishes sdk_swift and
sdk_kotlin, and each gated job reads the output it cares about. ci-status aggregates
every job in the workflow and counts a job skipped by its filter as a pass, which makes it
the single check worth requiring.

This is the gating spine the host imports build on: adding a host means adding one output
and one consumer, not another workflow with its own trigger.

Required checks: no change needed

Confirmed on the no-direct-main-push ruleset, which requires exactly one check,
CI Status. No per-workflow job is required, so renaming the change filter and removing
the separate Android workflow changes nothing that is enforced. Details in #671.

Why the Android compile gate lives here

In its own workflow with a paths trigger it reported nothing at all on a pull request
touching other areas, so it could never have satisfied a required status check. It also had
no merge_group trigger, so the Kotlin shell went uncompiled on merge queue runs. As a
gated job it reports skipped when it does not apply, and it runs on the merge queue with
everything else.

Its steps are carried over unchanged, including the codegen step the gitignored generated
modules require.

Filter behaviour

Change sdk_swift sdk_kotlin
docs/ only false false
playground/ only false false
ios/ true false
android/ false true
js/container/ true false
rust/crates/truapi-provider/ true false
rust/crates/uniffi-bindgen-cli/ false true
rust/crates/truapi/ true true
rust/crates/truapi-server/ true true
Makefile true true
.github/workflows/ci.yml true true

The Kotlin filter names rust/crates/truapi for the same reason the Swift filter does:
neither binding set is committed, so a protocol change there leaves no android/ diff to
key on. It did not name it before.

Both filters name this workflow, so this pull request exercises both gates.

Known cost

The Kotlin gate now also evaluates on rust/crates/truapi and Makefile changes, and on
merge queue runs, so it adds some merge queue latency. The old filter already covered
rust/crates/truapi-server/**, which most protocol changes touch, so the widening is
smaller than the table suggests.

Verification

  • Eleven filter cases exercised against the extracted gate script, covering every pattern
    branch and both negative cases.
  • actionlint and shellcheck clean on the changed workflow. The findings that remain
    across the workflows are all pre-existing on main, including the intentional
    if: false on the end-to-end job.
  • The folded job's steps compare equal to the previous workflow's job, so behaviour is
    unchanged and only its trigger differs.
  • ci-status needs and its result list agree exactly: no job needed without being
    checked, none checked without being needed.
  • Prettier clean.
  • CI on this pull request is the end-to-end check, since both gates evaluate true here.

Not included

CLAUDE.md on main already fails npm run format:check under the markdown rules added
in #630, which reformatted no existing docs and which no workflow enforces. Fixing that
rewrites around 300 lines, so it belongs in its own change. The addition here is wrapped to
match the surrounding file.

The iOS change filter becomes a general `changes` job publishing one output
per gated area, `sdk_swift` and `sdk_kotlin`, and the Android compile gate
moves out of its own workflow into a job that reads it. That job's steps are
carried over unchanged, codegen and all.

Living in a separate workflow left the Android gate with two problems. Its
`paths` trigger meant it reported nothing at all on a pull request that
touched other areas, so it could never satisfy a required status check; as a
job in this workflow it reports skipped, which `ci-status` already counts as
a pass. It also had no `merge_group` trigger, so the Kotlin shell was never
compiled on a merge queue run.

The Kotlin filter now also names `rust/crates/truapi`, which it did not
before, for the same reason the Swift filter names it: the bindings are
generated rather than committed, so a protocol change there leaves no
`android/` diff to key on.

Both filters name this workflow, so editing a filter exercises the jobs the
filter gates.
Records where the compile gates are computed and which check is worth
requiring, now that a filtered job reports skipped rather than not reporting.
@TarikGul
TarikGul marked this pull request as ready for review September 9, 2026 13:52
@TarikGul
TarikGul requested a review from a team September 9, 2026 13:52
@TarikGul

TarikGul commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@lore-bot-app review

@lore-bot-app

lore-bot-app Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reading the diff and checking what the record says. Back in a few minutes.

@lore-bot-app lore-bot-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR
Folds the standalone ci-android workflow into ci.yml as android-bindings, gated by a shared changes job that emits sdk_swift and sdk_kotlin. 0 blocking, 3 minor.

Summary
The ios-changes job becomes changes and computes both path gates with one gate shell helper. ios-swift reads sdk_swift; a new android-bindings job reads sdk_kotlin and runs the same steps the deleted workflow did. Both feed ci-status, where skipped counts as pass. Two side effects follow from the workflow triggers: the old workflow had no merge_group trigger, so the merge queue never ran the Kotlin compile, and it now does. Because release.yml fires on a successful CI run, Android releases are now gated on this compile as well. CLAUDE.md is updated to match. All of this is from static reading; nothing was executed.

What the record says

  • Issue #671 confirms CI Status is the only required check on main, enforced by the no-direct-main-push ruleset, and that skipped jobs count as passes. Deleting ci-android therefore leaves no dangling required check.
  • PR #551 untracked the generated Rust and iOS outputs and introduced ci-android. That decision is why the filters must name source crates rather than ios/ or android/ diffs.
  • Issue #636 plans multi-host change detection in CI, and issue #677 wants a breaking core change to fail the Android host build in the same PR. The changes job is the natural attachment point for both.
  • Issue #674 settled that CI invokes each host build directly rather than composing Gradle builds, which is consistent with gradle :truapi-host:assembleRelease here.
  • The record has no discussion of the filter path lists themselves. Nothing covers the two omissions below.

Concerns

  1. .github/workflows/ci.yml:239 The sdk_kotlin list omits the root Gradle files. build.gradle.kts pins AGP 8.5.2 and Kotlin 1.9.24, and settings.gradle.kts wires the module paths. A toolchain bump there skips android-bindings. The Swift list names Package.swift$ for exactly this case. Pre-existing in the deleted workflow's paths, but this PR is where the list now lives and the comment claims it is complete. Dependabot only covers github-actions, so these bumps are manual, which makes them rarer but not safer.
  2. .github/workflows/ci.yml:238 The sdk_swift list omits rust/crates/uniffi-bindgen-cli/ while sdk_kotlin includes it. ios-swift runs make xcframework, which depends on make uniffi, which runs cargo run -p uniffi-bindgen-cli. A bindgen change that alters the emitted Swift would skip the compile gate. ios-bindings still generates, but generation passing is not the failure this gate exists to catch.
  3. android/truapi-host/README.md:440 States "No CI job compiles this package." That is false after this change and was already false under ci-android. CLAUDE.md requires docs to reflect the repo, and this PR rewrites the CLAUDE.md paragraph about the same gate.

Questions for the author

  1. android-bindings re-runs npm ci and codegen.sh with a nightly toolchain instead of adding needs: codegen and downloading the codegen-output artifact as ios-bindings and ios-swift do. Is the independence deliberate?
  2. Changes under android/truapi-provider/ trip sdk_kotlin, but the job only builds :truapi-host. make provider-android-check exists in the Makefile. Is compiling the provider module in this job in scope, or tracked elsewhere?

🤖 Reviewed by Lore (Parity knowledge base) · 30 agent turns · 424.9s · knowledge as of 2026-09-09

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
The Swift list did not name the bindings generator. `ios-swift` runs
`make xcframework`, which depends on `uniffi`, which runs
`cargo run -p uniffi-bindgen-cli`, so a generator change that alters the
emitted Swift would have skipped the compile gate.

The Kotlin list did not name the root Gradle files. `build.gradle.kts` pins
AGP and Kotlin and `settings.gradle.kts` wires the module paths, so a
toolchain bump would have skipped `android-bindings`. The Swift list already
named `Package.swift` for exactly this case.

Both were inherited from the workflows this consolidates, but the lists live
here now, so they are fixed here. The comment above them said they name every
crate the bindings come from; it now says what they actually have to name,
which is every input the gated job reads.
@TarikGul

TarikGul commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Both correct, fixed in the latest commit. Verified each chain before changing anything.

The bindgen gap. ios-swift runs make xcframework; xcframework: uniffi; and uniffi:
runs cargo run -p uniffi-bindgen-cli. So the chain is real and a generator change that
altered emitted Swift would have skipped the compile gate. rust/crates/uniffi-bindgen-cli/
added to sdk_swift.

The root Gradle gap. build.gradle.kts pins AGP 8.5.2 and Kotlin 1.9.24,
settings.gradle.kts wires the module paths, and gradle.properties is there too. Added
all three to sdk_kotlin. Your point about the asymmetry with Package.swift$ is exactly
right, that is the same case on the other platform.

Also rewrote the comment above the lists. It claimed they name every crate the bindings are
generated from, which is not the requirement; they have to name every input the gated job
reads. Saying the weaker thing is how both gaps survived.

Re-ran the filter cases, now 13 including the four new ones:

uniffi-bindgen-cli        swift=true   kotlin=true
root build.gradle.kts     swift=false  kotlin=true
root settings.gradle.kts  swift=false  kotlin=true
gradle.properties         swift=false  kotlin=true
docs only                 swift=false  kotlin=false
hosts/ios                 swift=false  kotlin=false

That last one is worth noting: an imported host tree does not trigger the SDK gates, which
is what makes the import pull requests inert.

Seven inputs both gated jobs read were matched by neither filter, so a change
to any of them skipped the gate that exists to catch it:

  Cargo.toml, Cargo.lock   pin the bindings generator and hold the codegen
                           profile, whose whole purpose is keeping the
                           metadata symbols bindgen reads
  truapi-macros            a path dependency of truapi, compiled with the
                           uniffi feature, and it emits type definitions
  truapi-codegen           emits the generated Rust the Kotlin job compiles
  scripts/codegen.sh       the Kotlin job runs it directly
  package.json, lockfile   the Kotlin job runs npm ci against them

Root Cargo.toml is anchored, so a crate's own manifest still matches through
its crate path. The npm entries are on the Kotlin list only: the Swift job
installs js/container's package, which that list already names.

The comment above the lists said they name every crate the bindings are
generated from. That is the weaker claim, and stating it is how these
survived. It now says what the lists have to name.

Folding the Android gate in puts it on the release path, since release.yml
gates on the CI run concluding successfully. So it gets a 30 minute timeout
like the Swift gate: unbounded, a hung Gradle fetch would hold a release for
the six hour default while reporting nothing. It also gets a pinned cache
key, so the rename does not orphan the warm cache and a future rename
cannot either.

The definition-of-done skill still said nothing compiles Kotlin. It does now.
@github-actions github-actions Bot added documentation Improvements or additions to documentation github_actions Pull requests that update GitHub Actions code labels Sep 10, 2026
@TarikGul

Copy link
Copy Markdown
Member Author

Ran three independent adversarial audits over this before merging, since the approving
reviewer is away. They found seven real gaps. All fixed in 5a91c0b, each verified before
changing anything.

The filters were missing seven inputs both gated jobs read. A change to any of them
skipped the gate that exists to catch it:

input why it matters
Cargo.toml, Cargo.lock pin the bindings generator, and hold [profile.codegen] whose whole purpose is keeping the metadata symbols bindgen reads
rust/crates/truapi-macros/ path dependency of truapi, compiled with the uniffi feature, and it emits type definitions
rust/crates/truapi-codegen/ emits the generated Rust the Kotlin job compiles
scripts/codegen.sh the Kotlin job runs it directly
package.json, package-lock.json the Kotlin job runs npm ci against them

The Cargo.toml one is the sharpest: uniffi is a caret range, so cargo update -p uniffi
touches only the lockfile, and a 0.32.x bump that renames a generated symbol would have
landed with both compile gates skipped and this check green.

Root Cargo.toml is anchored so a crate manifest still matches via its crate path. The npm
entries are Kotlin-only, since the Swift job installs js/container's own package.

I also rewrote the comment above the lists. It claimed they name every crate the bindings
are generated from, which is the weaker claim and is how these survived.

The Android gate is now on the release path, because release.yml gates on the CI run
concluding successfully and this job has never run outside a pull request: 0 runs on
push, merge_group and workflow_dispatch against 249 on pull_request. So it now has
timeout-minutes: 30 like the Swift gate. Unbounded, a hung Gradle fetch would have held a
release for the six hour default while reporting nothing, since workflow_run fires on
completed and a false if skips silently.

It also gets shared-key: android-bindings-gate, so the rename does not orphan the warm
Rust cache and a future rename cannot either.

Confirmed clean, independently and with evidence: the folded job is byte-exact against
the deleted workflow, all 10 steps in order with identical pins including the nightly
toolchain, Node setup and codegen step; the only required check is CI Status and nothing
removed or renamed was ever required; ci-status needs and checked results agree exactly at
14 and 14; no stale references anywhere; actionlint findings are identical to main; and
git diff HEAD^1 HEAD under fetch-depth: 2 was proven correct against a synthetic
two-parent merge ref, with root-commit PRs failing closed rather than open.

Two things left as known and tracked rather than fixed here. ci-status counts skipped as
a pass, so it cannot tell a correctly filtered job from one that never ran; the disabled
e2e job already relies on that, and #697 is the fix. And the merge queue forces every gate
true, so path gating saves nothing there.

Also updated the definition-of-done skill, which still claimed nothing compiles Kotlin.

@TarikGul
TarikGul added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 3ef54d0 Sep 10, 2026
20 checks passed
@TarikGul
TarikGul deleted the tg/ci-change-gate branch September 10, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate CI change detection into one gate

2 participants