Skip to content

Commit 8afbfe4

Browse files
authored
fix: disable interactive shell prompts in CI environment (#551)
1 parent 0df35eb commit 8afbfe4

File tree

7 files changed

+204
-15
lines changed

7 files changed

+204
-15
lines changed

cmd/commands/install.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ func installCmd(ctx context.Context, cmd *cli.Command) error {
9797
var resolvedVersion = manager.ResolveVersion(sdk.Name, version)
9898
logger.Debugf("resolved version: %s\n", resolvedVersion)
9999
if resolvedVersion == "" {
100+
if !internal.IsInteractiveTerminal() {
101+
return cli.Exit(fmt.Sprintf("install requires specifying a version for %s", name), 1)
102+
}
100103
showAvailable, _ := pterm.DefaultInteractiveConfirm.Show(fmt.Sprintf("No %s version provided, do you want to select a version to install?", pterm.Red(name)))
101104
if showAvailable {
102105
err := RunSearch(name, []string{})
@@ -143,9 +146,13 @@ func installAll(autoConfirm bool) error {
143146
printSdk(sdks, nil)
144147

145148
if !autoConfirm {
146-
if result, _ := pterm.DefaultInteractiveConfirm.
149+
if !internal.IsInteractiveTerminal() {
150+
return cli.Exit("Use the -y flag to automatically confirm installation in non-interactive environments", 1)
151+
}
152+
result, _ := pterm.DefaultInteractiveConfirm.
147153
WithDefaultValue(true).
148-
Show("Do you want to install these plugins and SDKs?"); !result {
154+
Show("Do you want to install these plugins and SDKs?")
155+
if !result {
149156
return nil
150157
}
151158
}

cmd/commands/remove.go

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ import (
2424
)
2525

2626
var Remove = &cli.Command{
27-
Name: "remove",
28-
Usage: "Remove a plugin",
27+
Name: "remove",
28+
Usage: "Remove a plugin",
29+
Flags: []cli.Flag{
30+
&cli.BoolFlag{
31+
Name: "yes",
32+
Aliases: []string{"y"},
33+
Usage: "Skip confirmation prompt",
34+
},
35+
},
2936
Action: removeCmd,
3037
Category: CategoryPlugin,
3138
}
@@ -36,18 +43,26 @@ func removeCmd(ctx context.Context, cmd *cli.Command) error {
3643
if l < 1 {
3744
return cli.Exit("invalid arguments", 1)
3845
}
46+
yes := ctx.Bool("yes")
47+
3948
manager := internal.NewSdkManager()
4049
defer manager.Close()
4150
pterm.Println("Removing this plugin will remove the installed sdk along with the plugin.")
42-
result, _ := pterm.DefaultInteractiveConfirm.
43-
WithTextStyle(&pterm.ThemeDefault.DefaultText).
44-
WithConfirmStyle(&pterm.ThemeDefault.DefaultText).
45-
WithRejectStyle(&pterm.ThemeDefault.DefaultText).
46-
WithDefaultText("Please confirm").
47-
Show()
48-
if result {
49-
return manager.Remove(args.First())
50-
} else {
51-
return cli.Exit("remove canceled", 1)
51+
52+
if !yes {
53+
if !internal.IsInteractiveTerminal() {
54+
return cli.Exit("Use the -y flag to skip confirmation in non-interactive environments", 1)
55+
}
56+
result, _ := pterm.DefaultInteractiveConfirm.
57+
WithTextStyle(&pterm.ThemeDefault.DefaultText).
58+
WithConfirmStyle(&pterm.ThemeDefault.DefaultText).
59+
WithRejectStyle(&pterm.ThemeDefault.DefaultText).
60+
WithDefaultText("Please confirm").
61+
Show()
62+
if !result {
63+
return cli.Exit("remove canceled", 1)
64+
}
5265
}
66+
67+
return manager.Remove(args.First())
5368
}

cmd/commands/search.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ func RunSearch(sdkName string, availableArgs []string) error {
7979
installedVersions.Add(string(version))
8080
}
8181

82+
if !internal.IsInteractiveTerminal() {
83+
fmt.Println("Available versions:")
84+
for _, option := range options {
85+
label := option.Value
86+
if installedVersions.Contains(option.Key) {
87+
label = fmt.Sprintf("%s (installed)", label)
88+
}
89+
fmt.Printf(" - %s\n", label)
90+
}
91+
return nil
92+
}
93+
8294
_, height, _ := terminal.GetSize(int(os.Stdout.Fd()))
8395
kvSelect := printer.PageKVSelect{
8496
TopText: "Please select a version of " + sdkName + " to install",

cmd/commands/use.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ func useCmd(ctx context.Context, cmd *cli.Command) error {
9595
for _, version := range list {
9696
arr = append(arr, string(version))
9797
}
98+
if len(arr) == 0 {
99+
return fmt.Errorf("no versions available for %s", name)
100+
}
101+
if !internal.IsInteractiveTerminal() {
102+
return cli.Exit("Please specify a version to use in non-interactive environments", 1)
103+
}
98104
selectPrinter := pterm.InteractiveSelectPrinter{
99105
TextStyle: &pterm.ThemeDefault.DefaultText,
100106
OptionStyle: &pterm.ThemeDefault.DefaultText,

internal/ci.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package internal
2+
3+
import (
4+
"os"
5+
"strings"
6+
)
7+
8+
var (
9+
ciTruthyEnvVars = []string{
10+
"CI",
11+
"CONTINUOUS_INTEGRATION",
12+
}
13+
14+
ciPresenceEnvVars = []string{
15+
"GITHUB_ACTIONS",
16+
"GITLAB_CI",
17+
"BUILDKITE",
18+
"TF_BUILD",
19+
"TEAMCITY_VERSION",
20+
"TRAVIS",
21+
"CIRCLECI",
22+
"APPVEYOR",
23+
"BITBUCKET_BUILD_NUMBER",
24+
"JENKINS_URL",
25+
"DRONE",
26+
"HUDSON_URL",
27+
"GO_SERVER_URL",
28+
"CODEBUILD_BUILD_ID",
29+
// https://docs.gitlab.com/ci/variables/predefined_variables/
30+
"CI_PIPELINE_ID",
31+
}
32+
)
33+
34+
// isCI checks if the current environment is CI.
35+
func isCI() bool {
36+
for _, key := range ciTruthyEnvVars {
37+
if isTruthyEnv(os.Getenv(key)) {
38+
return true
39+
}
40+
}
41+
42+
for _, key := range ciPresenceEnvVars {
43+
if os.Getenv(key) != "" {
44+
return true
45+
}
46+
}
47+
48+
return false
49+
}
50+
51+
// IsInteractiveTerminal checks if the current environment supports interactive terminal operations.
52+
// Returns false if running in CI or if stdout is not a terminal (e.g., piped output).
53+
func IsInteractiveTerminal() bool {
54+
if isCI() {
55+
return false
56+
}
57+
return true
58+
}
59+
60+
func isTruthyEnv(value string) bool {
61+
normalized := strings.TrimSpace(strings.ToLower(value))
62+
switch normalized {
63+
case "", "0", "false", "no", "off":
64+
return false
65+
}
66+
return true
67+
}

internal/ci_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package internal
2+
3+
import "testing"
4+
5+
func TestIsCI(t *testing.T) {
6+
allEnvVars := append([]string{}, ciTruthyEnvVars...)
7+
allEnvVars = append(allEnvVars, ciPresenceEnvVars...)
8+
9+
testCases := []struct {
10+
name string
11+
env map[string]string
12+
want bool
13+
}{
14+
{
15+
name: "ci_true",
16+
env: map[string]string{
17+
"CI": "true",
18+
},
19+
want: true,
20+
},
21+
{
22+
name: "ci_false",
23+
env: map[string]string{
24+
"CI": "false",
25+
},
26+
want: false,
27+
},
28+
{
29+
name: "ci_numeric",
30+
env: map[string]string{
31+
"CI": "1",
32+
},
33+
want: true,
34+
},
35+
{
36+
name: "continuous_integration_truthy",
37+
env: map[string]string{
38+
"CI": "0",
39+
"CONTINUOUS_INTEGRATION": "yes",
40+
},
41+
want: true,
42+
},
43+
{
44+
name: "github_actions",
45+
env: map[string]string{
46+
"CI": "",
47+
"GITHUB_ACTIONS": "true",
48+
},
49+
want: true,
50+
},
51+
{
52+
name: "jenkins_url",
53+
env: map[string]string{
54+
"CI": "0",
55+
"JENKINS_URL": "http://jenkins.example",
56+
},
57+
want: true,
58+
},
59+
{
60+
name: "no_indicators",
61+
env: map[string]string{},
62+
want: false,
63+
},
64+
}
65+
66+
for _, tc := range testCases {
67+
t.Run(tc.name, func(t *testing.T) {
68+
for _, key := range allEnvVars {
69+
t.Setenv(key, "")
70+
}
71+
for key, value := range tc.env {
72+
t.Setenv(key, value)
73+
}
74+
75+
if got := isCI(); got != tc.want {
76+
t.Fatalf("IsCI() = %v, want %v", got, tc.want)
77+
}
78+
})
79+
}
80+
}

internal/manager.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ func (m *Manager) LookupSdkWithInstall(name string, autoConfirm bool) (*Sdk, err
271271
if errors.As(err, &NotFoundError{}) {
272272
if autoConfirm {
273273
fmt.Printf("[%s] not added yet, automatically proceeding with installation.\n", pterm.LightBlue(name))
274+
} else if !IsInteractiveTerminal() {
275+
return nil, cli.Exit(fmt.Sprintf("Plugin %s is not installed. Use the -y flag to automatically install plugins in non-interactive environments", name), 1)
274276
} else {
275277
fmt.Printf("[%s] not added yet, confirm that you want to use [%s]? \n", pterm.LightBlue(name), pterm.LightRed(name))
276278
result, _ := pterm.DefaultInteractiveConfirm.
@@ -280,7 +282,7 @@ func (m *Manager) LookupSdkWithInstall(name string, autoConfirm bool) (*Sdk, err
280282
WithDefaultText("Please confirm").
281283
Show()
282284
if !result {
283-
return nil, cli.Exit("", 1)
285+
return nil, cli.Exit(fmt.Sprintf("Plugin %s is not installed. Installation cancelled by user", name), 1)
284286
}
285287
}
286288
manifest, err := m.fetchPluginManifest(m.GetRegistryAddress(name + ".json"))

0 commit comments

Comments
 (0)