Skip to content

Latest commit

 

History

History
286 lines (242 loc) · 15.6 KB

releasing.md

File metadata and controls

286 lines (242 loc) · 15.6 KB

Local release operation

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.

Release versions and branches

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.

Preparation state

nix run .#release -- prepare --repo lightningdevkit/ldk-node-bindings

Run 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.

Complete verification

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.json

Add --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.

Publication commands

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.

Consumer qualification

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.

Resume and inspect

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.