Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation command #63

Merged
merged 4 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,24 @@ $ shuttle run build tag=v1
* ...

## Documentation
*Documentation is under development*

Plan documentation can be inspected using the `shuttle documentation` command.

When writing shuttle plans you can hint as to where to find documentation for the plan.
Users of the plan will open the specified URL when requesting documentation.

```yaml
# plan.yaml
documentation: https://docs.my-corp.com
```

If no specific `documentation` field is set in the plan it will be inferred from the plan reference.
In below example shuttle will open `https://github.com/lunarway/shuttle-example-go-plan.git`

```yaml
# shuttle.yaml
plan: git@github.com:lunarway/shuttle-example-go-plan.git
```

### Git Plan
When using a git plan a url should look like:
Expand Down
42 changes: 42 additions & 0 deletions cmd/documentation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"github.com/lunarway/shuttle/pkg/browser"
"github.com/lunarway/shuttle/pkg/errors"
"github.com/spf13/cobra"
)

var documentationCommand = &cobra.Command{
Use: "documentation",
Aliases: []string{"docs"},
Short: "Open documentation for the configured shuttle plan",
Long: `Open documentation for the configured shuttle plan.
By default shuttle will try to open the plan's documentation in a web browser.

If no docs are explicitly configured in the plan, the plan it self is opened.
Usually this will target a hosted git repository, eg. GitHub README.

The application to open the documentation is inferred from the operating system
and respects the BROWSER environment variable.`,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
context, err := getProjectContext()
checkError(err)

url, err := context.DocumentationURL()
checkError(err)
uii.Infoln("Documentation available at: %s", url)

browseCmd, err := browser.Command(url)
checkError(err)

err = browseCmd.Run()
if err != nil {
checkError(errors.NewExitCode(1, "Failed to open document reference: %v", err))
}
},
}

func init() {
rootCmd.AddCommand(documentationCommand)
}
1 change: 1 addition & 0 deletions examples/station-plan/plan.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
documentation: https://github.com/lunarway/shuttle
scripts:
build:
description: Build the docker image
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ require (
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.16.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
github.com/cli/safeexec v1.0.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-cmd/cmd v1.2.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.0.0 // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-cmd/cmd v1.2.0 h1:Aohz0ZG0nQbvT4z55Mh+fdegX48GSAXL3cSsbYxRfvI=
github.com/go-cmd/cmd v1.2.0/go.mod h1:XgKkd0L6sv9WcYV0FS8RfG1RJCSTVHTsLeAD2pTgHt0=
github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc=
github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
Expand Down
77 changes: 77 additions & 0 deletions pkg/browser/browser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package browser

import (
"os"
"os/exec"
"runtime"
"strings"

"github.com/cli/safeexec"
"github.com/google/shlex"
)

// This package is copied from github.com/cli/cli

// Command returns an exec.Cmd instance respecting runtime.GOOS and $BROWSER
// environment variable.
func Command(url string) (*exec.Cmd, error) {
launcher := os.Getenv("BROWSER")
if launcher != "" {
return fromBrowserEnv(launcher, url)
}
return forOS(runtime.GOOS, url), nil
}

func forOS(goos, url string) *exec.Cmd {
exe := "open"
var args []string
switch goos {
case "darwin":
args = append(args, url)
case "windows":
exe, _ = lookPath("cmd")
r := strings.NewReplacer("&", "^&")
args = append(args, "/c", "start", r.Replace(url))
default:
exe = linuxExe()
args = append(args, url)
}

cmd := exec.Command(exe, args...)
cmd.Stderr = os.Stderr
return cmd
}

// fromBrowserEnv parses the BROWSER string based on shell splitting rules.
func fromBrowserEnv(launcher, url string) (*exec.Cmd, error) {
args, err := shlex.Split(launcher)
if err != nil {
return nil, err
}

exe, err := lookPath(args[0])
if err != nil {
return nil, err
}

args = append(args, url)
cmd := exec.Command(exe, args[1:]...)
cmd.Stderr = os.Stderr
return cmd, nil
}

func linuxExe() string {
exe := "xdg-open"

_, err := lookPath(exe)
if err != nil {
_, err := lookPath("wslview")
if err == nil {
exe = "wslview"
}
}

return exe
}

var lookPath = safeexec.LookPath
78 changes: 78 additions & 0 deletions pkg/browser/browser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package browser

import (
"errors"
"reflect"
"testing"
)

// This package is copied from github.com/cli/cli

func TestForOS(t *testing.T) {
type args struct {
goos string
url string
}
tests := []struct {
name string
args args
exe string
want []string
}{
{
name: "macOS",
args: args{
goos: "darwin",
url: "https://example.com/path?a=1&b=2",
},
want: []string{"open", "https://example.com/path?a=1&b=2"},
},
{
name: "Linux",
args: args{
goos: "linux",
url: "https://example.com/path?a=1&b=2",
},
exe: "xdg-open",
want: []string{"xdg-open", "https://example.com/path?a=1&b=2"},
},
{
name: "WSL",
args: args{
goos: "linux",
url: "https://example.com/path?a=1&b=2",
},
exe: "wslview",
want: []string{"wslview", "https://example.com/path?a=1&b=2"},
},
{
name: "Windows",
args: args{
goos: "windows",
url: "https://example.com/path?a=1&b=2&c=3",
},
exe: "cmd",
want: []string{"cmd", "/c", "start", "https://example.com/path?a=1^&b=2^&c=3"},
},
}
for _, tt := range tests {
origLookPath := lookPath
lookPath = func(file string) (string, error) {
if file == tt.exe {
return file, nil
} else {
return "", errors.New("not found")
}
}
defer func() {
lookPath = origLookPath
}()

t.Run(tt.name, func(t *testing.T) {
cmd := forOS(tt.args.goos, tt.args.url)
if !reflect.DeepEqual(cmd.Args, tt.want) {
t.Errorf("ForOS() = %v, want %v", cmd.Args, tt.want)
}
})
}
}
50 changes: 50 additions & 0 deletions pkg/config/documentation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package config

import (
"fmt"
"path/filepath"
"strings"

"github.com/lunarway/shuttle/pkg/errors"
"github.com/lunarway/shuttle/pkg/git"
)

// DocumentationURL returns a URL pointing to plan documentation if any is
// available. Plan reference and plan documentation field is inspected and
// parsed.
func (p *ShuttleProjectContext) DocumentationURL() (string, error) {
var ref string
switch {
case p.Plan.Documentation != "":
ref = p.Plan.Documentation
case p.Config.Plan != "":
ref = p.Config.Plan
default:
return "", errors.NewExitCode(1, "Could not find any plan documentation")
}

switch {
case git.IsPlan(ref):
return normalizeGitPlan(git.ParsePlan(ref))
case isMatching("^(http|https)://", ref):
Crevil marked this conversation as resolved.
Show resolved Hide resolved
return ref, nil
case filepath.IsAbs(ref), strings.HasPrefix(ref, "./"), strings.HasPrefix(ref, "../"):
return "", errors.NewExitCode(2, "Local plan has no documentation")
default:
return "", errors.NewExitCode(1, "Could not detect protocol for plan '%s'", ref)
}
}

func normalizeGitPlan(p git.Plan) (string, error) {
switch p.Protocol {
case "https":
return fmt.Sprintf("%s://%s", p.Protocol, p.Repository), nil
case "ssh":
repoSlug := strings.TrimPrefix(p.Repository, fmt.Sprintf("%s:", p.Host))
return fmt.Sprintf("https://%s/%s", p.Host, repoSlug), nil
default:
// this should never happen as parsed git plans always has a protocol of ssh
// or https
return "", errors.NewExitCode(1, "Could not parse git plan reference")
}
}
Loading