Skip to content

fix(ios): move the published fallback forward and stop it rotting silently - #812

Merged
TarikGul merged 5 commits into
mainfrom
tg/ios-published-fallback-guard
Sep 16, 2026
Merged

TarikGul merged 5 commits into
mainfrom
tg/ios-published-fallback-guard

Conversation

@TarikGul

@TarikGul TarikGul commented Sep 16, 2026

Copy link
Copy Markdown
Member

The default branch fell back to the 0.7.0 xcframework while 0.16.0 was the
newest release. Bindings are generated from whatever commit you are on and
UniFFI checks the pairing at initialization, so anything resolving this package
got apiChecksumMismatch for the whole library.

It went unnoticed because the manifest follow-up exists only on the
workflow_call path. workflow_dispatch declared no manifest_branch and the
step is gated on it, so the manual publish that shipped 0.16.0 skipped it in
silence. The input exists on dispatch now, empty by default, since a dispatch is
also how a pre-release is cut.

And because every job sets TRUAPI_USE_LOCAL_BINARY=1, nothing ever resolved the
published asset, so a stale URL or checksum could not fail anything.
release-guard now compares the version the manifest names against the newest
published release.

This does not decide whether the default branch should carry a fallback at all.

Closes #723.

…ently

The default branch fell back to the 0.7.0 xcframework while 0.16.0 was the
newest release. The Swift bindings are generated from whatever commit you are
on and UniFFI checks the pairing at initialization, so anything resolving this
package paired bindings from this tree with an August binary and got
apiChecksumMismatch for the whole library rather than one degraded callback.

Two things kept that invisible.

The manifest follow-up existed only on the workflow_call path. A dispatch
declared no manifest branch at all, so the manual publish that shipped 0.16.0
skipped it without comment. The input now exists on dispatch too, still empty
by default, because a dispatch is also how a pre-release is cut and the default
branch should not fall back to one.

Every job sets TRUAPI_USE_LOCAL_BINARY=1, so nothing ever resolved the
published asset and a stale URL or checksum could not fail anything.
release-guard now compares the version the manifest names against the newest
published release. It runs unconditionally, because releases move this and pull
requests do not, so there is no path to filter on.

The manifest names 0.16.0. Its checksum is the sha256 of the published asset,
confirmed by hashing the 0.7.0 asset and getting exactly the checksum this file
already carried.
@TarikGul
TarikGul requested a review from a team September 16, 2026 14:37
@github-actions github-actions Bot added the github_actions Pull requests that update GitHub Actions code label Sep 16, 2026
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

CI Status: 18 required jobs green, 14 passed and 4 skipped by path filter.

All job results
job result
android-bindings skipped
changes success
changeset-guard skipped
cli-package skipped
codegen success
e2e skipped
explorer success
ios-bindings success
ios-swift success
licenses success
playground success
release-guard success
rust success
ts-client success
ts-debugger success
ts-host success
wasm-provider success
workflow-lint success

Commit feb0a125 · run log

@TarikGul

Copy link
Copy Markdown
Member Author

@lore-bot-app review

@lore-bot-app

lore-bot-app Bot commented Sep 16, 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.

Evidence gathered. Writing the review now.

TL;DR
Points the SwiftPM fallback at the 0.16.0 xcframework, lets a manual release-ios dispatch open the manifest PR, and adds a CI step that fails when the manifest is not the newest stable iOS release. 1 blocking, 4 minor.

Summary
Package.swift now names the 0.16.0 asset and checksum instead of 0.7.0. release-ios.yml gains a manifest_branch input on workflow_dispatch, empty by default, so a manual publish can open the Package.swift follow-up PR. ci.yml's release-guard job gets an unconditional step that reads the version out of publishedBinaryURL, lists @parity/ios-host@X.Y.Z releases with gh, and fails when the manifest does not name the highest one.

What the record says

  • Issue #723 is the root of this: main's fallback binary can never match bindings generated from HEAD, because UniFFI checks the pairing at initialization. The recorded resolution there is to drop the fallback on main. PR #725 instead kept it for remote consumers and made the staged local xcframework win when present. This PR continues the #725 line.
  • 0.16.0 was cut by #758, a normal release: commit that should have passed manifest_branch: main. Issue #769 records that a later push to main cancelled release-commit CI and silently skipped publishes at that time. That explains why 0.16.0 went out through a dispatch, and #809 has since fixed the cancellation. So the dispatch gap this PR closes was reached through a bug that is already fixed.
  • PR #747 set up the three guards: release-guard and changeset-guard block per PR, while "default branch manifest versus what is published" lives in the daily registry-drift.yml that maintains one issue. This PR puts a check of the second shape into the first slot.
  • Issue #671 records that CI Status is the only required check and that skipped jobs count as passes precisely so unrelated PRs are not stalled.
  • Owners per Lore: pgherveou, decrypto21 (opened #758), TarikGul, valentinfernandez1.

Concerns

  1. Blocking. Every PR and the merge queue go red between an iOS release and its manifest PR merging. .github/workflows/ci.yml:794 runs on pull_request, push, and merge_group with no condition. Once release-ios publishes 0.17.0, main still names 0.16.0, so this step fails on every branch until release/ios-host-0.17.0 merges. That PR needs a host-sdk-team review (release-ios.yml:259) and only gets CI through an explicit dispatch (release-ios.yml:266). During the window nothing can merge, and because release.yml gates on green CI, no other package can publish either. This is the situation #671 was set up to avoid. Options, cheapest first: move the check into registry-drift.yml next to the npm comparison it mirrors, or enable auto-merge on the manifest PR, or fail only on the manifest PR and on pushes to the default branch.

  2. Release branches fail permanently. .github/workflows/ci.yml:816-819 takes the global maximum across all @parity/ios-host releases. release.yml:416 passes the release branch as manifest_branch, so a release cut from release/v0.3.2 (a CI trigger branch) gets a manifest naming its own version, and that branch's CI then fails forever because main's release is higher.

  3. A stable-version dispatch with no manifest branch replays the 0.16.0 incident, louder. .github/workflows/release-ios.yml:42-46 leaves the input optional and nothing ties "empty branch" to "pre-release version". Dispatching 0.17.0 with the default publishes a stable release, opens no PR, and concern 1 fires on every branch until someone hand-edits Package.swift. One guard in the workflow closes it: a version without a pre-release suffix requires manifest_branch, one with a suffix forbids it.

  4. Docs now stale. docs/RELEASE_PROCESS.md:194-195 says a dispatched run "passes no manifest_branch". It can now. CLAUDE.md requires docs to move with the code.

  5. Silent pass past 200 releases. .github/workflows/ci.yml:816 lists 200 releases across six packages plus the CLI. Once 200 releases of other packages postdate the newest iOS one, newest is empty and the step exits 0 with a notice. gh api --paginate on the releases endpoint removes the ceiling.

Not verified here: the 0.16.0 checksum at Package.swift:38. The sandbox blocked the download. The commit message says it was hashed against the asset.

Questions for the author

  • Who consumes main's fallback? A consumer pinning exact("0.16.0") gets the tag's own Package.swift, so main's value never reaches them. A branch: "main" consumer hits #723 again the first time a UniFFI surface changes after 0.16.0, so the error text at ci.yml:826 ("every use of TrUAPIHost fails to initialize" until the URL is updated) promises more than the update delivers. Is the intent to keep the fallback only for trees whose surface still matches the newest release, or is #723's "drop it on main" still the plan and this is a stopgap?
  • Given #809 already fixed the cancellation that pushed 0.16.0 onto the dispatch path, is the per-PR blocking check still worth the freeze window, or does the daily drift issue cover the remaining risk?

🤖 Reviewed by Lore (Parity knowledge base) · 34 agent turns · 244.1s · knowledge as of 2026-09-16

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/release-ios.yml
Checking it in ci.yml deadlocked the repository. That job runs on every pull
request, push and merge queue entry with no condition, so between an iOS
release publishing and its manifest follow-up merging, every branch went red.
The follow-up needs a review and gets CI only through an explicit dispatch, and
release.yml gates on green CI, so nothing could merge and nothing could
publish. Taking the global maximum across releases also broke release branches
permanently, since a branch whose manifest names its own version always sees a
higher one on the default branch.

The check now runs in registry-drift.yml, beside the npm comparison it mirrors.
That workflow is scheduled, checks out the default branch and reports by
opening an issue, so drift is visible without gating a merge, and a release
branch is never compared against the default branch's releases.

release-ios.yml now requires the version and the manifest branch to agree. A
stable release has to name a branch for the follow-up, and a pre-release must
not, since a pre-release exists for app-side testing of an unmerged change and
must never become what the default branch falls back to.
@TarikGul

Copy link
Copy Markdown
Member Author

All three fixed in the latest commit. The first was a deadlock I introduced and
you are right that it is exactly what #671 exists to prevent.

Blocking, the merge window. Moved out of ci.yml entirely and into
registry-drift.yml, beside the npm comparison it mirrors. That workflow is
scheduled, checks out the default branch and reports by opening an issue, so
drift stays visible without gating anything. Nothing in the merge path looks at
it now.

Release branches failing permanently. The same move fixes it. The comparison
only ever runs against the default branch, so a release branch is never measured
against a release the default branch has and it does not.

Stable dispatch with no manifest branch. release-ios.yml now requires the
two to agree, as you suggested: a version with no pre-release suffix must name a
manifest branch, one with a suffix must not. Checked all four combinations.

The Package.swift bump to 0.16.0 stays, since that part was only ever
bookkeeping that had gone unrecorded.

…pm step

The comparison ran inside the step that queries npm, which made that step also
query GitHub. The test harness stubs every external command to fail closed, so
four of its cases broke, and they were right to: the step's contract had
quietly grown.

It is its own step now, writing what it finds to a file the npm step folds into
the same report, so one issue still covers both kinds of drift and the npm step
is byte for byte what it was.

Five cases cover it: a fallback naming the newest release, one naming an older
release, a pre-release never counting as newest, a manifest naming no release
at all, and a recorded finding reaching the report.
@TarikGul

Copy link
Copy Markdown
Member Author

@lore-bot-app review

@lore-bot-app

lore-bot-app Bot commented Sep 16, 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.

Since last review

  • Resolved: the per-PR blocking check is gone from ci.yml; the comparison now lives in registry-drift.yml, which checks out the default branch only, so the merge freeze and the permanent release-branch failure are both gone. The stable-dispatch-without-manifest-branch gap is closed by the new guard step in release-ios.yml. Five script tests cover the new step.
  • Remaining: the docs still describe the old behaviour (concern 4), and the 200-release ceiling on gh release list is unchanged (concern 5, now a missed report rather than a false pass). The 0.16.0 checksum is still unverified here because the sandbox blocks the download. I also could not run the script tests for the same reason.
  • New: the guard rejects pre-release versions on the workflow_call path that release.yml still permits, the drift issue body points iOS drift at an exceptions file that cannot except it, and there are two leftovers from the earlier revision.

TL;DR
Moves the SwiftPM fallback to 0.16.0, makes release-ios refuse a stable dispatch without a manifest branch, and reports a stale fallback through the daily drift issue. 0 blocking, 5 minor.

Summary
Package.swift names the 0.16.0 asset and checksum. release-ios.yml gains manifest_branch on dispatch and a first step that requires a stable version to name a branch and a pre-release not to. registry-drift.yml gains a step that reads the version out of publishedBinaryURL, finds the highest stable @parity/ios-host release, and writes a drift line that the npm step folds into the single daily issue. release-consistency.test.mjs exercises the new step through the same fixture as the npm comparison.

What the record says

  • #723 is the root cause and records "drop the publishedBinaryURL fallback on main entirely" as the resolution. #725 kept the fallback for remote consumers and made a staged local xcframework win. This PR stays on the #725 line and adds monitoring instead of removal.
  • #747 placed "default branch versus what is published" in the daily registry-drift.yml with one maintained issue and an explicit exceptions file. The revised PR now follows that split. #776 is the live instance of that issue, so iOS drift lines will land in an issue whose body was written for npm.
  • 0.16.0 was cut by #758. #769 records the CI cancellation that pushed it onto the dispatch path, fixed since by #809. The record has nothing on whether a pre-release has ever gone through release.yml rather than a dispatch.
  • Owners per Lore: TarikGul, Imod7, filvecchiato.

Concerns

  1. Pre-release through release.yml now fails after the release commit has merged. .github/workflows/release.yml:100 accepts 0.17.0-beta.1 as an iOS version and :416 always passes the head branch as manifest_branch. The new guard at .github/workflows/release-ios.yml:77-82 then exits 1 on the workflow_call path, after the release: commit is already on the branch. docs/RELEASE_PROCESS.md:190-195 says pre-releases are cut by dispatch, so either tighten the release.yml regex to stable-only for iOS or let the guard skip the manifest step for a pre-release instead of failing.

  2. iOS drift has no exception path, and the issue body sends the reader to the wrong file. The exceptions file is consulted only inside the npm loop at .github/workflows/registry-drift.yml:75-89; the iOS line is appended afterwards at :106-110. The body at :144 still says to document exceptions in registry-drift-exceptions.json. Between a release and its manifest PR merging, which needs a host-sdk-team review and a dispatched CI run per release-ios.yml:292-299, the issue will report iOS drift with no way to acknowledge it.

  3. Docs are stale in four places. docs/RELEASE_PROCESS.md:194-195 says a dispatched run "passes no manifest_branch". docs/RELEASE_PROCESS.md:303, README.md:403, and CLAUDE.md:137 describe the drift workflow as npm-only. CLAUDE.md requires docs to move with the code.

  4. Silent miss past 200 releases. .github/workflows/registry-drift.yml:57 lists 200 releases across every package plus the CLI. Once 200 other releases postdate the newest iOS one, newest is empty and the step reports no drift. gh api --paginate repos/$GH_REPO/releases removes the ceiling.

  5. Leftovers from the earlier revision. .github/workflows/ci.yml:786 is a lone added blank line, so ci.yml is in the diff for no reason. scripts/lib/release-consistency.test.mjs:10 has existsSync,} appended out of order; prettier only covers ts, tsx, and md, so nothing in CI will fix it. The comment at .github/workflows/release-ios.yml:37-41 narrates what the dispatch used to do, which CLAUDE.md asks to keep out of code comments.

Questions for the author

  • #723's recorded resolution is to remove the fallback on main. This PR keeps it and adds a daily check. Is the fallback now intended to stay, and if so, who resolves main's Package.swift without a staged xcframework? A consumer pinning exact("0.16.0") never reads it, and a branch: "main" consumer still hits the pairing mismatch the moment a UniFFI surface changes after 0.16.0.
  • Was the 0.16.0 checksum at Package.swift:38 hashed from the release asset? The commit message says the method was validated against 0.7.0. I could not download the asset here to confirm.

🤖 Reviewed by Lore (Parity knowledge base) · 37 agent turns · 200.8s · knowledge as of 2026-09-16 · re-review

Comment thread .github/workflows/release-ios.yml
Comment thread .github/workflows/registry-drift.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
…ease

release.yml accepts a pre-release as an iOS version and always passes the head
branch, so requiring the two to disagree failed the call after the release
commit had already merged. Whether the manifest follows is now a decision the
step makes: a pre-release leaves it alone whichever path asked, and a stable
release still has to name a branch.

The release listing is paginated. A fixed limit of 200 spans every package, so
once that many other releases postdate the newest iOS one the comparison would
have found nothing and reported no drift.

Also drops a stray blank line that put ci.yml in the diff for nothing, restores
the import ordering in the test file, and removes a comment narrating what the
dispatch input used to do.
@TarikGul

Copy link
Copy Markdown
Member Author

All three fixed.

Pre-release through release.yml. You are right, and it was a break I
introduced: release.yml accepts a pre-release as an iOS version and always
passes the head branch, so the guard failed the call after the release commit had
already merged. Rather than tightening the regex, whether the manifest follows is
now a decision the step makes and exposes as an output. A pre-release leaves the
manifest alone whichever path asked for it, and a stable release still has to
name a branch. That removes the failure without letting a pre-release become the
fallback.

The 200 ceiling. Replaced with gh api --paginate, as suggested. The limit
spanned every package, so the failure mode was reporting no drift rather than
erroring.

Leftovers. The stray blank line is gone, so ci.yml is out of the diff
entirely. Import ordering restored. The dispatch comment no longer narrates what
the input used to do.

49 tests still pass.

@filvecchiato filvecchiato left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking good

@filvecchiato
filvecchiato self-requested a review September 16, 2026 18:28
Comment thread .github/workflows/registry-drift.yml
@filvecchiato
filvecchiato self-requested a review September 16, 2026 18:36

@filvecchiato filvecchiato left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

approving to unblock, a small nit

Under set -euo pipefail a grep that matches nothing returns 1, the assignment
inherits it, and the step dies before the emptiness test below it can run. It
died silently too, with no error annotation, and because it runs ahead of the
npm comparison the whole day's drift report went with it.

Covered by a case that stubs a release list with no iOS entry, which fails
against the previous version and passes now.
@TarikGul

Copy link
Copy Markdown
Member Author

Fixed in feb0a12, and your reproduction is exactly right.

set -euo pipefail means a grep matching nothing returns 1, the assignment
inherits it, and the step dies before the [ -n "${newest}" ] below it can run.
Silently, with no annotation, and since this step runs ahead of the npm
comparison the whole day's drift report went with it. Took your fix verbatim.

Covered by a case that stubs a release list with no iOS entry. It fails against
the previous version and passes now, so the guard is exercised rather than just
present. 50 tests.

Worth noting this is the same shape as the earlier finding on this PR: a check
that reads as protective but cannot reach the condition it claims to test.

@TarikGul
TarikGul added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit cb49b78 Sep 16, 2026
29 checks passed
@TarikGul
TarikGul deleted the tg/ios-published-fallback-guard branch September 16, 2026 22:59
johnthecat added a commit that referenced this pull request Sep 17, 2026
The four wire-breaking entries sit under Major Changes in the truapi
changelog while the release stays at 0.17.0.

Regenerate the root changelog over the merge of main, which adds the
signed iOS build (#815), the vendored host refresh (#811) and the
published-fallback fix (#812).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The published binary fallback on main cannot match HEAD bindings

2 participants