Record publication recovery integration findings

This commit is contained in:
2026-11-02 20:53:01 +00:00
parent 3280f734ef
commit 0ef135bfda

View File

@@ -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.