Skip to content

Commit eaada7a

Browse files
update readme
0 parents  commit eaada7a

15 files changed

Lines changed: 765 additions & 0 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"features": {
3+
"ghcr.io/devcontainers/features/github-cli:1": {},
4+
"ghcr.io/devcontainers/features/go:1": {}
5+
}
6+
}

.github/workflows/release.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: release
2+
on:
3+
push:
4+
tags:
5+
- "v*"
6+
permissions:
7+
contents: write
8+
id-token: write
9+
attestations: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: cli/gh-extension-precompile@v2
17+
with:
18+
generate_attestations: true
19+
go_version_file: go.mod

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/gh-runtime-cli
2+
/gh-runtime-cli.exe
3+
/test

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @github/copilot-workbench-reviewers

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# gh-runtime-cli
2+
This GH CLI extension that allows you to deploy to the GitHub runtime.
3+
4+
### Versioning
5+
The version of the CLI app is defined in the `cmd/version.go` and can be accessed via the `version` command.
6+
Please update accordingly when making changes to the app.

cmd/create.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
"net/url"
9+
"github.com/MakeNowJust/heredoc"
10+
"github.com/cli/go-gh/v2/pkg/api"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
type createCmdFlags struct {
15+
app string
16+
EnvironmentVariables []string
17+
Secrets []string
18+
RevisionName string
19+
}
20+
21+
type createReq struct {
22+
EnvironmentVariables map[string]string `json:"environment_variables"`
23+
Secrets map[string]string `json:"secrets"`
24+
}
25+
26+
type createResp struct {
27+
}
28+
29+
func init() {
30+
createCmdFlags := createCmdFlags{}
31+
createCmd := &cobra.Command{
32+
Use: "create",
33+
Short: "Create a GitHub Runtime app",
34+
Long: heredoc.Doc(`
35+
Create a GitHub Runtime app
36+
`),
37+
Example: heredoc.Doc(`
38+
$ 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'
40+
`),
41+
Run: func(cmd *cobra.Command, args []string) {
42+
if createCmdFlags.app == "" {
43+
fmt.Println("Error: --app flag is required")
44+
return
45+
}
46+
47+
// Construct the request body
48+
requestBody := createReq{
49+
EnvironmentVariables: map[string]string{},
50+
Secrets: map[string]string{},
51+
}
52+
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+
}
64+
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+
}
76+
77+
body, err := json.Marshal(requestBody)
78+
if err != nil {
79+
fmt.Printf("Error marshalling request body: %v\n", err)
80+
return
81+
}
82+
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+
}
103+
104+
fmt.Printf("App created: %s\n", response) // TODO pretty print details
105+
},
106+
}
107+
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)
113+
}

cmd/delete.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"github.com/MakeNowJust/heredoc"
7+
"github.com/cli/go-gh/v2/pkg/api"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
type deleteCmdFlags struct {
12+
app string
13+
revisionName string
14+
}
15+
16+
type deleteResp struct {
17+
}
18+
19+
func init() {
20+
deleteCmdFlags := deleteCmdFlags{}
21+
deleteCmd := &cobra.Command{
22+
Use: "delete",
23+
Short: "Delete a GitHub Runtime app",
24+
Long: heredoc.Doc(`
25+
Delete a GitHub Runtime app
26+
`),
27+
Example: heredoc.Doc(`
28+
$ gh runtime delete --app my-app
29+
# => Deletes the app named 'my-app'
30+
`),
31+
Run: func(cmd *cobra.Command, args []string) {
32+
if deleteCmdFlags.app == "" {
33+
fmt.Println("Error: --app flag is required")
34+
return
35+
}
36+
37+
deleteUrl := fmt.Sprintf("runtime/%s/deployment", deleteCmdFlags.app)
38+
params := url.Values{}
39+
if deleteCmdFlags.revisionName != "" {
40+
params.Add("revision_name", deleteCmdFlags.revisionName)
41+
}
42+
if len(params) > 0 {
43+
deleteUrl += "?" + params.Encode()
44+
}
45+
46+
client, err := api.DefaultRESTClient()
47+
if err != nil {
48+
fmt.Println(err)
49+
return
50+
}
51+
var response string
52+
err = client.Delete(deleteUrl, &response)
53+
if err != nil {
54+
// print err and response
55+
fmt.Printf("Error deleting app: %v\n", err)
56+
fmt.Printf("Response: %v\n", response)
57+
return
58+
}
59+
60+
fmt.Printf("App deleted: %s\n", response)
61+
},
62+
}
63+
64+
deleteCmd.Flags().StringVarP(&deleteCmdFlags.app, "app", "a", "", "The app to delete")
65+
deleteCmd.Flags().StringVarP(&deleteCmdFlags.revisionName, "revision-name", "r", "", "The revision name to use for the app")
66+
rootCmd.AddCommand(deleteCmd)
67+
}

cmd/deploy.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package cmd
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"net/url"
9+
"os"
10+
"path/filepath"
11+
12+
"github.com/MakeNowJust/heredoc"
13+
"github.com/cli/go-gh/v2/pkg/api"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type deployCmdFlags struct {
18+
dir string
19+
app string
20+
revisionName string
21+
sha string
22+
}
23+
24+
func zipDirectory(sourceDir, destinationZip string) error {
25+
zipFile, err := os.Create(destinationZip)
26+
if err != nil {
27+
return fmt.Errorf("error creating zip file '%s': %w", destinationZip, err)
28+
}
29+
defer zipFile.Close()
30+
31+
zipWriter := zip.NewWriter(zipFile)
32+
defer zipWriter.Close()
33+
34+
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
35+
if err != nil {
36+
return fmt.Errorf("error accessing path '%s': %w", path, err)
37+
}
38+
39+
relPath, err := filepath.Rel(sourceDir, path)
40+
if err != nil {
41+
return fmt.Errorf("error calculating relative path for '%s': %w", path, err)
42+
}
43+
44+
if info.IsDir() {
45+
if relPath == "." {
46+
return nil
47+
}
48+
relPath += "/"
49+
}
50+
51+
header, err := zip.FileInfoHeader(info)
52+
if err != nil {
53+
return fmt.Errorf("error creating zip header for '%s': %w", path, err)
54+
}
55+
header.Name = relPath
56+
if !info.IsDir() {
57+
header.Method = zip.Deflate
58+
}
59+
60+
writer, err := zipWriter.CreateHeader(header)
61+
if err != nil {
62+
return fmt.Errorf("error creating zip writer for '%s': %w", path, err)
63+
}
64+
65+
if !info.IsDir() {
66+
file, err := os.Open(path)
67+
if err != nil {
68+
return fmt.Errorf("error opening file '%s': %w", path, err)
69+
}
70+
defer file.Close()
71+
72+
_, err = io.Copy(writer, file)
73+
if err != nil {
74+
return fmt.Errorf("error writing file '%s' to zip: %w", path, err)
75+
}
76+
}
77+
78+
return nil
79+
})
80+
81+
if err != nil {
82+
return fmt.Errorf("error zipping directory '%s': %w", sourceDir, err)
83+
}
84+
85+
return nil
86+
}
87+
88+
func init() {
89+
deployCmdFlags := deployCmdFlags{}
90+
deployCmd := &cobra.Command{
91+
Use: "deploy",
92+
Short: "Deploy app to GitHub Runtime",
93+
Long: heredoc.Doc(`
94+
Deploys a directory to a GitHub Runtime app
95+
`),
96+
Example: heredoc.Doc(`
97+
$ gh runtime deploy --dir ./dist --app my-app [--sha <sha>]
98+
# => Deploys the contents of the 'dist' directory to the app named 'my-app'.
99+
`),
100+
RunE: func(cmd *cobra.Command, args []string) error {
101+
if deployCmdFlags.dir == "" {
102+
return fmt.Errorf("--dir flag is required")
103+
}
104+
if deployCmdFlags.app == "" {
105+
return fmt.Errorf("--app flag is required")
106+
}
107+
108+
if _, err := os.Stat(deployCmdFlags.dir); os.IsNotExist(err) {
109+
return fmt.Errorf("directory '%s' does not exist", deployCmdFlags.dir)
110+
}
111+
112+
_, err := os.ReadDir(deployCmdFlags.dir)
113+
if err != nil {
114+
return fmt.Errorf("error reading directory '%s': %v", deployCmdFlags.dir, err)
115+
}
116+
117+
// Zip the directory
118+
zipPath := fmt.Sprintf("%s.zip", deployCmdFlags.dir)
119+
err = zipDirectory(deployCmdFlags.dir, zipPath)
120+
if err != nil {
121+
return fmt.Errorf("error zipping directory '%s': %v", deployCmdFlags.dir, err)
122+
}
123+
defer os.Remove(zipPath)
124+
125+
client, err := api.DefaultRESTClient()
126+
if err != nil {
127+
return fmt.Errorf("error creating REST client: %v", err)
128+
}
129+
130+
deploymentsUrl := fmt.Sprintf("runtime/%s/deployment/bundle", deployCmdFlags.app)
131+
params := url.Values{}
132+
133+
if deployCmdFlags.revisionName != "" {
134+
params.Add("revision_name", deployCmdFlags.revisionName)
135+
}
136+
137+
if deployCmdFlags.sha != "" {
138+
params.Add("revision", deployCmdFlags.sha)
139+
}
140+
141+
if len(params) > 0 {
142+
deploymentsUrl += "?" + params.Encode()
143+
}
144+
145+
fmt.Printf("Deploying app to %s\n", deploymentsUrl)
146+
147+
// body is the full zip RAW
148+
body, err := os.ReadFile(zipPath)
149+
if err != nil {
150+
return fmt.Errorf("error reading zip file '%s': %v", zipPath, err)
151+
}
152+
153+
err = client.Post(deploymentsUrl, bytes.NewReader(body), nil)
154+
if err != nil {
155+
return fmt.Errorf("error deploying app: %v", err)
156+
}
157+
158+
fmt.Printf("Successfully deployed app\n")
159+
return nil
160+
},
161+
}
162+
deployCmd.Flags().StringVarP(&deployCmdFlags.dir, "dir", "d", "", "The directory to deploy")
163+
deployCmd.Flags().StringVarP(&deployCmdFlags.app, "app", "a", "", "The app to deploy")
164+
deployCmd.Flags().StringVarP(&deployCmdFlags.revisionName, "revision-name", "r", "", "The revision name to deploy")
165+
deployCmd.Flags().StringVarP(&deployCmdFlags.sha, "sha", "s", "", "SHA of the app being deployed")
166+
167+
rootCmd.AddCommand(deployCmd)
168+
}

0 commit comments

Comments
 (0)