# ReleaseKit staged implementation plan Status: Proposed. All stages are pending; this document introduces no implemented features or published contracts. Date: 2026-10-05 ## Purpose and baseline This plan gives a Luna coding agent bounded implementation assignments, in execution order, for the target state in the [feature roadmap](releasekit.md). The roadmap owns product intent and selected release conventions. This document owns implementation steps, dependencies, acceptance evidence, and progress. The baseline is ReleaseKit commit `43c20b0`. It contains documentation, adopted policies, licensing notices, and [ADR 0001](../adr/0001-public-facade.md). It has no Go module, packages, executable, tests, or publication automation. The worktree was clean when this plan was prepared. The available tools were Go 1.26.8 on linux/amd64 and Git 2.53.0; these observations are not minimum support requirements. The six sibling checkouts are consumer evidence and future migration targets. They are independent repositories. Their source and instructions must be rechecked before each migration; the earlier line counts are historical review measurements. Consult their code through the knowledge graph, indexing a repository when necessary. Documents, scripts, and CI configuration may be read directly under the discovery instructions. ## Execution instructions - Implement stages in integer order. Each stage is one assignment; finish its acceptance checks and local commit before starting the next stage. Record completed work and evidence here. Read the stage's dependencies and relevant decisions before editing. - Start with the [developer guide](../development.md) and applicable policies. Architecture, testing, documentation, and release policies apply to the core implementation. Read the OSS policy when incorporating third-party material. - Follow the [architecture policy](../policy/architecture.md) and ADR 0001. Add exported declarations as their behavior is implemented, with GoDoc. Implement the selected contract with proportionate internal collaborators. - The design choices below specify this draft's recommended implementation. They become implementation instructions when the plan is approved. Later agent-authored code or tests provide evidence of implementation, rather than independent authority to strengthen the contract. - For each stage, run focused behavioral checks for its changed risks and the established offline default suite. Reuse unchanged evidence where appropriate. Stage acceptance checks are development checks; they add no publication gates beyond the release policy and a repository's selected candidate checks. - Use temporary repositories, local bare remotes, test programs, and local HTTP servers. Keep fixtures independent of the six consumer checkouts and real services. Temporary fixture module replacements may connect test programs to this checkout; keep them outside committed production module configuration. - Update canonical documentation with each implemented responsibility. Create consumer, CLI, configuration, integration, and internal documents when they have substantive content. Keep future behavior and stage progress here. - Consumer migration stages change the named sibling repository as well as any corresponding ReleaseKit examples. Follow that repository's policies, preserve unrelated work, and commit each repository separately. When adding a runtime dependency, update its module metadata and OSS attribution/companion license texts in the same change using the actual selected source pin. A migration needs a reachable immutable ReleaseKit pin, as described under Open Questions. - Local implementation commits and tests against temporary remotes complete stages. Real release publication, remote pushes, CI activation, and credential provisioning require the maintainer's corresponding authorization. Do not equate prepared integration with verified live delivery. ## Implementation decisions ### Module, runtime access, and public ownership Use one module, `gitea.maximumdirect.net/eric/ReleaseKit`, preserving the case advertised by the forge's Go import metadata. Set the initial Go language baseline to `go 1.25.0`, matching the oldest consumer module line. Use APIs available on that baseline. Actual execution uses a prepared local toolchain capable of building the consumer; Narratio already needs Go 1.26. There is no mandatory multi-toolchain release matrix. The supported packages are the root `releasekit` facade and `buildinfo`. Put the executable at `cmd/releasekit`. Start with standard-library implementations and the installed Git and Go executables. Introduce internal packages as their responsibilities land: runtime information, process execution, release engine, Git operations, builds, Gitea transport, and CLI adaptation. Put command-owned parsing and file-format helpers under `cmd/releasekit/internal/cli`; keep facade calls in the `cmd/releasekit` controller. Those helpers return their own settings for explicit conversion by the controller and import neither public facade. The executable therefore consumes the supported APIs while library internal packages retain inward dependencies. There are no public transport clients or assembly APIs. The initial runtime API is `buildinfo.Version() string`. It returns an injected release identifier when supplied; otherwise it uses the running executable's main-module version when present and different from `(devel)`, then `dev` when that metadata is unavailable. This supports source installs as well as explicit release builds. It describes the application version, not the ReleaseKit dependency version. [Go build information](https://pkg.go.dev/runtime/debug#ReadBuildInfo) documents the main-module metadata available to implement this fallback. Keep the injected string private in the internal runtime implementation. Keep the linker symbol identifier in a small internal contract used by the builder. The builder and runtime implementation change together. Consumers call the accessor; their configuration supplies no linker symbol. The runtime import graph uses the standard library and small runtime implementation, independent of orchestration. A tool/runtime version mismatch must be exposed by the native version smoke check; do not introduce an exact dependency-version equality gate. ### Orchestration API Implement these root operations as the corresponding stages land: | Operation | Request and result responsibilities | | --- | --- | | `Check(ctx, ReleaseRequest)` | Capture and validate a local committed candidate; return its commit and version. This diagnostic operation does not query publication services, create a tag, or claim remote readiness. | | `Publish(ctx, ReleaseRequest)` | Validate once, check authoritative state, create and publish the selected annotated tag, and return publication state and retained recovery facts. | | `Build(ctx, BuildRequest)` | Build the selected targets with the requested version, verify a native version invocation, and return source identity and generated artifacts. | | `Deliver(ctx, DeliveryRequest)` | Verify tagged source and supplied artifact identity, create or find a hosted release, compare content, and add missing assets with explicit partial or uncertain outcomes. | Each operation returns a facade-owned result and an error. Requests carry the repository directory explicitly. `ReleaseRequest` also carries the display product name, version, selected remote, optional validation callback, optional build specification, and optional output writer. The remote defaults to `origin`; publication uses `main`. The callback receives a facade-owned `Candidate` containing directory, captured commit, and selected version. A nil callback selects the default Go test invocation. A supplied callback replaces that invocation with the repository's complete selected validation; it is called once. The CLI adapts configured commands into this callback. A build specification carries main package, binary name, and targets. Each facade-owned `Target` contains Go OS and architecture strings. A build request adds version and output directory. A delivery request identifies version, product, repository directory, Gitea destination, and optional build result. The Gitea destination carries base URL, owner, repository name, and caller-supplied token. Library code obtains no credentials from ambient configuration. Before the first remote write, an inspection failure reports that this invocation has not attempted mutation. Reserve uncertain-mutation classification and exit 3 for writes whose authoritative outcome remains unresolved. Publication results identify version, candidate commit, captured endpoint, phase, and tag object when created. Use distinct states for `not_attempted`, `not_published`, `published`, `conflict`, and `unknown`. These describe this invocation; a pre-existing remote release is not described as unpublished. Delivery results identify release URL and the matching, uploaded, pending, and uncertain assets, with `not_attempted`, `complete`, `partial`, `conflict`, or `unknown` state. Ordinary errors preserve useful causes through wrapping. Document exactly when nonzero results accompany errors. Add only the named types needed to represent these responsibilities; field spelling and internal package layout can follow idiomatic Go without changing the decisions here. ### CLI and configuration Use standard-library flag parsing and JSON configuration. The default profile is `/releasekit.json`; `--config` selects another file. Configuration paths are resolved from the selected repository, whose default is the current directory. A profile supplies product identity, remote, candidate commands, optional build specification, and optional Gitea delivery declaration. The command supplies the version rather than persisting it in the profile. Use this eventual profile shape; add each optional responsibility only when its stage implements it: ```json { "product": "Scriptorium", "remote": "origin", "checks": [{"args": ["sh", "scripts/check-release-candidate.sh"]}], "build": { "main_package": "./cmd/scriptorium", "binary_name": "scriptorium", "targets": ["linux/amd64", "linux/arm64"] }, "delivery": { "base_url": "https://gitea.maximumdirect.net", "owner": "eric", "repository": "scriptorium", "token_env": "GITEA_RELEASE_TOKEN" } } ``` This is a planned example, not an existing file or working invocation. Command entries also accept an `env` map. Run argument vectors directly, without implicit shell parsing. A repository that needs shell syntax selects `sh` or `bash` explicitly. Run commands in the captured repository directory and supply `RELEASEKIT_VERSION` and `RELEASEKIT_CANDIDATE` for candidate-specific checks. Those captured values take precedence over conflicting command environment entries. Inherit the remaining environment and overlay explicitly owned values without mutating the caller process. An absent or empty check list selects `go test -count=1 ./...`; a nonempty list is the repository's complete validation sequence. Decode profile fields strictly so unsupported build or delivery settings cannot silently disappear from the selected checks. Validate required values for the operation being run; a source-only profile needs neither a main package nor credentials. The CLI loads the token named by `delivery.token_env`, defaulting to `RELEASEKIT_GITEA_TOKEN`, only for delivery. Build receipt identity covers resolved build inputs; delivery credentials, destination, and unrelated check settings are excluded from that identity. The eventual command forms are: ```text releasekit version releasekit check [--repo DIR] [--config FILE] VERSION releasekit publish [--repo DIR] [--config FILE] VERSION releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION ``` Flags precede the positional version. `version` prints `releasekit `. Use exit 0 for completed success, 1 for a known failure or conflict, 2 for usage or invalid configuration, and 3 for an uncertain remote mutation. Library outcomes retain more detail. Send validation progress and diagnostics to stderr; stdout presents the command's final result. Signals cancel the operation through its context. There is no interactive confirmation loop. ### Candidate checks and Git publication Parse stable versions using the roadmap's exact syntax. Validate decimal components as strings without introducing a machine-integer size limit. Read the release note from the captured commit at `docs/releases/.md`. Its first line must be a Markdown H1 containing the configured product name and exact version as complete title elements. Preserve the note content for the annotated tag. The shared tool imposes no additional note section inventory. `Check` checks a clean worktree and index on `main`, captures HEAD, checks the committed note and unused local version, runs validation and selected builds, and rechecks local mutable state. `Publish` adds authoritative endpoint and remote branch/tag checks before validation and immediately before publication. Cleanliness includes untracked files; ordinary Git ignore rules still apply. Snapshot resolved request values and CLI profile content for the invocation so configuration is not reloaded into different checks midway through the run. Source changes remain subject to the ordinary candidate rechecks. The Go API accepts caller-supplied values without requiring a particular profile file. Resolve the selected remote's effective push destination using Git's own remote configuration semantics. A missing destination or more than one distinct effective publication destination is an actionable configuration error because this operation needs one authoritative endpoint. A separate fetch URL is supported. Inspect the publication endpoint, rather than assuming the selected remote's normal fetch URL is authoritative. Use a transient, command-scoped named remote with explicit read and push URLs and mirror mode disabled. Verify that both effective URLs equal the captured publication destination after Git URL rewriting. Reuse that scoped remote for queries and push; persist no new remote configuration. Retain normal Git credential and transport settings, carrying remote-specific transport settings into the scoped remote as needed. If rewriting prevents establishing this equality, report the unresolved destination before mutation. Recheck the original remote resolution before pushing. Test ordinary `insteadOf`, `pushInsteadOf`, and explicit push-URL cases using local remotes. [Git's remote documentation](https://git-scm.com/docs/git-remote) describes the effective URL lookup on which this relies. Use exact ref names in remote queries. Require authoritative `main` to equal the captured candidate and the selected tag to be absent. Create the annotated tag from the committed note with signing disabled for this invocation, inspect its object type and peeled commit, then freeze its object ID. Push `:refs/tags/` with automatic following of tags and mirror defaults disabled. Publish no branch updates, unrelated tags, or force updates. A successful push returns immediately as published. After a failed or ambiguous push, make one authoritative tag query: an exact object match confirms success; absence confirms not published at observation; a different object is conflict; an unavailable or uninterpretable answer is unknown. Retain the local tag in all failed or uncertain cases. Caller cancellation after push starts also needs this bounded reconciliation attempt using a separate finite recovery context. Use a 30-second recovery deadline as an operational default, not a guarantee that the endpoint will respond. Cancellation before push does not initiate it. The last remote branch check is an observation before the tag push. This design provides no transaction locking remote `main` against concurrent writers. Do not add branch pushes or assert stronger concurrency guarantees in tests. ### Go execution, builds, and assets For ReleaseKit-owned candidate Go commands and builds, use prepared dependencies and a local toolchain. Set child environment values `GOWORK=off`, `GOPROXY=off`, `GONOPROXY=none`, `GOSUMDB=off`, and `GOTOOLCHAIN=local`. Set module mode to readonly, preserving a repository's deliberately selected testing flags. ReleaseKit-owned builds replace ambient `GOFLAGS` with `-mod=readonly` and use their explicit build arguments; custom validation commands retain their deliberately configured flags. Preparation of caches and tools occurs separately. These settings do not sandbox arbitrary repository callbacks or establish a general network firewall. Build with `CGO_ENABLED=0`, `-mod=readonly`, `-trimpath`, `-buildvcs=false`, and linker flags `-s -w` plus the shared version injection. This selects the pure-Go build convention already used by the current asset builders. Additional cgo toolchains remain outside this implementation. The injected string must use a linker-supported initializer. [Go's linker documentation](https://pkg.go.dev/cmd/link) defines the `-X` mechanism. Initially verify linux/amd64, linux/arm64, and darwin/arm64. Represent targets as values rather than a closed enum; other explicit Go OS/architecture pairs can be passed to the prepared compiler and may fail according to its support. This establishes no native-execution promise for other platforms. Asset names are `---`, with `.exe` when the requested Go OS needs it. Binary names and target components must be nonempty filename components without path separators, backslashes, CR, or LF, so names stay in the output directory and have unambiguous manifest entries. Normalize repeated target pairs to one target, retaining their first position. Distinguish the display product name used by notes from this binary name. Preserve selected target order for progress; generate manifests in filename order. For nonempty targets, invoke the built application with `version` and require ` ` plus a final newline. Reuse a target artifact when it matches `GOHOSTOS/GOHOSTARCH`; otherwise build a temporary native executable with the same flags and injection. Determine the host independently of ambient `GOOS/GOARCH`. Native smoke binaries are temporary and are not extra assets. An empty target set performs no Go build or version invocation. Source-only projects may retain an explicitly selected application check; no binary check is silently imposed on them. Candidate build checks use a temporary output directory outside the worktree and discard it. Artifact builds record the commit, version, main package, targets, compiler identity, and generated filename/hash pairs. Return these facts in `BuildResult`. The CLI persists them in an internal local receipt, `.releasekit-build.json`, alongside the generated files after a successful complete build and smoke check. A partial build returns progress without a receipt authorizing delivery. This receipt is for tool coordination, with no public format or attestation promise. It contains no credentials and is not a hosted asset. Build output defaults to `dist`; create the directory as needed, regenerate owned local filenames, and preserve unrelated files. Report generated files on partial local failure and clean up only tool-owned temporary paths. Delivery uses a clean checkout whose HEAD equals the selected local tag's peeled commit; detached HEAD is valid. Read the note from that commit and snapshot the selected settings. Repository-local tracked profiles come from that source; explicit caller-supplied settings are identified in the build result. Verify the hosted repository already has the same tag targeting that commit before creating a release. Existing lightweight historical tags may be inspected and delivered; new publication follows the annotated-tag standard. Do not require current remote `main` to remain at an old release commit during delivery retries. The CLI's `deliver --assets DIR` consumes a verified build receipt and checks file hashes, selected filenames, and source/build-input identity before remote mutation. When no directory is supplied, build into a temporary directory from the tagged checkout and then deliver. With zero targets, optional hosted delivery creates a note page without a binary manifest. Neither mode reruns the candidate test suite or changes source-publication status. ### Hosted delivery and Woodpecker Use a small internal standard-library Gitea REST client. On 2026-10-05 the host's [version endpoint](https://gitea.maximumdirect.net/api/v1/version) reported Gitea 1.25.4; its [served API schema](https://gitea.maximumdirect.net/swagger.v1.json) confirmed release-by-tag lookup, JSON release creation, asset listing, and multipart attachment upload. Consult the host's schema during implementation; record used operations in the integration guide. This observation is not an all-version Gitea compatibility promise. Read the tag through `/api/v1/repos/{owner}/{repo}/tags/{tag}` and verify its `commit.sha`. Use the repository API prefix for each following operation. Find the release through `/releases/tags/{tag}`. Create it through `POST /releases` with the existing tag, captured commit, title from the note's H1, complete committed note as body, and stable, non-draft settings. A found release must match the note body and tag and be stable/non-draft; retain its existing title, including a historical title chosen by other tooling. A content conflict is reported; its body, title, assets, and tag are not edited. Existing historical pages remain intact. List attachments through `/releases/{id}/assets`. Identify expected filenames and stream their downloads to compare SHA-256 content; size and name alone do not prove a match. Use multipart field `attachment` and the filename query parameter for missing uploads. The client obtains no automatic tag-creation responsibility. Keep authorization on the configured service origin when following attachment URLs or redirects; never include token values in receipts or diagnostics. For nonempty binary sets, generate `SHA256SUMS` with lowercase hashes, two spaces, filenames, and LF-terminated lines, sorted by filename. It describes the complete declared binary set and excludes itself and the private receipt. An existing manifest must match that expected content. Fill missing assets covered by that manifest only with matching bytes. A manifest describing a different or smaller set cannot be replaced to extend that version. Leave unrelated attachments untouched. Inspect all known declared asset and manifest conflicts before uploading. Upload missing binaries in filename order, then a missing manifest. HTTP writes are separate operations. After an ambiguous creation or upload, re-read the affected resource once; classify matching content as completed, conflict as conflict, and inaccessible or still-indeterminate content as unknown. Handle concurrent creation by finding and verifying the created resource. Do not blindly retry writes or delete attachments. Record partial progress so an operator can rerun against the same tag and retained assets. Woodpecker uses tag-triggered repository-local YAML with a pinned Go/tool image and a pinned ReleaseKit source install. Provide an example using an official Go image plus a separate provisioning step that installs ReleaseKit from its immutable release pin and prepares consumer module caches. Build and deliver then run with offline Go execution. This honors ReleaseKit's source-only initial distribution; a dedicated ReleaseKit container image would require a separate deliverable decision. Use the repository's selected Go toolchain image, which must satisfy its own module requirements. Use `CI_COMMIT_TAG` and `CI_COMMIT_SHA` to bind the workflow to tagged source, with an explicit tag fetch when the checkout omits local tag refs. The build step writes to ignored `dist`; the delivery step shares the workspace and supplies the Gitea token from a Woodpecker secret. Pin actual image digests and source versions when adopting the example. Pipeline preparation may access dependency services; candidate validation and builds use the prepared caches. CI does not call `publish`, push refs, or gate source completion. The supported retry is a rerun from the same tagged source with content comparison; reproducible bytes across arbitrary toolchains or elapsed time are not promised. ## Stage 1: Establish the Go module and development baseline Status: Pending. Depends on the baseline only. 1. Add the module with the selected path and language baseline. Add root package documentation for the orchestration facade and establish ordinary Go layout. 2. Keep committed module state independent of sibling checkouts and workspaces. Start without external runtime dependencies or a generic plugin framework. 3. Add the working offline developer test command and initial implemented-package inventory. Update README to its actual implemented boundary. Acceptance: module loading and `go test ./...` work with the prepared local toolchain and network disabled. Review module identity and licensing notices. Do not invent tests merely to assert that an empty package exists. ## Stage 2: Implement the runtime version facade Status: Pending. Depends on Stage 1. 1. Implement `buildinfo.Version` and its internal injected-string and main-module fallback logic. Keep runtime state private and the public accessor small. 2. Add the internal injection-symbol contract without importing orchestration into the runtime path. Test injected precedence and missing/development metadata through stable internal seams and a small executable fixture. 3. Add `cmd/releasekit` with a working `version` command using this accessor. Record its implemented command contract and the runtime consumer guidance. Acceptance: a source-built fixture reports `dev`; a fixture linked with a stable version reports that version. Fallback metadata uses the executable main module. CLI version output is correct and no runtime orchestration initialization occurs. ## Stage 3: Protect public contracts and dependency direction Status: Pending. Depends on Stage 2. 1. Add a compiled-export boundary checker for both facades using `go list` export data and `go/types`. Inspect exported signatures, fields, callbacks, aliases, generic constraints, and pointer/value method sets recursively. 2. Assert facade ownership of public named types. Permit deliberately selected standard-library contracts. Protect the private-state forwarding approach against leaked internal types and promoted implementation methods. 3. Add import-direction checks: internal production packages use internal contracts; the runtime facade's dependency closure excludes orchestration, process, build, Gitea, and CLI implementation. 4. Use small negative fixtures proving that the checker catches a leaked type and promoted internal method, and accepts a private implementation field. Acceptance: actual exports pass and meaningful negative fixtures fail. Tests inspect contracts and imports, with no fixed inventory of future internal package names. Extend these checks as subsequent exports land. ## Stage 4: Implement profile loading and command adaptation Status: Pending. Depends on Stage 3. 1. Implement JSON product, remote, and command settings under an internal CLI adapter. Add strict field decoding, directory resolution, required-value diagnostics, direct argument execution, and per-child environment overlays. 2. Implement an internal cancellable process runner with directory, argument, environment, stdout, stderr, and exit-error support. Use `os/exec` directly; expose no public runner abstraction solely for test assembly. 3. Add CLI parsing infrastructure and selected usage/exit behavior. Advertise commands only when their following stages implement them. 4. Add small profile examples and `docs/config.md` for implemented fields. Acceptance: paths and arguments containing spaces remain separate arguments; selected environment values reach the child without changing process globals; missing executables, invalid configuration, and cancellation report useful errors. Tests use controlled local child commands and temporary directories. ## Stage 5: Capture and validate one local candidate Status: Pending. Depends on Stage 4. 1. Implement stable-version syntax, clean `main` and index/worktree inspection, captured HEAD, profile snapshot/note loading, title identity, and unused local tag checks. Preserve immutable candidate facts in internal values. 2. Implement default offline Go validation and callback adaptation. Bind the callback to the captured candidate; pass captured version/commit to CLI commands. Call the complete validation callback once, stopping on failure. 3. Recheck branch, HEAD, and cleanliness after validation. Retain the captured settings throughout the operation. Implement the public `Check` operation and local diagnostic `check` command. 4. Document that local check success provides local candidate evidence and makes no authoritative remote availability claim. Acceptance: temporary repositories cover invalid versions, missing or mismatched committed notes, dirty tracked/untracked state, existing local tags, detached HEAD, callback failure, and a callback changing HEAD or the worktree. Count a successful callback once. No test needs a publication service, and `Check` creates no tag or remote ref. ## Stage 6: Resolve and inspect the authoritative endpoint Status: Pending. Depends on Stage 5. 1. Implement effective push-URL capture, command-scoped remote configuration, matching read/write resolution, and endpoint rechecks described above. 2. Implement exact remote main/tag queries with distinct present, absent, conflicting, and unreadable results. Query failures never establish absence. 3. Compose the pre-validation and pre-publication remote guards around captured local state. Require main equality, rather than ancestor-only publication. Acceptance: local remotes cover a separate fetch endpoint, explicit push URL, ordinary URL rewrites, multiple push destinations, missing main, conflicting tags, main ahead/behind the candidate, endpoint changes, and remote failure. Queries leave local tags, persistent remote configuration, and remote refs intact. ## Stage 7: Create and push the exact annotated tag Status: Pending. Depends on Stage 6. 1. Implement public publication request/result values and explicit conversion into the engine. Validate once between the complete preflight guards. 2. Create an annotated tag from captured note bytes, inspect type and peeled commit, capture its object ID, and push only that object to the selected ref. 3. Recheck mutable local state and endpoint before the push, including the local tag still identifying the captured object. A changed local tag prevents this invocation from proceeding; cleanup remains deliberate recovery. 4. On successful push, return published state without another remote read-back. On push error, retain the tag and return unknown until Stage 8 adds resolution. Acceptance: a local bare remote receives the expected tag object and commit; its main and unrelated refs do not change. Follow-tags and mirror configuration cannot expand publication. Note content, tag identity, and one-validation behavior are checked through the public facade. Restrict stage trials to test remotes while failure handling is incomplete. ## Stage 8: Resolve rejected and ambiguous publication Status: Pending. Depends on Stage 7. 1. Add the one-query reconciliation path and public error/result classification. Preserve the original push cause when resolution does not confirm success. 2. Give post-push cancellation a separate bounded recovery context. Retain local tag/object facts when the read fails or recovery is cancelled/times out. 3. Add real rejected-push tests using bare-remote hooks. Simulate lost acknowledgments at the subprocess boundary after a real successful push. Separate known absence, conflicting object, and unavailable-query cases. 4. Cover a remote branch change during validation and local tag mutation before push. Keep concurrency promises at the observed guard boundaries. Acceptance: exact remote object match succeeds despite a reported push error; missing, conflicting, and unavailable remote tags have distinct outcomes. Failed pushes issue one reconciliation query, successful pushes issue none, and no recovery force-update or automatic tag deletion occurs. ## Stage 9: Expose publication and document ReleaseKit bootstrap Status: Pending. Depends on Stage 8. 1. Wire the complete public publisher into `releasekit publish`, with selected progress output, final result, exit classification, and signal cancellation. 2. Add ReleaseKit's source-only profile and one short maintainer entry script. The profile selects the offline default suite; no inherited race, lint, documentation, image, or platform gates are added. 3. Create `docs/release.md` now that publication works. Name authoritative `origin` at the ReleaseKit forge URL and `main`; document preparation, successful-push completion, exact-object recovery, local tag retention, and deliberate cleanup of proven unpublished tags. 4. Document first-release execution from the captured candidate source using the local Go toolchain, before a released ReleaseKit executable exists. Include immutable release-note licensing footer construction and dependency preparation separate from validation. 5. Update current development documentation and the policy-adoption roadmap's bootstrap status to the implemented boundary. Preserve pending deliverables. Acceptance: an end-to-end CLI fixture publishes to a local remote through a committed profile and returns the expected exit/output behavior. Runbook commands are exercised using a temporary repository configured like ReleaseKit. This stage prepares self-publication and does not publish a real ReleaseKit release. ## Stage 10: Build selected targets with shared version injection Status: Pending. Depends on Stage 9. 1. Implement public build values and internal build execution using the fixed flags, version symbol, filename convention, and prepared Go environment. 2. Build explicit OS/architecture pairs and verify the application `version` invocation using a host artifact or temporary native build. Preserve caller-owned output files and report partial generated output on failure. 3. Return source identity, artifact paths, filenames, targets, hashes, and compiler identity. Keep byte reproducibility and cross-platform execution promises outside the result contract. 4. Add a temporary consumer module importing only `buildinfo` and exposing the canonical version command. Its local replacement is fixture-only. Acceptance: the fixture compiles for the three initial targets and a native execution reports the injected version. An ambient cross-target environment does not select the smoke host. Empty targets perform no build or smoke test; missing main packages, build failures, incorrect version output, unsafe binary names, and cancellation fail with useful partial results. ## Stage 11: Integrate candidate builds and artifact command Status: Pending. Depends on Stage 10. 1. Add build profile fields, CLI `build`, and the private artifact receipt. Bind receipts to source/build-input identity and generated hashes. Validate data on reload; leave the receipt outside the public API and hosted asset list. 2. Integrate selected builds into `Check` and `Publish` after the single candidate callback. Use disposable output outside the worktree for candidate builds. Recheck mutable state after the entire validation/build sequence. 3. Add ignored `dist` output and source-only and binary-producing examples. Document the API, profile target selection, version invocation, and build output ownership. 4. Refresh facade and runtime dependency checks with the new values. Acceptance: source-only publication invokes no builder; selected build failure prevents tag creation; selected builds inject the exact candidate version; callback validation runs once. CLI artifacts and receipts agree on filenames, hashes, commit, and resolved build settings. Generated validation output does not dirty the candidate. This completes the first source-publication/build capability slice. ## Stage 12: Pilot publication in PromptKit Status: Pending. Depends on Stage 11 and a reachable immutable tool pin. 1. Review PromptKit's current instructions, release policies, and entry scripts. Add a source-only profile using its actual note display name, currently `Promptkit`. Keep its application-specific consumer example checks and other deliberately adopted candidate gates in its validation command. 2. Replace repeated release guards and tag publication with a short ReleaseKit invocation. Move note identity validation to ReleaseKit; remove extra shared note-section requirements only through the corresponding local policy change. 3. Update installation prerequisites, local release wiring tests, and maintainer documentation. Retain no duplicate mandatory pre-check before publication. Acceptance: isolated repository fixtures exercise the new wrapper and profile against a local remote, preserve source-only behavior, and run the selected candidate checker once. PromptKit's own suite passes. Record deleted shared mechanics separately from retained product checks. No Go runtime dependency or hosted page is introduced for this library. ## Stage 13: Pilot publication in Scriptorium Status: Pending. Depends on Stage 12. 1. Adapt Scriptorium's publication entry point and release policy to the common profile/command. Preserve its selected candidate test flags and genuinely product-owned checks in a small local candidate command. 2. Retain its existing candidate build check temporarily: its application has no canonical runtime version command yet. Document that shared builds are deferred to Stage 19; the existing delivery pipeline continues meanwhile. 3. Transfer exact-object publication/failure tests into shared coverage and retain a small wrapper/configuration test locally. Update its runbook. Acceptance: local-remote fixtures confirm common publication and bounded recovery without changing the existing asynchronous deliverables. Scriptorium's own suite passes. The two pilots substantiate the same public facade with different product checks and endpoint configurations. ## Stage 14: Find and create Gitea release pages Status: Pending. Depends on Stage 13. 1. Implement the small internal REST transport using the served API operations, explicit destination/token inputs, context cancellation, and structured failure mapping. Use standard HTTP and JSON packages. 2. Verify the hosted tag targets the captured source commit. Find a release by tag, create a missing stable release from the committed note, and verify existing note body/tag and stable, non-draft state before accepting it. Preserve the existing title. 3. Handle create conflicts and lost acknowledgments by one lookup and content comparison. Never edit the existing page to make it match. 4. Add `docs/integrations/gitea.md` describing implemented API interactions, credential supply, source binding, and failure semantics. Acceptance: `httptest.Server` covers found, missing, created, conflicting, unauthorized, malformed, cancelled, and ambiguous-create results. A missing or wrong hosted tag prevents page creation. Existing historical titles are retained when the body, tag, and release state match. Fixture assertions cover actual request paths, payloads, and authentication without using real credentials. ## Stage 15: Inspect asset and manifest content Status: Pending. Depends on Stage 14. 1. Generate the expected binary manifest from the declared build result. Add attachment discovery and streaming hash comparison of existing assets. 2. Compare existing `SHA256SUMS` with the complete declared manifest. Identify matching, missing, conflicting, and unverifiable content separately. 3. Inspect all declared conflicts before any upload and preserve unrelated attachments. Keep download authorization confined to the configured origin. Acceptance: equal name/size with different bytes is conflict; matching content is accepted; a full matching manifest can cover missing binaries; a subset or different manifest is conflict; unreadable content is not treated as missing. Source-only delivery generates no binary manifest. No write endpoint is used by inspection tests. ## Stage 16: Upload missing assets and report partial delivery Status: Pending. Depends on Stage 15. 1. Add multipart uploads for missing binaries in filename order, followed by a missing manifest. Record completed work and pending asset identities. 2. Resolve ambiguous uploads by one resource read and content comparison. Handle concurrent matching uploads without creating replacement content; detect duplicate/conflicting attachment names as conflicts. 3. Implement rerun behavior that accepts existing matching files, adds missing ones, and stops on conflicts. Preserve every existing page and attachment. 4. Add tests where the first binary succeeds, a later write fails, and a second invocation completes only the missing assets; also cover a lost manifest acknowledgment and failure of the reconciliation read. Acceptance: partial and unknown outcomes retain exact completed/pending facts; retries do not overwrite or delete content; no blind repeat POST is made after an ambiguous acknowledgment. Published manifest constraints hold across reruns. ## Stage 17: Expose tagged-source delivery through the facade and CLI Status: Pending. Depends on Stage 16. 1. Implement `Deliver`, its facade-owned request/result values, and conversions. Bind source note, resolved build settings, tag commit, and hosted repository as specified. 2. Add delivery profile fields, environment-name token loading in the CLI, and `deliver` with an optional verified assets directory. With no directory, build tagged-source artifacts in temporary storage before uploading. 3. Accept detached tagged checkouts and historical tags without requiring old remote main equality. Verify receipt identity/hashes before supplied assets are used. Keep the library credential input explicit. 4. Add current consumer/CLI guidance for source status, delivery status, retained assets, and retry commands. Extend public boundary checks. Acceptance: a facade/CLI end-to-end fixture builds tagged source and delivers to a local server; a rerun is content-aware. Wrong commit, stale build settings or receipt, tampered assets, and missing credentials prevent relevant writes. Candidate tests and Git publication are not run by delivery. A delivery failure leaves the existing source tag intact and reports only delivery failure. ## Stage 18: Provide the common Woodpecker integration Status: Pending. Depends on Stage 17 and the delivery-capable tool pin. 1. Add a maintained example tag pipeline using a pinned Go image, explicit ReleaseKit source installation and cache preparation, artifact build, and delivery with the configured secret. Explain how consumers pin their image and tool source; create no new ReleaseKit image-distribution requirement. 2. Bind tag and commit variables to the checkout, fetch the selected tag when necessary, share generated assets between steps, and keep output ignored. 3. Document preparation versus offline execution, asynchronous status, and rerunning the same immutable tag. The workflow executes no publisher. 4. Exercise its shell command sequence locally against a fixture and local server. Verify YAML with an available supported Woodpecker validator when available; report that live runner behavior remains unverified otherwise. Acceptance: the maintained commands reach common build/delivery operations with the selected version and commit, and secrets enter only the delivery step. Static/manual YAML review and local command evidence are recorded accurately. CI success is not a source release gate. The [Woodpecker environment reference](https://woodpecker-ci.org/docs/usage/environment) owns the runner-provided tag and commit variables. ## Stage 19: Standardize Scriptorium runtime information and builds Status: Pending. Depends on Stage 18 and a released runtime dependency pin. 1. Add the `buildinfo` dependency and canonical `scriptorium version` command before configuration/provider initialization. Use graph discovery to locate its CLI boundary; the earlier implementation has no release version command. 2. Set main package `./cmd/scriptorium`, binary name `scriptorium`, and its existing Linux amd64/arm64 asset targets in the profile. 3. Replace the temporary local candidate build loop from Stage 13 with shared candidate builds. Retain only product checks and a small version/wiring test. 4. Update current CLI contracts and compatibility impact documentation. Acceptance: shared builds report the injected version without credentials or service access. The unchanged two-asset selection is used for candidate checks. Runtime dependency checks show the application needs only `buildinfo` for this responsibility; its ordinary suite passes. ## Stage 20: Pilot common delivery in Scriptorium Status: Pending. Depends on Stage 19. 1. Replace its duplicated Woodpecker build loop and release-plugin step with the common example adapted to its pinned Go/tool versions and Gitea destination. 2. Keep the committed note as the hosted body and maintain the existing two Linux assets and checksum deliverable. Add ignored local build output. 3. Remove its repeated asset builder code once local fixture evidence exercises common build and retry delivery. Update its delivery runbook and tests. Acceptance: local fixture execution verifies first delivery, matching rerun, partial delivery recovery, and conflicting bytes. The repository's source publication path waits for no CI or attachments. Record live activation as a separate maintainer action; preserve historical release workflows at old tags. ## Stage 21: Migrate ConfigKit publication Status: Pending. Depends on Stage 20. 1. Add a source-only profile and wrapper using the common publisher. Its current sequential main/tag publication becomes main-first preparation and tag-only publication. Update the adopted policy and maintainer runbook together. 2. Preserve its existing selected baseline-toolchain, test, vet, and build checks in its candidate checker where its local policy justifies them. These remain ConfigKit checks, not ReleaseKit-wide requirements. 3. Remove duplicated ref checks, annotated-tag creation, and push recovery. Retain a small profile/wrapper test and run its suite. Acceptance: a local fixture refuses publication until authoritative main equals the candidate, then publishes only the tag with one candidate-check invocation. ConfigKit gains no runtime import, hosted page, or distributed binary. ## Stage 22: Migrate Notarius publication Status: Pending. Depends on Stage 21. 1. Add a source-only profile and common publication wrapper. Its existing branch push and lightweight tag become prepared main plus annotated tag. Update the adopted policy and procedure with that deliberate change. 2. Retain its existing explicit host version check temporarily in its local checker. Remove repeated common release guards and shared publication tests. 3. Preserve a small wrapper/check invocation test and verify ordinary behavior. Acceptance: local remotes demonstrate the new publication approach and bounded recovery. Source-only deliverables and the current version-check prerequisite remain unchanged until the next stage. ## Stage 23: Standardize Notarius runtime version access Status: Pending. Depends on Stage 22 and a released runtime dependency pin. 1. Replace its private override/resolution mechanism with `buildinfo`. Preserve its existing `--version` invocation and add the canonical `version` command for common smoke checks, with `notarius ` output. 2. Document the common metadata/`dev` fallback and its deliberate effect on any existing unversioned-build diagnostic behavior. Update owned tests/contracts rather than silently preserving obsolete private override assumptions. 3. Replace the explicit host check's private linker flags with a small local invocation of the shared Build API for the host target. This remains an explicitly selected local check, not a distributed target declaration. The API accepts explicit host Go targets as described above. Acceptance: both version invocations report the same injected value without configuration or credentials, and the deliberate source-build behavior is covered. The source-only profile still has no automatic binary targets or Gitea page; the separately selected host check runs once with its candidate checker. ## Stage 24: Migrate Audita publication Status: Pending. Depends on Stage 23. 1. Replace atomic main/tag publication with main-first preparation and the common annotated-tag publisher. Retain its `--check` user entry by mapping it to common local `check`; map `--publish` to common publication. 2. Preserve current tests and its existing builder temporarily as selected candidate checks. Update the local release policy and completion wording. 3. Transfer shared atomic/lightweight publication tests to the common contract, retaining tests of the Audita command mapping and local checks. Acceptance: diagnostic mode performs no remote publication; publication refuses an unprepared main and uses an annotated tag. The configured candidate sequence runs once and existing asynchronous Linux deliverables remain selected. ## Stage 25: Standardize Audita runtime information and builds Status: Pending. Depends on Stage 24 and a released runtime dependency pin. 1. Add `audita version` using `buildinfo`, before application configuration and provider initialization. The earlier CLI has no release-version accessor. 2. Declare `./cmd/audita`, binary name `audita`, and linux/amd64 plus linux/arm64. Replace `scripts/build-release.sh` mechanics with shared candidate/build operations and retain product-owned tests. 3. Add a small application version-command test and update current contracts. Acceptance: native smoke verifies the injected tag and needs no credentials; both existing Linux assets retain their names/target selection. Audita's own suite passes and duplicate candidate build work is removed. ## Stage 26: Migrate Audita delivery Status: Pending. Depends on Stage 25. 1. Adapt the common tag workflow to Audita's pinned image/tool, Gitea repository, and secret name. Replace release-plugin skip-by-name behavior with shared content verification and additive delivery. 2. Update delivery/retry documentation and remove unused build wrapper code. Acceptance: local first-run, rerun, conflicting-content, and partial-delivery fixtures exercise the actual configured filenames and complete manifest. Source completion and live activation remain separate from delivery verification. ## Stage 27: Migrate Narratio publication Status: Pending. Depends on Stage 26. 1. Retain main-first preparation and replace lightweight tags with common annotated publication. Adapt its candidate checker and release entry point, keeping its selected product checks and build mechanics temporarily. 2. Update policy/runbook and retire common version/note/ref helpers where their behavior is now shared. Preserve remaining helper uses until Stage 28. Acceptance: local publication fixtures cover exact candidate/tag identity and common recovery; the checker runs once. Narratio retains its three declared asset targets and existing delivery pipeline until subsequent stages. ## Stage 28: Standardize Narratio runtime information and builds Status: Pending. Depends on Stage 27 and a released runtime dependency pin. 1. Replace its private buildinfo variable with the facade in its existing `narratio version` command. Preserve output shape and update tests to use supported behavior rather than mutating a private version variable. 2. Declare `./cmd/narratio`, binary name `narratio`, and linux/amd64, linux/arm64, darwin/arm64. Use the prepared toolchain required by its module. 3. Replace `build-release-assets.sh` flags, host smoke, and target loop with shared builds. Remove duplicate build invocation from its candidate checker. Preserve product checks and remove helpers that now have no remaining use. Acceptance: the native version probe and all three cross-builds pass; foreign platform compilation is reported without a native-execution claim. A nonempty caller output directory can retain unrelated files, and no broad recursive cleanup is inherited from the former builder. ## Stage 29: Migrate Narratio delivery Status: Pending. Depends on Stage 28. 1. Replace its release plugin and repeated tag-pipeline candidate validation with the common prepared build/delivery workflow. Candidate validation belongs to source publication; tagged delivery does not repeat it as a gate. 2. Keep its three binary assets, committed note, Gitea destination, and complete checksum manifest. Adapt pinned images and tool versions to its Go baseline. 3. Update retry documentation and small repository-local integration checks. Acceptance: fixture workflow evidence verifies all three filenames, full manifest, matching rerun, and partial/ambiguous delivery. Existing historical tags/pages/assets are untouched and source publication waits for no delivery. ## Stage 30: Audit the completed target state and documentation Status: Pending. Depends on Stage 29. 1. Map each feature-roadmap responsibility to its implemented owner and tests. Verify both public facades, source-only behavior, candidate checks, exact publication, runtime injection, selected targets, hosted content verification, partial recovery, and Woodpecker integration against current source. 2. Review the six migrations against both architecture success criteria: shared infrastructure was removed and one coherent model was adopted. Count actual removed scripts/tests, retained product checks, and new ReleaseKit code separately; do not present the historical estimate as measured savings. 3. Check current documentation owners, links, examples, dependency attribution, license texts, and maintainer runbooks. Record live integration verification separately from local acceptance, including anything awaiting activation. 4. Update feature status and this plan from evidence. Retire completed execution details during release preparation while preserving target intent, deferred decisions, and useful navigation. Acceptance: each roadmap responsibility has direct source and behavioral evidence, each migrated repository retains only justified product checks and small wiring coverage, and no unimplemented or unverified responsibility is marked complete. Run affected suites according to their policies; add no new permanent release matrix or synchronization gate. ## Coverage of the feature roadmap | Target responsibility | Implementing stages | | --- | --- | | Two public facades and small runtime dependencies | 1–3, extended in 10–11 and 17 | | Stable version and committed note identity | 5 | | Candidate capture, one validation, mutable guards | 5–8, 11 | | Authoritative endpoint and remote ref state | 6–8 | | Annotated object publication and failure recovery | 7–9 | | ReleaseKit source-only bootstrap/runbook | 9 | | Shared version injection and selected cross-builds | 2, 10–11 | | Native smoke, empty target sets, consistent names | 10–11 | | Hosted page/note creation and immutable content | 14–17 | | Full checksums, comparison, partial/additive retries | 15–17 | | Woodpecker tag integration and separate statuses | 18, 20, 26, 29 | | PromptKit and Scriptorium pilots | 12–13, 19–20 | | ConfigKit, Notarius, Audita, Narratio adoption | 21–29 | | Actual infrastructure reduction and canonical docs | 30 | ## Open Questions The core implementation choices above are complete for the recommended design. These maintainer decisions affect adoption and live use; do not fill in unknown release pins or silently broaden deliverables. ### Which immutable ReleaseKit versions should consumers pin? Required before Stage 12, and refreshed before Stage 18 and runtime migrations. **Recommended:** publish a first source release such as `v0.1.0` after Stage 11, then a delivery-capable source release after Stage 17. Verify version availability at preparation time and use the actual published pins in profiles' accompanying installation instructions, CI provisioning, and application module dependencies. This gives the publication pilots an early usable tool and lets later delivery and runtime adopters depend on documented versioned contracts. Publication is a separate maintainer-authorized action under the release policy. **Viable alternative:** make immutable source-commit/pseudo-version pins available for the pilots and publish the first numbered release after Stage 17. This supports integration feedback before a numbered release, at the cost of more operator bookkeeping and less friendly installation instructions. Neither approach commits local checkout replacements or claims an unpublished version exists. Record the selected pins here before dependent migration stages proceed. ### Should source-only projects also receive hosted Gitea release pages? This decision is required only if adopting pages for PromptKit, ConfigKit, Notarius, or ReleaseKit; their current plan stages preserve source-only delivery without hosted-page requirements. **Recommended:** retain their existing delivery scope initially. Support explicit zero-asset hosted delivery in ReleaseKit, while using the three existing binary publishers to establish its behavior. This meets the requested standardization without adding credentials, CI operations, or a new deliverable to other projects. **Viable alternative:** declare a hosted note page for every project, with zero binary assets where appropriate. This gives uniform forge presentation and uses the same delivery API, but needs deliberate local release-policy changes, credentials, and responsibility for retries. If selected, add a small adoption stage for each affected project before completing Stage 30.