diff --git a/docs/roadmap/commandkit-feature-request.md b/docs/roadmap/commandkit-feature-request.md new file mode 100644 index 0000000..c621f4b --- /dev/null +++ b/docs/roadmap/commandkit-feature-request.md @@ -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. diff --git a/docs/roadmap/host-native-launchers.md b/docs/roadmap/host-native-launchers.md index 6b3c655..6addbba 100644 --- a/docs/roadmap/host-native-launchers.md +++ b/docs/roadmap/host-native-launchers.md @@ -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. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..4d2675a --- /dev/null +++ b/docs/roadmap/implementation.md @@ -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 ``, + ``, and `` standing for shell-quoted operands: + + ```sh + (export GOWORK=off GOTOOLCHAIN=local && releasekit_host_os=$(go -C env GOHOSTOS) && releasekit_host_arch=$(go -C env GOHOSTARCH) && GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch" go -C tool '-modfile=' reconcile --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.”