Plan host native release launchers

This commit is contained in:
2026-11-04 00:43:57 +00:00
parent 373f7a69cd
commit bf07bed2ae
3 changed files with 280 additions and 89 deletions

View File

@@ -0,0 +1,93 @@
# CommandKit feature request: host-safe consumer launchers
Status: proposed; not an accepted contract or implementation assignment.
## Consumer need
CommandKit selects ReleaseKit v0.8.0 as a Go tool and uses a small launcher to
publish its core and two optional integration modules. Maintainers can inherit
`GOOS` and `GOARCH` from application cross-build work. The release tool must run
on the maintainer's host even when those variables name another target.
CommandKit currently handles this by querying `go env GOHOSTOS` and
`go env GOHOSTARCH`, then using those values when building its native ReleaseKit
executable. Its `TestReleaseLauncherDelegatesPublication` test supplies foreign
target variables and checks native bootstrap behavior together with argument
and exit-status forwarding. Consumer evidence is in CommandKit's
`scripts/release` and `scripts/release_test.go` at local commit
`a74fcb20041cbccde93f9b9a1e3b24cb8dd131e7`.
Please incorporate this behavior and regression coverage into ReleaseKit's
maintained consumer launcher example so downstream projects can reuse the
recipe and rely on upstream tests for its generic bootstrap mechanics.
## Observed failure
Reviewed ReleaseKit v0.8.0, commit
`23203f235cc414c4ffe0fd3941030eb0c9f9943f`. The
[multi-module launcher](../../examples/multi-module/release.sh) invokes
`go tool` without normalizing the inherited target environment.
On Linux/amd64 with Go 1.26.8, from a prepared consumer module registering
`go.maximumdirect.net/releasekit/cmd/releasekit` at v0.8.0:
```sh
GOWORK=off GOTOOLCHAIN=local GOFLAGS=-mod=readonly \
GOPROXY=off GONOPROXY=none GOSUMDB=off \
GOOS=windows GOARCH=arm64 go tool releasekit version
```
The command built a Windows executable and failed to launch it with an
`exec format error`. Repeating the command with `GOOS=linux GOARCH=amd64`
succeeded and printed `releasekit v0.8.0`. These observations establish the
tool-execution failure; the complete upstream launcher was inspected rather
than executed in this reproduction. No publication was attempted.
## Requested change
Update the maintained launcher to select the native host target when preparing
and executing ReleaseKit, regardless of inherited application target variables.
Resolve the host through the selected Go toolchain rather than hard-coding a
platform. Scope this change to running the maintainer tool; preserve the
application targets selected through existing release profiles.
Retain the launcher's existing module/version selection, profile routing,
source-file preservation, workspace/toolchain settings, and child exit-status
behavior. Document the host-target handling in the
[consumer adoption guidance](../consumers/adoption.md) where appropriate.
Prefer a small change to the supported example and its tests. This request does
not require a new validation option, public bootstrap API, or test-helper
package. CommandKit's choice to select one tool version from its root module
remains consumer-owned; this request does not change ReleaseKit's example policy
of selecting the tool from the chosen module.
## Acceptance evidence
Extend the existing
[multi-module launcher tests](../../cmd/releasekit/multi_module_example_test.go)
with a controlled foreign-target environment. Demonstrate that:
1. The selected tool executes on the host despite foreign `GOOS` and `GOARCH`.
2. The selected module's tool version and publication arguments still reach the
child, and child statuses 0, 1, 2, and 3 are preserved.
3. Source module files remain unchanged and existing bootstrap cleanup behavior
is preserved.
Reuse existing coverage for unaffected behavior rather than adding a parallel
suite. A host-run test can use a synthetic local tool and needs no real release
or external service. Synthetic module downloads must use test-owned caches as
described in [the developer guide](../development.md#testing-synthetic-modules).
## Ownership and downstream follow-up
CommandKit's review found no locally maintained candidate validator that needs
to become a new ReleaseKit feature. Existing formatting, Markdown, and module
distribution options already cover their respective reusable checks; selecting
them as CommandKit release gates is a separate consumer decision.
ReleaseKit already owns publication-engine tests for annotated tags, exact
remote objects, nested tag prefixes, and endpoint preflight. CommandKit can
narrow its local publication exercise to its launcher and profile wiring
without upstreaming a duplicate engine test suite. Its selectors, product names,
publication endpoint, and release order remain local responsibilities.

View File

@@ -16,9 +16,10 @@ and describes work to be implemented after this documentation pass.
## Current baseline
CommandKit reports that a prepared v0.8.0 tool invocation on Linux/amd64 with
`GOOS=windows GOARCH=arm64` built a Windows executable and failed with an exec
format error. Its local launcher already selects the Go host target before
The [CommandKit feature request](commandkit-feature-request.md) describes a
prepared v0.8.0 tool invocation on Linux/amd64 with `GOOS=windows GOARCH=arm64`
that built a Windows executable and failed with an exec format error.
CommandKit's local launcher already selects the Go host target before
building the release executable. The report's reproduction exercised tool
execution without attempting publication.
@@ -65,89 +66,8 @@ does not establish a hermetic environment, broaden hardware or platform support,
or add release gates. Existing Go/toolchain failures remain ordinary bootstrap
failures. No new fallback toolchain or download policy is introduced.
## Work needed
### 1. Normalize the maintained multi-module launcher
**Rationale:** A consumer should be able to invoke its release launcher after
cross-build work without first repairing its shell environment.
**Target:** The selected ReleaseKit executable and its dependency preparation
use the Go host target.
**Work:** Query the host OS and architecture through the same Go toolchain used
for bootstrap, under the launcher's existing workspace and toolchain settings.
Apply both values within the tool-running scope before target-dependent Go
commands. Preserve `go tool`, temporary module metadata, selected-module version
resolution, and existing publication arguments. Use ordinary shell error handling
for host-query failures. Avoid hard-coded host values or a new bootstrap layer.
### 2. Align supported invocation recipes
**Rationale:** The same inherited-target failure can affect local and CI tool
execution. Maintained recipes should give consumers consistent behavior.
**Target:** The adoption example, consumer and maintainer launchers, CI
templates, and generated recovery commands select the host target when
preparing and invoking ReleaseKit.
**Work:** Apply the same small normalization recipe to the adoption example and
the single-module and multi-module CI templates. Preserve tag verification before
bootstrap, tagged module selection, profile routing, credentials, and temporary
metadata handling. Apply host selection to the maintainer launcher, source
delivery template, and manual delivery retry. Update the multi-module README
and consumer guidance to explain host selection and profile-owned application
targets. Document that consumers adopt the corrected launcher or template; a
module dependency upgrade alone does not rewrite their copied scripts.
Apply host selection to module-based recovery dependency preparation and to the
Go invocation printed by `publish`. Preserve registered-tool selection, private
bootstrap metadata for API-only consumers, the native executable fallback,
explicit record selection, and recovery exit statuses.
### 3. Extend existing behavioral coverage
**Rationale:** Successful execution under a foreign target directly protects
against the reported defect while existing fixtures protect delegation behavior.
**Target:** A synthetic selected tool runs on the test host despite foreign
target variables, with the current routing and cleanup behavior intact.
**Work:** Extend the existing launcher fixture with a controlled target different
from the test host. Exercise real Go tool preparation and execution; a source-text
assertion alone cannot establish that the executable runs. Verify that the
selected module's tool version and publication arguments reach the child and
that statuses 0, 1, 2, and 3 propagate. Check unchanged source module files and
existing temporary bootstrap cleanup. Reuse unaffected tests rather than adding
a parallel launcher or publication-engine suite.
Verify host selection at the dependency inspection step itself, using a
host-specific fixture or an environment assertion at `go list -deps`. Cover
affected CI/bootstrap recipes through their existing test harnesses where
practical. Extend the existing handoff tests to exercise recovery preparation
under a foreign target, then run the printed command under a foreign target in
a separate invocation. Preserve profile-selected application targets. Use
synthetic local tools and test-owned module caches under the
[fixture-isolation guidance](../development.md#testing-synthetic-modules).
The regression needs no actual publication, hosted service, or external platform
runner. Choose a representative supported foreign target instead of introducing
a platform matrix.
### 4. Validate and document adoption
**Rationale:** Consumers should be able to copy a verified recipe and reduce
their local tests to their own wiring.
**Target:** Maintained examples, guidance, and tests agree on the native bootstrap
behavior and its scope.
**Work:** Run the relevant launcher and workflow tests, the normal offline suite,
and required facade checks when implementation lands. Check shell/example syntax,
documentation links, and whitespace. Include a concise release-note entry for
the inherited-target fix and affected recipes. Explain that ReleaseKit owns
generic bootstrap and publication-engine coverage while consumers verify their
selectors and profile mapping. This does not require consumers to remove tests
that protect their own behavior.
The [implementation plan](implementation.md) defines the work to reach this
state.
## Completion evidence
@@ -157,6 +77,3 @@ Preserve selected tool identity and arguments, child-status forwarding, unchange
source metadata, and existing cleanup behavior. The maintained local and CI
recipes and adoption guidance must use the same host-selection approach.
Application build targets must retain their existing profile-driven behavior.
There are no significant open design decisions for this scope. Exact shell
variable names and fixture arrangement can be selected during implementation.

View File

@@ -0,0 +1,181 @@
# Implementation plan: host-native ReleaseKit launchers
Follow the stages in order as one sprint. The
[feature roadmap](host-native-launchers.md) defines the intended behavior and
scope. The focused checks below describe regression coverage; intermediate
stages need no separate review, release, or passing build. Run the assembled
checks in Stage 6.
## Stage 1: Normalize the local multi-module launcher
1. In [the multi-module launcher](../../examples/multi-module/release.sh), after
entering the selected module and exporting its existing `GOWORK=off` and
`GOTOOLCHAIN=local` settings, insert the following before the first Go
bootstrap command. Reuse this shell recipe in the subsequent stages:
```sh
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
```
Keep the checked assignments separate from `export` so a failed Go query
stops the launcher. Keep host selection inside the existing launcher scope;
preserve selected module metadata, dependency preparation, publication
arguments, traps, and the child status. Application targets still come from
release profiles.
2. Extend [the existing launcher tests](../../cmd/releasekit/multi_module_example_test.go).
In `TestMultiModuleLauncherUsesSelectedModuleAndProfile`, let the Go shim
answer the two host queries and assert the exported target at
`go list -deps` in the selected module. In
`TestMultiModuleLauncherRunsToolFromSelectedDependency`, supply a foreign
target to the launcher and retain real Go tool preparation and execution.
Keep the checks for the nested module's version, forwarded profile and
arguments, statuses 0 through 3, unchanged source, and bootstrap cleanup.
For these fixtures, choose `windows` unless the host OS is `windows`
(then `linux`), and `arm64` unless the host architecture is `arm64` (then
`amd64`). This makes both variables differ from the host pair. Keep
synthetic versions in a test-owned `GOMODCACHE`.
## Stage 2: Normalize consumer Woodpecker templates
1. Apply the Stage 1 recipe in the
[single-module](../../examples/woodpecker/tag-delivery.yml) and
[multi-module](../../examples/multi-module/woodpecker.yml) tag delivery
commands, under the existing Go settings and in the selected module. Insert
it after tag verification and checkout, before the first Go bootstrap
command. Write the new shell variable references as
`$${releasekit_host_os}` and `$${releasekit_host_arch}` so Woodpecker
passes them through. Keep the verified commit, selected tool, profile
mapping, credentials, alternate module and sum files, cleanup, and
`ci-deliver --prepared` invocation.
2. Update the existing
[single-module](../../cmd/releasekit/woodpecker_test.go) and
[multi-module](../../cmd/releasekit/multi_module_woodpecker_test.go) workflow
harnesses. Teach the controlled Go shim to answer host queries before
enforcing its alternate module file checks. In both Go shims, log the
target at `go list -deps`. Feed a foreign target to
`TestWoodpeckerTagDeliveryTemplate`,
`TestWoodpeckerTagDeliveryUsesTaggedModuleVersion`,
and `TestMultiModuleWoodpeckerDelivery` and check that dependency inspection
uses the host pair. Retain real execution where the fixture already uses it,
and keep the existing checks that a rejected tag or commit reaches no Go
command.
## Stage 3: Normalize source-built launchers
1. Apply the same recipe in the [maintainer launcher](../../scripts/release)
before building and executing ReleaseKit. Prefix the two queries with
`GOTOOLCHAIN=local` to match its existing build. Preserve usage checking,
output location, build settings, and the final `exec`.
2. Apply the recipe in the
[source delivery template](../../examples/woodpecker/releasekit-source-delivery.yml)
after tag verification and checkout, under its existing Go settings and
before the source build. Use the Woodpecker escaping from Stage 2. Keep
its temporary build output, dependency preparation through the existing
build, and prepared delivery arguments.
Exercise `TestWoodpeckerSourceDeliveryTemplateSelfBootstraps` with a foreign
target; log and assert the host pair at the real `go build`, retain actual
execution, and keep the checkout, cleanup, and dependency preparation
assertions.
3. Update the manual delivery retry in the [maintainer runbook](../release.md).
After fetching and checking out the tagged version, run host selection,
dependency download, build, and delivery inside a subshell so the caller's
target environment stays intact. Query with `GOTOOLCHAIN=local` as above;
make the sequence stop on failure inside the subshell. Preserve the
dependency download before the offline build, the build-only proxy and sum
settings, the output path, and the older-tag credential example.
## Stage 4: Normalize module-based recovery preparation
1. In [the Go recovery implementation](../../internal/gotool/recovery.go),
keep the existing registered-tool, API-only, and no-dependency selections.
Have `RecoveryTool` return `(*RecoveryInvocation, error)` within the
internal package. Give the descriptor `Directory string` and
`Args []string` fields. A nil result still selects the native executable fallback.
Set the directory to the original module for a registered tool or the
durable bootstrap root for an API-only consumer. Put `tool -modfile=`
and the existing full command package path in `Args`; the caller supplies
the working directory. Preserve the selected version, local and versioned
replacements, private module and sum files, and their existing lifetime.
2. Immediately before the API-only branch runs `go list -deps`, query
`go env -json GOHOSTOS GOHOSTARCH` in its private root with its existing
`GOWORK=off` and `GOTOOLCHAIN=local` overrides. Reuse the internal
`discover` runner with the existing context. Decode both fields, treat
missing values as a preparation error, and add only `GOOS` and `GOARCH`
to the environment of the dependency command. Keep the target-independent
module-selection query and the registered-tool early return. Use the
existing preparation error and cleanup paths; leave candidate Go settings,
proxy policy, and fallback selection as they are.
3. Update the direct `RecoveryTool` callers in
[the handoff tests](../../cmd/releasekit/publication_handoff_test.go) to run
the descriptor's arguments in its directory. In
`TestRecoveryToolPreservesVersionedReplacement`, set a foreign target for
the preparation call, log the target of the real `go list -deps` through a
delegating shim, and verify the host pair there. Run the test's subsequent
direct tool invocation with the host pair and retain its version and
metadata assertions. Retain the source-module and vendored API-only cases.
Update the CLI caller in Stage 5.
## Stage 5: Normalize the printed recovery command when it runs
1. In `selectedPublicationRecoveryCommand` in
[the CLI](../../cmd/releasekit/main.go), render the descriptor as one
copyable POSIX shell command. Use this structure, with `<dir>`,
`<package>`, and `<record>` standing for shell-quoted operands:
```sh
(export GOWORK=off GOTOOLCHAIN=local && releasekit_host_os=$(go -C <dir> env GOHOSTOS) && releasekit_host_arch=$(go -C <dir> env GOHOSTARCH) && GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch" go -C <dir> tool '-modfile=' <package> reconcile --record <record>)
```
Build the common `go -C` prefix from the descriptor directory and render
its tool arguments using the existing diagnostic redaction and shell
quoting. Keep the explicit absolute record operand. Both host queries run
in the invocation directory with the same Go settings as the final command.
A failed query stops the command, and the subshell returns the final command
status when launched. Leave the native executable command, handoff timeout
and cancellation behavior, and existing fallback and diagnostic behavior
intact.
2. In `TestColdGoToolRecoverySurvivesLauncherExit`, execute the printed
command in a separate shell with a foreign target, after the original
launcher and its temporary files are gone. Exercise both registered and
API-only handoffs and retain status checks for 0 through 3. For the
alternate-record cases, replace the quoted original record operand inside
the command instead of appending flags after the closing subshell.
Retain the vendored API-only, native fallback, record-selection, and
redaction checks. Exercise the printed command itself; running the Go
descriptor directly would miss a regression in the shell recipe.
## Stage 6: Align consumer guidance and verify the sprint
1. In the [adoption guide](../consumers/adoption.md), make the minimal
launcher use the Stage 1 recipe followed by `exec go tool releasekit "$@"`.
Run it from the selected module, retaining that consumer's workspace and
toolchain settings. Move the registration smoke check to follow the launcher
example and invoke `version` through that launcher. In the
[multi-module README](../../examples/multi-module/README.md), apply the same
recipe to the direct `check` smoke commands: enter each selected module in
its own subshell, select the host there, then run the existing check with
its repository-root profile and version. Preserve the source-selected
fixture instructions and avoid turning the smoke check into publication.
2. Align the [consumer delivery guide](../consumers/delivery.md),
[CLI contract](../cli.md), and [internal Go tool guide](../internal/gotool.md)
with the maintained recipes and handoff behavior. Explain that consumers
update copied launchers and templates separately from upgrading the module
dependency. ReleaseKit tests generic bootstrap behavior; consumers can
focus their own wiring tests on selectors and profile mapping. Profile
build targets continue to select application binaries.
3. Run the focused launcher, workflow, and recovery fixtures from the preceding
stages with the existing local Git and module proxy fixtures. Run
`go test ./...` and `go run ./internal/apicheck` as documented in the
[development guide](../development.md#validation). Check shell syntax,
including Woodpecker command blocks after interpolation, local documentation
links, and `git diff --check`. Use the existing offline fixtures; no hosted
delivery, live publication, or external runner is needed.
4. Include the following entry in the next release note during release
preparation, when its version is selected: “Maintained launchers and
generated Go recovery commands now use the host target to prepare and run
ReleaseKit. Update copied launchers and CI templates to get the fix;
release profiles still select application build targets.”