├── .drone.yml ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── go-starter-drone │ └── main.go ├── go-starter-github │ └── main.go ├── go-starter-replace │ ├── main.go │ └── main_test.go └── go-starter │ └── main.go ├── go.mod ├── go.sum └── pkg ├── console ├── console.go └── console_test.go ├── keychainx ├── errors.go ├── keychain_darwin.go └── keychain_others.go └── maker ├── asker.go ├── checkout.go ├── config.go ├── config_test.go ├── resolver.go ├── resolver_test.go ├── runner.go ├── vars.go └── vars_test.go /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | type: docker 3 | kind: pipeline 4 | name: test 5 | 6 | workspace: 7 | base: /go 8 | path: src/github.com/adobe/go-starter 9 | 10 | steps: 11 | - name: dep 12 | image: mcom/gobuilder:1.12 13 | pull: always 14 | commands: [ "make dep" ] 15 | 16 | - name: lint 17 | image: golangci/golangci-lint:v1.23-alpine 18 | pull: always 19 | commands: [ "golangci-lint run" ] 20 | 21 | - name: test 22 | image: mcom/gobuilder:1.12 23 | pull: always 24 | commands: [ "make test" ] 25 | 26 | --- 27 | type: docker 28 | kind: pipeline 29 | name: release 30 | 31 | workspace: 32 | base: /go 33 | path: src/github.com/adobe/go-starter 34 | 35 | depends_on: [ test ] 36 | 37 | trigger: 38 | event: [ tag ] 39 | status: [ "success"] 40 | 41 | steps: 42 | - name: dep 43 | image: mcom/gobuilder:1.12 44 | pull: always 45 | commands: [ "make dep" ] 46 | 47 | - name: build linux 48 | image: mcom/gobuilder:1.12 49 | pull: always 50 | commands: [ "make build" ] 51 | environment: 52 | BUILDARGS: -a -installsuffix cgo 53 | BUILDCOMMIT: ${DRONE_COMMIT_SHA:0:7} 54 | BUILDLDFLAGS: -extldflags "-static" 55 | BUILDOUTPREFIX: build/linux-amd64/ 56 | BUILDTAG: ${DRONE_TAG} 57 | GOARCH: amd64 58 | GOOS: linux 59 | 60 | - name: build darwin 61 | image: mcom/gobuilder:1.12cross 62 | pull: always 63 | commands: [ "make build" ] 64 | environment: 65 | BUILDCOMMIT: ${DRONE_COMMIT_SHA:0:7} 66 | BUILDLDFLAGS: -linkmode external -s 67 | BUILDOUTPREFIX: build/darwin-amd64/ 68 | BUILDTAG: ${DRONE_TAG} 69 | CC: o64-clang 70 | CXX: o64-clang++ 71 | GOARCH: amd64 72 | GOOS: darwin 73 | 74 | - name: make tarball 75 | image: mcom/zipbuilder 76 | commands: 77 | - "tar -C build/darwin-amd64 -cvzf go-starter-darwin-amd64.tgz ." 78 | - "tar -C build/linux-amd64 -cvzf go-starter-linux-amd64.tgz ." 79 | 80 | - name: github release 81 | image: socialengine/github-release 82 | commands: 83 | - github-release release --user adobe --repo go-starter --tag ${DRONE_TAG} --name ${DRONE_TAG} 84 | - github-release upload --user adobe --repo go-starter --tag ${DRONE_TAG} --name go-starter-darwin-amd64.tgz --file ./go-starter-darwin-amd64.tgz 85 | - github-release upload --user adobe --repo go-starter --tag ${DRONE_TAG} --name go-starter-linux-amd64.tgz --file ./go-starter-linux-amd64.tgz 86 | environment: 87 | GITHUB_RELEASE_VERSION: v0.7.2 88 | GITHUB_TOKEN: { from_secret: github_token } 89 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - deadcode 5 | - errcheck 6 | - gosimple 7 | - govet 8 | - ineffassign 9 | - staticcheck 10 | - structcheck 11 | - typecheck 12 | - unused 13 | - varcheck 14 | - gofmt 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](./CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | ## Security Issues 34 | 35 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk -U add ca-certificates 4 | 5 | ADD build/linux-amd64 /bin/ 6 | 7 | ENTRYPOINT [ "go-starter" ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | © Copyright 2019 Adobe. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build install 2 | 3 | COMMIT?=${BUILDCOMMIT} 4 | VERSION?=${BUILDTAG} 5 | 6 | # enable cgo because it's required by OSX keychain library 7 | CGO_ENABLED=1 8 | 9 | # enable go modules 10 | GO111MODULE=on 11 | 12 | export CGO_ENABLED GO111MODULE 13 | 14 | dep: 15 | go get ./... 16 | 17 | test: 18 | go test ./... 19 | 20 | lint: 21 | golangci-lint run 22 | 23 | go-starter: cmd/go-starter/* pkg/* 24 | go build -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} ${BUILDLDFLAGS}" ${BUILDARGS} \ 25 | -o ${BUILDOUTPREFIX}go-starter cmd/go-starter/main.go 26 | 27 | go-starter-replace: cmd/go-starter-replace/* pkg/* 28 | go build -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} ${BUILDLDFLAGS}" ${BUILDARGS} \ 29 | -o ${BUILDOUTPREFIX}go-starter-replace cmd/go-starter-replace/main.go 30 | 31 | go-starter-github: cmd/go-starter-github/* pkg/* 32 | go build -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} ${BUILDLDFLAGS}" ${BUILDARGS} \ 33 | -o ${BUILDOUTPREFIX}go-starter-github cmd/go-starter-github/main.go 34 | 35 | go-starter-drone: cmd/go-starter-drone/* pkg/* 36 | go build -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} ${BUILDLDFLAGS}" ${BUILDARGS} \ 37 | -o ${BUILDOUTPREFIX}go-starter-drone cmd/go-starter-drone/main.go 38 | 39 | clean: 40 | rm ${BUILDOUTPREFIX}go-starter* 2> /dev/null || exit 0 41 | 42 | build: go-starter go-starter-replace go-starter-github go-starter-drone 43 | 44 | install: build 45 | cp ${BUILDOUTPREFIX}go-starter* /usr/local/bin 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Starter 2 | 3 | Go-starter allows to bootstrap a new project from a template. It uses Git repositories as templates and is shipped with batch of utilities to make bootstarpping easier. 4 | 5 | [Learn more](https://medium.com/@sergey.kolodyazhnyy/go-starter-f47daac57f44) 6 | 7 | ## Installation 8 | 9 | Download latest release from [release](https://github.com/adobe/go-starter/releases) page using one of the commands below. 10 | 11 | **Mac OS** 12 | 13 | ``` 14 | curl https://github.com/adobe/go-starter/releases/latest/download/go-starter-darwin-amd64.tgz \ 15 | -sSfL -o go-starter.tgz 16 | ``` 17 | 18 | **Linux** 19 | 20 | ``` 21 | curl https://github.com/adobe/go-starter/releases/latest/download/go-starter-linux-amd64.tgz \ 22 | -sSfL -o go-starter.tgz 23 | ``` 24 | 25 | Unpack content of the archive to a directory listed in `$PATH`. The archive includes multiple binaries shipped with `go-starter`. 26 | 27 | ``` 28 | tar -xvzf go-starter.tgz -C /usr/local/bin 29 | rm go-starter.tgz 30 | ``` 31 | 32 | Run `go-starter` to verify it's installed correctly. 33 | 34 | ## Usage 35 | 36 | Run go-starter with template repository URL and path where you would like to create a new project, for example: 37 | 38 | ```bash 39 | go-starter starter-template/hello-world-starter awesome-project 40 | ``` 41 | 42 | You can specify full GitHub URL or just repository name (like so `starter-template/hello-world-starter`). 43 | 44 | Now, go-starter will clone [hello-world-starter](https://github.com/starter-template/hello-world-starter) into `./awesome-project` directory and run tasks defined in [`.starter.yml`](https://github.com/starter-template/hello-world-starter/blob/master/.starter.yml). See "Templates" for more details. 45 | 46 | ### Advanced usage 47 | 48 | You can skip cloning, for example if template is already cloned, but task failed to execute by passing `-skip-clone` flag. 49 | 50 | You can also pass additional variables (or pre-define variables instead of entering them using prompt) using `-var` flag. 51 | 52 | ## Templates 53 | 54 | Templates are regular Git repositories like this one. If you try to use go-starter with random repository it will just clone it to your computer. To make use of go-starter you would need to let it know how to "post-process" template repository after it has been cloned. To do so, you need to define `.starter.yml` configuration file. 55 | 56 | After go-starter clones repository it tries to load `.starter.yml` to execute additional actions and turn template into a project. This configuration file defines two sections: 57 | 58 | - `questions` - list of questions to be asked from user, for example: project name, binary name, team etc 59 | - `tasks` - list of commands to be executed (these can be globally installed binaries, or binaries packed with template itself) 60 | 61 | Here is an example of `.starter.yml`: 62 | 63 | ```yaml 64 | questions: 65 | - message: Application name 66 | name: "application_name" 67 | type: input 68 | regexp: ^[a-z0-9\-]+$ 69 | help_msg: Must be lowercase characters or digits or hyphens 70 | - message: GitHub owners 71 | name: "github_owners" 72 | type: input 73 | regexp: ^[a-z0-9\-]+$ 74 | help_msg: Must be lowercase characters or digits or hyphens 75 | 76 | tasks: 77 | - command: [ "go-starter-replace" ] 78 | - command: [ "rm", ".starter.yml" ] 79 | ``` 80 | 81 | This file defines two questions, asking user to enter `application_name` and `github_owners` variables. Then, go-starter will execute `go-starter-replace` binary (shipped with go-starter) to replace placeholders in the files of the template, turning generic tempalte into something more specific to the project. Finally it will use standard `rm ` command to remove `.starter.yml`. 82 | 83 | Each template may contain custom tasks placed in `.starter` folder. For example, you can create a bash script which would generate CODEOWNERS file and place it under `.starter/make-owners`. Then, add it as tasks in `.starter.yml` like so: 84 | 85 | ```yaml 86 | ... 87 | 88 | tasks: 89 | ... 90 | - command: [ "./.starter/make-owners" ] 91 | ``` 92 | 93 | Custom scripts may access variables (answers to the questions) through environment variables. They are uppercased and prefixed with `STARTER_`. Following example above, `./.starter/make-owners` may get `github_owners` variable using `STARTER_GITHUB_OWNERS` environment variable. 94 | 95 | ## Build-in tasks 96 | 97 | Go-starter ships with few additional binaries which can be used as tasks in `.starter.yml`. 98 | 99 | ### go-starter-replace 100 | 101 | This binary recursively goes through files in current folder and replaces placeholders to variable values in files and their names. By default, placeholders are surrounded by `<` and `>`. 102 | 103 | #### Usage 104 | 105 | ```bash 106 | Usage: go-starter-replace [flags] 107 | 108 | Example: 109 | STARTER_PLACEHOLDER1=VALUE1 STARTER_PLACEHOLDER2=VALUE2 go-starter-replace 110 | 111 | Flags: 112 | -prefix string 113 | Placeholder prefix (default "<") 114 | -suffix string 115 | Placeholder suffix (default ">") 116 | ``` 117 | 118 | ### go-starter-github 119 | 120 | This binary automatically created GitHub repository, initiates local Git repository, adds GitHub remote and pushes changes to GitHub. 121 | 122 | #### Usage 123 | 124 | ```bash 125 | Usage: go-starter-github [flags] 126 | 127 | Example: 128 | go-starter-github adobe awesome-project 129 | 130 | Flags: 131 | -branch string 132 | Name of the master branch (default "master") 133 | -collaborator value 134 | Add collaborators to the repository by GitHub username. You can grant permissions using following format: :. Permission can be: pull (read only), push (read and write) or admin (everything), default is push. Can be specified multiple times. Example: --collaborator octocat:pull 135 | -deploy-key string 136 | Add SSH deployment key to the repository, add ':rw' suffix to grant write permissions to the key 137 | -public 138 | Make repository public 139 | -remote string 140 | Name of the remote in local repository (default "upstream") 141 | -with-issues 142 | Enable issues in GitHub 143 | -with-projects 144 | Enable projects in GitHub 145 | -with-wiki 146 | Enable wiki page in GitHub 147 | ``` 148 | 149 | ### go-starter-drone 150 | 151 | This binary configures drone integration and runs first build. 152 | 153 | #### Usage 154 | 155 | ```bash 156 | Usage: go-starter-drone [flags] 157 | 158 | Example: 159 | go-starter-drone https://cloud.drone.io adobe awesome-project 160 | 161 | Flags: 162 | -pull-secret-file value 163 | Create a secret from file available for pull-requests (eq. --pull-secret-file=secret_name=./path/to/file) 164 | -pull-secret-literal value 165 | Create a secret from literal available for pull-requests (eq. --pull-secret-literal=secret_name=value) 166 | -secret-file value 167 | Create a secret from file (eq. --secret-file=secret_name=./path/to/file) 168 | -secret-literal value 169 | Create a secret from literal (eq. --secret-literal=secret_name=value) 170 | ``` 171 | -------------------------------------------------------------------------------- /cmd/go-starter-drone/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "context" 16 | "flag" 17 | "fmt" 18 | "github.com/adobe/go-starter/pkg/console" 19 | "github.com/adobe/go-starter/pkg/keychainx" 20 | "github.com/drone/drone-go/drone" 21 | "golang.org/x/oauth2" 22 | "io/ioutil" 23 | "net/url" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | var version, commit string 31 | 32 | var ( 33 | fileSecrets SliceFlag 34 | fileSecretsPull SliceFlag 35 | literalSecrets SliceFlag 36 | literalSecretsPull SliceFlag 37 | ) 38 | 39 | func usage() { 40 | out := flag.CommandLine.Output() 41 | _, _ = fmt.Fprintf(out, "go-starter-drone version %v (commit %v)\n", version, commit) 42 | _, _ = fmt.Fprintf(out, "\n") 43 | _, _ = fmt.Fprintf(out, "Usage: %s [flags] \n", os.Args[0]) 44 | _, _ = fmt.Fprintf(out, "\nExample:\n") 45 | _, _ = fmt.Fprintf(out, " %s https://cloud.drone.io adobe awesome-project\n", os.Args[0]) 46 | _, _ = fmt.Fprintf(out, "\nFlags:\n") 47 | flag.PrintDefaults() 48 | _, _ = fmt.Fprintf(out, "\n") 49 | } 50 | 51 | func main() { 52 | flag.Usage = usage 53 | flag.Var(&fileSecrets, "secret-file", "Create a secret from file (eq. --secret-file=secret_name=./path/to/file)") 54 | flag.Var(&fileSecretsPull, "pull-secret-file", "Create a secret from file available for pull-requests (eq. --pull-secret-file=secret_name=./path/to/file)") 55 | flag.Var(&literalSecrets, "secret-literal", "Create a secret from literal (eq. --secret-literal=secret_name=value)") 56 | flag.Var(&literalSecretsPull, "pull-secret-literal", "Create a secret from literal available for pull-requests (eq. --pull-secret-literal=secret_name=value)") 57 | flag.Parse() 58 | 59 | ui := console.New(os.Stdin, os.Stdout) 60 | 61 | uri, err := url.Parse(flag.Arg(0)) 62 | if err != nil { 63 | flag.Usage() 64 | ui.Fatalf("An error occurred while parsing Drone URL: %v. Enter URL with protocol, for example: https://cloud.drone.io.\n", err) 65 | } 66 | 67 | if uri.Host == "" { 68 | flag.Usage() 69 | ui.Fatalf("Drone hostname is empty. Enter URL with protocol, for example: https://cloud.drone.io.\n") 70 | } 71 | 72 | org, repo := flag.Arg(1), flag.Arg(2) 73 | if org == "" { 74 | flag.Usage() 75 | ui.Fatalf("Organisation name is empty\n") 76 | } 77 | 78 | if repo == "" { 79 | flag.Usage() 80 | ui.Fatalf("Repository name is empty\n") 81 | } 82 | 83 | // get credentials from OSX keychain 84 | _, pass, err := keychainx.Load(uri.Host) 85 | if err != nil && err != keychainx.ErrNotFound { 86 | ui.Fatalf("An error occurred while reading Drone token from keychain: %v\n", err) 87 | } 88 | 89 | if err == keychainx.ErrNotFound { 90 | pass = AskCredentials(ui, uri) 91 | 92 | if err := keychainx.Save(uri.Host, "drone", pass); err != nil { 93 | ui.Errorf("An error occurred while writing Drone token to keychain: %v\n", err) 94 | } 95 | } 96 | 97 | // create an http client with oauth authentication 98 | auth := new(oauth2.Config).Client(context.Background(), &oauth2.Token{AccessToken: pass}) 99 | auth.Timeout = 30 * time.Second 100 | 101 | // create the drone client with authenticator 102 | dcli := drone.NewClient(uri.String(), auth) 103 | 104 | ui.Printf("Sync repository list in drone\n") 105 | if _, err := dcli.RepoListSync(); err != nil { 106 | ui.Fatalf("An error occurred: %v\n", err) 107 | } 108 | 109 | ui.Printf("Activating repository in drone\n") 110 | if _, err = dcli.RepoEnable(org, repo); err != nil && !strings.Contains(err.Error(), "Repository is already active") { 111 | ui.Fatalf("An error occurred: %v\n", err) 112 | } 113 | 114 | ImportSecrets(ui, dcli, org, repo) 115 | 116 | ui.Titlef("Triggering build by making empty commit\n") 117 | if err := run("git", "commit", "--allow-empty", "-m", "Trigger CI build"); err != nil { 118 | ui.Fatalf("An error occurred when making commit: %v\n", err) 119 | } 120 | 121 | if err := run("git", "push"); err != nil { 122 | ui.Fatalf("An error occurred when pushing commit: %v\n", err) 123 | } 124 | 125 | // give drone few seconds to start build 126 | time.Sleep(3 * time.Second) 127 | 128 | // get last build 129 | if last, err := dcli.BuildLast(org, repo, ""); err == nil { 130 | ui.Printf("Build #%v started: %v://%v/%v/%v/%v\n", last.Number, uri.Scheme, uri.Host, org, repo, last.Number) 131 | } 132 | } 133 | 134 | func ImportSecrets(ui *console.Console, dcli drone.Client, org string, repo string) { 135 | if len(fileSecrets)+len(fileSecretsPull)+len(literalSecrets)+len(literalSecretsPull) == 0 { 136 | return 137 | } 138 | 139 | ui.Titlef("Importing repository secrets...\n") 140 | 141 | for _, secret := range literalSecretsPull { 142 | key, value := SplitKeyValue(secret) 143 | 144 | ui.Printf("Adding secret %#v from literal...\n", key) 145 | if err := CreateOrUpdateSecret(dcli, org, repo, key, value, true); err != nil { 146 | ui.Errorf("An error occurred while adding secret: %v\n", err) 147 | } 148 | } 149 | 150 | for _, secret := range literalSecrets { 151 | key, value := SplitKeyValue(secret) 152 | 153 | ui.Printf("Adding secret %#v from literal...\n", key) 154 | if err := CreateOrUpdateSecret(dcli, org, repo, key, value, false); err != nil { 155 | ui.Errorf("An error occurred while adding secret: %v\n", err) 156 | } 157 | } 158 | 159 | for _, secret := range fileSecretsPull { 160 | key, file := SplitKeyValue(secret) 161 | value, err := ioutil.ReadFile(file) 162 | if err != nil { 163 | ui.Errorf("An error occurred while reading secret file %#v: %v\n", file, err) 164 | } 165 | 166 | ui.Printf("Adding secret %#v from file...\n", key) 167 | if err := CreateOrUpdateSecret(dcli, org, repo, key, string(value), true); err != nil { 168 | ui.Errorf("An error occurred while adding secret: %v\n", err) 169 | } 170 | } 171 | 172 | for _, secret := range fileSecrets { 173 | key, file := SplitKeyValue(secret) 174 | value, err := ioutil.ReadFile(file) 175 | if err != nil { 176 | ui.Errorf("An error occurred while reading secret file %#v: %v\n", file, err) 177 | } 178 | 179 | ui.Printf("Adding secret %#v from file...\n", key) 180 | if err := CreateOrUpdateSecret(dcli, org, repo, key, string(value), false); err != nil { 181 | ui.Errorf("An error occurred while adding secret: %v\n", err) 182 | } 183 | } 184 | } 185 | 186 | // CreateOrUpdateSecret in drone 187 | func CreateOrUpdateSecret(dcli drone.Client, owner, repo, key string, value string, pulls bool) error { 188 | secret, err := dcli.Secret(owner, repo, key) 189 | if err != nil && strings.Contains(err.Error(), "client error 404") { 190 | _, err = dcli.SecretCreate(owner, repo, &drone.Secret{ 191 | Name: key, 192 | Data: value, 193 | PullRequest: pulls, 194 | PullRequestPush: pulls, 195 | }) 196 | 197 | return err 198 | } 199 | 200 | if err != nil { 201 | return err 202 | } 203 | 204 | secret.Name = key 205 | secret.Data = value 206 | secret.PullRequest = pulls 207 | secret.PullRequestPush = pulls 208 | 209 | _, err = dcli.SecretUpdate(owner, repo, secret) 210 | return err 211 | } 212 | 213 | // SplitKeyValue for string in format key=value 214 | func SplitKeyValue(c string) (string, string) { 215 | if parts := strings.SplitN(c, "=", 2); len(parts) == 2 { 216 | return parts[0], parts[1] 217 | } 218 | 219 | return c, "" 220 | } 221 | 222 | // AskCredentials (token) for drone 223 | func AskCredentials(ui *console.Console, u *url.URL) (pass string) { 224 | for { 225 | ui.Printf("Follow this link to get your Personal Token: %v://%v/account.\n", u.Scheme, u.Host) 226 | 227 | pass = ui.ReadString("Enter your personal token: ") 228 | 229 | // build drone client 230 | auth := new(oauth2.Config).Client(context.Background(), &oauth2.Token{AccessToken: pass}) 231 | auth.Timeout = 30 * time.Second 232 | 233 | // create the drone client with authenticator 234 | client := drone.NewClient(u.String(), auth) 235 | 236 | // gets the current user 237 | _, err := client.Self() 238 | if err == nil { 239 | return 240 | } 241 | 242 | ui.Errorf("An error occurred while validating your personal token: %v\n", err) 243 | ui.Errorf("Credentials do not appear to be valid, try again...\n") 244 | } 245 | } 246 | 247 | // run a cli command 248 | func run(name string, args ...string) error { 249 | cmd := exec.Command(name, args...) 250 | cmd.Stderr = os.Stderr 251 | cmd.Stdout = os.Stdout 252 | cmd.Stdin = os.Stdin 253 | cmd.Env = os.Environ() 254 | 255 | return cmd.Run() 256 | } 257 | 258 | type SliceFlag []string 259 | 260 | func (s *SliceFlag) Set(v string) error { 261 | values := strings.Split(v, ",") 262 | for _, v := range values { 263 | *s = append(*s, strings.TrimSpace(v)) 264 | } 265 | 266 | return nil 267 | } 268 | 269 | func (s *SliceFlag) String() string { 270 | return strings.Join(*s, ",") 271 | } 272 | -------------------------------------------------------------------------------- /cmd/go-starter-github/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "context" 16 | "flag" 17 | "fmt" 18 | "github.com/adobe/go-starter/pkg/console" 19 | "github.com/adobe/go-starter/pkg/keychainx" 20 | "github.com/google/go-github/github" 21 | "io/ioutil" 22 | "net/http" 23 | "os" 24 | "os/exec" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | var version, commit string 30 | 31 | func usage() { 32 | out := flag.CommandLine.Output() 33 | _, _ = fmt.Fprintf(out, "go-starter-github version %v (commit %v)\n", version, commit) 34 | _, _ = fmt.Fprintf(out, "\n") 35 | _, _ = fmt.Fprintf(out, "Usage: %s [flags] \n", os.Args[0]) 36 | _, _ = fmt.Fprintf(out, "\nExample:\n") 37 | _, _ = fmt.Fprintf(out, " %s adobe awesome-project\n", os.Args[0]) 38 | _, _ = fmt.Fprintf(out, "\nFlags:\n") 39 | flag.PrintDefaults() 40 | _, _ = fmt.Fprintf(out, "\n") 41 | } 42 | 43 | func main() { 44 | var remote, branch, deployKey string 45 | var public, issues, projects, wiki bool 46 | var collaborators SliceFlag 47 | 48 | flag.Usage = usage 49 | flag.StringVar(&remote, "remote", "upstream", "Name of the remote in local repository") 50 | flag.StringVar(&branch, "branch", "master", "Name of the master branch") 51 | flag.BoolVar(&issues, "with-issues", false, "Enable issues in GitHub") 52 | flag.BoolVar(&projects, "with-projects", false, "Enable projects in GitHub") 53 | flag.BoolVar(&wiki, "with-wiki", false, "Enable wiki page in GitHub") 54 | flag.StringVar(&deployKey, "deploy-key", "", "Add SSH deployment key to the repository, add ':rw' suffix to grant write permissions to the key") 55 | flag.BoolVar(&public, "public", false, "Make repository public") 56 | flag.Var(&collaborators, "collaborator", "Add collaborators to the repository by GitHub username. You can grant permissions using following format: :. Permission can be: pull (read only), push (read and write) or admin (everything), default is push. Can be specified multiple times. Example: --collaborator octocat:pull") 57 | flag.Parse() 58 | 59 | ui := console.New(os.Stdin, os.Stdout) 60 | 61 | org, name := flag.Arg(0), flag.Arg(1) 62 | if org == "" { 63 | flag.Usage() 64 | ui.Fatalf("GitHub organisation is empty\n") 65 | } 66 | 67 | if name == "" { 68 | flag.Usage() 69 | ui.Fatalf("GitHub repository name is empty\n") 70 | } 71 | 72 | // get credentials from OSX keychain 73 | user, pass, err := keychainx.Load("github.com") 74 | if err != nil && err != keychainx.ErrNotFound { 75 | ui.Fatalf("An error occurred while loading keychain: %v\n", err) 76 | } 77 | 78 | if err == keychainx.ErrNotFound { 79 | user, pass = AskCredentials(ui) 80 | if err := keychainx.Save("github.com", user, pass); err != nil { 81 | ui.Errorf("An error occurred while saving keychain: %v\n", err) 82 | } 83 | } 84 | 85 | // build github client 86 | cli := github.NewClient(&http.Client{ 87 | Timeout: 30 * time.Second, 88 | Transport: &github.BasicAuthTransport{ 89 | Username: user, 90 | Password: pass, 91 | }, 92 | }) 93 | 94 | // get authenticated user so we can check if org value is username 95 | self, _, err := cli.Users.Get(context.Background(), "") 96 | if err != nil { 97 | ui.Fatalf("An error occurred while fetching username from GitHub: %v\n", err) 98 | } 99 | 100 | // GitHub API requires org to be empty when creating repository under "current" account 101 | createOrg := org 102 | if self.Login != nil && createOrg == *self.Login { 103 | createOrg = "" 104 | } 105 | 106 | // create repository 107 | repo, _, err := cli.Repositories.Create(context.Background(), createOrg, &github.Repository{ 108 | Name: github.String(name), 109 | Private: github.Bool(!public), 110 | HasIssues: github.Bool(issues), 111 | HasProjects: github.Bool(projects), 112 | HasWiki: github.Bool(wiki), 113 | MasterBranch: github.String(branch), 114 | }) 115 | 116 | if err != nil { 117 | if !strings.Contains(err.Error(), "name already exists on this account") { 118 | ui.Fatalf("An error occurred while creating GitHub repository: %v\n", err) 119 | } 120 | 121 | ui.Printf("Repository %v/%v already exists, skipping repository configuration...\n", org, name) 122 | return 123 | } 124 | 125 | ui.Successf("New repository created at %v\n", *repo.HTMLURL) 126 | 127 | // init repository 128 | if err := run("git", "init"); err != nil { 129 | ui.Fatalf("An error occurred while running git init: %v\n", err) 130 | } 131 | 132 | // add all files 133 | if err := run("git", "add", "-A"); err != nil { 134 | ui.Fatalf("An error occurred while running git add: %v\n", err) 135 | } 136 | 137 | // remove starter files 138 | if err := run("git", "rm", "--cached", "--ignore-unmatch", ".starter*"); err != nil { 139 | ui.Fatalf("An error occurred while running git rm: %v\n", err) 140 | } 141 | 142 | // commit 143 | if err := run("git", "commit", "-m", "Initial commit"); err != nil { 144 | ui.Fatalf("An error occurred while running git commit: %v\n", err) 145 | } 146 | 147 | // add remote 148 | if err := run("git", "remote", "add", remote, *repo.CloneURL); err != nil { 149 | ui.Fatalf("An error occurred while running git remote add: %v\n", err) 150 | } 151 | 152 | // push to remote 153 | if err := run("git", "push", "--set-upstream", remote, branch); err != nil { 154 | ui.Fatalf("An error occurred while running git push: %v\n", err) 155 | } 156 | 157 | for _, c := range collaborators { 158 | user, perm := SplitPermissions(c, "push") 159 | 160 | _, err := cli.Repositories.AddCollaborator(context.Background(), org, name, user, &github.RepositoryAddCollaboratorOptions{ 161 | Permission: perm, 162 | }) 163 | 164 | if err != nil { 165 | ui.Errorf("An error occurred while adding %#v collaborator: %v\n", c, err) 166 | } 167 | } 168 | 169 | if deployKey != "" { 170 | key, perm := SplitPermissions(deployKey, "ro") 171 | 172 | ui.Printf("Adding deployment key %#v with %#v permissions\n", key, perm) 173 | 174 | data, err := ioutil.ReadFile(key) 175 | if err != nil { 176 | ui.Fatalf("Unable to read deployment key: %v\n", err) 177 | } 178 | 179 | _, _, err = cli.Repositories.CreateKey(context.Background(), org, name, &github.Key{ 180 | Title: StringPtr("Deploy Key"), 181 | Key: StringPtr(string(data)), 182 | ReadOnly: BoolPtr(perm != "rw"), 183 | }) 184 | 185 | if err != nil { 186 | ui.Errorf("An error occurred while adding %#v deployment key: %v\n", key, err) 187 | } 188 | } 189 | } 190 | 191 | // Run a cli command 192 | func run(name string, args ...string) error { 193 | cmd := exec.Command(name, args...) 194 | cmd.Stderr = os.Stderr 195 | cmd.Stdout = os.Stdout 196 | cmd.Stdin = os.Stdin 197 | cmd.Env = os.Environ() 198 | 199 | return cmd.Run() 200 | } 201 | 202 | func AskCredentials(ui *console.Console) (user string, pass string) { 203 | for { 204 | user = ui.ReadString("Enter your GitHub username: ") 205 | 206 | ui.Printf("\n") 207 | ui.Printf("Follow this link and generate personal token: https://git.io/fjisU. Scroll to the bottom of the page and click \"Generate token\".\n") 208 | 209 | pass = ui.ReadString("Enter your personal token: ") 210 | 211 | // build github client 212 | cli := github.NewClient(&http.Client{ 213 | Timeout: 30 * time.Second, 214 | Transport: &github.BasicAuthTransport{ 215 | Username: user, 216 | Password: pass, 217 | }, 218 | }) 219 | 220 | _, _, err := cli.Zen(context.Background()) 221 | if err == nil { 222 | return 223 | } 224 | 225 | ui.Errorf("An error occurred while validating credentials: %v\n", err) 226 | ui.Errorf("Credentials do not appear to be valid, try again...\n") 227 | } 228 | } 229 | 230 | func SplitPermissions(c, d string) (string, string) { 231 | if parts := strings.SplitN(c, ":", 2); len(parts) == 2 { 232 | return parts[0], parts[1] 233 | } 234 | 235 | return c, d 236 | } 237 | 238 | type SliceFlag []string 239 | 240 | func (s *SliceFlag) Set(v string) error { 241 | values := strings.Split(v, ",") 242 | for _, v := range values { 243 | *s = append(*s, strings.TrimSpace(v)) 244 | } 245 | 246 | return nil 247 | } 248 | 249 | func (s *SliceFlag) String() string { 250 | return strings.Join(*s, ",") 251 | } 252 | 253 | func BoolPtr(v bool) *bool { 254 | return &v 255 | } 256 | 257 | func StringPtr(v string) *string { 258 | return &v 259 | } 260 | -------------------------------------------------------------------------------- /cmd/go-starter-replace/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "flag" 17 | "fmt" 18 | "github.com/adobe/go-starter/pkg/console" 19 | "io/ioutil" 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | ) 24 | 25 | var version, commit string 26 | var skips = []string{".starter/", ".starter.yml", ".git/"} 27 | var prefix, suffix = "<", ">" 28 | var reverse bool 29 | 30 | func usage() { 31 | out := flag.CommandLine.Output() 32 | _, _ = fmt.Fprintf(out, "go-starter-update version %v (commit %v)\n", version, commit) 33 | _, _ = fmt.Fprintf(out, "\n") 34 | _, _ = fmt.Fprintf(out, "Usage: %s [flags]\n", os.Args[0]) 35 | _, _ = fmt.Fprintf(out, "\nExample:\n") 36 | _, _ = fmt.Fprintf(out, " STARTER_PLACEHOLDER1=VALUE1 STARTER_PLACEHOLDER2=VALUE2 %s\n", os.Args[0]) 37 | _, _ = fmt.Fprintf(out, "\nFlags:\n") 38 | flag.PrintDefaults() 39 | _, _ = fmt.Fprintf(out, "\n") 40 | } 41 | 42 | func main() { 43 | flag.Usage = usage 44 | flag.StringVar(&prefix, "prefix", prefix, "Placeholder prefix") 45 | flag.StringVar(&suffix, "suffix", suffix, "Placeholder suffix") 46 | flag.BoolVar(&reverse, "reverse", reverse, "Replace values with placeholders (useful to revert changes made by go-starter-update)") 47 | flag.Parse() 48 | 49 | replace(console.New(os.Stdin, os.Stdout), variables()) 50 | } 51 | 52 | func replace(ui *console.Console, vars map[string]string) { 53 | // check if .starter.yml exists to prevent running in wrong directory 54 | if _, err := os.Stat(".starter.yml"); os.IsNotExist(err) { 55 | ui.Errorf("Current folder does not look like template, .starter.yml does not exist\n") 56 | return 57 | } 58 | 59 | // create replace dictionary 60 | dict := make(map[string]string) 61 | for k, v := range vars { 62 | key, val := prefix+k+suffix, v 63 | if reverse { 64 | key, val = val, key 65 | } 66 | 67 | dict[key] = val 68 | } 69 | 70 | // list of paths to rename 71 | var renames []string 72 | 73 | // walk through current folder and update variables 74 | err := filepath.Walk(".", func(path string, file os.FileInfo, err error) error { 75 | if err != nil { 76 | ui.Errorf("Unable to process path %#v: %v\n", path, err) 77 | return nil 78 | } 79 | 80 | for _, skip := range skips { 81 | if strings.HasPrefix(path, skip) { 82 | return nil 83 | } 84 | } 85 | 86 | name := file.Name() 87 | 88 | if renamed := rename(name, dict); renamed != name { 89 | renames = append(renames, path) 90 | } 91 | 92 | if file.IsDir() { 93 | return nil 94 | } 95 | 96 | ok, err := update(path, dict) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | if ok { 102 | ui.Printf("Updating %#v\n", path) 103 | } 104 | 105 | return nil 106 | }) 107 | 108 | for _, path := range renames { 109 | renamed := rename(path, dict) 110 | 111 | ui.Printf("Renaming %#v to %#v\n", path, renamed) 112 | if err := os.Rename(path, renamed); err != nil { 113 | ui.Errorf("Unable to rename path %#v: %v\n", path, err) 114 | } 115 | } 116 | 117 | if err != nil { 118 | ui.Fatalf("An error occurred while traversing file system: %v\n", err) 119 | } 120 | } 121 | 122 | // variables from environment 123 | func variables() map[string]string { 124 | vars := make(map[string]string) 125 | 126 | for _, pair := range os.Environ() { 127 | if !strings.HasPrefix(pair, "STARTER_") { 128 | continue 129 | } 130 | 131 | parts := strings.SplitN(pair, "=", 2) 132 | 133 | key := strings.TrimPrefix(parts[0], "STARTER_") 134 | value := "1" 135 | if len(parts) == 2 { 136 | value = parts[1] 137 | } 138 | 139 | vars[key] = value 140 | } 141 | 142 | return vars 143 | } 144 | 145 | // rename - update placeholders in file name 146 | func rename(filename string, params map[string]string) string { 147 | for k, v := range params { 148 | filename = strings.Replace(filename, k, v, -1) 149 | } 150 | 151 | return filename 152 | } 153 | 154 | // update placeholders in file 155 | func update(filename string, params map[string]string) (bool, error) { 156 | input, err := ioutil.ReadFile(filename) 157 | if err != nil { 158 | return false, err 159 | } 160 | 161 | output := input 162 | 163 | for k, v := range params { 164 | output = bytes.Replace(output, []byte(k), []byte(v), -1) 165 | } 166 | 167 | if bytes.Equal(input, output) { 168 | return false, nil 169 | } 170 | 171 | if err = ioutil.WriteFile(filename, output, 0666); err != nil { 172 | return false, err 173 | } 174 | 175 | return true, nil 176 | } 177 | -------------------------------------------------------------------------------- /cmd/go-starter-replace/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "github.com/adobe/go-starter/pkg/console" 18 | "io/ioutil" 19 | "math/rand" 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func init() { 28 | rand.Seed(time.Now().UnixNano()) 29 | } 30 | 31 | // Test happy path: replacing placeholders with values 32 | func TestReplace(t *testing.T) { 33 | teardown := setup(t) 34 | defer teardown() 35 | 36 | create(t, ".starter.yml", "") 37 | create(t, "file.txt", "foo bar") 38 | create(t, "nested/file.txt", "foo bar") 39 | create(t, "nested//file.txt", "foo bar") 40 | 41 | ui := console.New(bytes.NewBuffer(nil), ioutil.Discard) 42 | 43 | replace(ui, map[string]string{ 44 | "PLACEHOLDER": "REPLACED", 45 | }) 46 | 47 | assert(t, "file.txt", "foo REPLACED bar") 48 | assert(t, "nested/file.txt", "foo REPLACED bar") 49 | assert(t, "nested/REPLACED/file.txt", "foo REPLACED bar") 50 | } 51 | 52 | // Test reverse replace: values back to placeholders 53 | func TestReplaceReverse(t *testing.T) { 54 | teardown := setup(t) 55 | defer teardown() 56 | 57 | create(t, ".starter.yml", "") 58 | create(t, "file.txt", "foo REPLACED bar") 59 | create(t, "nested/file.txt", "foo REPLACED bar") 60 | create(t, "nested/REPLACED/file.txt", "foo REPLACED bar") 61 | 62 | reverse = true 63 | 64 | ui := console.New(bytes.NewBuffer(nil), ioutil.Discard) 65 | 66 | replace(ui, map[string]string{ 67 | "PLACEHOLDER": "REPLACED", 68 | }) 69 | 70 | assert(t, "file.txt", "foo bar") 71 | assert(t, "nested/file.txt", "foo bar") 72 | assert(t, "nested//file.txt", "foo bar") 73 | } 74 | 75 | // Make sure go-starter-replace won't run in non-template directory 76 | func TestReplaceChecksStarterConfig(t *testing.T) { 77 | teardown := setup(t) 78 | defer teardown() 79 | 80 | create(t, "file.txt", "foo bar") 81 | 82 | out := bytes.NewBuffer(nil) 83 | 84 | ui := console.New(bytes.NewBuffer(nil), out) 85 | 86 | replace(ui, map[string]string{ 87 | "PLACEHOLDER": "REPLACED", 88 | }) 89 | 90 | assert(t, "file.txt", "foo bar") 91 | 92 | want, got := ".starter.yml does not exist", out.String() 93 | if !strings.Contains(got, want) { 94 | t.Errorf("Error message should be printed: want %v, got %v", want, got) 95 | } 96 | } 97 | 98 | // setup test, create temporary path and chdir into it 99 | func setup(t *testing.T) func() { 100 | cwd, err := os.Getwd() 101 | if err != nil { 102 | t.Fatal("Unable to get working dir:", err) 103 | } 104 | 105 | path := filepath.Join(os.TempDir(), fmt.Sprint(rand.Int()%10000)) 106 | 107 | if err := os.Mkdir(path, 0777); err != nil { 108 | t.Fatalf("Unable to create test workspace %v: %v", path, err) 109 | } 110 | 111 | if err := os.Chdir(path); err != nil { 112 | t.Fatalf("Unable to chdir to test workspace: %v", err) 113 | } 114 | 115 | return func() { 116 | // chdir back to cwd 117 | _ = os.Chdir(cwd) 118 | 119 | // reset flags 120 | prefix, suffix, reverse = "<", ">", false 121 | 122 | // remove test workspace 123 | if err := os.RemoveAll(path); err != nil { 124 | t.Logf("Unable to remove test workspace: %v", err) 125 | } 126 | } 127 | } 128 | 129 | // create test file at given path 130 | func create(t *testing.T, filename, content string) { 131 | path := filepath.Dir(filename) 132 | if path != "" { 133 | if err := os.MkdirAll(path, 0777); err != nil { 134 | t.Fatalf("Unable to create test path %v: %v", path, err) 135 | } 136 | } 137 | 138 | if err := ioutil.WriteFile(filename, []byte(content), 0666); err != nil { 139 | t.Fatalf("Unable to create test file %v: %v", filename, err) 140 | } 141 | } 142 | 143 | // assert file content matching 144 | func assert(t *testing.T, filename, want string) { 145 | data, err := ioutil.ReadFile(filename) 146 | if err != nil { 147 | t.Errorf("Unable to read test file %v: %v", filename, err) 148 | return 149 | } 150 | 151 | got := string(data) 152 | 153 | if want != got { 154 | t.Errorf("Test file %#v: content does not match, want: %v, got %v", filename, want, got) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /cmd/go-starter/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "fmt" 17 | "github.com/adobe/go-starter/pkg/console" 18 | "github.com/adobe/go-starter/pkg/maker" 19 | "gopkg.in/yaml.v2" 20 | "io/ioutil" 21 | "os" 22 | "strings" 23 | ) 24 | 25 | var version, commit string 26 | 27 | func usage() { 28 | out := flag.CommandLine.Output() 29 | _, _ = fmt.Fprintf(out, "go-starter version %v (commit %v)\n", version, commit) 30 | _, _ = fmt.Fprintf(out, "\n") 31 | _, _ = fmt.Fprintf(out, "Usage: %s [flags]