Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,33 @@ jobs:
git diff --exit-code -- go.mod go.sum

test:
name: test
runs-on: ubuntu-latest
name: test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
# Git Bash on Windows too; make is not installed there, so call go directly.
shell: bash
env:
BIN: bin/devlyft${{ matrix.os == 'windows-latest' && '.exe' || '' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: make test
- run: make build
- run: ./bin/devlyft version
- run: go test -race ./...
- run: go build -trimpath -o "$BIN" ./cmd/devlyft
- run: '"./$BIN" version'
- name: Completion scripts generate
run: |
for shell in bash zsh fish powershell; do
"./$BIN" completion "$shell" > /dev/null
done

security:
name: security
Expand Down
57 changes: 57 additions & 0 deletions internal/cli/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"
)

// newCompletionCmd replaces Cobra's default completion command (Cobra skips
// its own when one named "completion" exists) so the help text is ours.
func newCompletionCmd() *cobra.Command {
return &cobra.Command{
Use: "completion bash|zsh|fish|powershell",
Short: "Generate a shell completion script",
Long: `Generate a completion script for devlyft for the given shell.

The script is printed to stdout. Load it in your shell's start-up file so
completions are available in every session.

Bash (needs the bash-completion package):

echo 'eval "$(devlyft completion bash)"' >> ~/.bashrc

Zsh (enable completion first with "autoload -U compinit && compinit"):

devlyft completion zsh > "${fpath[1]}/_devlyft"

Fish:

devlyft completion fish > ~/.config/fish/completions/devlyft.fish

PowerShell:

devlyft completion powershell | Out-String | Invoke-Expression

To load it in every session, add that line to the file shown by $PROFILE.`,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
root := cmd.Root()
out := cmd.OutOrStdout()
switch args[0] {
case "bash":
return root.GenBashCompletionV2(out, true)
case "zsh":
return root.GenZshCompletion(out)
case "fish":
return root.GenFishCompletion(out, true)
case "powershell":
return root.GenPowerShellCompletionWithDesc(out)
default:
return fmt.Errorf("unsupported shell %q", args[0])
}
},
}
}
70 changes: 70 additions & 0 deletions internal/cli/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cli

import (
"strings"
"testing"

"github.com/devlyft/cli/internal/exitcode"
"github.com/devlyft/cli/internal/iostreams"
)

func TestCompletion(t *testing.T) {
t.Parallel()

// Each script must mention the binary so it registers completions for it.
for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
t.Run(shell, func(t *testing.T) {
t.Parallel()

ios, _, stdout, stderr := iostreams.Test()
code := Execute(t.Context(), ios, []string{"completion", shell})

if code != exitcode.OK {
t.Fatalf("exit code = %d, want %d (stderr: %q)", code, exitcode.OK, stderr.String())
}
if !strings.Contains(stdout.String(), "devlyft") {
t.Errorf("%s script does not mention devlyft", shell)
}
if stderr.Len() != 0 {
t.Errorf("stderr = %q, want empty", stderr.String())
}
})
}
}

func TestCompletionInvalidArgs(t *testing.T) {
t.Parallel()

tests := map[string][]string{
"unknown shell": {"completion", "tcsh"},
"no shell": {"completion"},
"two shells": {"completion", "bash", "zsh"},
}
for name, args := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

ios, _, stdout, _ := iostreams.Test()
code := Execute(t.Context(), ios, args)

if code != exitcode.Usage {
t.Fatalf("exit code = %d, want %d", code, exitcode.Usage)
}
if stdout.Len() != 0 {
t.Errorf("stdout = %q, want empty", stdout.String())
}
})
}
}

func TestCompletionReplacesCobraDefault(t *testing.T) {
t.Parallel()

ios, _, stdout, _ := iostreams.Test()
if code := Execute(t.Context(), ios, []string{"completion", "--help"}); code != exitcode.OK {
t.Fatalf("exit code = %d, want %d", code, exitcode.OK)
}
if !strings.Contains(stdout.String(), "devlyft completion powershell | Out-String") {
t.Errorf("help text is not ours:\n%s", stdout.String())
}
}
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command {
pf.StringVar(&flags.verbosity, "verbosity", "warning", "Log level for diagnostics on stderr: "+strings.Join(cmdutil.Verbosities, ", ")+" (DEVLYFT_DEBUG=1 for debug)")

root.AddCommand(newVersionCmd())
root.AddCommand(newCompletionCmd())

return root
}
Expand Down
34 changes: 34 additions & 0 deletions internal/cli/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cli

import (
"regexp"
"testing"

"github.com/devlyft/cli/internal/exitcode"
"github.com/devlyft/cli/internal/iostreams"
)

// The version line is read by scripts and bug reports, so its shape is
// pinned: "devlyft <version>[ (<commit>[, <date>])] <go version> <os>/<arch>".
var versionLine = regexp.MustCompile(`^devlyft \S+( \([0-9a-f]{1,12}(, \S+)?\))? go\S+ [a-z0-9]+/[a-z0-9]+\n$`)

func TestVersionLineFormat(t *testing.T) {
t.Parallel()

ios, _, stdout, stderr := iostreams.Test()
if code := Execute(t.Context(), ios, []string{"version"}); code != exitcode.OK {
t.Fatalf("exit code = %d, want %d (stderr: %q)", code, exitcode.OK, stderr.String())
}
if !versionLine.MatchString(stdout.String()) {
t.Errorf("version line %q does not match %s", stdout.String(), versionLine)
}
}

func TestVersionRejectsArgs(t *testing.T) {
t.Parallel()

ios, _, _, _ := iostreams.Test()
if code := Execute(t.Context(), ios, []string{"version", "extra"}); code != exitcode.Usage {
t.Fatalf("exit code = %d, want %d for unexpected argument", code, exitcode.Usage)
}
}
Loading