github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/releaser/releaser.go (about) 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 "github.com/gohugoio/hugo/common/hugo" 30 "github.com/pkg/errors" 31 ) 32 33 const commitPrefix = "releaser:" 34 35 type releaseNotesState int 36 37 const ( 38 releaseNotesNone = iota 39 releaseNotesCreated 40 releaseNotesReady 41 ) 42 43 // ReleaseHandler provides functionality to release a new version of Hugo. 44 type ReleaseHandler struct { 45 cliVersion string 46 47 skipPublish bool 48 49 // Just simulate, no actual changes. 50 try bool 51 52 git func(args ...string) (string, error) 53 } 54 55 func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) { 56 newVersion := hugo.MustParseVersion(r.cliVersion) 57 finalVersion := newVersion.Next() 58 finalVersion.PatchLevel = 0 59 60 if newVersion.Suffix != "-test" { 61 newVersion.Suffix = "" 62 } 63 64 finalVersion.Suffix = "-DEV" 65 66 return newVersion, finalVersion 67 } 68 69 // New initialises a ReleaseHandler. 70 func New(version string, skipPublish, try bool) *ReleaseHandler { 71 // When triggered from CI release branch 72 version = strings.TrimPrefix(version, "release-") 73 version = strings.TrimPrefix(version, "v") 74 rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try} 75 76 if try { 77 rh.git = func(args ...string) (string, error) { 78 fmt.Println("git", strings.Join(args, " ")) 79 return "", nil 80 } 81 } else { 82 rh.git = git 83 } 84 85 return rh 86 } 87 88 // Run creates a new release. 89 func (r *ReleaseHandler) Run() error { 90 if os.Getenv("GITHUB_TOKEN") == "" { 91 return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new") 92 } 93 94 newVersion, finalVersion := r.calculateVersions() 95 96 version := newVersion.String() 97 tag := "v" + version 98 isPatch := newVersion.PatchLevel > 0 99 mainVersion := newVersion 100 mainVersion.PatchLevel = 0 101 102 // Exit early if tag already exists 103 exists, err := tagExists(tag) 104 if err != nil { 105 return err 106 } 107 108 if exists { 109 return fmt.Errorf("tag %q already exists", tag) 110 } 111 112 var changeLogFromTag string 113 114 if newVersion.PatchLevel == 0 { 115 // There may have been patch releases between, so set the tag explicitly. 116 changeLogFromTag = "v" + newVersion.Prev().String() 117 exists, _ := tagExists(changeLogFromTag) 118 if !exists { 119 // fall back to one that exists. 120 changeLogFromTag = "" 121 } 122 } 123 124 var ( 125 gitCommits gitInfos 126 gitCommitsDocs gitInfos 127 relNotesState releaseNotesState 128 ) 129 130 relNotesState, err = r.releaseNotesState(version) 131 if err != nil { 132 return err 133 } 134 135 prepareReleaseNotes := isPatch || relNotesState == releaseNotesNone 136 shouldRelease := isPatch || relNotesState == releaseNotesReady 137 138 defer r.gitPush() // TODO(bep) 139 140 if prepareReleaseNotes || shouldRelease { 141 gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try) 142 if err != nil { 143 return err 144 } 145 146 // TODO(bep) explicit tag? 147 gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try) 148 if err != nil { 149 return err 150 } 151 } 152 153 if relNotesState == releaseNotesCreated { 154 fmt.Println("Release notes created, but not ready. Rename to *-ready.md to continue ...") 155 return nil 156 } 157 158 if prepareReleaseNotes { 159 releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs) 160 if err != nil { 161 return err 162 } 163 164 if _, err := r.git("add", releaseNotesFile); err != nil { 165 return err 166 } 167 168 commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion) 169 if !isPatch { 170 commitMsg += "\n\nRename to *-ready.md to continue." 171 } 172 commitMsg += "\n[ci skip]" 173 174 if _, err := r.git("commit", "-m", commitMsg); err != nil { 175 return err 176 } 177 } 178 179 if !shouldRelease { 180 fmt.Printf("Skip release ... ") 181 return nil 182 } 183 184 // For docs, for now we assume that: 185 // The /docs subtree is up to date and ready to go. 186 // The hugoDocs/dev and hugoDocs/master must be merged manually after release. 187 // TODO(bep) improve this when we see how it works. 188 189 if err := r.bumpVersions(newVersion); err != nil { 190 return err 191 } 192 193 if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { 194 return err 195 } 196 197 releaseNotesFile := getReleaseNotesDocsTempFilename(version, true) 198 199 title, description := version, version 200 if isPatch { 201 title = "Hugo " + version + ": A couple of Bug Fixes" 202 description = "This version fixes a couple of bugs introduced in " + mainVersion.String() + "." 203 } 204 205 // Write the release notes to the docs site as well. 206 docFile, err := r.writeReleaseNotesToDocs(title, description, releaseNotesFile) 207 if err != nil { 208 return err 209 } 210 211 if _, err := r.git("add", docFile); err != nil { 212 return err 213 } 214 if _, err := r.git("commit", "-m", fmt.Sprintf("%s Add release notes to /docs for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { 215 return err 216 } 217 218 if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci skip]", commitPrefix, newVersion)); err != nil { 219 return err 220 } 221 222 if !r.skipPublish { 223 if _, err := r.git("push", "origin", tag); err != nil { 224 return err 225 } 226 } 227 228 if err := r.release(releaseNotesFile); err != nil { 229 return err 230 } 231 232 if err := r.bumpVersions(finalVersion); err != nil { 233 return err 234 } 235 236 if !r.try { 237 // No longer needed. 238 if err := os.Remove(releaseNotesFile); err != nil { 239 return err 240 } 241 } 242 243 if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil { 244 return err 245 } 246 247 return nil 248 } 249 250 func (r *ReleaseHandler) gitPush() { 251 if r.skipPublish { 252 return 253 } 254 if _, err := r.git("push", "origin", "HEAD"); err != nil { 255 log.Fatal("push failed:", err) 256 } 257 } 258 259 func (r *ReleaseHandler) release(releaseNotesFile string) error { 260 if r.try { 261 fmt.Println("Skip goreleaser...") 262 return nil 263 } 264 265 args := []string{"--parallelism", "3", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile} 266 if r.skipPublish { 267 args = append(args, "--skip-publish") 268 } 269 270 cmd, _ := hexec.SafeCommand("goreleaser", args...) 271 cmd.Stdout = os.Stdout 272 cmd.Stderr = os.Stderr 273 err := cmd.Run() 274 if err != nil { 275 return errors.Wrap(err, "goreleaser failed") 276 } 277 return nil 278 } 279 280 func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error { 281 toDev := "" 282 283 if ver.Suffix != "" { 284 toDev = ver.Suffix 285 } 286 287 if err := r.replaceInFile("common/hugo/version_current.go", 288 `Number:(\s{4,})(.*),`, fmt.Sprintf(`Number:${1}%.2f,`, ver.Number), 289 `PatchLevel:(\s*)(.*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel), 290 `Suffix:(\s{4,})".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil { 291 return err 292 } 293 294 snapcraftGrade := "stable" 295 if ver.Suffix != "" { 296 snapcraftGrade = "devel" 297 } 298 if err := r.replaceInFile("snap/snapcraft.yaml", 299 `version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver), 300 `grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil { 301 return err 302 } 303 304 var minVersion string 305 if ver.Suffix != "" { 306 // People use the DEV version in daily use, and we cannot create new themes 307 // with the next version before it is released. 308 minVersion = ver.Prev().String() 309 } else { 310 minVersion = ver.String() 311 } 312 313 if err := r.replaceInFile("commands/new.go", 314 `min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil { 315 return err 316 } 317 318 return nil 319 } 320 321 func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error { 322 fullFilename := hugoFilepath(filename) 323 fi, err := os.Stat(fullFilename) 324 if err != nil { 325 return err 326 } 327 328 if r.try { 329 fmt.Printf("Replace in %q: %q\n", filename, oldNew) 330 return nil 331 } 332 333 b, err := ioutil.ReadFile(fullFilename) 334 if err != nil { 335 return err 336 } 337 newContent := string(b) 338 339 for i := 0; i < len(oldNew); i += 2 { 340 re := regexp.MustCompile(oldNew[i]) 341 newContent = re.ReplaceAllString(newContent, oldNew[i+1]) 342 } 343 344 return ioutil.WriteFile(fullFilename, []byte(newContent), fi.Mode()) 345 } 346 347 func hugoFilepath(filename string) string { 348 pwd, err := os.Getwd() 349 if err != nil { 350 log.Fatal(err) 351 } 352 return filepath.Join(pwd, filename) 353 } 354 355 func isCI() bool { 356 return os.Getenv("CI") != "" 357 }