Preserve replacement selectors during recovery
This commit is contained in:
@@ -15,6 +15,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/modfile"
|
||||||
|
|
||||||
"go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
|
"go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
|
||||||
"go.maximumdirect.net/releasekit/internal/gotool"
|
"go.maximumdirect.net/releasekit/internal/gotool"
|
||||||
"go.maximumdirect.net/releasekit/internal/publish"
|
"go.maximumdirect.net/releasekit/internal/publish"
|
||||||
@@ -159,16 +161,25 @@ func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
|
|||||||
if err := os.Symlink(source, selectedSource); err != nil {
|
if err := os.Symlink(source, selectedSource); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !registered {
|
const toolModule = "go.maximumdirect.net/releasekit"
|
||||||
relative, err := filepath.Rel(repo, selectedSource)
|
const toolVersion = "v0.0.0"
|
||||||
|
module := "module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire " + toolModule + " " + toolVersion + "\n"
|
||||||
|
var wildcardSource string
|
||||||
|
if registered {
|
||||||
|
module += "\nreplace " + toolModule + " => " + strconv.Quote(selectedSource) + "\n"
|
||||||
|
module += "\ntool " + toolModule + "/cmd/releasekit\n"
|
||||||
|
} else {
|
||||||
|
wildcardSource = t.TempDir()
|
||||||
|
writeRepositoryFile(t, wildcardSource, "go.mod", "module "+toolModule+"\n\ngo 1.26.0\n")
|
||||||
|
wildcard, err := filepath.Rel(repo, wildcardSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
selectedSource = relative
|
specific, err := filepath.Rel(repo, selectedSource)
|
||||||
}
|
if err != nil {
|
||||||
module := "module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => " + strconv.Quote(selectedSource) + "\n"
|
t.Fatal(err)
|
||||||
if registered {
|
}
|
||||||
module += "\ntool go.maximumdirect.net/releasekit/cmd/releasekit\n"
|
module += "\nreplace (\n" + toolModule + " => " + strconv.Quote(wildcard) + "\n" + toolModule + " " + toolVersion + " => " + strconv.Quote(specific) + "\n)\n"
|
||||||
}
|
}
|
||||||
writeRepositoryFile(t, repo, "go.mod", module)
|
writeRepositoryFile(t, repo, "go.mod", module)
|
||||||
sums, err := os.ReadFile(filepath.Join(source, "go.sum"))
|
sums, err := os.ReadFile(filepath.Join(source, "go.sum"))
|
||||||
@@ -251,8 +262,34 @@ func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
|
|||||||
if !strings.Contains(command, "go ") || strings.Contains(command, "/exe/releasekit") {
|
if !strings.Contains(command, "go ") || strings.Contains(command, "/exe/releasekit") {
|
||||||
t.Fatalf("unstable handoff: %s", command)
|
t.Fatalf("unstable handoff: %s", command)
|
||||||
}
|
}
|
||||||
if !registered && !strings.Contains(command, "publication-recovery") {
|
if !registered {
|
||||||
t.Fatalf("API-only handoff lacks durable metadata: %s", command)
|
store := filepath.Join(repo, ".git", "releasekit", "publication-recovery")
|
||||||
|
entries, err := os.ReadDir(store)
|
||||||
|
if err != nil || len(entries) != 1 {
|
||||||
|
t.Fatalf("API-only recovery modules = %v, %v", entries, err)
|
||||||
|
}
|
||||||
|
dir := filepath.Join(store, entries[0].Name())
|
||||||
|
if !strings.Contains(command, shellQuote(dir)) {
|
||||||
|
t.Fatalf("API-only handoff lacks durable metadata: %s", command)
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, "go.mod")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prepared, err := modfile.Parse(path, data, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
paths := map[string]string{}
|
||||||
|
for _, replacement := range prepared.Replace {
|
||||||
|
if replacement.Old.Path == toolModule {
|
||||||
|
paths[replacement.Old.Version] = replacement.New.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paths) != 2 || paths[""] != wildcardSource || paths[toolVersion] != selectedSource {
|
||||||
|
t.Fatalf("durable recovery lost replacement selection: %v", paths)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Execute from another directory after the original driver and the
|
// Execute from another directory after the original driver and the
|
||||||
// API-only launcher's temporary files are gone.
|
// API-only launcher's temporary files are gone.
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ For CLI publication recovery, the package selects a registered module tool or
|
|||||||
prepares a durable private module root for an API-only consumer. Go's module
|
prepares a durable private module root for an API-only consumer. Go's module
|
||||||
graph query includes transitive dependencies; preparation retains module
|
graph query includes transitive dependencies; preparation retains module
|
||||||
requirements, versioned replacements, and checksums. Local replacements resolve
|
requirements, versioned replacements, and checksums. Local replacements resolve
|
||||||
against their original module directory before relocation. The original main
|
against their original module directory before relocation. Each relocation
|
||||||
module remains a local replacement, including when releasing ReleaseKit itself.
|
updates the directive's destination and retains its path and version selector.
|
||||||
|
The original main module remains a local replacement, including when releasing
|
||||||
|
ReleaseKit itself.
|
||||||
The private root provides tool resolution independently of consumer vendoring.
|
The private root provides tool resolution independently of consumer vendoring.
|
||||||
API-only preparation discovers the host target for dependency inspection. The
|
API-only preparation discovers the host target for dependency inspection. The
|
||||||
returned invocation carries its directory and tool arguments; the CLI's later
|
returned invocation carries its directory and tool arguments; the CLI's later
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ and a new module-selection defect in the smoke recipe.
|
|||||||
|
|
||||||
### Relocate recovery replacements in one pass without merging selectors
|
### Relocate recovery replacements in one pass without merging selectors
|
||||||
|
|
||||||
|
**Completed.** Recovery rewrites relative destinations in one pass. The CLI regression verifies both selectors, the selected tool, the rejected-push status, and later reconciliation.
|
||||||
|
|
||||||
**Evidence:** The API-only path in
|
**Evidence:** The API-only path in
|
||||||
[`RecoveryTool`](../../internal/gotool/recovery.go) walks the parsed
|
[`RecoveryTool`](../../internal/gotool/recovery.go) walks the parsed
|
||||||
`go.mod` replacements. It calls `modfile.AddReplace` for each relative
|
`go.mod` replacements. It calls `modfile.AddReplace` for each relative
|
||||||
|
|||||||
@@ -100,9 +100,11 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) (*RecoveryIn
|
|||||||
// replacements relative to the directory that originally owned go.mod.
|
// replacements relative to the directory that originally owned go.mod.
|
||||||
for _, replacement := range module.Replace {
|
for _, replacement := range module.Replace {
|
||||||
if replacement.New.Version == "" && !filepath.IsAbs(replacement.New.Path) {
|
if replacement.New.Version == "" && !filepath.IsAbs(replacement.New.Path) {
|
||||||
if err := module.AddReplace(replacement.Old.Path, replacement.Old.Version, filepath.Join(moduleDir, replacement.New.Path), ""); err != nil {
|
// A local replacement ends in its path token. Edit that token on
|
||||||
return nil, err
|
// the parsed directive so a wildcard does not absorb versioned ones.
|
||||||
}
|
replacement.New.Path = filepath.Join(moduleDir, replacement.New.Path)
|
||||||
|
tokens := replacement.Syntax.Token
|
||||||
|
tokens[len(tokens)-1] = modfile.AutoQuote(replacement.New.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The original main module's source remains local if a dependency refers
|
// The original main module's source remains local if a dependency refers
|
||||||
|
|||||||
Reference in New Issue
Block a user