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