Skip to content

Commit fd97ceb

Browse files
authored
Improve usability of CLI, particularly create command (#20)
Updates to improve usability of the CLI: * Refactoring and adding tests along with CI * `create` without an ID (relies on API change shipping first) * Support `--visibility` and `--name` to set those properties on `create` command. * `--init` option on `create` command to do both at once * Fix for spurious Go marshaling error printed by `create` command
1 parent 3493bec commit fd97ceb

File tree

14 files changed

+1107
-256
lines changed

14 files changed

+1107
-256
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
packages: read
12+
13+
jobs:
14+
build-and-test:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v6
18+
19+
- uses: actions/setup-go@v6
20+
with:
21+
go-version-file: 'go.mod'
22+
23+
- name: Build and test
24+
run: |
25+
go build
26+
go test -v ./...

cmd/client.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cmd
2+
3+
import "io"
4+
5+
// restClient is the subset of api.RESTClient methods needed by the various commands.
6+
type restClient interface {
7+
Get(path string, resp interface{}) error
8+
Delete(path string, resp interface{}) error
9+
Do(method string, path string, body io.Reader, resp interface{}) error
10+
Patch(path string, body io.Reader, resp interface{}) error
11+
Post(path string, body io.Reader, resp interface{}) error
12+
Put(path string, body io.Reader, resp interface{}) error
13+
}

cmd/create.go

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,34 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"strings"
87
"net/url"
8+
"strings"
9+
910
"github.com/MakeNowJust/heredoc"
1011
"github.com/cli/go-gh/v2/pkg/api"
1112
"github.com/spf13/cobra"
1213
)
1314

1415
type createCmdFlags struct {
1516
app string
16-
EnvironmentVariables []string
17-
Secrets []string
18-
RevisionName string
17+
name string
18+
visibility string
19+
environmentVariables []string
20+
secrets []string
21+
revisionName string
22+
init bool
1923
}
2024

2125
type createReq struct {
26+
Name string `json:"friendly_name,omitempty"`
27+
Visibility string `json:"visibility,omitempty"`
2228
EnvironmentVariables map[string]string `json:"environment_variables"`
2329
Secrets map[string]string `json:"secrets"`
2430
}
2531

2632
type createResp struct {
33+
AppUrl string `json:"app_url"`
34+
ID string `json:"id"`
2735
}
2836

2937
func init() {
@@ -36,78 +44,105 @@ func init() {
3644
`),
3745
Example: heredoc.Doc(`
3846
$ gh runtime create --app my-app --env key1=value1 --env key2=value2 --secret key3=value3 --secret key4=value4
39-
# => Creates the app named 'my-app'
47+
# => Creates the app with the ID 'my-app'
48+
49+
$ gh runtime create --name my-new-app
50+
# => Creates a new app with the given name
4051
`),
41-
Run: func(cmd *cobra.Command, args []string) {
42-
if createCmdFlags.app == "" {
43-
fmt.Println("Error: --app flag is required")
44-
return
52+
RunE: func(cmd *cobra.Command, args []string) error {
53+
client, err := api.DefaultRESTClient()
54+
if err != nil {
55+
return fmt.Errorf("failed creating REST client: %v", err)
4556
}
4657

47-
// Construct the request body
48-
requestBody := createReq{
49-
EnvironmentVariables: map[string]string{},
50-
Secrets: map[string]string{},
58+
resp, err := runCreate(client, createCmdFlags)
59+
if err != nil {
60+
return err
5161
}
5262

53-
for _, pair := range createCmdFlags.EnvironmentVariables {
54-
parts := strings.SplitN(pair, "=", 2)
55-
if len(parts) == 2 {
56-
key := parts[0]
57-
value := parts[1]
58-
requestBody.EnvironmentVariables[key] = value
59-
} else {
60-
fmt.Printf("Error: Invalid environment variable format (%s). Must be in the form 'key=value'\n", pair)
61-
return
62-
}
63+
fmt.Printf("App created: %s\n", resp.AppUrl)
64+
if resp.ID != "" {
65+
fmt.Printf("ID: %s\n", resp.ID)
6366
}
67+
return nil
68+
},
69+
}
6470

65-
for _, pair := range createCmdFlags.Secrets {
66-
parts := strings.SplitN(pair, "=", 2)
67-
if len(parts) == 2 {
68-
key := parts[0]
69-
value := parts[1]
70-
requestBody.Secrets[key] = value
71-
} else {
72-
fmt.Printf("Error: Invalid secret format (%s). Must be in the form 'key=value'\n", pair)
73-
return
74-
}
75-
}
71+
createCmd.Flags().StringVarP(&createCmdFlags.app, "app", "a", "", "The app ID to create")
72+
createCmd.Flags().StringVarP(&createCmdFlags.name, "name", "n", "", "The name for the app")
73+
createCmd.Flags().StringVarP(&createCmdFlags.visibility, "visibility", "v", "", "The visibility of the app (e.g. 'only_owner' or 'github')")
74+
createCmd.Flags().StringSliceVarP(&createCmdFlags.environmentVariables, "env", "e", []string{}, "Environment variables to set on the app in the form 'key=value'")
75+
createCmd.Flags().StringSliceVarP(&createCmdFlags.secrets, "secret", "s", []string{}, "Secrets to set on the app in the form 'key=value'")
76+
createCmd.Flags().StringVarP(&createCmdFlags.revisionName, "revision-name", "r", "", "The revision name to use for the app")
77+
createCmd.Flags().BoolVar(&createCmdFlags.init, "init", false, "Initialize a runtime.config.json file in the current directory after creating the app")
78+
rootCmd.AddCommand(createCmd)
79+
}
7680

77-
body, err := json.Marshal(requestBody)
78-
if err != nil {
79-
fmt.Printf("Error marshalling request body: %v\n", err)
80-
return
81-
}
81+
func runCreate(client restClient, flags createCmdFlags) (createResp, error) {
82+
if flags.app == "" && flags.name == "" {
83+
return createResp{}, fmt.Errorf("either --app or --name flag is required")
84+
}
8285

83-
createUrl := fmt.Sprintf("runtime/%s/deployment", createCmdFlags.app)
84-
params := url.Values{}
85-
if createCmdFlags.RevisionName != "" {
86-
params.Add("revision_name", createCmdFlags.RevisionName)
87-
}
88-
if len(params) > 0 {
89-
createUrl += "?" + params.Encode()
90-
}
91-
92-
client, err := api.DefaultRESTClient()
93-
if err != nil {
94-
fmt.Println(err)
95-
return
96-
}
97-
var response string
98-
err = client.Put(createUrl, bytes.NewReader(body), &response)
99-
if err != nil {
100-
fmt.Printf("Error creating app: %v\n", err)
101-
return
102-
}
86+
requestBody := createReq{
87+
Name: flags.name,
88+
Visibility: flags.visibility,
89+
EnvironmentVariables: map[string]string{},
90+
Secrets: map[string]string{},
91+
}
10392

104-
fmt.Printf("App created: %s\n", response) // TODO pretty print details
105-
},
93+
for _, pair := range flags.environmentVariables {
94+
parts := strings.SplitN(pair, "=", 2)
95+
if len(parts) == 2 {
96+
requestBody.EnvironmentVariables[parts[0]] = parts[1]
97+
} else {
98+
return createResp{}, fmt.Errorf("invalid environment variable format (%s). Must be in the form 'key=value'", pair)
99+
}
106100
}
107101

108-
createCmd.Flags().StringVarP(&createCmdFlags.app, "app", "a", "", "The app to create")
109-
createCmd.Flags().StringSliceVarP(&createCmdFlags.EnvironmentVariables, "env", "e", []string{}, "Environment variables to set on the app in the form 'key=value'")
110-
createCmd.Flags().StringSliceVarP(&createCmdFlags.Secrets, "secret", "s", []string{}, "Secrets to set on the app in the form 'key=value'")
111-
createCmd.Flags().StringVarP(&createCmdFlags.RevisionName, "revision-name", "r", "", "The revision name to use for the app")
112-
rootCmd.AddCommand(createCmd)
102+
for _, pair := range flags.secrets {
103+
parts := strings.SplitN(pair, "=", 2)
104+
if len(parts) == 2 {
105+
requestBody.Secrets[parts[0]] = parts[1]
106+
} else {
107+
return createResp{}, fmt.Errorf("invalid secret format (%s). Must be in the form 'key=value'", pair)
108+
}
109+
}
110+
111+
body, err := json.Marshal(requestBody)
112+
if err != nil {
113+
return createResp{}, fmt.Errorf("error marshalling request body: %v", err)
114+
}
115+
116+
var createUrl string
117+
if flags.app != "" {
118+
createUrl = fmt.Sprintf("runtime/%s/deployment", flags.app)
119+
} else {
120+
createUrl = "runtime"
121+
}
122+
123+
params := url.Values{}
124+
if flags.revisionName != "" {
125+
params.Add("revision_name", flags.revisionName)
126+
}
127+
if len(params) > 0 {
128+
createUrl += "?" + params.Encode()
129+
}
130+
131+
response := createResp{}
132+
err = client.Put(createUrl, bytes.NewReader(body), &response)
133+
if err != nil {
134+
return createResp{}, fmt.Errorf("error creating app: %v", err)
135+
}
136+
137+
if flags.init {
138+
if response.ID == "" {
139+
return response, fmt.Errorf("error initializing config: server did not return an app ID")
140+
}
141+
err = writeRuntimeConfig(response.ID, "")
142+
if err != nil {
143+
return response, fmt.Errorf("error initializing config: %v", err)
144+
}
145+
}
146+
147+
return response, nil
113148
}

0 commit comments

Comments
 (0)