feat(cli): generate platform binary patch artifacts - #150
Draft
floyd-soomgo wants to merge 5 commits into
Draft
Conversation
An absolute --output-path produced an absolute bundle directory, which the './' prefix turned into a path below the current working directory.
`bundle` and `release` accept --binary-bundle-path, the JS bundle of the target binary. With it, `release` publishes two artifacts per platform: the full bundle named after its packageHash, and `<packageHash>-patch.zip`, which carries the target bundle only as a patch against the binary's bundle plus a `codepush-binary-patch.json` manifest. Every other file is copied unchanged, so applying the patch and dropping the two patch-only files reproduces the full contents byte for byte - and therefore the same packageHash. The Hermes compilation is aligned with the base bundle through `-base-bytecode` when the app's own compiler advertises the flag, which is what keeps the patch small. A compiler without the flag only warns; a compilation that fails with it fails the release, since the base is then wrong. Both archive sizes and the saving are printed before anything is uploaded, and the full bundle is uploaded before the patch. A failed upload of either leaves the release history untouched. Carrying the patch URL in the release history is deliberately not part of this change.
The release action read `options.bundleName`, but commander stores the `-j, --js-bundle-name` flag as `options.jsBundleName`, so a custom JS bundle name never reached `release()` and the platform default was always used. Optionality is now expressed in the types instead of asserted away, so "not given" cannot pass for a name again.
The suites that generate real patches each built the tools in their own `beforeAll`, so a suite without that hook - the release flow suite - failed on a machine with no `.hdiffpatch-tools` yet, and two workers could run the same build at the same time. A jest global setup builds them once before any worker starts, which also removes the duplicated helper from the two suites that had one.
A patch is only worth publishing when it is smaller than the archive it replaces, and the CLI runs unattended in CI, so what happens otherwise is decided up front instead of being left to whoever reads the summary. `skip`, the default, warns, records the skip in the summary and releases the full bundle alone. `fail` stops the release before either artifact is uploaded, so the release history stays untouched. Equal sizes count as oversized: a patch that saves nothing still costs a client an extra download and an apply step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second PR of the binary differential OTA series, building on the codec contract from the previous PR (stacked on
feature/binary-patch-codec-contract— merge that first).This PR makes the CLI produce and upload two artifacts per platform release: the existing full archive, plus a patch archive that clients can apply against the Hermes bytecode bundle embedded in the app binary. Everything downstream of artifact generation is intentionally untouched: the patch URL is printed but not yet written into the release history — propagating it through the release history JSON, typings, and the JS runtime is the next PR, and the native appliers come after that. A release without the new option behaves exactly as before.
Usage
--binary-bundle-pathmust point at the exact bundle bytes shipped in the store binary (extracted from the APK/IPA or preserved from the release build). Without it, nothing changes.Changes
cli/functions/makeBinaryPatchBundle.ts— builds the patch archive from the full-archive contents: the target bundle is replaced by<bundlePath>.patch(same relative location) plus a root manifestcodepush-binary-patch.json(formatVersion/algorithmimported from the codec-contract constants,bundlePath,patchFile,baseBundleHash,targetBundleHash,targetBundleSize). Assets and all other files ship unchanged, so a client that applies the patch reconstructs a folder with the samepackageHashas the full archive — pinned by a round-trip test. Archive name:<packageHash>-patch.zip.-base-bytecodealignment — when a base bundle is provided, the target compile passes-base-bytecode <base>so the new bytecode is laid out for minimal diffs (patches shrink ~3-5x in our measurements). Support is detected fromhermesc --help; unsupported compilers get a warning and a plain compile, while a compile failure with the flag fails the release (it usually means the base file is not valid bytecode).bundle --binary-bundle-path— applies the same alignment and recordsbinary-patch-base.json({"baseBundleHash"}) at the output root, deliberately outside the CodePush contents so it never affectspackageHash(tested).release --skip-bundle --binary-bundle-path— patches the archive actually being released (it unpacks the bundle from that archive rather than trusting leftover build contents), errors clearly when no bundle is found, and warns when the base differs from the one recorded at bundle time (the patch stays valid; only the alignment benefit is lost).1 − patch/full) are printed before any upload. If the patch archive is not smaller than the full archive,--on-oversized-patchdecides (non-interactive, CI-friendly):skip(default) warns, marks theskip in the summary, and releases the full bundle only;
failaborts the release before anything is uploaded. The skipped patch archive is left in the output directory for inspection.bundleUploader; if either upload fails the release fails and the release history is not updated. Temp directories are cleaned on success and failure.makeCodePushBundleresolved absolute output directories under the CWD (latent path bug, caught by the new tests).release's-j, --js-bundle-namewas read from the wrong commander attribute and never took effect; it now works and has a command-level regression test.globalSetup, fixing cold-checkout CI (this is what turned CI green on this branch).Note for config authors
Both artifacts go through
bundleUploader(filePath, platform, identifier). The patch archive arrives as a second call for the same platform/identifier — a config that derives its storage key from platform/identifier alone (ignoring the file name) would overwrite the full archive with the patch. Derive keys from the file name (as the sample configs do).Test plan
CI runs the same suite on a cold checkout (tools are built once in jest global setup).