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