Add linux/arm64 extended to release setup

Fixes #8257
This commit is contained in:
Bjørn Erik Pedersen 2022-08-05 16:35:24 +02:00
parent c98348416c
commit 45e1084ff2
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
13 changed files with 447 additions and 1192 deletions

View file

@ -1,51 +1,114 @@
defaults: &defaults parameters:
docker:
- image: bepsays/ci-goreleaser:1.21900.20000
environment:
CGO_ENABLED: "0"
# v2: 11m.
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.21900.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2 version: 2
jobs: jobs:
build: prepare_release:
<<: *defaults <<: *defaults
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
steps: steps:
- &remote-docker
setup_remote_docker:
version: 20.10.14
- checkout: - checkout:
path: hugo path: hugo
- run: - &git-config
run:
command: | command: |
git clone git@github.com:gohugoio/hugoDocs.git
cd hugo
go mod download
sleep 5
go mod verify
- persist_to_workspace:
root: .
paths: .
release:
<<: *defaults
steps:
- attach_workspace:
at: /root/project
- run:
command: |
cd hugo
git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com" git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com"
git config --global user.name "hugoreleaser" git config --global user.name "hugoreleaser"
go run -tags release main.go release -r ${CIRCLE_BRANCH} - run:
command: |
cd hugo
go mod download
go run -tags release main.go release --step 1
- save_cache:
key: git-sha-{{ .Revision }}
paths:
- hugo
- gomodcache
build_container1:
<<: [*defaults]
environment:
<<: [*buildenv]
steps:
- &restore-cache
restore_cache:
key: git-sha-{{ .Revision }}
- run:
no_output_timeout: 20m
command: |
mkdir -p /tmp/files/dist1
cd hugo
hugoreleaser build -paths "builds/container1/**" -workers 3 -dist /tmp/files/dist1 -chunks $CIRCLE_NODE_TOTAL -chunk-index $CIRCLE_NODE_INDEX
- &persist-workspace
persist_to_workspace:
root: /tmp/files
paths:
- dist1
- dist2
parallelism: 7
build_container2:
<<: [*defaults]
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.21900.20000
steps:
- *restore-cache
- &attach-workspace
attach_workspace:
at: /tmp/workspace
- run:
command: |
mkdir -p /tmp/files/dist2
cd hugo
hugoreleaser build -paths "builds/container2/**" -workers 1 -dist /tmp/files/dist2
- *persist-workspace
archive_and_release:
<<: [*defaults]
environment:
<<: [*buildenv]
steps:
- *restore-cache
- *attach-workspace
- *git-config
- run:
command: |
cp -a /tmp/workspace/dist1/. ./hugo/dist
cp -a /tmp/workspace/dist2/. ./hugo/dist
- run:
command: |
cd hugo
hugoreleaser archive
hugoreleaser release
go run -tags release main.go release --step 2
workflows: workflows:
version: 2 version: 2
release: release:
jobs: jobs:
- build: - prepare_release:
filters: filters:
branches: branches:
only: /release-.*/ only: /release-.*/
- hold: - build_container1:
type: approval
requires: requires:
- build - prepare_release
- release: - build_container2:
requires:
- prepare_release
- archive_and_release:
context: org-global context: org-global
requires: requires:
- hold - build_container1
- build_container2

View file

@ -17,8 +17,6 @@
package commands package commands
import ( import (
"errors"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/releaser" "github.com/gohugoio/hugo/releaser"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -29,9 +27,8 @@ var _ cmder = (*releaseCommandeer)(nil)
type releaseCommandeer struct { type releaseCommandeer struct {
cmd *cobra.Command cmd *cobra.Command
version string step int
skipPush bool
skipPublish bool
try bool try bool
} }
@ -50,9 +47,9 @@ func createReleaser() cmder {
return r.release() return r.release()
} }
r.cmd.PersistentFlags().StringVarP(&r.version, "rel", "r", "", "new release version, i.e. 0.25.1") r.cmd.PersistentFlags().BoolVarP(&r.skipPush, "skip-push", "", false, "skip pushing to remote")
r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "skip all publishing pipes of the release") r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "no changes")
r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "simulate a release, i.e. no changes") r.cmd.PersistentFlags().IntVarP(&r.step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
return r return r
} }
@ -65,8 +62,10 @@ func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
} }
func (r *releaseCommandeer) release() error { func (r *releaseCommandeer) release() error {
if r.version == "" { rel, err := releaser.New(r.skipPush, r.try, r.step)
return errors.New("must set the --rel flag to the relevant version number") if err != nil {
return err
} }
return releaser.New(r.version, r.skipPublish, r.try).Run()
return rel.Run()
} }

View file

@ -1,214 +0,0 @@
project_name: hugo
env:
- GO111MODULE=on
- GOPROXY=https://proxy.golang.org
before:
hooks:
- go mod download
builds:
-
binary: hugo
id: hugo
ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
env:
- CGO_ENABLED=0
flags:
- -buildmode
- exe
goos:
- darwin
- linux
- windows
goarch:
- amd64
- 386
- arm
- arm64
goarm:
- 7
ignore:
- goos: darwin
goarch: 386
-
binary: hugo
id: hugo_unix
ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
env:
- CGO_ENABLED=0
flags:
- -buildmode
- exe
goos:
- freebsd
- netbsd
- openbsd
- dragonfly
goarch:
- amd64
-
binary: hugo
id: hugo_extended_windows
ldflags:
- -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
- "-extldflags '-static'"
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
flags:
- -buildmode
- exe
- -tags
- extended
goos:
- windows
goarch:
- amd64
- binary: hugo
id: hugo_extended_darwin
ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
flags:
- -buildmode
- exe
- -tags
- extended
goos:
- darwin
goarch:
- amd64
- arm64
- binary: hugo
id: hugo_extended_linux
ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
env:
- CGO_ENABLED=1
flags:
- -buildmode
- exe
- -tags
- extended
goos:
- linux
goarch:
- amd64
hooks:
post: ./goreleaser-hook-post-linux.sh
release:
draft: true
archives:
-
id: "hugo"
builds: ['hugo', 'hugo_unix', 'hugo_fat_darwin']
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
files:
- README.md
- LICENSE
-
id: "hugo_extended"
builds: ['hugo_extended_windows', 'hugo_extended_linux', 'hugo_extended_fat_darwin']
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "{{.ProjectName}}_extended_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
files:
- README.md
- LICENSE
universal_binaries:
-
id: hugo_fat_darwin
ids:
- hugo
replace: true
-
id: hugo_extended_fat_darwin
ids:
- hugo_extended_darwin
replace: true
nfpms:
-
id: "hugo"
builds: ['hugo']
formats:
- deb
vendor: "gohugo.io"
homepage: "https://gohugo.io/"
maintainer: "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
description: "A Fast and Flexible Static Site Generator built with love in GoLang."
license: "Apache-2.0"
file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
-
id: "hugo_extended"
builds: ['hugo_extended_linux']
formats:
- deb
vendor: "gohugo.io"
homepage: "https://gohugo.io/"
maintainer: "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
description: "A Fast and Flexible Static Site Generator built with love in GoLang."
license: "Apache-2.0"
file_name_template: "{{.ProjectName}}_extended_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD

6
hugoreleaser.env Normal file
View file

@ -0,0 +1,6 @@
HUGO_RELEASE_NAME=New release setup
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=xxx
HUGORELEASER_COMMITISH=xxx

232
hugoreleaser.toml Normal file
View file

@ -0,0 +1,232 @@
project = "hugo"
[go_settings]
go_proxy = "https://proxy.golang.org"
go_exe = "go"
[build_settings]
binary = "hugo"
flags = ["-buildmode", "exe"]
env = ["CGO_ENABLED=0"]
ldflags = "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio"
[archive_settings]
name_template = "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
extra_files = [
{ source_path = "README.md", target_path = "README.md" },
{ source_path = "LICENSE", target_path = "LICENSE" },
]
[archive_settings.type]
format = "tar.gz"
extension = ".tar.gz"
[archive_settings.replacements]
amd64 = "64bit"
386 = "32bit"
arm = "ARM"
arm64 = "ARM64"
darwin = "macOS"
linux = "Linux"
windows = "Windows"
openbsd = "OpenBSD"
netbsd = "NetBSD"
freebsd = "FreeBSD"
dragonfly = "DragonFlyBSD"
[release_settings]
name = "${HUGO_RELEASE_NAME}"
type = "github"
repository = "hugo"
repository_owner = "gohugoio"
draft = true
prerelease = false
[release_settings.release_notes_settings]
# Use Hugoreleaser's autogenerated release notes.
generate = true
# Collapse relases with < 10 changes below one title.
short_threshold = 10
short_title = "What's Changed"
groups = [
# Group the changes in the release notes by title.
# You need at least one.
# The groups will be tested in order until a match is found.
# The titles will so be listed in the given order in the release note.
# Any match with ignore=true title will be dropped.
{ regexp = "Merge commit|Squashed", ignore = true },
{ title = "Bug fixes", regexp = "fix", ordinal = 10 },
{ title = "Dependency Updates", regexp = "deps", ordinal = 30 },
{ title = "Build Setup", regexp = "(snap|release|update to)", ordinal = 40 },
{ title = "Documentation", regexp = "(doc|readme)", ordinal = 40 },
{ title = "Improvements", regexp = ".*", ordinal = 20 },
]
[[builds]]
path = "container1/unix/regular"
[[builds.os]]
goos = "darwin"
[[builds.os.archs]]
goarch = "universal"
[[builds.os]]
goos = "linux"
[[builds.os.archs]]
goarch = "amd64"
[[builds.os.archs]]
goarch = "arm64"
[[builds.os.archs]]
goarch = "arm"
[builds.os.archs.build_settings]
env = ["CGO_ENABLED=0", "GOARM=7"]
# Unix BSD variants
[[builds.os]]
goos = "dragonfly"
[[builds.os.archs]]
goarch = "amd64"
[[builds.os]]
goos = "freebsd"
[[builds.os.archs]]
goarch = "amd64"
[[builds.os]]
goos = "netbsd"
[[builds.os.archs]]
goarch = "amd64"
[[builds.os]]
goos = "openbsd"
[[builds.os.archs]]
goarch = "amd64"
[[builds]]
path = "container1/unix/extended"
[builds.build_settings]
flags = ["-buildmode", "exe", "-tags", "extended"]
env = ["CGO_ENABLED=1"]
[[builds.os]]
goos = "darwin"
[builds.os.build_settings]
env = ["CGO_ENABLED=1", "CC=o64-clang", "CXX=o64-clang++"]
[[builds.os.archs]]
goarch = "universal"
[[builds.os]]
goos = "linux"
[[builds.os.archs]]
goarch = "amd64"
[[builds]]
path = "container2/linux/extended"
[builds.build_settings]
flags = ["-buildmode", "exe", "-tags", "extended"]
[[builds.os]]
goos = "linux"
[builds.os.build_settings]
env = [
"CGO_ENABLED=1",
"CC=aarch64-linux-gnu-gcc",
"CXX=aarch64-linux-gnu-g++",
]
[[builds.os.archs]]
goarch = "arm64"
[[builds]]
path = "container1/windows/regular"
[[builds.os]]
goos = "windows"
[builds.os.build_settings]
binary = "hugo.exe"
[[builds.os.archs]]
goarch = "amd64"
[[builds.os.archs]]
goarch = "arm64"
[[builds]]
path = "container1/windows/extended"
[builds.build_settings]
flags = ["-buildmode", "exe", "-tags", "extended"]
env = [
"CGO_ENABLED=1",
"CC=x86_64-w64-mingw32-gcc",
"CXX=x86_64-w64-mingw32-g++",
]
ldflags = "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static'"
[[builds.os]]
goos = "windows"
[builds.os.build_settings]
binary = "hugo.exe"
[[builds.os.archs]]
goarch = "amd64"
[[archives]]
paths = ["builds/container1/unix/regular/**"]
[[archives]]
paths = ["builds/container1/unix/extended/**"]
[archives.archive_settings]
name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
[[archives]]
# Only extended builds in container2.
paths = ["builds/container2/**"]
[archives.archive_settings]
name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
[[archives]]
paths = ["builds/**/windows/regular/**"]
[archives.archive_settings.type]
format = "zip"
extension = ".zip"
[[archives]]
paths = ["builds/**/windows/extended/**"]
[archives.archive_settings]
name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
[archives.archive_settings.type]
format = "zip"
extension = ".zip"
[[archives]]
paths = ["builds/**/regular/linux/{arm64,amd64}"]
[archives.archive_settings]
[archives.archive_settings.type]
format = "_plugin"
extension = ".deb"
[archives.archive_settings.plugin]
id = "deb"
type = "gorun"
command = "github.com/gohugoio/hugoreleaser-archive-plugins/deb@v0.5.0"
[archives.archive_settings.custom_settings]
vendor = "gohugo.io"
homepage = "https://github.com/gohugoio/hugoreleaser"
maintainer = "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
description = "Build, archive and release Go programs."
license = "Apache-2.0"
[[archives]]
paths = ["builds/**/extended/linux/{arm64,amd64}"]
[archives.archive_settings]
name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
[archives.archive_settings.type]
format = "_plugin"
extension = ".deb"
[archives.archive_settings.plugin]
id = "deb"
type = "gorun"
command = "github.com/gohugoio/hugoreleaser-archive-plugins/deb@latest"
[archives.archive_settings.custom_settings]
vendor = "gohugo.io"
homepage = "https://github.com/gohugoio/hugoreleaser"
maintainer = "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
description = "Build, archive and release Go programs."
license = "Apache-2.0"
[[releases]]
paths = ["archives/**"]
path = "r1"
# The above should allow the following build commands:
# hugoreleaser build -paths "builds/container1/**"
# hugoreleaser build -paths "builds/container2/**"
# hugoreleaser archive
# hugoreleaser release

View file

@ -1,253 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package releaser
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"github.com/gohugoio/hugo/common/hexec"
)
var issueRe = regexp.MustCompile(`(?i)(?:Updates?|Closes?|Fix.*|See) #(\d+)`)
type changeLog struct {
Version string
Notes gitInfos
All gitInfos
Docs gitInfos
// Overall stats
Repo *gitHubRepo
ContributorCount int
ThemeCount int
}
func newChangeLog(infos, docInfos gitInfos) *changeLog {
log := &changeLog{
Docs: docInfos,
}
for _, info := range infos {
// TODO(bep) improve
if regexp.MustCompile("(?i)deprecate|note").MatchString(info.Subject) {
log.Notes = append(log.Notes, info)
}
log.All = append(log.All, info)
info.Subject = strings.TrimSpace(info.Subject)
}
return log
}
type gitInfo struct {
Hash string
Author string
Subject string
Body string
GitHubCommit *gitHubCommit
}
func (g gitInfo) Issues() []int {
return extractIssues(g.Body)
}
func (g gitInfo) AuthorID() string {
if g.GitHubCommit != nil {
return g.GitHubCommit.Author.Login
}
return g.Author
}
func extractIssues(body string) []int {
var i []int
m := issueRe.FindAllStringSubmatch(body, -1)
for _, mm := range m {
issueID, err := strconv.Atoi(mm[1])
if err != nil {
continue
}
i = append(i, issueID)
}
return i
}
type gitInfos []gitInfo
func git(args ...string) (string, error) {
cmd, _ := hexec.SafeCommand("git", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
}
return string(out), nil
}
func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
return getGitInfosBefore("HEAD", tag, repo, repoPath, remote)
}
type countribCount struct {
Author string
GitHubAuthor gitHubAuthor
Count int
}
func (c countribCount) AuthorLink() string {
if c.GitHubAuthor.HTMLURL != "" {
return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HTMLURL)
}
if !strings.Contains(c.Author, "@") {
return c.Author
}
return c.Author[:strings.Index(c.Author, "@")]
}
type contribCounts []countribCount
func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count }
func (c contribCounts) Len() int { return len(c) }
func (c contribCounts) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (g gitInfos) ContribCountPerAuthor() contribCounts {
var c contribCounts
counters := make(map[string]countribCount)
for _, gi := range g {
authorID := gi.AuthorID()
if count, ok := counters[authorID]; ok {
count.Count = count.Count + 1
counters[authorID] = count
} else {
var ghA gitHubAuthor
if gi.GitHubCommit != nil {
ghA = gi.GitHubCommit.Author
}
authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA}
counters[authorID] = authorCount
}
}
for _, v := range counters {
c = append(c, v)
}
sort.Sort(c)
return c
}
func getGitInfosBefore(ref, tag, repo, repoPath string, remote bool) (gitInfos, error) {
client := newGitHubAPI(repo)
var g gitInfos
log, err := gitLogBefore(ref, tag, repoPath)
if err != nil {
return g, err
}
log = strings.Trim(log, "\n\x1e'")
entries := strings.Split(log, "\x1e")
for _, entry := range entries {
items := strings.Split(entry, "\x1f")
gi := gitInfo{}
if len(items) > 0 {
gi.Hash = items[0]
}
if len(items) > 1 {
gi.Author = items[1]
}
if len(items) > 2 {
gi.Subject = items[2]
}
if len(items) > 3 {
gi.Body = items[3]
}
if remote && gi.Hash != "" {
gc, err := client.fetchCommit(gi.Hash)
if err == nil {
gi.GitHubCommit = &gc
}
}
g = append(g, gi)
}
return g, nil
}
// Ignore autogenerated commits etc. in change log. This is a regexp.
const ignoredCommits = "snapcraft:|Merge commit|Squashed"
func gitLogBefore(ref, tag, repoPath string) (string, error) {
var prevTag string
var err error
if tag != "" {
prevTag = tag
} else {
prevTag, err = gitVersionTagBefore(ref)
if err != nil {
return "", err
}
}
defaultArgs := []string{"log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag + ".." + ref}
var args []string
if repoPath != "" {
args = append([]string{"-C", repoPath}, defaultArgs...)
} else {
args = defaultArgs
}
log, err := git(args...)
if err != nil {
return ",", err
}
return log, err
}
func gitVersionTagBefore(ref string) (string, error) {
return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^")
}
func gitShort(args ...string) (output string, err error) {
output, err = git(args...)
return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
}
func tagExists(tag string) (bool, error) {
out, err := git("tag", "-l", tag)
if err != nil {
return false, err
}
if strings.Contains(out, tag) {
return true, nil
}
return false, nil
}

View file

@ -1,86 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package releaser
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestGitInfos(t *testing.T) {
c := qt.New(t)
skipIfCI(t)
infos, err := getGitInfos("v0.20", "hugo", "", false)
c.Assert(err, qt.IsNil)
c.Assert(len(infos) > 0, qt.Equals, true)
}
func TestIssuesRe(t *testing.T) {
c := qt.New(t)
body := `
This is a commit message.
Updates #123
Fix #345
closes #543
See #456
`
issues := extractIssues(body)
c.Assert(len(issues), qt.Equals, 4)
c.Assert(issues[0], qt.Equals, 123)
c.Assert(issues[2], qt.Equals, 543)
bodyNoIssues := `
This is a commit message without issue refs.
But it has e #10 to make old regexp confused.
Streets #20.
`
emptyIssuesList := extractIssues(bodyNoIssues)
c.Assert(len(emptyIssuesList), qt.Equals, 0)
}
func TestGitVersionTagBefore(t *testing.T) {
skipIfCI(t)
c := qt.New(t)
v1, err := gitVersionTagBefore("v0.18")
c.Assert(err, qt.IsNil)
c.Assert(v1, qt.Equals, "v0.17")
}
func TestTagExists(t *testing.T) {
skipIfCI(t)
c := qt.New(t)
b1, err := tagExists("v0.18")
c.Assert(err, qt.IsNil)
c.Assert(b1, qt.Equals, true)
b2, err := tagExists("adfagdsfg")
c.Assert(err, qt.IsNil)
c.Assert(b2, qt.Equals, false)
}
func skipIfCI(t *testing.T) {
if isCI() {
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
// Also Travis clones very shallowly, making some of the tests above shaky.
t.Skip("Skip git test on Linux to make Travis happy.")
}
}

View file

@ -1,143 +0,0 @@
package releaser
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
)
var (
gitHubCommitsAPI = "https://api.github.com/repos/gohugoio/REPO/commits/%s"
gitHubRepoAPI = "https://api.github.com/repos/gohugoio/REPO"
gitHubContributorsAPI = "https://api.github.com/repos/gohugoio/REPO/contributors"
)
type gitHubAPI struct {
commitsAPITemplate string
repoAPI string
contributorsAPITemplate string
}
func newGitHubAPI(repo string) *gitHubAPI {
return &gitHubAPI{
commitsAPITemplate: strings.Replace(gitHubCommitsAPI, "REPO", repo, -1),
repoAPI: strings.Replace(gitHubRepoAPI, "REPO", repo, -1),
contributorsAPITemplate: strings.Replace(gitHubContributorsAPI, "REPO", repo, -1),
}
}
type gitHubCommit struct {
Author gitHubAuthor `json:"author"`
HTMLURL string `json:"html_url"`
}
type gitHubAuthor struct {
ID int `json:"id"`
Login string `json:"login"`
HTMLURL string `json:"html_url"`
AvatarURL string `json:"avatar_url"`
}
type gitHubRepo struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
HTMLURL string `json:"html_url"`
Stars int `json:"stargazers_count"`
Contributors []gitHubContributor
}
type gitHubContributor struct {
ID int `json:"id"`
Login string `json:"login"`
HTMLURL string `json:"html_url"`
Contributions int `json:"contributions"`
}
func (g *gitHubAPI) fetchCommit(ref string) (gitHubCommit, error) {
var commit gitHubCommit
u := fmt.Sprintf(g.commitsAPITemplate, ref)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return commit, err
}
err = doGitHubRequest(req, &commit)
return commit, err
}
func (g *gitHubAPI) fetchRepo() (gitHubRepo, error) {
var repo gitHubRepo
req, err := http.NewRequest("GET", g.repoAPI, nil)
if err != nil {
return repo, err
}
err = doGitHubRequest(req, &repo)
if err != nil {
return repo, err
}
var contributors []gitHubContributor
page := 0
for {
page++
var currPage []gitHubContributor
url := fmt.Sprintf(g.contributorsAPITemplate+"?page=%d", page)
req, err = http.NewRequest("GET", url, nil)
if err != nil {
return repo, err
}
err = doGitHubRequest(req, &currPage)
if err != nil {
return repo, err
}
if len(currPage) == 0 {
break
}
contributors = append(contributors, currPage...)
}
repo.Contributors = contributors
return repo, err
}
func doGitHubRequest(req *http.Request, v any) error {
addGitHubToken(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if isError(resp) {
b, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("GitHub lookup failed: %s", string(b))
}
return json.NewDecoder(resp.Body).Decode(v)
}
func isError(resp *http.Response) bool {
return resp.StatusCode < 200 || resp.StatusCode > 299
}
func addGitHubToken(req *http.Request) {
gitHubToken := os.Getenv("GITHUB_TOKEN")
if gitHubToken != "" {
req.Header.Add("Authorization", "token "+gitHubToken)
}
}

View file

@ -1,46 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package releaser
import (
"fmt"
"os"
"testing"
qt "github.com/frankban/quicktest"
)
func TestGitHubLookupCommit(t *testing.T) {
skipIfNoToken(t)
c := qt.New(t)
client := newGitHubAPI("hugo")
commit, err := client.fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
c.Assert(err, qt.IsNil)
fmt.Println(commit)
}
func TestFetchRepo(t *testing.T) {
skipIfNoToken(t)
c := qt.New(t)
client := newGitHubAPI("hugo")
repo, err := client.fetchRepo()
c.Assert(err, qt.IsNil)
fmt.Println(">>", len(repo.Contributors))
}
func skipIfNoToken(t *testing.T) {
if os.Getenv("GITHUB_TOKEN") == "" {
t.Skip("Skip test against GitHub as no GITHUB_TOKEN set.")
}
}

View file

@ -1,191 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package releaser implements a set of utilities and a wrapper around Goreleaser
// to help automate the Hugo release process.
package releaser
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
)
const (
issueLinkTemplate = "#%d"
linkTemplate = "[%s](%s)"
releaseNotesMarkdownTemplatePatchRelease = `
{{ if eq (len .All) 1 }}
This is a bug-fix release with one important fix.
{{ else }}
This is a bug-fix release with a couple of important fixes.
{{ end }}
{{ range .All }}
{{- if .GitHubCommit -}}
* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }}
{{ else -}}
* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
{{ end -}}
{{- end }}
`
releaseNotesMarkdownTemplate = `
{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}}
{{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}}
This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base.
{{- if gt (len $contribsPerAuthor) 3 -}}
{{- $u1 := index $contribsPerAuthor 0 -}}
{{- $u2 := index $contribsPerAuthor 1 -}}
{{- $u3 := index $contribsPerAuthor 2 -}}
{{- $u4 := index $contribsPerAuthor 3 -}}
{{- $u1.AuthorLink }} leads the Hugo development with a significant amount of contributions, but also a big shoutout to {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their ongoing contributions.
And thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his ongoing work on keeping the themes site in pristine condition.
{{ end }}
Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs),
which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**.
{{- if gt (len $docsContribsPerAuthor) 3 -}}
{{- $u1 := index $docsContribsPerAuthor 0 -}}
{{- $u2 := index $docsContribsPerAuthor 1 -}}
{{- $u3 := index $docsContribsPerAuthor 2 -}}
{{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site.
{{ end }}
Hugo now has:
{{ with .Repo -}}
* {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers)
* {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors)
{{- end -}}
{{ with .ThemeCount }}
* {{ . }}+ [themes](http://themes.gohugo.io/)
{{ end }}
{{ with .Notes }}
## Notes
{{ template "change-section" . }}
{{- end -}}
{{ with .All }}
## Changes
{{ template "change-section" . }}
{{ end }}
{{ define "change-section" }}
{{ range . }}
{{- if .GitHubCommit -}}
* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }}
{{ else -}}
* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
{{ end -}}
{{- end }}
{{ end }}
`
)
var templateFuncs = template.FuncMap{
"isPatch": func(c changeLog) bool {
return !strings.HasSuffix(c.Version, "0")
},
"issue": func(id int) string {
return fmt.Sprintf(issueLinkTemplate, id)
},
"commitURL": func(info gitInfo) string {
if info.GitHubCommit.HTMLURL == "" {
return ""
}
return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HTMLURL)
},
"author": func(info gitInfo) string {
return "@" + info.GitHubCommit.Author.Login
},
}
func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error {
client := newGitHubAPI("hugo")
changes := newChangeLog(infosMain, infosDocs)
changes.Version = version
repo, err := client.fetchRepo()
if err == nil {
changes.Repo = &repo
}
themeCount, err := fetchThemeCount()
if err == nil {
changes.ThemeCount = themeCount
}
mtempl := releaseNotesMarkdownTemplate
if !strings.HasSuffix(version, "0") {
mtempl = releaseNotesMarkdownTemplatePatchRelease
}
tmpl, err := template.New("").Funcs(templateFuncs).Parse(mtempl)
if err != nil {
return err
}
err = tmpl.Execute(to, changes)
if err != nil {
return err
}
return nil
}
func fetchThemeCount() (int, error) {
resp, err := http.Get("https://raw.githubusercontent.com/gohugoio/hugoThemesSiteBuilder/main/themes.txt")
if err != nil {
return 0, err
}
defer resp.Body.Close()
b, _ := ioutil.ReadAll(resp.Body)
return bytes.Count(b, []byte("\n")) - bytes.Count(b, []byte("#")), nil
}
func getReleaseNotesFilename(version string) string {
return filepath.FromSlash(fmt.Sprintf("temp/%s-relnotes-ready.md", version))
}
func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, isPatch bool, infosMain, infosDocs gitInfos) (string, error) {
filename := getReleaseNotesFilename(version)
var w io.WriteCloser
if !r.try {
f, err := os.Create(filename)
if err != nil {
return "", err
}
defer f.Close()
w = f
} else {
w = os.Stdout
}
if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil {
return "", err
}
return filename, nil
}

View file

@ -1,46 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package commands defines and implements command-line commands and flags
// used by Hugo. Commands and flags are implemented using Cobra.
package releaser
import (
"bytes"
"fmt"
"os"
"testing"
qt "github.com/frankban/quicktest"
)
func _TestReleaseNotesWriter(t *testing.T) {
skipIfNoToken(t)
if os.Getenv("CI") != "" {
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
t.Skip("Skip git test on CI to make Travis happy..")
}
c := qt.New(t)
var b bytes.Buffer
// TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster.
infos, err := getGitInfosBefore("HEAD", "v0.89.0", "hugo", "", false)
c.Assert(err, qt.IsNil)
c.Assert(writeReleaseNotes("0.89.0", infos, infos, &b), qt.IsNil)
fmt.Println(b.String())
}

View file

@ -17,7 +17,6 @@ package releaser
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -25,22 +24,56 @@ import (
"strings" "strings"
"github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/hexec"
"errors"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
) )
const commitPrefix = "releaser:" const commitPrefix = "releaser:"
// New initialises a ReleaseHandler.
func New(skipPush, try bool, step int) (*ReleaseHandler, error) {
if step < 1 || step > 2 {
return nil, fmt.Errorf("step must be 1 or 2")
}
prefix := "release-"
branch, err := git("rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return nil, err
}
if !strings.HasPrefix(branch, prefix) {
return nil, fmt.Errorf("branch %q is not a release branch", branch)
}
logf("Branch: %s\n", branch)
version := strings.TrimPrefix(branch, prefix)
version = strings.TrimPrefix(version, "v")
rh := &ReleaseHandler{branchVersion: version, skipPush: skipPush, try: try, step: step}
if try {
rh.git = func(args ...string) (string, error) {
logln("git", strings.Join(args, " "))
return "", nil
}
} else {
rh.git = git
}
return rh, nil
}
// ReleaseHandler provides functionality to release a new version of Hugo. // ReleaseHandler provides functionality to release a new version of Hugo.
// Test this locally without doing an actual release: // Test this locally without doing an actual release:
// go run -tags release main.go release --skip-publish --try -r 0.90.0 // go run -tags release main.go release --skip-publish --try -r 0.90.0
// Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only. // Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only.
type ReleaseHandler struct { type ReleaseHandler struct {
cliVersion string branchVersion string
skipPublish bool // 1 or 2.
step int
// No remote pushes.
skipPush bool
// Just simulate, no actual changes. // Just simulate, no actual changes.
try bool try bool
@ -48,111 +81,17 @@ type ReleaseHandler struct {
git func(args ...string) (string, error) git func(args ...string) (string, error)
} }
func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
newVersion := hugo.MustParseVersion(r.cliVersion)
finalVersion := newVersion.Next()
finalVersion.PatchLevel = 0
if newVersion.Suffix != "-test" {
newVersion.Suffix = ""
}
finalVersion.Suffix = "-DEV"
return newVersion, finalVersion
}
// New initialises a ReleaseHandler.
func New(version string, skipPublish, try bool) *ReleaseHandler {
// When triggered from CI release branch
version = strings.TrimPrefix(version, "release-")
version = strings.TrimPrefix(version, "v")
rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
if try {
rh.git = func(args ...string) (string, error) {
fmt.Println("git", strings.Join(args, " "))
return "", nil
}
} else {
rh.git = git
}
return rh
}
// Run creates a new release. // Run creates a new release.
func (r *ReleaseHandler) Run() error { func (r *ReleaseHandler) Run() error {
if os.Getenv("GITHUB_TOKEN") == "" {
return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new")
}
fmt.Printf("Start release from %q\n", wd())
newVersion, finalVersion := r.calculateVersions() newVersion, finalVersion := r.calculateVersions()
version := newVersion.String() version := newVersion.String()
tag := "v" + version tag := "v" + version
isPatch := newVersion.PatchLevel > 0
mainVersion := newVersion mainVersion := newVersion
mainVersion.PatchLevel = 0 mainVersion.PatchLevel = 0
// Exit early if tag already exists defer r.gitPush()
exists, err := tagExists(tag)
if err != nil {
return err
}
if exists {
return fmt.Errorf("tag %q already exists", tag)
}
var changeLogFromTag string
if newVersion.PatchLevel == 0 {
// There may have been patch releases between, so set the tag explicitly.
changeLogFromTag = "v" + newVersion.Prev().String()
exists, _ := tagExists(changeLogFromTag)
if !exists {
// fall back to one that exists.
changeLogFromTag = ""
}
}
var (
gitCommits gitInfos
gitCommitsDocs gitInfos
)
defer r.gitPush() // TODO(bep)
gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
if err != nil {
return err
}
// TODO(bep) explicit tag?
gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
if err != nil {
return err
}
releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs)
if err != nil {
return err
}
if _, err := r.git("add", releaseNotesFile); err != nil {
return err
}
commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion)
commitMsg += "\n[ci skip]"
if _, err := r.git("commit", "-m", commitMsg); err != nil {
return err
}
if r.step == 1 {
if err := r.bumpVersions(newVersion); err != nil { if err := r.bumpVersions(newVersion); err != nil {
return err return err
} }
@ -161,31 +100,29 @@ func (r *ReleaseHandler) Run() error {
return err return err
} }
if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { // The above commit will be the target for this release, so print it to the console in a env friendly way.
sha, err := git("rev-parse", "HEAD")
if err != nil {
return err return err
} }
if !r.skipPublish { // Hugoreleaser will do the actual release using these values.
if _, err := r.git("push", "origin", tag); err != nil { if err := r.replaceInFile("hugoreleaser.env",
`HUGORELEASER_TAG=(\S*)`, "HUGORELEASER_TAG="+tag,
`HUGORELEASER_COMMITISH=(\S*)`, "HUGORELEASER_COMMITISH="+sha,
); err != nil {
return err return err
} }
} logf("HUGORELEASER_TAG=%s\n", tag)
logf("HUGORELEASER_COMMITISH=%s\n", sha)
if err := r.release(releaseNotesFile); err != nil { return nil
return err
} }
if err := r.bumpVersions(finalVersion); err != nil { if err := r.bumpVersions(finalVersion); err != nil {
return err return err
} }
if !r.try {
// No longer needed.
if err := os.Remove(releaseNotesFile); err != nil {
return err
}
}
if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil { if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
return err return err
} }
@ -193,36 +130,6 @@ func (r *ReleaseHandler) Run() error {
return nil return nil
} }
func (r *ReleaseHandler) gitPush() {
if r.skipPublish {
return
}
if _, err := r.git("push", "origin", "HEAD"); err != nil {
log.Fatal("push failed:", err)
}
}
func (r *ReleaseHandler) release(releaseNotesFile string) error {
if r.try {
fmt.Println("Skip goreleaser...")
return nil
}
args := []string{"--parallelism", "2", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile}
if r.skipPublish {
args = append(args, "--skip-publish")
}
cmd, _ := hexec.SafeCommand("goreleaser", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("goreleaser failed: %w", err)
}
return nil
}
func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error { func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
toDev := "" toDev := ""
@ -264,6 +171,29 @@ func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
return nil return nil
} }
func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
newVersion := hugo.MustParseVersion(r.branchVersion)
finalVersion := newVersion.Next()
finalVersion.PatchLevel = 0
if newVersion.Suffix != "-test" {
newVersion.Suffix = ""
}
finalVersion.Suffix = "-DEV"
return newVersion, finalVersion
}
func (r *ReleaseHandler) gitPush() {
if r.skipPush {
return
}
if _, err := r.git("push", "origin", "HEAD"); err != nil {
log.Fatal("push failed:", err)
}
}
func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error { func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error {
filename = filepath.FromSlash(filename) filename = filepath.FromSlash(filename)
fi, err := os.Stat(filename) fi, err := os.Stat(filename)
@ -272,11 +202,11 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error
} }
if r.try { if r.try {
fmt.Printf("Replace in %q: %q\n", filename, oldNew) logf("Replace in %q: %q\n", filename, oldNew)
return nil return nil
} }
b, err := ioutil.ReadFile(filename) b, err := os.ReadFile(filename)
if err != nil { if err != nil {
return err return err
} }
@ -287,18 +217,22 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error
newContent = re.ReplaceAllString(newContent, oldNew[i+1]) newContent = re.ReplaceAllString(newContent, oldNew[i+1])
} }
return ioutil.WriteFile(filename, []byte(newContent), fi.Mode()) return os.WriteFile(filename, []byte(newContent), fi.Mode())
} }
func isCI() bool { func git(args ...string) (string, error) {
return os.Getenv("CI") != "" cmd, _ := hexec.SafeCommand("git", args...)
} out, err := cmd.CombinedOutput()
func wd() string {
p, err := os.Getwd()
if err != nil { if err != nil {
log.Fatal(err) return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
}
return string(out), nil
} }
return p
func logf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
}
func logln(args ...interface{}) {
fmt.Fprintln(os.Stderr, args...)
} }