Each SDK is a thin client over the Rust core (the libsecretspec C ABI, a
pyo3 extension for Python, or the napi-rs addon for Node). A release builds the
native artifact per platform and publishes it through that ecosystem's
registry or as a checksummed SwiftPM binary, so users install with no native
build.
Version tags are vX.Y.Z; the publish jobs trigger on them. After the Go
release build succeeds, CI also creates the submodule tag
secretspec-go/vX.Y.Z, which is the version the Go module proxy resolves.
SecretSpec 0.20 renames the embedded native library from secretspec-ffi /
secretspec_ffi to libsecretspec without renaming its exported C symbols.
Before tagging 0.20:
- Run the SDK workflow and every platform packaging workflow from the release commit, including the Windows native assets.
- Verify Go purego, .NET, and PHP FFI discover the pre-0.20 shared-library
filenames for legacy operations. Static-link SDKs must use the matching 0.20
archive because their source references the new
secretspec_callsymbol. - Verify newly packaged artifacts contain only the new public filename and
that
SECRETSPEC_FFI_LIBcan explicitly select either compatible library. - Test one clean install per SDK without a source checkout or Cargo
target/directory present, so development fallback discovery cannot hide a missing packaged asset. - Keep
libsecretspecand each SDK at the same release version. Filename fallback preserves loading, not newer request fields or behavior in an old library.
The detailed compatibility matrix is maintained in
libsecretspec/README.md.
The X.509 backend uses p12-keystore 0.4.1 from crates.io, which includes the
Unicode certificate-label fix
and the bag-preserving archive API. The minimum dependency version is 0.4.1,
so packaged crates retain both fixes without a Git override.
Before publishing 0.22:
- Align the workspace
secretspec-ipcdependency with the release version and publish that crate beforesecretspec, aspublish.ymlalready does. Currentmainusessecretspec_ipc::MAX_JSON_INTEGER, which the published 0.21.1 IPC crate does not provide. Package verification against that older registry crate fails even though the archive API is available. - Run the Rust tests, SDK CI, Unicode-subject interoperability checks, and
cargo package -p secretspecwith the published dependencies. Verify the packaged manifest resolves the fixed registry version without a Git override. - Keep the documented BMP password restriction until
RustCrypto/formats#2477
ships and the
pkcs12dependency resolves to that release. Then add supplementary-Unicode interoperability coverage before lifting the check.
The OpenSSL-generated test archives live in secretspec/tests/fixtures/x509.
They allow the Rust test suite to check imports without compiling OpenSSL.
install.secretspec.dev uses a Scarf file package to redirect to cargo-dist's
published installer assets. Configure these routes on that domain:
| Incoming path | Outgoing URL |
|---|---|
/ |
https://github.com/cachix/secretspec/releases/latest/download/secretspec-installer.sh |
/v{version} |
https://github.com/cachix/secretspec/releases/download/v{version}/secretspec-installer.sh |
The version variable is the release version without a leading v, for
example 0.21.1. The incoming pattern supplies the literal v prefix, so the
public URL is /v0.21.1. Replace the old /{version} pattern rather than keeping
both: the old pattern can match / with an empty version and interfere with the
latest-release redirect. The literal prefix prevents that overlap.
Scarf substitutes the captured version into the GitHub release URL. The selected release's installer retains its own platform detection, checksums, updater, and receipt. No installer rewriting or cargo-dist workflow customization is needed.
Before publishing documentation for the versioned URL, activate the Scarf route and verify both redirects:
curl -fsSI https://install.secretspec.dev/
curl -fsSI https://install.secretspec.dev/v0.21.1The first must still redirect to releases/latest/download; the second must
redirect to releases/download/v0.21.1/secretspec-installer.sh. Also verify that
the selected URL serves the same installer as the direct GitHub release URL.
A version without an installer asset must fail rather than install latest.
Once the release artifacts are available, update the secretspec package in
Nixpkgs. From a Nixpkgs checkout, let nix-update update the crate source and
Cargo dependency hashes and verify that the package builds:
nix-update --version=X.Y.Z --build secretspecCommit the generated package change as secretspec: OLD -> X.Y.Z, include the
GitHub release URL in the commit body, and submit it to Nixpkgs.
External registry status was last verified on 2026-08-31. Recheck every item still marked pending before tagging a release.
Trusted Publishing works differently per registry. PyPI and RubyGems let you register a pending publisher before anything is published — the first tagged release creates the project/gem automatically, no manual publish step. npm has no such mechanism: the package must already exist before you can attach a Trusted Publisher to it, so the very first version has to go up with a temporary token. Do each of these once; every release after it needs no secrets (except Hackage, which has no Trusted Publishing at all yet).
publish.yml publishes secretspec-ipc, secretspec, and
secretspec-derive in that order, because secretspec has a registry
dependency on secretspec-ipc and secretspec-derive on secretspec.
secretspec and secretspec-derive already exist on crates.io and Trusted
Publishing is already wired up for them. Confirm the linked GitHub repo is
still correct at https://crates.io/crates/secretspec/settings if this repo is
ever renamed or transferred.
secretspec-ipc is new in 0.21 and does not exist on crates.io yet. Like npm,
crates.io only accepts a Trusted Publisher for a crate that already exists, so
its first version goes up by hand. After the release PR merges and before
pushing the tag, publish it from the merged main with a temporary API token
scoped to publish-new:
devenv shell cargo publish -p secretspec-ipc --token <temporary token>Then add a Trusted Publisher at https://crates.io/crates/secretspec-ipc/settings
(repository cachix/secretspec, workflow publish.yml) and revoke the token.
The tag's publish run finds the version already on crates.io, skips it, and
continues with secretspec.
The pypi GitHub Environment and PyPI Trusted Publisher are active for project
secretspec (owner cachix, repo secretspec, workflow
python-wheels.yml, environment pypi). OIDC-authenticated releases have
published through 0.19.1; no token or additional setup is needed.
The RubyGems Trusted Publisher is active for gem secretspec (repository owner
cachix, repository name secretspec, workflow filename ruby-gems.yml,
environment release). OIDC-authenticated releases have published through
0.19.1; no token or additional setup is needed.
npm has no pending-publisher mechanism, so this needed a manual first publish
for the main secretspec package and every platform sub-package
(secretspec-linux-x64-gnu, secretspec-linux-arm64-gnu,
secretspec-linux-x64-musl, secretspec-linux-arm64-musl,
secretspec-darwin-arm64, secretspec-win32-x64-msvc) — 7 packages that each
had to exist before a Trusted Publisher could be attached. This has been done:
all 7 packages are published (bootstrap-published once with a temporary
granular access token, "All Packages" / "Read and write" scope — narrower
"select packages" scopes 404 on brand-new package names, since that picker
can't reference a package that doesn't exist yet), each has a Trusted
Publisher configured (GitHub Actions, repo cachix/secretspec, workflow
node-addon.yml, no environment). Revoke every temporary bootstrap token
immediately after setup; release automation does not use or store one. Every
release from here publishes via OIDC. napi pre-publish skips a platform
package version that is already present, so the manually bootstrapped 0.20
musl packages do not make the tag workflow fail.
Hackage doesn't support OIDC yet (tracked upstream:
haskell/hackage-server#1443,
open as of this writing), so this stays a long-lived token rather than a
one-time setup step. The HACKAGE_TOKEN repo secret is set. Nothing left to
do — each vX.Y.Z tag's haskell-build.yml publish job uploads with it.
No registry involved. go get reads the secretspec-go/vX.Y.Z submodule tag
directly from git. go-embed.yml creates that tag after its full platform
matrix succeeds and attaches the per-platform cdylibs to the GitHub Release for
the optional self-contained build.
Packagist has no OIDC/Trusted-Publishing mechanism; it reads a git repo and its
root composer.json directly. This repo publishes the PHP package straight from
the monorepo — the manifest lives at the repository root (/composer.json, with
vendor-dir pointed into secretspec-php/ and autoload sourcing
secretspec-php/src/), so no split/mirror repo is needed.
The cachix/secretspec
package and its GitHub auto-update integration are active; Packagist has
ingested releases through 0.19.1. No split/mirror repository, CI workflow, or
token is needed. Packagist pulls each vX.Y.Z tag automatically.
For recovery after a missed update, ask Packagist to refresh the package or reconnect its GitHub hook.
Cachix.SecretSpec publishes via NuGet Trusted Publishing (OIDC), so no
long-lived API key is stored. The nuget.org policy is configured (repository
owner cachix, repository secretspec, workflow file dotnet-package.yml,
environment nuget), and the repository's nuget GitHub environment holds
NUGET_USER — the nuget.org profile name that owns the policy. The first
publish (0.15.0, a manual dotnet-package.yml run with publish: true)
claimed the package ID and permanently activated the policy. It is an
unsupported bootstrap artifact rather than the C# SDK release; the first
supported package is 0.16.0, whose publish job also unlists 0.15.0. Nothing
else is needed — version tags build all native runtime assets, pack them into
one .nupkg, run clean-package and NativeAOT consumers on every RID, and
publish with a short-lived OIDC-issued API key (--skip-duplicate makes
re-runs of an already-published version harmless).
The root Package.swift makes this repository a Swift package and downloads
CSecretSpec.xcframework.zip from the matching GitHub Release. SwiftPM requires
that remote binary's SHA-256 checksum to already be present in Package.swift,
so Swift has one required release-preparation step after the workspace version
is bumped and before the version tag is created:
-
Run
swift-package.ymlon the release branch withpublish: false. -
Download its
swift-xcframeworkartifact and run:swift package compute-checksum CSecretSpec.xcframework.zip
-
Replace
secretSpecBinaryChecksumin/Package.swiftwith that value, commit it, and rerun the workflow. -
Create the version tag only after the rerun passes. The tag workflow rebuilds the deterministic archive, refuses to publish if its checksum differs, and attaches the ZIP to the existing GitHub Release.
scripts/sync-sdk-versions.sh updates the version in the XCFramework URL but
intentionally leaves the checksum alone, making a missing checksum refresh
visible during release review. There is no Swift registry credential or
separate repository.
The JVM workflow builds and tests the multi-platform JAR on version tags, then
signs and publishes it automatically to Maven Central. It waits for the
asynchronous Central deployment to reach PUBLISHED, so a successful workflow
run means the package is publicly available. A manual dispatch from a release
tag with publish: true can retry JVM publication independently.
Publishing uses the org.cachix namespace and these GitHub Actions secrets:
MAVEN_CENTRAL_SIGNING_KEY: ASCII-armored OpenPGP private key. Its public key must be available from a Maven Central-supported key server.MAVEN_CENTRAL_SIGNING_PASSPHRASE: passphrase for the signing key, or empty when the stored key has no passphrase.MAVEN_CENTRAL_TOKEN_USERNAMEandMAVEN_CENTRAL_TOKEN_PASSWORD: publisher token credentials generated by the Maven Central Portal.
Cachix.SecretSpec was bootstrapped manually in
microsoft/winget-pkgs#413776
and is updated with a manual pull request after each stable GitHub Release.
WinGet is intentionally not published from GitHub Actions: cross-repository
submission would require storing a classic GitHub personal access token with
access to every public repository the release operator can write to.
After the stable release and its Windows ZIP are published:
- Generate an updated
Cachix.SecretSpecmanifest withwingetcreate update, using version<version>and installer URLhttps://github.com/cachix/secretspec/releases/download/v<version>/secretspec-x86_64-pc-windows-msvc.zip. Write the manifest to a local output directory; do not use--submitor provide a token. - Add the generated version directory to the existing
domenkozar/winget-pkgsfork, based on the latestmicrosoft/winget-pkgsdefault branch. - Review the version, installer URL, and SHA-256, then open a pull request from
the fork to
microsoft/winget-pkgs. Use the release operator's normal local GitHub or browser login; no SecretSpec Actions secret is needed. - Confirm the upstream validation checks pass and follow the pull request through merge. The WinGet update is a post-release distribution step, not a gate for creating the SecretSpec version tag.
- Build: the Rust resolver is statically linked into a pyo3 extension
(
secretspec._native, built from thesecretspec-py-nativecrate via maturin) — there is no separate cdylib bundled. The extension targets pyo3'sabi3-py39feature, so onecp39-abi3-<platform>wheel per platform serves all CPython >= 3.9. Linux wheels are built viaPyO3/maturin-actioninside amanylinux_2_28container (old glibc); maturin repairs the wheel, so no separateauditwheelstep is needed. macOS builds natively; a Windows wheel is a follow-up. - Publish:
pypa/gh-action-pypi-publishvia PyPI Trusted Publishing (OIDC); no token needed. One-time setup is complete; see "One-time registry setup" above.
- Build: a platform gem (
Gem::Platform::CURRENT) bundling thelibsecretspecstaticlib invendor/. Atgem install, mkmf compiles a tiny C glue and statically links that archive, so the resolver is embedded in the extension and one platform gem serves every Ruby ABI (install needs a C compiler and Ruby headers). - Publish:
gem pushfor each platform gem, authenticated via RubyGems Trusted Publishing (OIDC) throughrubygems/configure-rubygems-credentials— no token stored in CI. One-time setup is complete; see "One-time registry setup" above. - Gap: the Linux gem currently links the runner's glibc; for a portable gem,
build the staticlib on an old-glibc baseline (e.g. a
manylinuxcontainer, as the Python job does, orrake-compiler-dock) and bundle that. Tracked follow-up.
Go has no binary registry, and the module proxy (proxy.golang.org) builds
module zips from raw git objects — it does not run git-LFS smudge filters, so
LFS-tracked files reach consumers as ~130-byte pointer text, not libraries.
go:embed over LFS therefore cannot ship a working library through go get.
(Committing the ~34 MB-per-platform libs to plain git would work but bloats
history permanently and ships every platform's lib in the module zip.)
So the Go SDK follows the purego norm: the cdylib is provided at runtime, not
shipped through the module. Consumers either set SECRETSPEC_FFI_LIB to an
installed/built libsecretspec, or build with -tags embed_lib after
staging the per-platform library into secretspec-go/lib/ themselves (a
self-contained, vendored build — not a module-proxy install).
- Build:
go-embed.ymlbuilds the per-platform libs, uploads them as artifacts, and smoke-tests an-tags embed_libbuild with a staged lib. - Release: nothing is pushed to a registry. On a
vX.Y.Zrelease,go-embed.ymlattaches the per-platform cdylibs to the GitHub Release and createssecretspec-go/vX.Y.Zat the same commit forgo get.go-static.ymlattaches its musl static SDK bundle separately. Do not commit binaries to the repo (plain git or LFS).
The loader rejects an embedded git-LFS pointer with a clear error, so a botched LFS-based build fails loudly instead of feeding pointer text to
dlopen.
- Build: statically links the
libsecretspecarchive at build time via the GHC FFI, so the Rust resolver is embedded in the binary with no runtime loader path. - Publish:
cabal upload --publishwith theHACKAGE_TOKENsecret — see "One-time registry setup" above. Hackage has no Trusted Publishing yet (haskell/hackage-server#1443), so this stays a long-lived token. - Note: Hackage's own build bots cannot compile this package (it statically links a Rust archive Hackage doesn't build); the upload still succeeds, it just won't show as "buildable" in Hackage's UI. The README documents the link requirement for anyone installing from source.
- Build: native Intel and Apple-silicon macOS runners build the
libsecretspeccdylib with a macOS 12 deployment target.scripts/build-swift-xcframework.shgives each dylib an@rpathinstall name, combines the slices into a universal dylib, adds the public header and Clang module map, and wraps it withxcodebuild -create-xcframework. - Test: the Swift package selects the local XCFramework when staged under
secretspec-swift/Artifacts/; its unit and cross-language conformance tests run against the final two-architecture artifact. - Publish: the workflow normalizes archive metadata, verifies the
precommitted SwiftPM checksum, and attaches
CSecretSpec.xcframework.zipto the version's GitHub Release. Follow the pre-tag checksum procedure above. - Platforms: macOS 12+ on Intel and Apple silicon. Mobile Apple platforms are intentionally unsupported because SecretSpec's development-workflow providers rely on desktop files, processes, CLIs, and credential stores.
- Build:
node-addon.ymlbuilds the napi-rs addon (secretspec.node) per platform via@napi-rs/cli(scripts/build-addon.shwrapsnapi build) and runs the SDK tests against it. - Publish: multi-platform npm distribution uses per-platform optional
packages (
secretspec-<platform>, e.g.secretspec-linux-x64-gnu) that the mainsecretspecpackage references viaoptionalDependenciesand loads at runtime — the layout@napi-rs/cliautomates (napi create-npm-dirs/napi pre-publish). Authenticated via npm Trusted Publishing (OIDC); no token stored in CI.napi.targetsinsecretspec-node/package.jsonis the source of truth for the platform list;napi pre-publishderivesoptionalDependenciesfrom it at publish time. - One-time setup: done for the four glibc, macOS, and Windows packages. The two musl packages added in 0.20 still need it — see "One-time registry setup" above.
The PHP SDK ships as two artifacts, because PHP delivers native code as an
extension (provisioned at the image/php.ini level, like ext-redis), not
through Composer.
- Client → Packagist. The pure-PHP client (
cachix/secretspec) is published straight from the monorepo: the Composer manifest is the repository-root/composer.json(autoload sourcessecretspec-php/src/;vendor-dirpoints intosecretspec-php/so the tooling stays there), which Packagist reads directly — no split/mirror repo, no CI, no token. Packagist auto-updates from eachvX.Y.Ztag. No version-sync is needed — Composer takes the version from the git tag (like Go), sosync-sdk-versions.shdoes not touch it. One-time setup: see "Packagist (PHP)" above. - Extension → GitHub Release (
php-ext.yml). Thesecretspec-php-nativeextension (an ext-php-rs cdylib embedding the resolver) is built as a prebuilt shared object per PHP minor (8.2–8.4, NTS) × platform, smoke-tested, and attached to the release. Users install it by dropping the.soin andextension=/docker-php-ext-enable, or by building from source with cargo. - ext-ffi fallback library. For the no-extension path,
ffi-build.ymlattaches the per-targetlibsecretspeclibrary (with a.sha256) to the release; the client'svendor/bin/secretspec-install-libcommand downloads the right one on demand. It is a deliberate opt-in command, not a Composer post-install hook (a dependency's install scripts do not run in the consumer project, and a secrets tool should not silently fetch a binary duringcomposer install). - Gaps (follow-up, unvalidated cross-platform): the extension matrix is
Linux + macOS, NTS-only (no ZTS), and links the runner's glibc/system libs
(same portability caveat as the Ruby/Python jobs — a baseline/manylinux build
is the fix). A Windows extension build is deferred (ext-php-rs on Windows
needs the PHP SDK dev pack +
rust-lld; Windows users can use the ext-ffi backend, whose cdylibffi-build.ymldoes build for Windows). A one-command PIE install is not wired (PIE builds non-Windows extensions from source via phpize, which does not fit a Cargo extension); and the release-asset uploads race cargo-dist's release creation, so they wait-then---clobber.
The first-release Packagist registration is complete. For each release:
-
Smoke-test off the release commit before tagging. In a scratch project, confirm the manifest resolves:
composer require cachix/secretspec:dev-main
-
Cut the
vX.Y.Ztag. Packagist ingests the tag as a version, andphp-ext.yml/ffi-build.ymlattach the extension + cdylib binaries to the GitHub Release. -
Verify against the live release (the one path CI cannot cover): in a clean project,
composer require cachix/secretspec, then exercise both backends —vendor/bin/secretspec-install-libfor the ext-ffi path, and a downloadedsecretspec-php-native.so(extension=…) for the extension path — and confirm a resolve works under each.
- Client: a trimming-safe, NativeAOT-compatible .NET 8 assembly with no managed package dependencies. It invokes the stable JSON C ABI through source-generated P/Invoke and exposes the same builder, resolved value, report, and typed-error vocabulary as the other SDKs.
- Native assets: one NuGet package carries
libsecretspecunder the standardruntimes/<rid>/native/layout for glibc and musl Linux x64/Arm64, macOS x64/Arm64, and Windows x64/Arm64. Glibc builds use a manylinux 2.28 baseline, and Windows builds statically link the MSVC runtime. - Publish:
dotnet-package.ymltests every native asset on its target, assemblesCachix.SecretSpec.<version>.nupkg, then installs that exact package in isolated framework-dependent and NativeAOT consumers on all eight RIDs before pushing it with a short-lived API key from NuGet Trusted Publishing (OIDC), running in thenugetGitHub environment. One-time setup is described above.