fix(cli): honor local installation discovery precedence
This commit is contained in:
@@ -81,11 +81,11 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprintf(stderr, "tht: %s\n\n%s", err, usage)
|
fmt.Fprintf(stderr, "tht: %s\n\n%s", err, usage)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if command == "help" && len(commandArgs) == 0 {
|
if isBootstrapCommand(command) {
|
||||||
fmt.Fprint(stdout, usage)
|
if command == "help" {
|
||||||
return 0
|
fmt.Fprint(stdout, usage)
|
||||||
}
|
return 0
|
||||||
if (command == "version" || command == "setup") && len(commandArgs) == 0 {
|
}
|
||||||
return commandUsageError(stderr, fmt.Sprintf("unknown command %q", command))
|
return commandUsageError(stderr, fmt.Sprintf("unknown command %q", command))
|
||||||
}
|
}
|
||||||
workingDirectory, err := os.Getwd()
|
workingDirectory, err := os.Getwd()
|
||||||
@@ -194,6 +194,10 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
return writeResult(result, err, secretValues, stdout, stderr)
|
return writeResult(result, err, secretValues, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBootstrapCommand(command string) bool {
|
||||||
|
return command == "help" || command == "setup" || command == "version"
|
||||||
|
}
|
||||||
|
|
||||||
func writeRemovalTargets(outputWriter io.Writer, project string, targets []serverops.Container) {
|
func writeRemovalTargets(outputWriter io.Writer, project string, targets []serverops.Container) {
|
||||||
fmt.Fprintf(outputWriter, "Removal targets for installation project %s:\n", project)
|
fmt.Fprintf(outputWriter, "Removal targets for installation project %s:\n", project)
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
|
|||||||
@@ -90,7 +90,15 @@ func TestBootstrapCommandsDoNotRequireAnInstallationDescriptor(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Setenv("THOTHII_INSTALLATION", "")
|
t.Setenv("THOTHII_INSTALLATION", "")
|
||||||
|
|
||||||
for _, args := range [][]string{nil, {"help"}, {"version"}, {"setup"}} {
|
for _, args := range [][]string{
|
||||||
|
nil,
|
||||||
|
{"help"},
|
||||||
|
{"help", "pi"},
|
||||||
|
{"version"},
|
||||||
|
{"version", "--json"},
|
||||||
|
{"setup"},
|
||||||
|
{"setup", "--configure-only"},
|
||||||
|
} {
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
_ = run(context.Background(), args, &stdout, &stderr)
|
_ = run(context.Background(), args, &stdout, &stderr)
|
||||||
if output := stdout.String() + stderr.String(); strings.Contains(output, "installation descriptor") {
|
if output := stdout.String() + stderr.String(); strings.Contains(output, "installation descriptor") {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ func Resolve(explicit string, environment func(string) string, workingDirectory
|
|||||||
return "", errors.New("installation discovery could not resolve the current directory")
|
return "", errors.New("installation discovery could not resolve the current directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := make(map[string]string)
|
|
||||||
for directory := filepath.Clean(start); ; directory = filepath.Dir(directory) {
|
for directory := filepath.Clean(start); ; directory = filepath.Dir(directory) {
|
||||||
|
candidates := make(map[string]string)
|
||||||
for _, path := range discoveryCandidates(directory) {
|
for _, path := range discoveryCandidates(directory) {
|
||||||
if hasArtifactsComponent(path) {
|
if hasArtifactsComponent(path) {
|
||||||
continue
|
continue
|
||||||
@@ -57,6 +57,17 @@ func Resolve(explicit string, environment func(string) string, workingDirectory
|
|||||||
canonical := canonicalPath(path)
|
canonical := canonicalPath(path)
|
||||||
candidates[canonical] = path
|
candidates[canonical] = path
|
||||||
}
|
}
|
||||||
|
paths := make([]string, 0, len(candidates))
|
||||||
|
for _, path := range candidates {
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
sort.Strings(paths)
|
||||||
|
if len(paths) == 1 {
|
||||||
|
return paths[0], nil
|
||||||
|
}
|
||||||
|
if len(paths) > 1 {
|
||||||
|
return "", fmt.Errorf("multiple valid installation descriptors found: %s; use --installation <absolute-path>/%s", formatCandidates(paths, root.Path), installationFileName)
|
||||||
|
}
|
||||||
if directory == root.Path {
|
if directory == root.Path {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -66,19 +77,7 @@ func Resolve(explicit string, environment func(string) string, workingDirectory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paths := make([]string, 0, len(candidates))
|
return "", fmt.Errorf("installation descriptor not found from %s; run tht setup or use --installation <absolute-path>/%s", filepath.Clean(friendlyRoot), installationFileName)
|
||||||
for _, path := range candidates {
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
sort.Strings(paths)
|
|
||||||
switch len(paths) {
|
|
||||||
case 1:
|
|
||||||
return paths[0], nil
|
|
||||||
case 0:
|
|
||||||
return "", fmt.Errorf("installation descriptor not found from %s; run tht setup or use --installation <absolute-path>/%s", filepath.Clean(friendlyRoot), installationFileName)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("multiple valid installation descriptors found: %s; use --installation <absolute-path>/%s", formatCandidates(paths, root.Path), installationFileName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCandidates(paths []string, root string) string {
|
func formatCandidates(paths []string, root string) string {
|
||||||
|
|||||||
@@ -64,6 +64,44 @@ func TestResolveFindsDeployDescriptorFromNestedDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveUsesLocalCandidateBeforeAncestorCandidate(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeDiscoverableProject(t, root)
|
||||||
|
ancestor := writeDiscoverableInstallation(t, root, filepath.Join(root, "deploy", "ancestor"))
|
||||||
|
localDirectory := filepath.Join(root, "frontend", "feature")
|
||||||
|
local := writeDiscoverableInstallation(t, root, filepath.Join(localDirectory, "deploy", "local"))
|
||||||
|
|
||||||
|
got, err := Resolve("", func(string) string { return "" }, localDirectory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want, err := filepath.EvalSymlinks(local)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("Resolve() = %q, want local %q before ancestor %q", got, want, ancestor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveDiscoversDescriptorFromLinkedWorktree(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeDiscoverableLinkedWorktree(t, root)
|
||||||
|
want := writeDiscoverableInstallation(t, root, filepath.Join(root, "deploy", "worktree"))
|
||||||
|
|
||||||
|
got, err := Resolve("", func(string) string { return "" }, filepath.Join(root, "frontend"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
canonicalWant, err := filepath.EvalSymlinks(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != canonicalWant {
|
||||||
|
t.Fatalf("Resolve() = %q, want %q from linked worktree", got, canonicalWant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveIgnoresArtifactsAndRejectsAmbiguousValidCandidates(t *testing.T) {
|
func TestResolveIgnoresArtifactsAndRejectsAmbiguousValidCandidates(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
writeDiscoverableProject(t, root)
|
writeDiscoverableProject(t, root)
|
||||||
@@ -112,13 +150,26 @@ func TestResolveDoesNotSearchAboveTheDiscoveredProjectRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeDiscoverableProject(t *testing.T, root string) {
|
func writeDiscoverableProject(t *testing.T, root string) {
|
||||||
|
writeDiscoverableProjectWithGitMarker(t, root, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDiscoverableLinkedWorktree(t *testing.T, root string) {
|
||||||
|
writeDiscoverableProjectWithGitMarker(t, root, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDiscoverableProjectWithGitMarker(t *testing.T, root string, linkedWorktree bool) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, path := range []string{"backend", "frontend", "harness", "tools", "deploy"} {
|
for _, path := range []string{"backend", "frontend", "harness", "tools", "deploy"} {
|
||||||
if err := os.MkdirAll(filepath.Join(root, path), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(root, path), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Join(root, ".git"), 0o755); err != nil {
|
gitPath := filepath.Join(root, ".git")
|
||||||
|
if linkedWorktree {
|
||||||
|
if err := os.WriteFile(gitPath, []byte("gitdir: /tmp/thothii-worktree.git\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else if err := os.MkdirAll(gitPath, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(root, "compose.yaml"), []byte("services: {}\n"), 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(root, "compose.yaml"), []byte("services: {}\n"), 0o600); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user