releaser.go (7394B)
1 // Copyright 2017-present The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package releaser implements a set of utilities and a wrapper around Goreleaser 15 // to help automate the Hugo release process. 16 package releaser 17 18 import ( 19 "fmt" 20 "io/ioutil" 21 "log" 22 "os" 23 "path/filepath" 24 "regexp" 25 "strings" 26 27 "github.com/gohugoio/hugo/common/hexec" 28 29 "errors" 30 31 "github.com/gohugoio/hugo/common/hugo" 32 ) 33 34 const commitPrefix = "releaser:" 35 36 // ReleaseHandler provides functionality to release a new version of Hugo. 37 // Test this locally without doing an actual release: 38 // go run -tags release main.go release --skip-publish --try -r 0.90.0 39 // Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only. 40 type ReleaseHandler struct { 41 cliVersion string 42 43 skipPublish bool 44 45 // Just simulate, no actual changes. 46 try bool 47 48 git func(args ...string) (string, error) 49 } 50 51 func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) { 52 newVersion := hugo.MustParseVersion(r.cliVersion) 53 finalVersion := newVersion.Next() 54 finalVersion.PatchLevel = 0 55 56 if newVersion.Suffix != "-test" { 57 newVersion.Suffix = "" 58 } 59 60 finalVersion.Suffix = "-DEV" 61 62 return newVersion, finalVersion 63 } 64 65 // New initialises a ReleaseHandler. 66 func New(version string, skipPublish, try bool) *ReleaseHandler { 67 // When triggered from CI release branch 68 version = strings.TrimPrefix(version, "release-") 69 version = strings.TrimPrefix(version, "v") 70 rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try} 71 72 if try { 73 rh.git = func(args ...string) (string, error) { 74 fmt.Println("git", strings.Join(args, " ")) 75 return "", nil 76 } 77 } else { 78 rh.git = git 79 } 80 81 return rh 82 } 83 84 // Run creates a new release. 85 func (r *ReleaseHandler) Run() error { 86 if os.Getenv("GITHUB_TOKEN") == "" { 87 return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new") 88 } 89 90 fmt.Printf("Start release from %q\n", wd()) 91 92 newVersion, finalVersion := r.calculateVersions() 93 94 version := newVersion.String() 95 tag := "v" + version 96 isPatch := newVersion.PatchLevel > 0 97 mainVersion := newVersion 98 mainVersion.PatchLevel = 0 99 100 // Exit early if tag already exists 101 exists, err := tagExists(tag) 102 if err != nil { 103 return err 104 } 105 106 if exists { 107 return fmt.Errorf("tag %q already exists", tag) 108 } 109 110 var changeLogFromTag string 111 112 if newVersion.PatchLevel == 0 { 113 // There may have been patch releases between, so set the tag explicitly. 114 changeLogFromTag = "v" + newVersion.Prev().String() 115 exists, _ := tagExists(changeLogFromTag) 116 if !exists { 117 // fall back to one that exists. 118 changeLogFromTag = "" 119 } 120 } 121 122 var ( 123 gitCommits gitInfos 124 gitCommitsDocs gitInfos 125 ) 126 127 defer r.gitPush() // TODO(bep) 128 129 gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try) 130 if err != nil { 131 return err 132 } 133 134 // TODO(bep) explicit tag? 135 gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try) 136 if err != nil { 137 return err 138 } 139 140 releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs) 141 if err != nil { 142 return err 143 } 144 145 if _, err := r.git("add", releaseNotesFile); err != nil { 146 return err 147 } 148 149 commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion) 150 commitMsg += "\n[ci skip]" 151 152 if _, err := r.git("commit", "-m", commitMsg); err != nil { 153 return err 154 } 155 156 if err := r.bumpVersions(newVersion); err != nil { 157 return err 158 } 159 160 if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { 161 return err 162 } 163 164 if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { 165 return err 166 } 167 168 if !r.skipPublish { 169 if _, err := r.git("push", "origin", tag); err != nil { 170 return err 171 } 172 } 173 174 if err := r.release(releaseNotesFile); err != nil { 175 return err 176 } 177 178 if err := r.bumpVersions(finalVersion); err != nil { 179 return err 180 } 181 182 if !r.try { 183 // No longer needed. 184 if err := os.Remove(releaseNotesFile); err != nil { 185 return err 186 } 187 } 188 189 if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil { 190 return err 191 } 192 193 return nil 194 } 195 196 func (r *ReleaseHandler) gitPush() { 197 if r.skipPublish { 198 return 199 } 200 if _, err := r.git("push", "origin", "HEAD"); err != nil { 201 log.Fatal("push failed:", err) 202 } 203 } 204 205 func (r *ReleaseHandler) release(releaseNotesFile string) error { 206 if r.try { 207 fmt.Println("Skip goreleaser...") 208 return nil 209 } 210 211 args := []string{"--parallelism", "2", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile} 212 if r.skipPublish { 213 args = append(args, "--skip-publish") 214 } 215 216 cmd, _ := hexec.SafeCommand("goreleaser", args...) 217 cmd.Stdout = os.Stdout 218 cmd.Stderr = os.Stderr 219 err := cmd.Run() 220 if err != nil { 221 return fmt.Errorf("goreleaser failed: %w", err) 222 } 223 return nil 224 } 225 226 func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error { 227 toDev := "" 228 229 if ver.Suffix != "" { 230 toDev = ver.Suffix 231 } 232 233 if err := r.replaceInFile("common/hugo/version_current.go", 234 `Minor:(\s*)(\d*),`, fmt.Sprintf(`Minor:${1}%d,`, ver.Minor), 235 `PatchLevel:(\s*)(\d*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel), 236 `Suffix:(\s*)".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil { 237 return err 238 } 239 240 snapcraftGrade := "stable" 241 if ver.Suffix != "" { 242 snapcraftGrade = "devel" 243 } 244 if err := r.replaceInFile("snap/snapcraft.yaml", 245 `version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver), 246 `grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil { 247 return err 248 } 249 250 var minVersion string 251 if ver.Suffix != "" { 252 // People use the DEV version in daily use, and we cannot create new themes 253 // with the next version before it is released. 254 minVersion = ver.Prev().String() 255 } else { 256 minVersion = ver.String() 257 } 258 259 if err := r.replaceInFile("commands/new.go", 260 `min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil { 261 return err 262 } 263 264 return nil 265 } 266 267 func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error { 268 filename = filepath.FromSlash(filename) 269 fi, err := os.Stat(filename) 270 if err != nil { 271 return err 272 } 273 274 if r.try { 275 fmt.Printf("Replace in %q: %q\n", filename, oldNew) 276 return nil 277 } 278 279 b, err := ioutil.ReadFile(filename) 280 if err != nil { 281 return err 282 } 283 newContent := string(b) 284 285 for i := 0; i < len(oldNew); i += 2 { 286 re := regexp.MustCompile(oldNew[i]) 287 newContent = re.ReplaceAllString(newContent, oldNew[i+1]) 288 } 289 290 return ioutil.WriteFile(filename, []byte(newContent), fi.Mode()) 291 } 292 293 func isCI() bool { 294 return os.Getenv("CI") != "" 295 } 296 297 func wd() string { 298 p, err := os.Getwd() 299 if err != nil { 300 log.Fatal(err) 301 } 302 return p 303 304 }