From 0ef135bfdacd843c5ce0ccd99f97dfaf356af6b8 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 20:53:01 +0000 Subject: [PATCH] Record publication recovery integration findings --- docs/roadmap/remediation.md | 159 ++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 3348d49..63147b6 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -206,3 +206,162 @@ preserving the actual observation in the implementation rather than converting it to unknown remote state. Document that output-failure status in the CLI contract. Add focused failing-writer coverage for both structured commands; ordinary writable-output state/status behavior should stay unchanged. + +## Integration audit evidence + +A subsequent audit examined the interaction with existing consumer launchers, +repository discovery, candidate cleanliness, and the maintainer runbook. The +production code remains identical to the code covered by the full-suite/API +checks above. A fresh focused run of the existing launcher, Woodpecker, +publication-handoff, and lost-output recovery integration tests passed with +Go 1.26.8. Disposable probes reproduced the additional issues below. R01–R06 +remain open and are preserved rather than duplicated here. + +## R07: Make the recovery handoff survive a cold Go-tool invocation — P2 + +**Relevant code:** `cmd/releasekit/main.go:134` (`publicationRecoveryCommand`); +`examples/multi-module/release.sh`; `docs/consumers/adoption.md`'s module-selected +Go-tool workflow; `cmd/releasekit/publication_handoff_test.go`. + +The new handoff always uses `os.Executable()` as the command to run later. +Existing consumer guidance and the maintained launcher execute ReleaseKit +through `go tool releasekit`. On an uncached tool build, Go 1.26.8 executes a +temporary build executable and removes it when the Go driver exits. A warm +invocation uses a surviving cached executable, so its behavior differs. + +**Evidence:** A disposable tool-registered consumer used the current CLI with a +cold tool build and a local remote that rejected the tag push. Publication +returned status 1 and saved a complete record, but its printed recovery command +named `/tmp/go-build.../exe/releasekit`. That executable was already absent when +the driver returned, and copying the command returned status 127. A surviving +CLI reconciled the same record successfully to `not_published` with status 1. +The existing handoff test builds a persistent executable directly and therefore +does not exercise this launch lifecycle. + +**Expected behavior:** The copyable handoff works with supported consumer launch +methods after the publication process has exited. Routine first use on a fresh +machine/cache should not require reconstructing another invocation manually. +The existing fallback guidance for a subsequently unavailable executable does +not supply a working handoff for this ordinary cold-build case. + +**Remediation:** Make the recovery invocation independent of an ephemeral +Go-tool executable. Use a supported module-selected entry point when available, +or otherwise provide a surviving supported entry point. Account for the +maintained launcher's temporary `-modfile` cleanup and its support for ordinary +package dependencies as well as registered tools; a bare `go tool` command +cannot assume every consumer has a permanent tool declaration. Keep explicit +record selection and distinct exit statuses. Add an assembled cold-build test +that executes the printed command after the original Go driver and launcher +have exited, rather than only testing a directly built CLI or a warm cache. + +## R08: Reuse the existing repository-path semantics for record storage — P2 + +**Relevant code:** `internal/publish/record.go:112`, especially line 117; +`internal/publish/attempts.go:74`, especially line 82; the existing +`internal/git/source.go:39` and `source.go:50` repository-root helpers. + +The pre-existing root resolver removes Git's output newline and resolves +filesystem aliases, preserving valid whitespace in the path itself. The new +recording and discovery resolvers independently use `strings.TrimSpace`, which +changes repository identity when a directory name ends in whitespace. Recording +then uses that altered root both for relative overrides and for deciding whether +an absolute override lies inside the candidate worktree. + +**Evidence:** In a temporary checkout whose final directory component was +`consumer `, the existing `git.RepositoryRoot` preserved the exact path and +the existing candidate `Check` completed successfully. The new code produced +three inconsistent results: + +- A relative `.releasekit/attempts` override tried to use the different sibling + path under `consumer` and failed its Git ignore check, despite the real + `.releasekit/` location being ignored. +- After a record was saved in the correct absolute ignored location, relative + discovery selected the wrong sibling directory and returned a successful + empty collection. The actual record still existed in the requested store. +- An absolute override inside the actual worktree was misclassified as + external because containment used the trimmed root. It successfully wrote an + unignored record, which Git reported as untracked. + +**Expected behavior:** Publication, discovery, relative overrides, and worktree +containment use the same repository identity already supported by the library. +Path data remains intact. This failure is independent of R02's filename mismatch: +the containment decision itself uses the wrong root. + +**Remediation:** Reuse `internal/git.RepositoryRoot` and share metadata-path +resolution through the Git boundary, coordinating with R04 rather than adding +another resolver. Remove output terminators without stripping path characters. +Apply the same preservation to common-directory output. Add focused regressions +for a valid root ending in whitespace, relative-store read/write agreement, and +absolute unignored-path rejection. Preserve existing filesystem-alias and +linked-worktree behavior instead of introducing new pathname restrictions. + +## R09: Align maintainer recovery instructions with the source bootstrap — P2 + +**Relevant code/docs:** `scripts/release:9`; `docs/release.md:104` and +`release.md:149`; `docs/roadmap/publication-reconciliation.md:21`. + +The existing maintainer procedure builds and executes +`.releasekit/bin/releasekit`. It does not install an executable named +`releasekit` on `PATH`. The new lost-output recovery instructions instead run +`releasekit publication-attempts` and `releasekit reconcile` without identifying +the executable that the procedure prepared. + +**Evidence:** With Go and Git available on `PATH` and only the source-built CLI +in `.releasekit/bin`, the newly documented discovery command returned 127 +(`releasekit: not found`). The equivalent command using +`./.releasekit/bin/releasekit` returned 0 and listed the saved attempt. A globally +installed CLI could also be a different version from the source-built one. + +The recovery-section rename additionally left the feature roadmap's link to +`docs/release.md#resolve-failures` pointing at a heading that no longer exists. + +**Expected behavior:** The repository's executable runbook uses the CLI prepared +by its own prerequisites, and its recovery links resolve to the current owner. +It requires no additional global installation for lost-output recovery. + +**Remediation:** Use the source-built executable for discovery and reconciliation +in the maintainer runbook, consistent with its existing delivery commands. +Retain explicit instructions for selecting another supported CLI when the +original executable is genuinely unavailable. Repair the renamed-section link. +Verify the recovery snippets in an environment without a global `releasekit` +executable; keep generic consumer command examples in their consumer-facing +documentation. + +## R10: Preserve endpoint bytes across the private-record handoff — P2 + +**Relevant code:** `internal/publish/record.go:77` and its evidence decoding; +the string fields in `internal/publish/reconcile.go`'s `Evidence`; the existing +Git endpoint resolution/query path in `internal/git/remote.go` and `tag.go`. + +On POSIX, the existing Git transport can use local repository paths containing +bytes that are not valid UTF-8. Typed evidence retains those bytes in its Go +strings. The new private JSON codec uses `json.Marshal` directly on those +strings, which replaces invalid UTF-8 with the Unicode replacement character +without reporting an error. Loading the record therefore supplies a different +endpoint to the existing Git observer. + +**Evidence:** A disposable valid candidate's publication attempt targeted a local +repository whose path contained a non-UTF-8 byte. Its hook rejected the push; +publication returned `not_published` with a complete saved record and the exact +original endpoint in typed evidence. A second local repository, named using the replacement-character +form of that path, held the same intended annotated tag object. Direct typed +reconciliation correctly returned `not_published` at the original destination. +Loading the saved record changed its endpoint to the second repository and +record-based reconciliation returned `published` there. The record save/load +reported no error. + +**Expected behavior:** Direct and record-loaded evidence identify the same +captured destination. The private handoff retains exact transport identity, +including pathname bytes supported by the existing Git boundary, or reports +that the handoff could not be saved faithfully. Diagnostic JSON is a display +format and can use safe displays; the private record supplies actual operation +inputs and must preserve their identity. + +**Remediation:** Make private serialization lossless for supported evidence, +using an explicit byte representation where needed. At minimum, detect values +that the selected record format cannot round-trip and return a separate +recording failure with no misleading saved handoff. Keep the original typed +evidence and publication behavior intact; avoid adding a general UTF-8 +restriction to previously valid Git publication inputs. Cover endpoint/path +round trips and equivalent direct-versus-loaded observations, including a +distinct replacement-character path, in focused regression tests.