Add an update app with a Bundle model and the update endpoint - #1299
Add an update app with a Bundle model and the update endpoint#1299johnpooch wants to merge 2 commits into
Conversation
POST /update/check/ implements the @capgo/capacitor-updater self-hosted protocol: it returns the newest active Bundle for the caller's platform whose minimum native version the installed binary satisfies, or a body with no url key when there is nothing to install. Bundle records the version, platform, checksum, R2 object key, minimum native version and whether it is active; the served url is built from R2_PUBLIC_BASE_URL. Version comparison parses the dotted components so 1.5.10 outranks 1.5.9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMxVHZUhpRht949vkWiDD3
parse_version gated on isdigit() but converted with int(), which disagree for Unicode digit characters, so a version_build of "1.0.²" raised ValueError and 500ed an unauthenticated endpoint. It also compared variable-length tuples, so "1.6" did not satisfy a minimum_native_version of "1.6.0" and a device reporting a two-component version was permanently denied the bundle it can run. Convert only decimal parts and strip trailing zero components so dotted versions of different lengths compare correctly. R2_PUBLIC_BASE_URL defaults to empty, and Bundle.url built a relative "/bundles/..." URL from it, which the plugin cannot download — every client would fail silently. Serve no bundle at all until the setting is configured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMxVHZUhpRht949vkWiDD3
|
Ran Fixed
Three regression tests added, one per fix. Full backend suite: 2345 passed, 8 skipped. Raising rather than fixing: the The stale-bundle guard is I did not change this because the obvious fix ("only serve a bundle strictly newer than what the client runs") would break The narrow version, if you want it: when Standing down: The review flagged that a pure lookup modelled as One thing for #1292 rather than this PR: the Generated by Claude Code |
What this PR does
Adds
service/update/— aBundlemodel andPOST /update/check/, implementing the@capgo/capacitor-updaterself-hosted update protocol so the app has something to pointupdateUrlat. Closes #1262.Bundlerecords the version, platform, checksum, R2 object key, minimum native version and whether it is active.Bundle.url(service/update/models.py:38) is built fromR2_PUBLIC_BASE_URL, added in #1292.BundleManager.latest_forreturns the highest-versioned active bundle for the caller's platform whoseminimum_native_versionthe installed binary satisfies;parse_version(service/update/utils.py:1) compares dotted components so1.5.10outranks1.5.9.The endpoint is a
CreateAPIViewwithAllowAny, matching thelogin/app's shape for a POST that carries no credentials.UpdateCheckSerializer.to_representationdelegates toUpdateCheckResponseSerializer, whose fields are all optional — so{"version", "url", "checksum"}is emitted when there is a bundle to install and theurlkey is simply absent when there is not.Two deviations from the issue, both deliberate:
kind: "up_to_date". The issue specified a body with nourlkey. I read the plugin source (@capgo/capacitor-updater@8.51.15): a 2xx body with neither anerrornor akindkey falls through toif (!jsRes.has("url")) { logger.error("Error no url or wrong format"); ... }inCapacitorUpdaterPlugin.java— i.e. every no-op check would be logged as a failure and end the background task witherror = true.normalizedUpdateResponseKindacceptsup_to_date,blocked,failed, and iOS has the same branch, so{"kind": "up_to_date", "message": ...}is the plugin's own clean "no new version available" path. It still carries nourlkey, so it satisfies the issue's contract as well.CreateAPIViewdefault and the repo's existing shape for POST-that-is-not-really-a-create (login/views.py), and avoids a view body override. Both native implementations accept any 2xx (response.isSuccessful()on Android,statusCode < 200 || statusCode >= 300on iOS), so 201 is safe for the plugin.Wire casing is as the issue predicted and is now pinned by tests:
CamelCaseJSONParserleavesplatform/version_build/version_nameunchanged on the way in, andversion/url/checksum/kind/messagecamelize to themselves on the way out.test_snake_case_request_and_response_keys_survive_camel_case_wiringposts raw JSON bytes and asserts the rendered key set, rather than going throughresponse.data.Schemas and both generated clients are regenerated.
npx tsc -b --noEmitinpackages/webis clean.Review fixes (f39b2a1)
/code-reviewfound three real defects in the first commit, fixed here and covered by a regression test each:parse_versiongated onstr.isdigit()but converted withint(), which disagree for Unicode digit characters — aversion_buildof"1.0.²"raisedValueErrorand 500'd this unauthenticated endpoint.parse_versioncompared variable-length tuples, so(1, 6, 0) <= (1, 6)isFalseand a device reporting a two-component"1.6"was permanently denied a bundle withminimum_native_version: "1.6.0". Trailing zero components are now stripped, matching whatUpdateGate.tsxdoes withcurrentParts[i] ?? 0.R2_PUBLIC_BASE_URLdefaults to"", so unconfigured the endpoint advertised a bundle at a relative/bundles/ios/1.5.10.zipthat no client can download.latest_fornow serves nothing until the setting is configured.One finding is raised in a comment rather than fixed — the stale-bundle guard never matches clients reporting
version_name: "builtin", which lets a fresh store install be downgraded below its own bundled web assets. The obvious fix conflicts with the deliberate rollback path intest_older_bundle_still_offered_to_a_client_ahead_of_it, so it needs your call.Checklist
/review-pragainst this PR in Claude Code and addressed (or responded to) its findingsservice/update/tests.pycovering newest-runnable selection, numeric (not lexical) version ordering, a bundle whose minimum native version the installed binary does not meet, exact-minimum match, a two-component native version against a three-component minimum, a non-decimal version component, platform andactivefiltering, no bundles at all, an unset public base URL, already running the newest bundle, an older bundle still offered to a client ahead of it (the rollback path), rejected and missingplatform, and the wire casing in both response shapes. Full backend suite: 2345 passed, 8 skipped.