12 KiB
ReleaseKit: shared release process
This is a proposal based on a review of PromptKit, ConfigKit, Audita, Notarius, Narratio, and Scriptorium on 2026-09-29. The implementation plan records the current implemented boundary; the internal Gitea page and asset-delivery client is implemented, while facade/CLI integration and broader integrations remain planned. The public facade architecture is accepted in ADR 0001 and is implemented for runtime version, candidate validation, and source publication. The implementation plan owns ordered stages, execution instructions, acceptance checks, and progress toward this target state.
Opportunity
All six repositories follow the same base release policy. Their release scripts repeat stable-version parsing, candidate and release-note checks, local and remote tag checks, validation of one captured commit, explicit Git publication, and recovery guidance for uncertain pushes. They differ in publication approach, tag type, validation commands, and artifact delivery. The mechanics of embedding a release version and cross-building an application are further candidates for standardization.
If the projects deliberately standardize publication approach and tag type, ReleaseKit can own most of the repeated release-process code. The current six repositories contain 942 lines of release shell scripts and 742 lines of dedicated release tests, or 1,684 lines total. A directional estimate is that roughly 1,000–1,250 lines could leave those repositories: about 450–500 lines of publication scripts, 150–250 lines of checks and builders, and 400–500 lines of repeated publication tests. ReleaseKit would gain its own implementation and tests, so this is a reduction in local code, not a net line-count saving.
Proposed publication standard
- Prepare and commit a version-matched note at
docs/releases/<version>.md. - Publish the candidate commit to authoritative
mainbefore invoking the release command. - Run one command that checks the candidate and unused stable version, runs that repository's offline candidate validation once, and rechecks mutable state.
- Create an annotated tag at the validated commit with a message derived from the checked-in note. Push only that exact tag object to the authoritative endpoint. A successful tag push completes the source release.
- Treat hosted release pages and binary assets as asynchronous delivery with their own status. Do not poll CI or require artifact availability to declare the source release complete.
This would replace Audita's atomic branch-and-tag push, ConfigKit's and Notarius's branch push during the release command, and Audita's, Notarius's, and Narratio's lightweight tags. Those changes require updates to each repository's adopted release policy and procedure; they are not merely code cleanup.
Architecture
ADR 0001 owns the accepted facade decision.
The architecture policy owns the adopted
responsibility and dependency rules, including the runtime version boundary.
The root releasekit package provides the public facade for candidate
validation, source publication, selected-target builds, and tagged-source
delivery. The Gitea page and asset-delivery client is integrated through that
facade and the CLI. The separate public buildinfo package provides lightweight
runtime version access.
Each facade owns its public named types and documented contracts. Public operations, inputs, outcomes, errors, and callbacks express deliberately selected consumer needs. Handles retain private implementation state; explicit forwarding and conversions connect the public contracts to internal representations.
Git operations, Go toolchain execution, Gitea integration, and orchestration
bookkeeping belong under Go's internal/ boundary. Dependency direction runs
from the facades into internal implementation, with internal collaborators using
contracts defined inside that boundary. Implementation packages can evolve
while the supported public contracts retain their compatibility obligations.
The buildinfo facade keeps runtime version access lightweight. Applications
read their embedded version through its supported accessor. ReleaseKit's builder
and runtime implementation own the linker symbol and injection mechanism
together.
The buildinfo facade follows three constraints:
- Its public API exposes only runtime information justified by consumer needs.
- Its implementation uses the standard library and a small internal dependency graph independent of release orchestration.
- Dependency checks protect that boundary, and build-and-run integration tests verify that the builder injects the selected version and the public accessor reports it correctly.
The release executable calls the orchestration facade and handles command parsing, output, and exit codes. Library errors and outcomes report meaningful publication state, including partial progress and uncertainty. Go declarations and GoDoc own exact public API contracts; additional public packages and extension interfaces require a deliberate decision grounded in durable consumer needs.
Compiled-export and dependency checks will protect facade ownership. External-package behavioral tests will protect error identity, value ownership, callback lifetime, and observable outcomes across forwarding and conversions.
Shared tool boundary
Provide a versioned ReleaseKit executable backed by a Go library. Existing shell entry points can become short invocations of that executable. A library alone would still require a caller in every shell-driven repository.
ReleaseKit should own:
- stable-version parsing and committed-note identity checks;
- authoritative Git endpoint resolution, candidate capture, clean
mainand remote-branch agreement checks, and unused local/remote tag checks; - one validation invocation bound to the captured candidate, followed by cheap rechecks before publication;
- annotated-tag creation and verification, explicit tag-object push, and one bounded authoritative-ref check after a failed or ambiguous push;
- a standard version accessor in the public
buildinfofacade, backed by release-build injection of the selected tag; - a cross-build API that accepts the application's main package and selected
targets, initially Linux amd64, Linux arm64, and macOS arm64 (Go's
darwin/arm64), including an empty set for source-only projects; - consistent release build flags and asset naming, plus a host-runnable build that checks the executable's reported version; and
- Git publication tests using temporary local remotes, including remote advancement, rejected pushes, lost acknowledgments, and unavailable remotes.
Each repository should retain its product name, compatibility decisions,
candidate-specific checks, main package, selected build targets, delivery
declaration, and a small test of its ReleaseKit wiring. For example, PromptKit
exercises its consumer examples. Applications would adapt their existing
version output to use the buildinfo accessor; one agreed version command
or flag would let ReleaseKit smoke-test a host-runnable build the same way.
Cross-built binaries would use the same injection mechanism, but compilation
alone would not establish that they run correctly on another platform.
ReleaseKit should run the selected checks and builds without silently adding
binary gates to source-only projects.
The build API should own the mechanics rather than require each application to provide a custom linker symbol or shell loop. The initial target list reflects the current projects, not a permanent limit on future platforms. It should support both candidate cross-build checks and artifact builds from the tagged source, using the same version and target selection.
Woodpecker and Gitea delivery
A common tag-triggered Woodpecker workflow can run ReleaseKit against the tagged source. Repository-local YAML should declare the pinned tool image and credentials; each repository's ReleaseKit configuration should declare its delivery destination and any binary main package and target set. ReleaseKit can own version injection, cross-builds, checksum generation, and delivery. All projects should receive hosted Gitea release pages. Source-only repositories declare zero binary assets and deliver the committed release note without a binary build or checksum manifest.
The shared delivery command should create or find the release for the tag, use the checked-in note, and add declared binary assets and their checksum manifest where applicable. A retry should inspect existing assets, verify matching content, add only missing items, and stop on a conflict. An existing checksum manifest must remain consistent with any later addition. The release and its attachments are separate remote operations, so partial delivery must be expected and reported clearly.
Audita, Narratio, and Scriptorium currently use the Woodpecker release plugin
with file-exists: skip and overwrite: false. Those settings avoid deliberate
replacement by filename, but they do not prove that a rebuilt file has the
same bytes as a previously published file. Content comparison would be a
reason to implement a shared Gitea delivery client rather than rely solely on
the plugin's skip behavior.
Hosted pages are selected for source-only projects, including PromptKit, ConfigKit, Notarius, and ReleaseKit. Adoption adds a page deliverable and delivery credentials to their procedures while preserving successful tag publication as source completion. The initial implementation should support both page-only and binary delivery through the same facade and command.
Failure states
ReleaseKit should report source publication and artifact delivery separately:
| Event | Result |
|---|---|
| Candidate validation or a pre-push guard fails | No release was published; correct and validate the candidate again. |
| Tag push reports success | Source release is complete; delivery may still be pending. |
| Tag push fails or its acknowledgment is lost | Query the authoritative ref once. Confirm success only if the exact intended tag object is present on the intended commit; otherwise report failed or unknown status and retain the local tag. |
| Woodpecker or Gitea delivery fails after tag publication | Keep the source release successful. Retry missing delivery from the immutable tag after checking existing content. |
| Published source or content needs correction | Publish a new version; never replace content under the published version. |
Consumer adoption target
All six projects should use the same selected publication, runtime version, build, and delivery conventions where those responsibilities apply. Repository entry points should contain only tool invocation and product-specific settings or checks. Shared mechanics and their repeated tests belong in ReleaseKit.
Application runtime code should obtain version information through buildinfo.
Source-only libraries need no runtime import or automatic binary deliverable.
Projects declaring binaries should use the common target selection and version
injection; hosted delivery should use content-aware retries for both page-only
and binary releases.
Existing published tags, notes, pages, and assets retain their historical contents.
Consumer policy and procedure changes should describe the deliberately adopted standard and its compatibility impact. Adoption should demonstrate reduced local infrastructure and one coherent release model. The implementation plan owns the complete initial ReleaseKit implementation and its development evidence. Architecture review follows that implementation; broader testing and consumer migration steps will be chosen afterward.