Merge pull request #4802 from cli/mislav/bin-ext-create

Extensions create 💅
This commit is contained in:
Nate Smith 2021-11-30 16:36:24 -06:00 committed by GitHub
commit 59082a1fde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 177 deletions

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
echo "TODO implement this script."
echo "It should build binaries in dist/<platform>-<arch>[.exe] as needed."
exit 1
exit 1

View file

@ -23,4 +23,4 @@ func main() {
}
// For more examples of using go-gh, see:
// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go
// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go

View file

@ -11,4 +11,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: cli/gh-extension-precompile@v1
- uses: cli/gh-extension-precompile@v1

View file

@ -13,4 +13,4 @@ jobs:
- uses: actions/checkout@v2
- uses: cli/gh-extension-precompile@v1
with:
build_script_override: "script/build.sh"
build_script_override: "script/build.sh"

View file

@ -29,7 +29,7 @@ echo "Hello %[1]s!"
# '
# TEMPLATE='
# {{- range $repo := .data.viewer.repositories.nodes -}}
# {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}}
# {{- printf "name: %%s - stargazers: %%v\n" $repo.nameWithOwner $repo.stargazerCount -}}
# {{- end -}}
# '
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"

View file

@ -510,17 +510,14 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
if err != nil {
return err
}
var cmds []*exec.Cmd
dir := filepath.Dir(ext.path)
if force {
fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD")
resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD")
cmds = []*exec.Cmd{fetchCmd, resetCmd}
} else {
pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
cmds = []*exec.Cmd{pullCmd}
if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil {
return err
}
return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run()
}
return runCmds(cmds)
return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run()
}
func (m *Manager) upgradeBinExtension(ext Extension) error {
@ -564,14 +561,7 @@ func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error
return err
}
err = os.Mkdir(name, 0755)
if err != nil {
return err
}
initCmd := m.newCommand(exe, "init", "--quiet", name)
err = initCmd.Run()
if err != nil {
if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil {
return err
}
@ -581,55 +571,26 @@ func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error
return m.otherBinScaffolding(exe, name)
}
script := fmt.Sprintf(scriptTmpl, name, "%s", "%v")
filePath := filepath.Join(name, name)
err = ioutil.WriteFile(filePath, []byte(script), 0755)
if err != nil {
script := fmt.Sprintf(scriptTmpl, name)
if err := writeFile(filepath.Join(name, name), []byte(script), 0755); err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
dir := filepath.Join(wd, name)
addCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", name, "--chmod=+x")
return addCmd.Run()
return m.newCommand(exe, "-C", name, "add", name, "--chmod=+x").Run()
}
func (m *Manager) otherBinScaffolding(gitExe, name string) error {
err := os.MkdirAll(filepath.Join(name, ".github", "workflows"), 0755)
if err != nil {
return err
}
workflowPath := filepath.Join(".github", "workflows", "release.yml")
err = ioutil.WriteFile(filepath.Join(name, workflowPath), otherBinWorkflow, 0644)
if err != nil {
return err
}
err = os.Mkdir(filepath.Join(name, "script"), 0755)
if err != nil {
if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), otherBinWorkflow, 0644); err != nil {
return err
}
buildScriptPath := filepath.Join("script", "build.sh")
err = ioutil.WriteFile(filepath.Join(name, buildScriptPath), buildScript, 0755)
if err != nil {
if err := writeFile(filepath.Join(name, buildScriptPath), buildScript, 0755); err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
if err := m.newCommand(gitExe, "-C", name, "add", buildScriptPath, "--chmod=+x").Run(); err != nil {
return err
}
dir := filepath.Join(wd, name)
addCmd := m.newCommand(gitExe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", buildScriptPath, "--chmod=+x")
err = addCmd.Run()
if err != nil {
return err
}
addCmd = m.newCommand(gitExe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", workflowPath)
return addCmd.Run()
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
}
func (m *Manager) goBinScaffolding(gitExe, name string) error {
@ -637,30 +598,16 @@ func (m *Manager) goBinScaffolding(gitExe, name string) error {
if err != nil {
return fmt.Errorf("go is required for creating Go extensions: %w", err)
}
err = os.MkdirAll(filepath.Join(name, ".github", "workflows"), 0755)
if err != nil {
return err
}
workflowPath := filepath.Join(".github", "workflows", "release.yml")
err = ioutil.WriteFile(filepath.Join(name, workflowPath), goBinWorkflow, 0644)
if err != nil {
if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), goBinWorkflow, 0644); err != nil {
return err
}
mainGo := fmt.Sprintf(mainGoTmpl, name)
mainPath := "main.go"
err = ioutil.WriteFile(filepath.Join(name, mainPath), []byte(mainGo), 0644)
if err != nil {
if err := writeFile(filepath.Join(name, "main.go"), []byte(mainGo), 0644); err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
dir := filepath.Join(wd, name)
host, err := m.config.DefaultHost()
if err != nil {
return err
@ -677,51 +624,35 @@ func (m *Manager) goBinScaffolding(gitExe, name string) error {
{"build"},
}
_, ext := m.platform()
ignore := name + ext
err = ioutil.WriteFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644)
if err != nil {
ignore := fmt.Sprintf("/%[1]s\n/%[1]s.exe\n", name)
if err := writeFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644); err != nil {
return err
}
for _, args := range goCmds {
goCmd := m.newCommand(goExe, args...)
goCmd.Dir = dir
err = goCmd.Run()
if err != nil {
goCmd.Dir = name
if err := goCmd.Run(); err != nil {
return fmt.Errorf("failed to set up go module: %w", err)
}
}
addArgs := []string{
"-C", dir,
"--git-dir=" + filepath.Join(dir, ".git"),
"add",
workflowPath,
mainPath,
".gitignore",
"go.mod",
"go.sum",
}
addCmd := m.newCommand(gitExe, addArgs...)
return addCmd.Run()
}
func runCmds(cmds []*exec.Cmd) error {
for _, cmd := range cmds {
if err := cmd.Run(); err != nil {
return err
}
}
return nil
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
}
func isSymlink(m os.FileMode) bool {
return m&os.ModeSymlink != 0
}
func writeFile(p string, contents []byte, mode os.FileMode) error {
if dir := filepath.Dir(p); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
return os.WriteFile(p, contents, mode)
}
// reads the product of makeSymlink on Windows
func readPathFromFile(path string) (string, error) {
f, err := os.Open(path)

View file

@ -9,6 +9,8 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
@ -19,6 +21,7 @@ import (
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
@ -27,6 +30,15 @@ func TestHelperProcess(t *testing.T) {
return
}
if err := func(args []string) error {
// git init should create the directory named by argument
if len(args) > 2 && strings.HasPrefix(strings.Join(args, " "), "git init") {
dir := args[len(args)-1]
if !strings.HasPrefix(dir, "-") {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
}
fmt.Fprintf(os.Stdout, "%v\n", args)
return nil
}(os.Args[3:]); err != nil {
@ -219,16 +231,14 @@ func TestManager_UpgradeExtensions(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[hello]: [git -C %s --git-dir=%s pull --ff-only]
[hello]: [git -C %s pull --ff-only]
upgrade complete
[local]: local extensions can not be upgraded
[two]: [git -C %s --git-dir=%s pull --ff-only]
[two]: [git -C %s pull --ff-only]
upgrade complete
`,
filepath.Join(tempDir, "extensions", "gh-hello"),
filepath.Join(tempDir, "extensions", "gh-hello", ".git"),
filepath.Join(tempDir, "extensions", "gh-two"),
filepath.Join(tempDir, "extensions", "gh-two", ".git"),
), stdout.String())
assert.Equal(t, "", stderr.String())
}
@ -263,10 +273,9 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %s --git-dir=%s pull --ff-only]
[git -C %s pull --ff-only]
`,
filepath.Join(tempDir, "extensions", "gh-remote"),
filepath.Join(tempDir, "extensions", "gh-remote", ".git"),
), stdout.String())
assert.Equal(t, "", stderr.String())
}
@ -274,7 +283,6 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
tempDir := t.TempDir()
extensionDir := filepath.Join(tempDir, "extensions", "gh-remote")
gitDir := filepath.Join(tempDir, "extensions", "gh-remote", ".git")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, io)
@ -288,13 +296,10 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %s --git-dir=%s fetch origin HEAD]
[git -C %s --git-dir=%s reset --hard origin/HEAD]
[git -C %[1]s fetch origin HEAD]
[git -C %[1]s reset --hard origin/HEAD]
`,
extensionDir,
gitDir,
extensionDir,
gitDir,
), stdout.String())
assert.Equal(t, "", stderr.String())
}
@ -378,10 +383,10 @@ func TestManager_MigrateToBinaryExtension(t *testing.T) {
func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
tempDir := t.TempDir()
io, _, _, _ := iostreams.Test()
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
assert.NoError(t, stubBinaryExtension(
filepath.Join(tempDir, "extensions", "gh-bin-ext"),
binManifest{
@ -390,7 +395,9 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
Host: "example.com",
Tag: "v1.0.1",
}))
m := newTestManager(tempDir, &client, io)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, io)
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
@ -432,8 +439,10 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"))
assert.NoError(t, err)
assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin))
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Install_git(t *testing.T) {
@ -444,7 +453,6 @@ func TestManager_Install_git(t *testing.T) {
client := http.Client{Transport: &reg}
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &client, io)
reg.Register(
@ -501,17 +509,16 @@ func TestManager_Install_binary_unsupported(t *testing.T) {
},
}))
io, _, _, _ := iostreams.Test()
io, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
err := m.Install(repo)
assert.Error(t, err)
assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`")
errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`"
assert.Equal(t, errText, err.Error())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Install_binary(t *testing.T) {
@ -519,7 +526,6 @@ func TestManager_Install_binary(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
@ -548,10 +554,10 @@ func TestManager_Install_binary(t *testing.T) {
httpmock.REST("GET", "release/cool"),
httpmock.StringResponse("FAKE BINARY"))
io, _, _, _ := iostreams.Test()
io, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, io)
err := m.Install(repo)
assert.NoError(t, err)
@ -573,85 +579,116 @@ func TestManager_Install_binary(t *testing.T) {
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"))
assert.NoError(t, err)
assert.Equal(t, "FAKE BINARY", string(fakeBin))
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create(t *testing.T) {
tempDir := t.TempDir()
oldWd, _ := os.Getwd()
assert.NoError(t, os.Chdir(tempDir))
t.Cleanup(func() { _ = os.Chdir(oldWd) })
m := newTestManager(tempDir, nil, nil)
chdirTemp(t)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, io)
err := m.Create("gh-test", extensions.GitTemplateType)
assert.NoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
files, err := ioutil.ReadDir("gh-test")
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
extFile := files[0]
assert.Equal(t, "gh-test", extFile.Name())
if runtime.GOOS == "windows" {
assert.Equal(t, os.FileMode(0666), extFile.Mode())
} else {
assert.Equal(t, os.FileMode(0755), extFile.Mode())
}
assert.Equal(t, []string{"gh-test"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[git -C gh-test add gh-test --chmod=+x]
`), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create_go_binary(t *testing.T) {
tempDir := t.TempDir()
oldWd, _ := os.Getwd()
assert.NoError(t, os.Chdir(tempDir))
t.Cleanup(func() { _ = os.Chdir(oldWd) })
chdirTemp(t)
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
m := newTestManager(tempDir, &client, nil)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", &http.Client{Transport: &reg}, io)
err := m.Create("gh-test", extensions.GoBinTemplateType)
assert.NoError(t, err)
require.NoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
assert.NoError(t, err)
assert.Equal(t, 3, len(files))
assert.Equal(t, ".gitignore", files[1].Name())
assert.Equal(t, "main.go", files[2].Name())
files, err := ioutil.ReadDir("gh-test")
require.NoError(t, err)
assert.Equal(t, []string{".github", ".gitignore", "main.go"}, fileNames(files))
files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", ".github", "workflows"))
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
workflowFile := files[0]
assert.Equal(t, "release.yml", workflowFile.Name())
gitignore, err := os.ReadFile(filepath.Join("gh-test", ".gitignore"))
require.NoError(t, err)
assert.Equal(t, heredoc.Doc(`
/gh-test
/gh-test.exe
`), string(gitignore))
files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows"))
require.NoError(t, err)
assert.Equal(t, []string{"release.yml"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[go mod init github.com/jillv/gh-test]
[go mod tidy]
[go build]
[git -C gh-test add .]
`), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create_other_binary(t *testing.T) {
tempDir := t.TempDir()
oldWd, _ := os.Getwd()
assert.NoError(t, os.Chdir(tempDir))
t.Cleanup(func() { _ = os.Chdir(oldWd) })
m := newTestManager(tempDir, nil, nil)
chdirTemp(t)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, io)
err := m.Create("gh-test", extensions.OtherBinTemplateType)
assert.NoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
files, err := ioutil.ReadDir("gh-test")
assert.NoError(t, err)
assert.Equal(t, 2, len(files))
files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", ".github", "workflows"))
files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows"))
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
workflowFile := files[0]
assert.Equal(t, "release.yml", workflowFile.Name())
assert.Equal(t, []string{"release.yml"}, fileNames(files))
files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", "script"))
files, err = ioutil.ReadDir(filepath.Join("gh-test", "script"))
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
buildFile := files[0]
assert.Equal(t, "build.sh", buildFile.Name())
assert.Equal(t, []string{"build.sh"}, fileNames(files))
assert.Equal(t, heredoc.Docf(`
[git init --quiet gh-test]
[git -C gh-test add %s --chmod=+x]
[git -C gh-test add .]
`, filepath.FromSlash("script/build.sh")), stdout.String())
assert.Equal(t, "", stderr.String())
}
// chdirTemp changes the current working directory to a temporary directory for the duration of the test.
func chdirTemp(t *testing.T) {
oldWd, _ := os.Getwd()
tempDir := t.TempDir()
if err := os.Chdir(tempDir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = os.Chdir(oldWd)
})
}
func fileNames(files []os.FileInfo) []string {
names := make([]string, len(files))
for i, f := range files {
names[i] = f.Name()
}
sort.Strings(names)
return names
}
func stubExtension(path string) error {