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 }