Skip to content

Resolve the native ref from the customer's gem version, not the newest tag #88

Description

@joemasilotti

Today

All three workflows that check out ruby-native/native resolve the ref the same way: sort every v* tag by version, take the top one.

# build-ios.yml, build-android.yml, screenshot-android.yml
TAG=$(git ls-remote --tags --refs --sort='-version:refname' \
  ... 'refs/tags/v*' | head -1 | sed 's|.*refs/tags/||')

Neither build workflow references gem_version at all. So the native code a customer gets is whatever the newest tag happens to be when they click Build, regardless of which gem they run.

Why that is wrong

Tagging a native release is a global switch. Every customer's next build silently moves to it, including customers on an older gem. Per our versioning cadence, patch releases stay gem/native compatible and a minor bump can break. So the day native v0.11.0 lands, a customer still on gem 0.10.x gets binaries expecting signals their gem does not emit, and it fails in their app rather than in CI.

A developer should keep building against the native code they tested against.

Screenshots have the same problem and arguably worse consequences: a capture is a preview of what you are about to ship, so rendering it with native code the customer's build will not use is actively misleading.

Most of the plumbing already exists

  • The gem sends it: gem/lib/ruby_native/cli/deploy.rb posts { gem_version: RubyNative::VERSION }
  • The API stores it: api/v1/builds_controller.rb does build.update!(gem_version: params[:gem_version])
  • The dashboard detects it independently: GemVersionChecker.check(app_url) fetches /native/config.json from the live app, so it works even without the CLI
  • It is persisted: builds.gem_version exists in the schema
  • There is already a compatibility floor: builds_controller.rb blocks deploys from gem < 0.5.0

The value is present at build time and simply never reaches ref resolution.

Proposed rule

Resolve the newest native tag within the customer's gem major.minor, rather than the newest tag overall.

  • gem 0.10.3 resolves the newest v0.10.*, never v0.11.0
  • Patch-level native fixes still reach them automatically, which matches the cadence rule
  • A minor bump becomes opt-in, taking effect when they upgrade the gem

iOS already has this concept client-side: it computes a major.minor gem/app mismatch to decide between "Gem update needed" and "App update needed". This makes the server side agree with it.

Open questions:

  • Behavior when no tag matches the customer's major.minor: fall back to newest, or fail the build with a clear message?
  • Should the resolved ref be recorded on the build so it is visible in the dashboard and in support conversations?

Gaps to close alongside

  • Android builds never record gem_version, so there is nothing to resolve against on that path
  • builds/latest is not platform-scoped, so deploy --if-needed can compare against the wrong platform's build
  • The screenshot workflow needs the same resolution, not just the manual native_ref input and ANDROID_NATIVE_REF override that exist today

Motivation

This already cost real debugging time: a capture run was diagnosed against a tag that predated several fixes sitting on main, because the pipeline floats to the newest tag and nothing surfaces which commit the binary came from. Recording and pinning the resolved ref would have made that obvious immediately.

Tracked as F3 in the Android plan.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions