The release skill orchestrates the same CLI that a
maintainer can run directly. Preparation and verification do not authorize
production publication. A candidate lives as flat assets on a public GitHub
prerelease in lightningdevkit/ldk-node-bindings until the maintainer explicitly
publishes it locally. lightningdevkit/ldk-node-swift is the separate SwiftPM
publication destination; it does not run the bindings candidate build pipeline.
Ask the release skill to prepare bindings for a native version. It resolves the
native tag to an exact commit and prepares a dated recipe branch/worktree with
the new release.json, native source and UDL hashes, dependency locks, and any
required bindings or consumer-test changes. The low-level prepare command
builds an already committed recipe; the skill performs the preceding update.
Normally, a new minor release is reviewed in a PR to main. A later patch for
an older series starts from that series' previous released recipe, identified
by its candidate manifest, even if main has advanced. Carry over relevant
tooling fixes deliberately; do not roll main back or silently adopt the newer
series' native API or dependency set.
When preparing a new minor or major release, the skill creates its stable
MAJOR.MINOR.x backport branch from the reviewed, committed recipe, such as
0.8.x for 0.8.0. Before advancing main, it also ensures the previous line
has its branch, such as 0.7.x, based on that line's last published recipe.
Existing branches are preserved; never reset or force-push one to another line.
These stable maintenance names are an explicit exception to the YYYY-MM-
convention. Preparation and PR branches still use the current date prefix.
Patch-release PRs target the matching maintenance branch. Creating a branch
records a recipe line; it does not establish that its candidate was published.
Keep each recipe reachable from the official repository's main, numeric
MAJOR.MINOR.x, or dated YYYY-MM-* preparation branch. After a squash merge,
retain the unsquashed preparation branch unless the original recipe remains
an ancestor of a retained main or maintenance branch. Candidate tags alone do
not establish the official branch ancestry required by anonymous verification.
Each candidate records its full recipe commit and immutable input
identity; verification does not depend on the current contents of main.
Use a separate worktree for each preparation, retaining its local .release
ledger. The candidate workflow must exist on the default branch, but dispatch
uses the selected dated recipe branch. Publication always needs a separate
explicit local request for the candidate and its destinations.
nix run .#release -- prepare --repo lightningdevkit/ldk-node-bindingsRun from a clean, committed recipe branch with a current YYYY-MM- prefix.
Use local gh authentication or GH_TOKEN with contents and Actions write
access. When pushing changes under .github/workflows, also grant Workflows
write permission, or the workflow scope for a classic PAT; see
GitHub's permission requirements.
The command records its resumable state in .release/prepare.json and
downloads candidates into .release/candidates/CANDIDATE_ID. Retain this local
state when continuing a release; do not commit it or its downloaded artifacts.
The dispatch input is the full recipe commit SHA and must equal the workflow's checked-out identity. The candidate workflow uses GitHub's temporary repository token to create the public prerelease. It never receives the maintainer's PAT, package registry tokens, or signing keys.
Candidate release assets are the durable public handoff. GitHub Actions artifacts are intermediate job transport with seven-day retention; they are not the verifier's download source. Rerunning a job may replace that run's transport artifact, but each published candidate tag includes the run attempt and is never reused to replace already-public candidate files. A partially uploaded candidate is not publishable; create a new attempt after diagnosing the failure.
Use the README's verify commands on the appropriate hosts and accept receipts
only from rebuilds you intend to trust. Return to the candidate's clean recipe
worktree (the full recipe commit in candidate.json) for both collection and
publication. These entrypoints do not automatically switch recipes. Paths are
relative to the calling directory within that worktree.
Collect the CI-built candidate. Receipt files are optional: if a group has no
receipt, collection asks whether its artifacts were independently rebuilt and
compared on another host. Answering yes records an out-of-band confirmation
bound to this candidate; it does not manufacture a rebuild receipt. No or blank
leaves the group without recorded confirmation and still permits collection.
Observing VERIFICATION SUCCESSFUL on another host does not require copying its
files. Comparing hashes is equivalent only when one hash comes from an
independent rebuild, rather than a second download of the same candidate.
candidate_id=CANDIDATE_ID
candidate_url="https://github.com/lightningdevkit/ldk-node-bindings/releases/tag/$candidate_id"
./scripts/collect.sh --candidate "$candidate_url" \
--output /tmp/collected-bindings.jsonAdd --receipts DIR to import selected trusted receipts; supplied receipts must
still match the exact candidate, host, files, and JVM imports. Add
--non-interactive to skip prompts, leaving missing evidence unconfirmed. EOF
during a prompt aborts collection rather than treating it as an answer.
--require-verification requires all six groups to have receipts or affirmative
out-of-band confirmations. Store the collection ledger outside the receipt
directory. It binds the candidate ID, manifest hash, recipe, input identity,
receipts, and out-of-band confirmations. Publication reports all three coverage
categories and requires every group to have a receipt or confirmation by default.
JVM assembly covers assembly itself; Linux and Darwin groups cover its imported native payloads. The complete Swift ZIP is a comparison target because SwiftPM checksums archive bytes, including container metadata. All candidate files are downloaded and hash-checked regardless of the chosen verification coverage.
For an older candidate, keep its exact clean recipe checkout and invoke the
updated tools by path, for example /path/to/updated-tools/scripts/collect.sh.
The wrappers preserve the caller's working directory, so newer release tooling
can collect an existing candidate without rebuilding it or changing its recipe.
The publisher requires a specific candidate confirmation and rejects CI
environments. It rechecks all candidate files against the collection ledger
immediately before upload. Supply credentials locally through the indicated
environment or credential store; do not place credential values in commands or
logs. All destinations use existing hash-checked candidate bytes. The canonical
ledger option is --collected; --verified remains an alias for older commands.
Every group must have a recorded receipt or out-of-band confirmation before
publication. To deliberately publish with incomplete comparisons, add
--allow-unverified to that invocation. It still validates supplied evidence and
all artifact hashes. --require-verification explicitly selects the default and
cannot be combined with the override. Receipt and manual coverage, unconfirmed
groups, and the override choice are preserved for each attempt in publication
state. Coverage is kept outside immutable Swift provenance.
scripts/collect.sh and scripts/publish.sh launch the same pinned CLI as
nix run .#release -- collect and nix run .#release -- publish, respectively.
Use --help for their options. One invocation publishes one explicitly selected
destination; no script adds confirmation or publishes the remaining destinations.
| Destination | Local authentication | Result |
|---|---|---|
testpypi |
UV_PUBLISH_TOKEN for TestPyPI |
Stage the four candidate wheels |
pypi |
UV_PUBLISH_TOKEN for PyPI |
Publish wheels after the TestPyPI gate |
maven |
CENTRAL_TOKEN_USERNAME and CENTRAL_TOKEN_PASSWORD (or MAVEN_CENTRAL_TOKEN), local GPG key |
Sign, submit, validate, and publish the Maven bundle through the Central Portal API |
swift |
gh authentication or GH_TOKEN for the Swift destination |
Publish source, metadata, a version tag, and the candidate ZIP to ldk-node-swift |
Example for an explicitly authorized TestPyPI upload:
./scripts/publish.sh --collected /tmp/collected-bindings.json \
--candidate "$candidate_url" --destination testpypi \
--confirm "$candidate_id"Production PyPI uses the same command with --destination pypi and a separate
PyPI token. The publisher checks TestPyPI's recorded digests against every
candidate wheel and performs a fresh installation/import check before production
promotion. That installation uses only TestPyPI, requires a candidate wheel hash,
and ignores local uv configuration, extra indexes, caches, and Python import
paths. Do not rebuild or rename wheels between staging and promotion.
For an explicitly authorized Maven publication:
./scripts/publish.sh --collected /tmp/collected-bindings.json \
--candidate "$candidate_url" --destination maven \
--confirm "$candidate_id"For Maven, MAVEN_CENTRAL_TOKEN is the Base64 encoding of the Central Portal
token's username:password; keep it in the local environment. GPG_KEY_ID can
select the local signing key. Signature bytes are produced only after unsigned
primary artifacts match the ledger. The local script submits a USER_MANAGED
deployment, records its deployment ID, polls validation, and invokes the Central
Portal publish API under the explicit candidate confirmation. No browser upload
or separate portal click is required. Verify the final API status before
reporting Maven completion; resume an existing deployment after an interruption
instead of uploading a replacement bundle.
Swift publication uses a separate destination repository. Review the generated
Swift wrapper, final archive URL/checksum, licenses, and provenance
before authorizing publication. The local publisher creates their distribution
commit and an independent vVERSION tag in lightningdevkit/ldk-node-swift, then
publishes the unchanged candidate XCFramework ZIP as that release's asset.
The distribution contains Sources/LDKNode, Package.swift, LICENSE-APACHE,
LICENSE-MIT, and release-provenance.json. Provenance identifies the native
source, bindings recipe, candidate, input identity, and archive hash.
Consumers use the Swift repository's SwiftPM URL and version tag; the native
source tag and candidate prerelease tag have distinct roles.
./scripts/publish.sh --collected /tmp/collected-bindings.json \
--candidate "$candidate_url" --destination swift \
--repo lightningdevkit/ldk-node-swift --confirm "$candidate_id"The publisher clones the distribution repository, or initializes its empty Git remote, and pushes a dated metadata branch and immutable version tag. It uploads the hash-checked archive to a draft destination release before publishing that release; inspect its recorded state before retrying an interrupted publication.
--repo selects the Swift GitHub destination and defaults to
lightningdevkit/ldk-node-swift for Swift publication. Optional --swift-git-url
and --swift-asset-base-url configure the Git remote and public archive URL
prefix. The current publication adapter uses GitHub's release API; using Forgejo
for Swift publication requires an adapter for its release API as well as changing
the Git destination. The native source URL is independently configurable in the
release inputs.
NativeSmokeTest verifies JNI/JNA loading and a generated API call without a
Bitcoin backend. The JVM/Python channel integration tests require bitcoind and
electrs. Android device tests require an emulator or device; compiling them is
not a runtime test. Swift's package is compiled against the built framework on
the Mac build host. Keep these functional checks separate from byte equality.
Android uses JNA 5.17.0 for its 16 KiB page fixes. Both JNA's native payload and the packaged LDK libraries are checked for compatible ELF alignment.
Consuming applications also need compatible APK and App Bundle packaging.
Use AGP 8.5.1 or newer to package uncompressed native libraries with 16 KiB ZIP
alignment. With older AGP versions, enable compressed native libraries in the
application's build.gradle.kts:
android {
packagingOptions {
jniLibs { useLegacyPackaging = true }
}
}For AGP 8.0 or older, App Bundles also require
android.bundle.enableUncompressedNativeLibs=false in gradle.properties.
Follow Android's packaging requirements
and test the final APKs, including APKs generated from App Bundles, on a 16 KiB
environment. Compressed libraries are extracted during installation and consume
additional device storage.
Applications supporting older Android releases need Java 8 library desugaring. For example, an application using AGP 7.4 or newer can configure:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = "1.8" }
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
}See Android's Java 8 API support for the desugaring version compatible with the application's AGP version. JNA's Android 6 issue remains open; enabling desugaring is not proof that every consumer is compatible. Runtime tests on API 21–23 and a 16 KiB device remain required release qualification checks. ELF inspection does not replace those tests.
Publication state is recorded next to the collection ledger as
publication-CANDIDATE_ID.json. Keep the collection ledger, any accepted receipts,
and publication record together. You may re-collect the same candidate to add
receipts or out-of-band confirmations before resuming, including Swift publication.
The append-only verification_history records each destination invocation's
coverage and override before contacting that destination. A destination's status
references the history entry through its zero-based verification_attempt.
History records attempts, not proof that an upload completed; use destination
status and remote state to determine the outcome. Earlier entries and other
destinations' status remain intact. Legacy global verification fields are
retained as historical data; their destination and timing cannot be recovered.
Swift provenance binds only candidate identity and artifacts. Older commits with
an embedded coverage field are accepted with that historical field intact;
resuming never rewrites metadata commits or moves tags.
A failed or interrupted upload may already have
changed the destination; inspect remote files/hashes and recorded status before
retrying. Never replace an existing registry version, move a published tag, or
upload newly built bytes under an old collection ledger.
Report exactly which destinations were staged, submitted, validated, or finally published. Candidate preparation, a successful hash comparison, and a TestPyPI upload each establish a different part of the release.