Keep publication recovery usable after cold Go tool runs
This commit is contained in:
108
internal/gotool/recovery.go
Normal file
108
internal/gotool/recovery.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package gotool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
|
||||
"go.maximumdirect.net/releasekit/internal/process"
|
||||
)
|
||||
|
||||
const releaseKitModule = "go.maximumdirect.net/releasekit"
|
||||
const releaseKitCommand = releaseKitModule + "/cmd/releasekit"
|
||||
|
||||
// RecoveryTool returns module-selected Go arguments for a later invocation.
|
||||
// An empty result means this module does not select ReleaseKit. API-only modules
|
||||
// receive private alternate module files that outlive launcher-owned bootstraps.
|
||||
func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, error) {
|
||||
path := filepath.Join(moduleDir, "go.mod")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
module, err := modfile.Parse(path, data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tool := range module.Tool {
|
||||
if tool.Path == releaseKitCommand {
|
||||
return []string{"-C", moduleDir, "tool", "-modfile=", releaseKitCommand}, nil
|
||||
}
|
||||
}
|
||||
// Ask Go about the selected module graph, including transitive dependencies.
|
||||
// This reads real module metadata, independently of a launcher's -modfile.
|
||||
var selection bytes.Buffer
|
||||
err = process.Run(ctx, process.Invocation{
|
||||
Args: []string{"go", "list", "-e", "-json", "-mod=readonly", "-modfile=", "-m", releaseKitModule},
|
||||
Dir: moduleDir, Env: map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"},
|
||||
Stdout: &selection, Stderr: io.Discard,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identify recovery tool selection: %w", err)
|
||||
}
|
||||
var selected struct {
|
||||
Main bool
|
||||
Version string
|
||||
Error *struct{ Err string }
|
||||
}
|
||||
if err := json.Unmarshal(selection.Bytes(), &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !selected.Main && selected.Version == "" {
|
||||
// A source-built CLI also supports consumers without a module dependency.
|
||||
return nil, nil
|
||||
}
|
||||
if selected.Error != nil {
|
||||
return nil, fmt.Errorf("identify recovery tool selection: %s", selected.Error.Err)
|
||||
}
|
||||
if err := module.AddTool(releaseKitCommand); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err = module.Format()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sums, err := os.ReadFile(filepath.Join(moduleDir, "go.sum"))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(recordDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
directory, err := os.MkdirTemp(recordDir, "recovery-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create durable recovery module directory: %w", err)
|
||||
}
|
||||
complete := false
|
||||
defer func() {
|
||||
if !complete {
|
||||
_ = os.RemoveAll(directory)
|
||||
}
|
||||
}()
|
||||
alternate := filepath.Join(directory, "releasekit.mod")
|
||||
if err := os.WriteFile(alternate, data, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, "releasekit.sum"), sums, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// go tool accepts an alternate module file but has no -mod flag. Prepare
|
||||
// the private graph and sums before the later read-only tool invocation.
|
||||
captured := &combinedWriter{mode: captureStatus}
|
||||
err = process.Run(ctx, process.Invocation{
|
||||
Args: []string{"go", "list", "-mod=mod", "-modfile=" + alternate, "-deps", releaseKitCommand},
|
||||
Dir: moduleDir, Env: map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"},
|
||||
Stdout: captured, Stderr: captured,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare recovery tool: %w: %s", err, diagnosticText(captured.take(), captured.omitted()))
|
||||
}
|
||||
complete = true
|
||||
return []string{"-C", moduleDir, "tool", "-modfile=" + alternate, releaseKitCommand}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user