Skip to content

Commit 340a105

Browse files
authored
Merge pull request #1162 from CircleCI-Public/circleci-plugins
Add run command
2 parents 7cc6570 + 0ae616a commit 340a105

File tree

6 files changed

+167
-26
lines changed

6 files changed

+167
-26
lines changed

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func MakeCommands() *cobra.Command {
157157

158158
rootCmd.AddCommand(newOpenCommand(rootOptions))
159159
rootCmd.AddCommand(newTestsCommand())
160+
rootCmd.AddCommand(newRunCommand(rootOptions))
160161
rootCmd.AddCommand(newContextCommand(rootOptions))
161162
rootCmd.AddCommand(project.NewProjectCommand(rootOptions, validator))
162163
rootCmd.AddCommand(trigger.NewTriggerCommand(rootOptions, validator))

cmd/root_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var _ = Describe("Root", func() {
1616
Describe("subcommands", func() {
1717
It("can create commands", func() {
1818
commands := cmd.MakeCommands()
19-
Expect(len(commands.Commands())).To(Equal(28))
19+
Expect(len(commands.Commands())).To(Equal(29))
2020
})
2121
})
2222

cmd/run.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
8+
"github.com/CircleCI-Public/circleci-cli/settings"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newRunCommand(config *settings.Config) *cobra.Command {
13+
runCmd := &cobra.Command{
14+
Use: "run <name> [args...]",
15+
Short: "Execute a circleci plugin",
16+
Long: `Execute a circleci plugin by looking for a binary called circleci-<name> in your PATH.
17+
This command implements a plugin system similar to git, where you can extend
18+
circleci functionality by creating executables with the 'circleci-' prefix.
19+
20+
For example, if you have a binary called 'circleci-foo' in your PATH,
21+
you can run it with: circleci run foo [args...]`,
22+
Example: ` circleci run foo --help
23+
circleci run my-plugin arg1 arg2`,
24+
DisableFlagParsing: true,
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
// When flag parsing is disabled, we need to manually validate args
27+
if len(args) < 1 {
28+
return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args))
29+
}
30+
31+
// Handle help flags manually since flag parsing is disabled
32+
if args[0] == "--help" || args[0] == "-h" {
33+
return cmd.Help()
34+
}
35+
36+
pluginName := args[0]
37+
pluginArgs := args[1:]
38+
39+
// Construct the plugin binary name
40+
binaryName := fmt.Sprintf("circleci-%s", pluginName)
41+
42+
// Look for the binary in PATH
43+
binaryPath, err := exec.LookPath(binaryName)
44+
if err != nil {
45+
return fmt.Errorf("plugin '%s' not found: could not find '%s' in PATH: %w", pluginName, binaryName, err)
46+
}
47+
48+
// Create the command to execute the plugin
49+
pluginCmd := exec.Command(binaryPath, pluginArgs...)
50+
51+
// Connect stdin, stdout, and stderr to the current process
52+
pluginCmd.Stdin = os.Stdin
53+
pluginCmd.Stdout = os.Stdout
54+
pluginCmd.Stderr = os.Stderr
55+
56+
// Run the plugin
57+
if err := pluginCmd.Run(); err != nil {
58+
return fmt.Errorf("failed to execute plugin '%s': %w", pluginName, err)
59+
}
60+
61+
return nil
62+
},
63+
}
64+
65+
return runCmd
66+
}

cmd/run_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
8+
"github.com/CircleCI-Public/circleci-cli/settings"
9+
. "github.com/onsi/ginkgo"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("run", func() {
14+
var (
15+
tempDir string
16+
pluginPath string
17+
config *settings.Config
18+
)
19+
20+
BeforeEach(func() {
21+
var err error
22+
tempDir, err = os.MkdirTemp("", "circleci-plugin-test")
23+
Expect(err).ToNot(HaveOccurred())
24+
25+
// Create a test plugin
26+
pluginPath = filepath.Join(tempDir, "circleci-test-plugin")
27+
if runtime.GOOS == "windows" {
28+
pluginPath = pluginPath + ".bat"
29+
}
30+
pluginScript := `#!/bin/bash
31+
echo "Plugin executed"
32+
echo "Args: $@"
33+
exit 0
34+
`
35+
err = os.WriteFile(pluginPath, []byte(pluginScript), 0755)
36+
Expect(err).ToNot(HaveOccurred())
37+
38+
config = &settings.Config{}
39+
})
40+
41+
AfterEach(func() {
42+
os.RemoveAll(tempDir)
43+
})
44+
45+
Describe("plugin execution", func() {
46+
It("should find and execute a plugin in PATH", func() {
47+
// Add tempDir to PATH
48+
oldPath := os.Getenv("PATH")
49+
os.Setenv("PATH", tempDir+string(os.PathListSeparator)+oldPath)
50+
defer os.Setenv("PATH", oldPath)
51+
52+
cmd := newRunCommand(config)
53+
cmd.SetArgs([]string{"test-plugin", "arg1", "arg2"})
54+
err := cmd.Execute()
55+
Expect(err).ToNot(HaveOccurred())
56+
})
57+
58+
It("should return an error when plugin is not found", func() {
59+
cmd := newRunCommand(config)
60+
cmd.SetArgs([]string{"nonexistent-plugin"})
61+
err := cmd.Execute()
62+
Expect(err).To(HaveOccurred())
63+
Expect(err.Error()).To(ContainSubstring("plugin 'nonexistent-plugin' not found"))
64+
})
65+
66+
It("should require at least one argument", func() {
67+
cmd := newRunCommand(config)
68+
cmd.SetArgs([]string{})
69+
err := cmd.Execute()
70+
Expect(err).To(HaveOccurred())
71+
Expect(err.Error()).To(ContainSubstring("requires at least 1 arg"))
72+
})
73+
})
74+
})

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/blang/semver v3.5.1+incompatible
1111
github.com/briandowns/spinner v1.23.0
1212
github.com/fatih/color v1.18.0
13-
github.com/go-git/go-git/v5 v5.16.2
13+
github.com/go-git/go-git/v5 v5.16.3
1414
github.com/google/go-querystring v1.1.0 // indirect
1515
github.com/google/uuid v1.6.0
1616
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
@@ -41,7 +41,7 @@ require (
4141
github.com/spf13/afero v1.12.0
4242
github.com/stretchr/testify v1.10.0
4343
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
44-
golang.org/x/term v0.35.0
44+
golang.org/x/term v0.37.0
4545
)
4646

4747
require (
@@ -114,11 +114,11 @@ require (
114114
go.opentelemetry.io/otel/metric v1.35.0 // indirect
115115
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
116116
go.opentelemetry.io/otel/trace v1.35.0 // indirect
117-
golang.org/x/crypto v0.42.1-0.20250916063316-ddb4e80c6ad3 // indirect
118-
golang.org/x/net v0.44.0 // indirect
119-
golang.org/x/sync v0.17.0 // indirect
120-
golang.org/x/sys v0.36.0 // indirect
121-
golang.org/x/text v0.29.0 // indirect
117+
golang.org/x/crypto v0.44.0 // indirect
118+
golang.org/x/net v0.46.0 // indirect
119+
golang.org/x/sync v0.18.0 // indirect
120+
golang.org/x/sys v0.38.0 // indirect
121+
golang.org/x/text v0.31.0 // indirect
122122
google.golang.org/protobuf v1.36.6 // indirect
123123
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
124124
gopkg.in/warnings.v0 v0.1.2 // indirect

go.sum

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
9898
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
9999
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
100100
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
101-
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
102-
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
101+
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
102+
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
103103
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
104104
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
105105
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -327,14 +327,14 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
327327
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
328328
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
329329
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
330-
golang.org/x/crypto v0.42.1-0.20250916063316-ddb4e80c6ad3 h1:XIKCgeHPqV0Q2fZ2VxwddpjfGe0ZLzHEbCTAZ/GwWYI=
331-
golang.org/x/crypto v0.42.1-0.20250916063316-ddb4e80c6ad3/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
330+
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
331+
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
332332
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
333333
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
334334
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
335335
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
336-
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
337-
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
336+
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
337+
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
338338
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
339339
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
340340
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -345,8 +345,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
345345
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
346346
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
347347
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
348-
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
349-
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
348+
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
349+
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
350350
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
351351
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
352352
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
@@ -355,8 +355,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
355355
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
356356
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
357357
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
358-
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
359-
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
358+
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
359+
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
360360
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
361361
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
362362
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -378,28 +378,28 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
378378
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
379379
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
380380
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
381-
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
382-
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
381+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
382+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
383383
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
384384
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
385385
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
386-
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
387-
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
386+
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
387+
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
388388
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
389389
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
390390
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
391391
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
392392
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
393393
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
394-
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
395-
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
394+
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
395+
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
396396
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
397397
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
398398
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
399399
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
400400
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
401-
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
402-
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
401+
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
402+
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
403403
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
404404
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
405405
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 commit comments

Comments
 (0)