Skip to content

Release pipeline ​

The repository's .github/workflows/publish.yml runs only when the head commit pushed to main explicitly contains the release marker. Ordinary pushes do not publish. Update [workspace.package].version before releasing; the prepare job refuses to continue if the corresponding v<version> tag already exists.

The same tag publishes the root composite Action: uses: lejunyang/one-sdk@vX.Y.Z reads action.yml from that revision. The final GitHub Release job creates the tag; it is an output of the pipeline, not an input. Creating and pushing it by hand first makes the prepare job fail. The Action's own CI uses uses: ./ to read the current checkout and bootstraps it from the latest release because a local reference has no semver action_ref and the job omits version. Action changes are therefore covered on all three platforms before a new tag exists.

What one release publishes ​

The workflow first builds both programs for Linux x64/arm64, macOS Intel/Apple Silicon, and Windows x64 in parallel and uploads temporary artifacts. Once every build succeeds, it publishes crates.io packages in this dependency order:

  1. osdk-core;
  2. wait until that version is visible through the crates.io API;
  3. osdk-cli;
  4. osdk-shim.

osdk-cli and osdk-shim both depend on an exact osdk-core version, so the registry visibility wait is required. Core passes its dry-run before upload; both dependents pass cargo publish --locked --dry-run together once core is visible. The publish job removes the repository build-mirror configuration and validates publishable dependencies against the official crates.io index and the repository lockfile. The workflow creates the GitHub tag and Release only after every crate has published, then attaches seven platform archives and SHA256SUMS: GNU and musl Linux archives for x64 and arm64, two macOS archives, and Windows x64. A crates.io failure therefore cannot leave behind a GitHub Release that appears complete.

The two GNU/Linux archives are built natively by architecture inside an ubuntu:20.04 container. This makes glibc 2.31 the declared minimum instead of inheriting whichever libc happens to be on the rolling GitHub runner. Packaging first smoke-tests both freshly built binaries with --version -- the one point that proves they actually start on the target -- then runs the glibc-baseline check (also available locally as the task of the same name) over both ELFs; it reads their imported symbol versions and rejects anything above GLIBC_2.31. The workflow runs the checker script directly via bash rather than through osdk run: the latter builds the full managed environment and requires every tool the project declares to be installed, while this contract only needs the script and readelf. The workflow first runs the same checker with an impossible 0.0 ceiling and requires that probe to fail, so a broken parser cannot turn the real green result into false evidence.

The two Linux musl targets are built natively on their matching x64 and arm64 runners. C build scripts use musl-gcc, while rustc's self-contained link produces a static PIE; using musl-gcc as rustc's final linker would instead emit an interpreter on Debian and Ubuntu. The musl-static check (also a task of the same name) rejects an interpreter or any dynamic NEEDED entry in either executable. It too has a negative control: the checker must reject copies of the runner's dynamic /bin/sh before its result for the release binaries is trusted.

Install the primary commands from crates.io with:

bash
cargo install osdk-cli --locked

This installs osdk. osdk-shim is a separate package. For a complete everyday installation, the GitHub Release installer remains preferred because it places both same-version programs in one directory.

Binary size ​

What users download is these two executables, so the release profile is tuned for size rather than for raw speed. Current sizes are roughly 8.9 MB for osdk and 3.5 MB for osdk-shim.

The settings that get there, measured on this workspace:

ProfileCombined size
opt-level = 3, lto = "thin"44.6 MB
opt-level = 3, lto = "fat", codegen-units = 137.0 MB
opt-level = "z", lto = "fat", codegen-units = 1, panic = "abort"16.7 MB

Optimizing for size is safe for the shim, which is the latency-sensitive binary because it runs on every node or npm invocation. Measured shim startup is 48.3 ms median at opt-level = "z" against 50.5 ms at opt-level = 3: process creation dominates, so shrinking the code costs nothing observable here.

One place does pay, and it is not the obvious one. opt-level = "z" costs sha2's portable backend about 65% of its throughput, dropping from 2300 MiB/s to 800 MiB/s when hashing 256 MiB. Every downloaded archive is checksummed, so left alone this would slow down every install. The workspace manifest therefore pins the hashing crates back to opt-level = 3 with per-package profile overrides, which restores full throughput for about 0.02 MB of size. BLAKE3 measured unaffected because it ships hand-written SIMD, but it is pinned as well since it hashes every file entering the content-addressed store.

Cargo only emits a warning, not an error, when a per-package override matches no package. A dependency rename would silently give the slowdown back with a green build, so hashing_crates_are_pinned_to_a_fast_opt_level in crates/osdk-core/src/pipeline/verify.rs asserts the pins are present.

panic = "abort" applies to the shipped binaries only. Cargo ignores the setting for test targets, so catch_unwind-based tests still work under cargo test --release.

Why the shim is much smaller than the CLI ​

Profile tuning is only half the story. The shim used to be nearly as large as the CLI for a structural reason: it only ever reads state, but it holds Arc<dyn Backend> values and Registry::new instantiates all thirteen backends, so every Backend method landed in a vtable the linker could not prove unreachable. That kept the whole install path alive inside the shim, including the sigstore subtree that accounts for 240 of osdk-core's 314 dependency crates.

The cost was measured by building probes that link osdk-core and exercise only the read-only surface:

What the probe linksSize
directory resolution only0.13 MB
plus Config::load0.74 MB
plus http::client1.83 MB
plus Registry and dyn Backend7.02 MB

That last step looks conclusive but conflates two things: the vtables keeping the install path alive, and the backends' own read-only code, which the shim needs anyway. Calling the same thirteen backends through static dispatch, where the linker can discard the unused install bodies, costs 1.83 MB. So the install path alone accounted for about 5.15 MB.

The four install-only trait methods -- list_remote_versions, resolve_version, install and uninstall -- are therefore behind a default-on install feature, and the shim depends on osdk-core with default-features = false. The sigstore crates are optional and pulled in by that feature, so the shim's dependency graph drops from 982 crates to 441 and no longer contains a second copy of reqwest.

Archive decoders follow the same rule. .7z support is needed because Windows GCC toolchains are commonly published in that format only, but it carries a second LZMA implementation (lzma-rust2) alongside the existing xz2. Since only the install path ever unpacks an archive, sevenz-rust2 is optional and gated behind install, and the ArchiveKind::SevenZ variant is #[cfg]-gated with it, so neither crate enters the shim's graph. The encoder half is a dev-dependency: tests build a real .7z fixture, while the shipped binaries only decode.

Compiling the install path out substitutes uninhabited stand-ins for GithubAttestation and VerificationEvidence. Every verification call sits inside if let Some(attestation) = attestation, and an Option of an uninhabited type is always None, so those branches are statically unreachable while signatures, struct fields and callers stay unchanged. The alternative -- #[cfg] on thirty-odd references including public fields -- would be much harder to follow.

The shim must be built in its own invocation ​

Cargo unifies features across a single cargo build --workspace, so building both binaries in one command re-enables install for the shim and silently restores the old size. The result still works, and nothing warns.

Release builds therefore run one invocation per binary:

bash
cargo build --release -p osdk-cli
cargo build --release -p osdk-shim

Both may share one target directory; Cargo caches the two feature variants side by side and does not rebuild when alternating between them. A compile-time assertion in the shim fails the build, with an explanation, if the install path is ever linked back in. It is limited to release builds so cargo check, cargo test and cargo clippy still work across the whole workspace during development.

First-release authentication ​

At the time of writing, none of the three crates exists on crates.io. crates.io Trusted Publishing requires an existing crate, so the first release needs a GitHub Environment named crates-io with a CARGO_REGISTRY_TOKEN secret. The token needs the publish-new and publish-update scopes. Never commit it or expose it in logs or ordinary configuration. Consider adding a required reviewer to the Environment so the irreversible bootstrap publish has a human approval gate.

After the first release, add the same GitHub Actions Trusted Publisher in the crates.io Settings page for each crate:

FieldValue
Repository ownerlejunyang
Repository nameone-sdk
Workflow filenamepublish.yml
Environmentcrates-io

After all three publishers are configured and the next release succeeds, delete the long-lived CARGO_REGISTRY_TOKEN from the GitHub Environment. Subsequent runs use rust-lang/crates-io-auth-action to exchange GitHub OIDC identity for a job-scoped token that the action revokes when the job ends. The workflow retains the first-release token as a bootstrap fallback while OIDC is not configured; removing the secret leaves Trusted Publishing as the only path.

Updating an existing installation ​

osdk self upgrade consumes exactly what the workflow above uploads: the platform archive plus the SHA256SUMS next to it. That coupling is the reason the command lives in the same document as the pipeline. The asset name it asks for is derived from the host, and the mapping is asserted against the build matrix by a unit test, so a platform that the workflow stops publishing becomes an "unsupported platform" error rather than a download of a 404 page. Linux includes the running binary's libc ABI in that mapping, so a musl installation keeps downloading musl releases during later upgrades.

An upgrade proceeds in four steps:

  1. Resolve. releases/latest gives the newest tag; --version selects a specific one instead and may move backwards, which is how a bad release is rolled back. Without --version, a target that is not strictly newer ends the command as a no-op unless --force is passed.
  2. Rank sources. The same speed probe, probe cache, pin handling, and --source override that tool downloads use. See below.
  3. Verify. The archive is checked against the release's SHA256SUMS. With --require-checksums, a release that publishes no usable entry is refused instead of installed.
  4. Replace. osdk and osdk-shim are replaced as a pair.

Why the mirror is measured, not assumed ​

The upgrade downloads from GitHub, which is exactly where a user in a region with poor connectivity needs a proxy. Rather than growing a second mirror policy, the command reuses source::select through its non-backend entry points, so osdk source list self and osdk source test self describe it, and osdk source pin self <id> or osdk source add self ... steer it. The tool id is self, deliberately distinct from github:lejunyang/one-sdk: pinning a mirror for upgrades should not silently change where a github: install of the same repository comes from.

Two details differ from a backend, both forced by what is being measured:

  • The probe target is the release asset, not a version index. The github: backend declines to probe at all because the API is rate-limited; measuring the artifact costs no API quota, and a source that cannot serve it cannot serve the upgrade either, so failing it is the correct answer.
  • The probe window is wider than probe_timeout_ms. That setting defaults to 1.5s, which suits a small index. Measured against the real sources, a CN proxy fronting github.com needed 6.1s just to first byte. At 1.5s every candidate times out, all of them are recorded unreachable, and ranking degrades silently to the fixed priority order — the opposite of choosing the faster route. A floor of 12s applies here; a larger configured value is still honoured. Probes run concurrently, so this bounds the whole step.

Why replacement is all-or-nothing ​

osdk-shim resolves installs that osdk wrote, so a pair at two different versions is a broken installation, not a partial success. The running program also cannot be deleted on Windows, and overwriting a mapped executable in place is unsafe on Unix. Each destination is therefore renamed aside before the new file is moved in, and a failure part-way rolls every rename back; a regression test asserts that a failure on the second program leaves the first one at its old contents. Backups are removed only after the whole set is in place. A backup that Windows refuses to delete because the process is still running is left behind and cleaned up at the start of the next upgrade.

This is also why the command is not a Backend. A backend installs a tool into the store under a version directory; this replaces the two programs the user is running, wherever they live. Registering it would additionally put another entry in Registry::new(), whose vtables the shim pays for, in exchange for a tool id no request can name.

Release checklist ​

  1. Update both the workspace version and the exact osdk-core version under [workspace.dependencies], plus the relevant user-facing release notes.
  2. Ensure CI, the Windows Wine workspace tests, and the docs build pass.
  3. Ensure the crates-io Environment reviewer and credential are ready.
  4. Push a main head commit that explicitly contains the release marker.
  5. Verify that osdk-core, osdk-cli, osdk-shim, and the GitHub Release all carry the same version.

Crate versions cannot be overwritten. If a run publishes only part of the workspace, fix the cause, bump the workspace version, and release again rather than trying to replace an uploaded version.

Released under the MIT License