Retire completed roadmaps and preserve adoption work

This commit is contained in:
2026-10-06 14:27:17 +00:00
parent af1a76a2f7
commit 536a57916f
7 changed files with 42 additions and 1636 deletions

View File

@@ -1,997 +0,0 @@
# ReleaseKit staged implementation plan
Status: Complete. Stages 1–18 are complete.
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 `6c2695f`. 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 provide design evidence; this implementation sequence
changes ReleaseKit only. Consumer migrations and measurement of deleted consumer
infrastructure belong to later adoption work. Consult code through the knowledge
graph, indexing a repository when necessary. Documents, scripts, and CI
configuration may be read directly under the discovery instructions.
## Scope and completion
Deliver the complete initial library, executable, configuration, shared build
and delivery mechanics, Woodpecker examples, and ReleaseKit's own prepared
release wiring. Hosted Gitea release pages are part of the selected standard
for every project, including source-only projects. Source-only delivery uses
the committed release note and zero binary assets; it needs no binary build or
checksum manifest.
Proceed through all stages using the local checkout and controlled fixtures.
No stage depends on an intermediate release, published tool/runtime pin,
consumer pilot, external service action, or human review. Ordinary automated
development checks accompany implementation under the testing policy.
After the complete initial implementation, the maintainer will review the
architecture, address discovered issues, and then choose the broader testing
plan and next steps. Consumer adoption, live integration testing, secret
provisioning, CI activation, and actual publication are outside this sequence.
## 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.
Apply the selected scope and decisions throughout the sequence. 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.
- Keep the implementation self-contained. Build the tool from this checkout;
temporary fixture modules may use local replacements. Integration templates
accept an operator-selected immutable tool pin for later deployment, without
requiring one to complete this plan or claiming that a release already exists.
- Complete stages with local implementation commits and controlled checks.
Prepare runnable automation and deployment instructions without activating
live workflows. Distinguish implemented integration from verified live use.
Finish the whole sequence before the maintainer's architecture review; add no
intermediate approval or external-action checkpoint.
## 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 `<repository>/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.
This composite profile shows the implemented fields together; working
[source-only](../../examples/releasekit.json) and
[binary-producing](../../examples/releasekit-binary.json) profiles are maintained
under `examples/`:
```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"
}
}
```
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;
source-only checks and publication need neither a main package nor credentials.
Hosted page delivery requires a destination and token for source-only profiles
as well as binary-producing profiles.
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 <version>`.
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/<version>.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
`<tag-object-id>:refs/tags/<version>` 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 `<binary-name>-<version>-<goos>-<goarch>`, 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
`<binary-name> <version>` 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, hosted delivery creates
the required note page without a binary manifest or build receipt. An absent
build specification is equivalent to zero targets for source-only delivery.
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. Implement the operations described here against
local fixtures and record them in the integration guide. Verification against
the deployed host belongs to later live testing and is not needed to complete
this sequence. 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 image
and an explicit tool preparation step. Provide source-only and binary-producing
examples. Consumer templates accept an operator-selected immutable ReleaseKit
source pin for deployment; template preparation and local execution use the
implementation checkout and require no published pin. Prepare consumer module
caches separately, then run builds and delivery with offline Go execution.
This honors ReleaseKit's source-only distribution; a dedicated ReleaseKit
container image would require a separate deliverable decision. Use a Go image
that satisfies the repository's module requirements.
ReleaseKit's own prepared workflow builds `cmd/releasekit` from its tagged
checkout using the local toolchain and delivers its hosted note page. It needs
no previously released executable and produces no distributed binary assets.
Prepare this workflow locally; activation belongs to the later next-step plan.
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. For binary
profiles, the build step writes to ignored `dist` and the delivery step shares
the workspace. Source-only workflows call delivery directly without an artifact
build step. Supply the Gitea token only to delivery from a Woodpecker secret.
Document operator substitution of image digests and source pins for eventual
consumer deployment. 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: Complete. 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.
Completion evidence: added the Go 1.25.0 module and documented root facade,
updated the README and developer guide to describe the implemented boundary and
offline test command, and confirmed module loading and `go test ./...` with
network access disabled. The module has no external dependencies; existing
LGPL licensing and attribution notices remain consistent.
## Stage 2: Implement the runtime version facade
Status: Complete. 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.
Completion evidence: implemented `buildinfo.Version`, its private injected
string and main-module fallback, and the internal linker-symbol contract. Unit
tests cover injection precedence and missing, development, and stable main
module metadata. Built and ran a separate consumer executable from source and
with an injected stable version using network-disabled Go commands; both
reported the selected value. CLI tests cover its version line, and the CLI's
dependency closure contains no orchestration package. Added runtime consumer
guidance and documented the command contract.
## Stage 3: Protect public contracts and dependency direction
Status: Complete. 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.
Completion evidence: added `internal/apicheck`, which loads compiled exports
from `go list -deps -export` and inspects signatures, fields, aliases, generic
constraints, and pointer and value method sets through `go/types`. The checks
pass for both public facades and enforce internal import direction and the
runtime dependency boundary. Negative fixtures detect leaked types, aliases,
callbacks, generic constraints, and promoted implementation methods; a private
implementation field and selected standard-library contracts pass. Added the
checker command and internal package inventory to the canonical developer docs.
## Stage 4: Implement profile loading and command adaptation
Status: Complete. 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.
Completion evidence: added strict profile decoding, repository-relative and
absolute profile resolution, required product and command diagnostics, and
direct check argument adaptation. Added a cancellable `os/exec` runner with
working directory, inherited environment plus child-only overlays, stdout,
stderr, and preserved exit errors. Tests cover spaced paths and arguments,
environment isolation, missing executables, exit failures, cancellation, and
invalid profiles. Added a profile example and canonical CLI, configuration, and
internal package documentation.
## Stage 5: Capture and validate one local candidate
Status: Complete. 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.
Evidence: `internal/candidate` validates stable versions and committed note
identity, captures a clean `main` commit, runs either the offline default or the
configured callback once, and rechecks local mutable state. The public
`releasekit.Check` API and `releasekit check` command provide the local
diagnostic. Temporary Git repositories cover the acceptance cases; offline
`go test -count=1 ./...` and `go run ./internal/apicheck` pass.
## Stage 6: Resolve and inspect the authoritative endpoint
Status: Complete. 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.
Evidence: `internal/git` captures effective push destinations through Git,
rejects multiple distinct destinations, verifies scoped URL rewriting, and
queries exact main and version-tag refs. A candidate check requires main to
equal the captured commit and the version tag to be absent. Local bare remote
tests cover separate fetch/push URLs, both rewrite modes, missing main, an
existing tag, main ahead and behind, endpoint changes, unreadable remote state,
and unchanged persistent config and refs.
## Stage 7: Create and push the exact annotated tag
Status: Complete. 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.
Evidence: the public `releasekit.Publish` API captures the committed note,
validates once between exact remote guards, creates an unsigned annotated tag,
rechecks local and remote state, and pushes only the captured tag object. Bare
remote tests verify the note bytes, peeled commit, tag object, unchanged branch
and unrelated refs, disabled follow-tags and mirror expansion, validation count,
remote conflicts, and retained local tags after push error; Stage 8 adds outcome
classification through remote reconciliation.
## Stage 8: Resolve rejected and ambiguous publication
Status: Complete. 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.
Evidence: failed tag pushes receive one bounded exact-tag query. Matching object
identity returns published; absence returns not_published with the original push
error; a different object returns conflict; query failure returns unknown while
preserving the push cause. Tests cover rejection by a bare remote's receive
hook, an installed conflicting tag, unavailable reconciliation, and a lost
acknowledgment after the bare remote accepted the tag while the caller context
was canceled. Query counts confirm one recovery read after errors and none after
a successful push.
Tests also verify that a remote branch change during validation and a changed
local tag before push prevent publication.
## Stage 9: Expose publication and document ReleaseKit bootstrap
Status: Complete. 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.
Evidence: `publish` runs the public publisher with the CLI signal context,
selected progress output, final commit and tag-object output, and separate exit
status for unresolved outcomes. A temporary committed profile runs the offline
test command and publishes to a local bare remote. ReleaseKit's root profile,
entry script, CLI and configuration guides, development guide, and maintainer
runbook describe source-only publication and exact-object recovery. The
runbook's dependency preparation is separate from offline validation. No real
ReleaseKit release was published.
## Stage 10: Build selected targets with shared version injection
Status: Complete. 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.
Evidence: `releasekit.Build` and `internal/build` compile Linux amd64, Linux
arm64, and macOS arm64 artifacts using the shared linker symbol and prepared
local Go environment. The consumer fixture imports `buildinfo` and reports the
injected version through its `version` invocation. Results retain the observed
source commit, compiler identity, normalized targets, absolute paths, filenames,
and SHA-256 digests. Tests cover host selection despite ambient cross-target
variables, empty target sets, existing output preservation, partial target
failure, missing packages, incorrect version output, unsafe names, and
cancellation with completed artifacts retained. No byte-reproducibility or
cross-platform execution guarantee is added.
## Stage 11: Integrate candidate builds and artifact command
Status: Complete. 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. Continue directly to hosted delivery implementation;
no publication or consumer pilot is needed to complete this stage.
## Stage 12: Find and create Gitea release pages
Status: Complete. Depends on Stage 11.
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 13: Inspect asset and manifest content
Status: Complete. Depends on Stage 12.
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 14: Upload missing assets and report partial delivery
Status: Complete. Depends on Stage 13.
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 15: Expose tagged-source delivery through the facade and CLI
Status: Complete. Depends on Stage 14.
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 declared tagged-source artifacts in temporary storage before uploading.
An absent build specification or empty target set selects page-only delivery:
skip the builder, native smoke, receipt requirement, and manifest generation.
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: facade/CLI fixtures cover both page-only delivery and binary delivery
from tagged source to a local server, with content-aware reruns. Page-only
delivery preserves the complete note, needs no main package or receipt, and
performs no Go build or asset upload. 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.
Evidence: `releasekit.Deliver` and `releasekit deliver` now verify lightweight or
annotated local tags from clean, attached or detached checkouts, compare the
hosted tag commit, and use the note read from the tagged commit. The CLI loads
delivery credentials only for delivery; it builds selected tagged-source
artifacts in temporary storage or verifies a supplied build receipt and its
artifact hashes before calling the facade. Page-only fixtures create and reuse
the complete note without a main package, receipt, manifest, build, or upload.
Binary fixtures build a tagged-source executable, upload it with `SHA256SUMS`,
and accept matching page and asset content on rerun. Local server tests verify
that wrong hosted and local commits, stale build settings, tampered receipt
assets, and missing credentials prevent remote writes; the tag remains intact
after delivery failure. `go test ./...`, `go run ./internal/apicheck`, and the
Markdown whitespace check pass. All delivery checks use a local HTTP fixture;
no live Gitea service or credentials were used.
## Stage 16: Provide the common Woodpecker integration
Status: Complete. Depends on Stage 15.
1. Add maintained source-only and binary-producing tag-pipeline templates.
Parameterize the Go image and immutable ReleaseKit source pin for eventual
deployment, with explicit installation/cache preparation and a delivery
secret. Document required template inputs rather than inventing a published
pin. 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 source-only template delivers the note
page directly; the binary template first builds its declared assets. Neither
workflow executes the publisher.
4. Exercise both command sequences locally with a tool built from this checkout,
fixture repositories, and a local server. Substitute the local executable for
the templates' installation step during development checks. Check YAML using
locally available tooling or static inspection; no live runner or new
external validator is needed to complete the stage.
Acceptance: the maintained commands reach common delivery for both profile types
and common builds for declared binary targets, with the selected version and
commit. Secrets enter only the delivery step. Agent-performed static YAML checks
and local command evidence are recorded accurately. No published tool pin,
container pull, configured secret, or live runner is a stage prerequisite.
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.
Evidence: maintained source-only and binary tag workflows are in
`examples/woodpecker/`. Both parameterize the Go image and immutable ReleaseKit
commit through their matrix entries, verify the fetched tag against the runner
commit, and keep shared output under the locally ignored `.releasekit/`
directory. Only the delivery step maps the configured Woodpecker secret to the
CLI token variable. The source-only workflow delivers the note page; the binary
workflow prepares module caches, builds declared targets offline, and passes
the build receipt and assets into delivery. The delivery guide covers template
inputs, source publication versus delivery status, and retries against the same
tag. A test builds the CLI from this checkout and exercises both workflows'
delivery command sequences against fixture repositories and a local HTTP
server; it substitutes the built executable for the installation command. Both
YAML files parse with locally available PyYAML 6.0.3, and static checks confirm
their tag filters, step layout, and delivery-only secret mapping. `go test ./...`,
`go run ./internal/apicheck`, and the Markdown whitespace check pass. No live
Woodpecker runner, image pull, secret, or hosted delivery service was used.
## Stage 17: Prepare ReleaseKit's own hosted-page delivery
Status: Complete. Depends on Stage 16.
1. Extend ReleaseKit's source-only profile with its Gitea destination and token
environment name. Keep the build target set empty and retain its selected
offline candidate suite. No distributed executable or binary manifest is
introduced.
2. Prepare a repository-specific Woodpecker workflow under the maintained
examples, using the tagged checkout to build the local release tool and call
page-only delivery. Keep it outside active CI discovery during this sequence.
Document where to place it for later activation and how to supply its secret.
3. Update the release policy's deliverable declaration and `docs/release.md`
together to identify the hosted note page, automation responsibility, and
page-delivery retry command. Source completion remains the exact tag push.
4. Exercise the prepared command sequence against a temporary tagged repository
and local HTTP server. Retain the note's complete body, including licensing
footer, on the hosted page.
Acceptance: ReleaseKit's source-only example creates a hosted page and accepts
an identical rerun without building consumer binaries, generating a manifest,
requiring a receipt, or consulting a previously released tool. Delivery failure
leaves source status unchanged. Workflow preparation requires no real tag,
credentials, remote push, CI activation, or maintainer checkpoint.
Evidence: ReleaseKit's root profile identifies its Gitea destination and
`RELEASEKIT_GITEA_TOKEN`, preserves the offline default test suite, and declares
no build targets. The repository-specific Woodpecker example remains under
`examples/` and builds the CLI from the fetched, commit-checked tag before
page-only delivery; the release procedure documents later activation and secret
configuration, source-tag completion, and same-tag retry. A local integration
test clones and tags a temporary copy of the repository, builds the CLI from
that tag, and exercises delivery against a local HTTP server. It verifies a
hosted-tag failure leaves the source tag intact, then checks page creation and
an identical rerun without assets; the complete note body, including its three
license links, is preserved. No real tag, push, credential, or hosted service
was used.
## Stage 18: Complete the initial implementation and handoff documentation
Status: Complete. Depends on Stage 17.
1. Map each initial capability in the feature roadmap to its implemented owner
and existing behavioral evidence. Record the concrete package and dependency
inventory, both supported facades, and their implemented responsibilities in
the canonical internal and consumer documentation.
2. Finish current API, CLI, configuration, integration, example, development,
and release documentation. Check links and consistency with actual behavior;
keep licensing and attribution accurate for incorporated material.
3. Run the established offline default suite and relevant existing boundary
checks for the assembled implementation. Resolve implementation defects
within this scope without adding a new permanent release matrix or live
testing requirement.
4. Record the completed initial implementation and evidence in this plan.
Identify prepared integrations whose live operation remains unverified and
hand off the implementation for the maintainer's subsequent architecture
review. Keep consumer migrations and their anticipated code savings marked
as future adoption work.
Acceptance: the initial library and executable provide check, publication,
build, runtime version access, page-only and binary delivery, partial/uncertain
recovery, and prepared Woodpecker integration. The documentation describes the
implemented boundary accurately. Completion depends on local implementation
and development evidence; architectural review, a broader testing plan,
consumer migrations, and release/deployment decisions follow this sequence.
Evidence and handoff: `docs/internal/overview.md` records both public Go
facades, all 14 implemented Go packages, the absence of third-party module
dependencies, capability ownership, and representative behavioral tests. The
README and canonical CLI, configuration, consumer, integration, development,
and release documents describe the implemented behavior; the policy and product
roadmaps no longer present completed facade, delivery, or workflow work as
pending. `go test ./...`, `go run ./internal/apicheck`, local Markdown link and
fragment checks, and prose whitespace checks pass. All three maintained
Woodpecker examples parse with local PyYAML 6.0.3, with the token mapped only in
each delivery step. Local Git and HTTP fixtures exercise source-only delivery,
binary delivery, publication recovery, and the ReleaseKit tagged-source workflow.
The Woodpecker workflows remain prepared, not activated. No live runner,
configured deployment secret, immutable deployment pin, hosted Gitea delivery,
or ReleaseKit source release was used or verified. Consumer migrations and
measurement of removed consumer infrastructure remain future adoption work.
The implementation is ready for the maintainer's subsequent architecture
review; broader testing and release or deployment decisions remain outside this
completed implementation sequence.
## Coverage of the feature roadmap
| Initial target responsibility | Implementing stages |
| --- | --- |
| Two public facades and small runtime dependencies | 1–3, extended in 10–11 and 15 |
| 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, 17 |
| Shared version injection and selected cross-builds | 2, 10–11 |
| Native smoke, empty target sets, consistent names | 10–11 |
| Hosted page/note creation, including source-only projects | 12, 15, 17 |
| Full checksums, comparison, partial/additive retries | 13–15 |
| Prepared Woodpecker workflows for both delivery profiles | 16–17 |
| Complete initial implementation and canonical documentation | 18 |
Consumer infrastructure reduction and convergence on the shared standard remain
feature-roadmap adoption goals. Their migration sequence, real-world testing,
and measured savings will be determined after the initial architecture review.
## Open Questions
None block this initial implementation sequence. The maintainer has selected
hosted pages for source-only projects and deferred pilots, releases, consumer
adoption, and broader testing decisions until after implementation and the
subsequent architecture review. Actual release pins and deployment credentials
are inputs to later adoption; no stage requires them.