Skip to content

Commit cb0563b

Browse files
committed
Add option to push to gh-pages
Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>
1 parent 41c62a4 commit cb0563b

File tree

7 files changed

+210
-18
lines changed

7 files changed

+210
-18
lines changed

cr/cmd/index.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package cmd
1616

1717
import (
1818
"github.com/helm/chart-releaser/pkg/config"
19+
"github.com/helm/chart-releaser/pkg/git"
1920
"github.com/helm/chart-releaser/pkg/github"
2021
"github.com/helm/chart-releaser/pkg/releaser"
2122
"github.com/spf13/cobra"
@@ -35,7 +36,7 @@ given GitHub repository's releases.
3536
return err
3637
}
3738
ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL)
38-
releaser := releaser.NewReleaser(config, ghc)
39+
releaser := releaser.NewReleaser(config, ghc, &git.Git{})
3940
_, err = releaser.UpdateIndexFile()
4041
return err
4142
},
@@ -56,4 +57,7 @@ func init() {
5657
flags.StringP("token", "t", "", "GitHub Auth Token (only needed for private repos)")
5758
flags.StringP("git-base-url", "b", "https://api.github.com/", "GitHub Base URL (only needed for private GitHub)")
5859
flags.StringP("git-upload-url", "u", "https://uploads.github.com/", "GitHub Upload URL (only needed for private GitHub)")
60+
flags.String("pages-branch", "gh-pages", "The GitHub pages branch")
61+
flags.Bool("push", false, "Push index.yaml to the GitHub Pages branch (must not be set if --pr is set)")
62+
flags.Bool("pr", false, "Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set)")
5963
}

cr/cmd/upload.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package cmd
1616

1717
import (
1818
"github.com/helm/chart-releaser/pkg/config"
19+
"github.com/helm/chart-releaser/pkg/git"
1920
"github.com/helm/chart-releaser/pkg/github"
2021
"github.com/helm/chart-releaser/pkg/releaser"
2122
"github.com/spf13/cobra"
@@ -32,7 +33,7 @@ var uploadCmd = &cobra.Command{
3233
return err
3334
}
3435
ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL)
35-
releaser := releaser.NewReleaser(config, ghc)
36+
releaser := releaser.NewReleaser(config, ghc, &git.Git{})
3637
return releaser.CreateReleases()
3738
},
3839
}

pkg/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ type Options struct {
4747
GitBaseURL string `mapstructure:"git-base-url"`
4848
GitUploadURL string `mapstructure:"git-upload-url"`
4949
Commit string `mapstructure:"commit"`
50+
PagesBranch string `mapstructure:"pages-branch"`
51+
Push bool `mapstructure:"push"`
52+
PR bool `mapstructure:"pr"`
5053
}
5154

5255
func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []string) (*Options, error) {
@@ -89,6 +92,10 @@ func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []strin
8992
return nil, errors.Wrap(err, "Error unmarshaling configuration")
9093
}
9194

95+
if opts.Push && opts.PR {
96+
return nil, errors.New("specify either --push or --pr, but not both")
97+
}
98+
9299
elem := reflect.ValueOf(opts).Elem()
93100
for _, requiredFlag := range requiredFlags {
94101
fieldName := kebabCaseToTitleCamelCase(requiredFlag)

pkg/git/git.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"os/exec"
8+
)
9+
10+
type Git struct{}
11+
12+
// AddWorktree creates a new Git worktree with a detached HEAD for the given committish and returns its path.
13+
func (g *Git) AddWorktree(workingDir string, committish string) (string, error) {
14+
dir, err := ioutil.TempDir("", "chart-releaser-")
15+
if err != nil {
16+
return "", err
17+
}
18+
command := exec.Command("git", "worktree", "add", "--detach", dir, committish)
19+
20+
if err := runCommand(workingDir, command); err != nil {
21+
return "", err
22+
}
23+
return dir, nil
24+
}
25+
26+
// RemoveWorktree removes the Git worktree with the given path.
27+
func (g *Git) RemoveWorktree(workingDir string, path string) error {
28+
command := exec.Command("git", "worktree", "remove", path, "--force")
29+
return runCommand(workingDir, command)
30+
}
31+
32+
// Add runs 'git add' with the given args.
33+
func (g *Git) Add(workingDir string, args ...string) error {
34+
if len(args) == 0 {
35+
return fmt.Errorf("no args specified")
36+
}
37+
addArgs := []string{"add"}
38+
addArgs = append(addArgs, args...)
39+
command := exec.Command("git", addArgs...)
40+
return runCommand(workingDir, command)
41+
}
42+
43+
// Commit runs 'git commit' with the given message. the commit is signed off.
44+
func (g *Git) Commit(workingDir string, message string) error {
45+
command := exec.Command("git", "commit", "--message", message, "--signoff")
46+
return runCommand(workingDir, command)
47+
}
48+
49+
// Push runs 'git push' with the given args.
50+
func (g *Git) Push(workingDir string, args ...string) error {
51+
pushArgs := []string{"push"}
52+
pushArgs = append(pushArgs, args...)
53+
command := exec.Command("git", pushArgs...)
54+
return runCommand(workingDir, command)
55+
}
56+
57+
func runCommand(workingDir string, command *exec.Cmd) error {
58+
command.Dir = workingDir
59+
command.Stdout = os.Stdout
60+
command.Stderr = os.Stderr
61+
return command.Run()
62+
}

pkg/github/github.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,29 @@ func (c *Client) CreateRelease(ctx context.Context, input *Release) error {
122122
return nil
123123
}
124124

125+
// CreatePullRequest creates a pull request in the repository specified by repoURL.
126+
// The return value is the pull request URL.
127+
func (c *Client) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) {
128+
split := strings.SplitN(message, "\n", 2)
129+
title := split[0]
130+
131+
pr := &github.NewPullRequest{
132+
Title: &title,
133+
Head: &head,
134+
Base: &base,
135+
}
136+
if len(split) == 2 {
137+
body := strings.TrimSpace(split[1])
138+
pr.Body = &body
139+
}
140+
141+
pullRequest, _, err := c.PullRequests.Create(context.Background(), owner, repo, pr)
142+
if err != nil {
143+
return "", err
144+
}
145+
return *pullRequest.HTMLURL, nil
146+
}
147+
125148
// UploadAsset uploads specified assets to a given release object
126149
func (c *Client) uploadReleaseAsset(ctx context.Context, releaseID int64, filename string) error {
127150

pkg/releaser/releaser.go

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import (
1919
"fmt"
2020
"io"
2121
"io/ioutil"
22+
"math/rand"
2223
"net/http"
2324
"net/url"
2425
"os"
2526
"path/filepath"
2627
"strings"
28+
"time"
2729

2830
"github.com/pkg/errors"
2931
"helm.sh/helm/v3/pkg/chart/loader"
@@ -41,13 +43,27 @@ import (
4143
type GitHub interface {
4244
CreateRelease(ctx context.Context, input *github.Release) error
4345
GetRelease(ctx context.Context, tag string) (*github.Release, error)
46+
CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error)
4447
}
4548

4649
type HttpClient interface {
4750
Get(url string) (*http.Response, error)
4851
}
4952

50-
type DefaultHttpClient struct {
53+
type Git interface {
54+
AddWorktree(workingDir string, committish string) (string, error)
55+
RemoveWorktree(workingDir string, path string) error
56+
Add(workingDir string, args ...string) error
57+
Commit(workingDir string, message string) error
58+
Push(workingDir string, args ...string) error
59+
}
60+
61+
type DefaultHttpClient struct{}
62+
63+
var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
64+
65+
func init() {
66+
rand.Seed(time.Now().UnixNano())
5167
}
5268

5369
func (c *DefaultHttpClient) Get(url string) (resp *http.Response, err error) {
@@ -58,17 +74,19 @@ type Releaser struct {
5874
config *config.Options
5975
github GitHub
6076
httpClient HttpClient
77+
git Git
6178
}
6279

63-
func NewReleaser(config *config.Options, github GitHub) *Releaser {
80+
func NewReleaser(config *config.Options, github GitHub, git Git) *Releaser {
6481
return &Releaser{
6582
config: config,
6683
github: github,
6784
httpClient: &DefaultHttpClient{},
85+
git: git,
6886
}
6987
}
7088

71-
// UpdateIndexFile index.yaml file for a give git repo
89+
// UpdateIndexFile updates the index.yaml file for a given Git repo
7290
func (r *Releaser) UpdateIndexFile() (bool, error) {
7391
// if path doesn't end with index.yaml we can try and fix it
7492
if filepath.Base(r.config.IndexPath) != "index.yaml" {
@@ -103,13 +121,13 @@ func (r *Releaser) UpdateIndexFile() (bool, error) {
103121
return false, err
104122
}
105123

106-
fmt.Printf("====> Using existing index at %s\n", r.config.IndexPath)
124+
fmt.Printf("Using existing index at %s\n", r.config.IndexPath)
107125
indexFile, err = repo.LoadIndexFile(r.config.IndexPath)
108126
if err != nil {
109127
return false, err
110128
}
111129
} else {
112-
fmt.Printf("====> UpdateIndexFile new index at %s\n", r.config.IndexPath)
130+
fmt.Printf("UpdateIndexFile new index at %s\n", r.config.IndexPath)
113131
indexFile = repo.NewIndexFile()
114132
}
115133

@@ -133,7 +151,7 @@ func (r *Releaser) UpdateIndexFile() (bool, error) {
133151
baseName := strings.TrimSuffix(name, filepath.Ext(name))
134152
tagParts := r.splitPackageNameAndVersion(baseName)
135153
packageName, packageVersion := tagParts[0], tagParts[1]
136-
fmt.Printf("====> Found %s-%s.tgz\n", packageName, packageVersion)
154+
fmt.Printf("Found %s-%s.tgz\n", packageName, packageVersion)
137155
if _, err := indexFile.Get(packageName, packageVersion); err != nil {
138156
if err := r.addToIndexFile(indexFile, downloadUrl.String()); err != nil {
139157
return false, err
@@ -144,15 +162,62 @@ func (r *Releaser) UpdateIndexFile() (bool, error) {
144162
}
145163
}
146164

147-
if update {
148-
fmt.Printf("--> Updating index %s\n", r.config.IndexPath)
149-
indexFile.SortEntries()
150-
return true, indexFile.WriteFile(r.config.IndexPath, 0644)
151-
} else {
152-
fmt.Printf("--> Index %s did not change\n", r.config.IndexPath)
165+
if !update {
166+
fmt.Printf("Index %s did not change\n", r.config.IndexPath)
167+
return false, nil
168+
}
169+
170+
fmt.Printf("Updating index %s\n", r.config.IndexPath)
171+
indexFile.SortEntries()
172+
173+
if err := indexFile.WriteFile(r.config.IndexPath, 0644); err != nil {
174+
return false, err
175+
}
176+
177+
if !r.config.Push && !r.config.PR {
178+
return true, nil
179+
}
180+
181+
worktree, err := r.git.AddWorktree("", "origin/"+r.config.PagesBranch)
182+
if err != nil {
183+
return false, err
184+
}
185+
defer r.git.RemoveWorktree("", worktree)
186+
187+
indexYamlPath := filepath.Join(worktree, "index.yaml")
188+
if err := copyFile(r.config.IndexPath, indexYamlPath); err != nil {
189+
return false, err
190+
}
191+
if err := r.git.Add(worktree, indexYamlPath); err != nil {
192+
return false, err
193+
}
194+
if err := r.git.Commit(worktree, "Update index.yaml"); err != nil {
195+
return false, err
196+
}
197+
198+
pushURL := fmt.Sprintf("https://x-access-token:%s@github.com/%s/%s", r.config.Token, r.config.Owner, r.config.GitRepo)
199+
200+
if r.config.Push {
201+
fmt.Printf("Pushing to branch %q\n", r.config.PagesBranch)
202+
if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+r.config.PagesBranch); err != nil {
203+
return false, err
204+
}
205+
} else if r.config.PR {
206+
branch := fmt.Sprintf("chart-releaser-%s", randomString(16))
207+
208+
fmt.Printf("Pushing to branch %q\n", branch)
209+
if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+branch); err != nil {
210+
return false, err
211+
}
212+
fmt.Printf("Creating pull request against branch %q\n", r.config.PagesBranch)
213+
prURL, err := r.github.CreatePullRequest(r.config.Owner, r.config.GitRepo, "Update index.yaml", branch, r.config.PagesBranch)
214+
if err != nil {
215+
return false, err
216+
}
217+
fmt.Println("Pull request created:", prURL)
153218
}
154219

155-
return false, nil
220+
return true, nil
156221
}
157222

158223
func (r *Releaser) splitPackageNameAndVersion(pkg string) []string {
@@ -164,13 +229,13 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error {
164229
arch := filepath.Join(r.config.PackagePath, filepath.Base(url))
165230

166231
// extract chart metadata
167-
fmt.Printf("====> Extracting chart metadata from %s\n", arch)
232+
fmt.Printf("Extracting chart metadata from %s\n", arch)
168233
c, err := loader.LoadFile(arch)
169234
if err != nil {
170235
return errors.Wrapf(err, "%s is not a helm chart package", arch)
171236
}
172237
// calculate hash
173-
fmt.Printf("====> Calculating Hash for %s\n", arch)
238+
fmt.Printf("Calculating Hash for %s\n", arch)
174239
hash, err := provenance.DigestFile(arch)
175240
if err != nil {
176241
return err
@@ -187,7 +252,7 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error {
187252
return nil
188253
}
189254

190-
// CreateReleases finds and uploads helm chart packages to github
255+
// CreateReleases finds and uploads Helm chart packages to GitHub
191256
func (r *Releaser) CreateReleases() error {
192257
packages, err := r.getListOfPackages(r.config.PackagePath)
193258
if err != nil {
@@ -230,3 +295,28 @@ func (r *Releaser) CreateReleases() error {
230295
func (r *Releaser) getListOfPackages(dir string) ([]string, error) {
231296
return filepath.Glob(filepath.Join(dir, "*.tgz"))
232297
}
298+
299+
func copyFile(srcFile string, dstFile string) error {
300+
source, err := os.Open(srcFile)
301+
if err != nil {
302+
return err
303+
}
304+
defer source.Close()
305+
306+
destination, err := os.Create(dstFile)
307+
if err != nil {
308+
return err
309+
}
310+
defer destination.Close()
311+
312+
_, err = io.Copy(destination, source)
313+
return err
314+
}
315+
316+
func randomString(n int) string {
317+
b := make([]rune, n)
318+
for i := range b {
319+
b[i] = letters[rand.Intn(len(letters))]
320+
}
321+
return string(b)
322+
}

pkg/releaser/releaser_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ func (f *FakeGitHub) GetRelease(ctx context.Context, tag string) (*github.Releas
7272
return release, nil
7373
}
7474

75+
func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) {
76+
f.Called(owner, repo, message, head, base)
77+
return "https://github.com/owner/repo/pull/42", nil
78+
}
79+
7580
func TestReleaser_UpdateIndexFile(t *testing.T) {
7681
indexDir, _ := ioutil.TempDir(".", "index")
7782
defer os.RemoveAll(indexDir)

0 commit comments

Comments
 (0)