github.com/jingweno/gh@v2.1.1-0.20221007190738-04a7985fa9a1+incompatible/commands/release.go (about) 1 package commands 2 3 import ( 4 "bytes" 5 "fmt" 6 "github.com/jingweno/gh/github" 7 "github.com/jingweno/gh/utils" 8 "github.com/jingweno/go-octokit/octokit" 9 "io" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "sync/atomic" 16 ) 17 18 var ( 19 cmdRelease = &Command{ 20 Run: release, 21 Usage: "release", 22 Short: "Retrieve releases from GitHub", 23 Long: `Retrieves releases from GitHub for the project that the "origin" remote points to.`} 24 25 cmdCreateRelease = &Command{ 26 Key: "create", 27 Run: createRelease, 28 Usage: "release create [-d] [-p] [-a <ASSETS_DIR>] [-m <MESSAGE>|-f <FILE>] <TAG>", 29 Short: "Create a new release in GitHub", 30 Long: `Creates a new release in GitHub for the project that the "origin" remote points to. 31 It requires the name of the tag to release as a first argument. 32 33 Specify the assets to include in the release from a directory via "-a". Without 34 "-a", it finds assets from "releases/TAG" of the current directory. 35 36 Without <MESSAGE> or <FILE>, a text editor will open in which title and body 37 of the release can be entered in the same manner as git commit message. 38 39 If "-d" is given, it creates a draft release. 40 41 If "-p" is given, it creates a pre-release. 42 `} 43 44 flagReleaseDraft, 45 flagReleasePrerelease bool 46 47 flagReleaseAssetsDir, 48 flagReleaseMessage, 49 flagReleaseFile string 50 ) 51 52 func init() { 53 cmdCreateRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT") 54 cmdCreateRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE") 55 cmdCreateRelease.Flag.StringVarP(&flagReleaseAssetsDir, "assets", "a", "", "ASSETS_DIR") 56 cmdCreateRelease.Flag.StringVarP(&flagReleaseMessage, "message", "m", "", "MESSAGE") 57 cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "f", "", "FILE") 58 59 cmdRelease.Use(cmdCreateRelease) 60 CmdRunner.Use(cmdRelease) 61 } 62 63 func release(cmd *Command, args *Args) { 64 runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { 65 if args.Noop { 66 fmt.Printf("Would request list of releases for %s\n", project) 67 } else { 68 releases, err := gh.Releases(project) 69 utils.Check(err) 70 var outputs []string 71 for _, release := range releases { 72 out := fmt.Sprintf("%s (%s)\n%s", release.Name, release.TagName, release.Body) 73 outputs = append(outputs, out) 74 } 75 76 fmt.Println(strings.Join(outputs, "\n\n")) 77 } 78 }) 79 } 80 81 func createRelease(cmd *Command, args *Args) { 82 if args.IsParamsEmpty() { 83 utils.Check(fmt.Errorf("Missed argument TAG")) 84 return 85 } 86 87 tag := args.LastParam() 88 89 assetsDir, err := getAssetsDirectory(flagReleaseAssetsDir, tag) 90 utils.Check(err) 91 92 runInLocalRepo(func(localRepo *github.GitHubRepo, project *github.Project, gh *github.Client) { 93 currentBranch, err := localRepo.CurrentBranch() 94 utils.Check(err) 95 branchName := currentBranch.ShortName() 96 97 title, body, err := getTitleAndBodyFromFlags(flagReleaseMessage, flagReleaseFile) 98 utils.Check(err) 99 100 if title == "" { 101 title, body, err = writeReleaseTitleAndBody(project, tag, branchName) 102 utils.Check(err) 103 } 104 105 params := octokit.ReleaseParams{ 106 TagName: tag, 107 TargetCommitish: branchName, 108 Name: title, 109 Body: body, 110 Draft: flagReleaseDraft, 111 Prerelease: flagReleasePrerelease} 112 113 finalRelease, err := gh.CreateRelease(project, params) 114 utils.Check(err) 115 116 uploadReleaseAssets(gh, finalRelease, assetsDir) 117 118 fmt.Printf("\n\nRelease created: %s", finalRelease.HTMLURL) 119 }) 120 } 121 122 func writeReleaseTitleAndBody(project *github.Project, tag, currentBranch string) (string, string, error) { 123 message := ` 124 # Creating release %s for %s from %s 125 # 126 # Write a message for this release. The first block 127 # of the text is the title and the rest is description. 128 ` 129 message = fmt.Sprintf(message, tag, project.Name, currentBranch) 130 131 editor, err := github.NewEditor("RELEASE", message) 132 if err != nil { 133 return "", "", err 134 } 135 136 return editor.EditTitleAndBody() 137 } 138 139 func getAssetsDirectory(assetsDir, tag string) (string, error) { 140 if assetsDir == "" { 141 pwd, err := os.Getwd() 142 utils.Check(err) 143 144 assetsDir = filepath.Join(pwd, "releases", tag) 145 } 146 147 if !isDir(assetsDir) { 148 return "", fmt.Errorf("The assets directory doesn't exist: %s", assetsDir) 149 } 150 151 if isEmptyDir(assetsDir) { 152 return "", fmt.Errorf("The assets directory is empty: %s", assetsDir) 153 } 154 155 return assetsDir, nil 156 } 157 158 func uploadReleaseAssets(gh *github.Client, release *octokit.Release, assetsDir string) { 159 var wg sync.WaitGroup 160 var totalAssets, countAssets uint64 161 162 filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { 163 if !fi.IsDir() { 164 totalAssets += 1 165 } 166 return nil 167 }) 168 169 printUploadProgress(&countAssets, totalAssets) 170 171 filepath.Walk(assetsDir, func(path string, fi os.FileInfo, err error) error { 172 if !fi.IsDir() { 173 wg.Add(1) 174 175 go func() { 176 defer func() { 177 atomic.AddUint64(&countAssets, uint64(1)) 178 printUploadProgress(&countAssets, totalAssets) 179 wg.Done() 180 }() 181 182 uploadUrl, err := release.UploadURL.Expand(octokit.M{"name": fi.Name()}) 183 utils.Check(err) 184 185 contentType := detectContentType(path, fi) 186 187 file, err := os.Open(path) 188 utils.Check(err) 189 defer file.Close() 190 191 err = gh.UploadReleaseAsset(uploadUrl, file, contentType) 192 utils.Check(err) 193 }() 194 } 195 196 return nil 197 }) 198 199 wg.Wait() 200 } 201 202 func detectContentType(path string, fi os.FileInfo) string { 203 file, err := os.Open(path) 204 utils.Check(err) 205 defer file.Close() 206 207 fileHeader := &bytes.Buffer{} 208 headerSize := int64(512) 209 if fi.Size() < headerSize { 210 headerSize = fi.Size() 211 } 212 213 // The content type detection only uses 512 bytes at most. 214 // This way we avoid copying the whole content for big files. 215 _, err = io.CopyN(fileHeader, file, headerSize) 216 utils.Check(err) 217 218 return http.DetectContentType(fileHeader.Bytes()) 219 } 220 221 func printUploadProgress(count *uint64, total uint64) { 222 out := fmt.Sprintf("Uploading assets (%d/%d)", atomic.LoadUint64(count), total) 223 fmt.Print("\r" + out) 224 }