Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to push to gh-pages #73

Merged
merged 2 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cr/cmd/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cmd

import (
"github.com/helm/chart-releaser/pkg/config"
"github.com/helm/chart-releaser/pkg/git"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@unguiculus what do you think about using https://github.com/go-git/go-git instead of our own package that shells out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had used that in a client project and dropped it. We hit bugs and had various issues with it and it covers only a small subset of Git which wasn't enough for us. Plus, it's not really straight forward to use. I'd rather not use it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'worktree' is not supported BTW.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that in the compatibility doc, but read through https://github.com/go-git/go-git/blob/master/worktree_test.go, which led me to think the doc just wasn't updated. But I don't have any real world experience using that package, so definitely trust your judgement 👍 My main thought was it could be cool to be able to run this without git as a dependency. Maybe something to reevaluate later?

"github.com/helm/chart-releaser/pkg/github"
"github.com/helm/chart-releaser/pkg/releaser"
"github.com/spf13/cobra"
Expand All @@ -35,7 +36,7 @@ given GitHub repository's releases.
return err
}
ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL)
releaser := releaser.NewReleaser(config, ghc)
releaser := releaser.NewReleaser(config, ghc, &git.Git{})
_, err = releaser.UpdateIndexFile()
return err
},
Expand All @@ -56,4 +57,8 @@ func init() {
flags.StringP("token", "t", "", "GitHub Auth Token (only needed for private repos)")
flags.StringP("git-base-url", "b", "https://api.github.com/", "GitHub Base URL (only needed for private GitHub)")
flags.StringP("git-upload-url", "u", "https://uploads.github.com/", "GitHub Upload URL (only needed for private GitHub)")
flags.String("pages-branch", "gh-pages", "The GitHub pages branch")
flags.String("remote", "origin", "The Git remote used when creating a local worktree for the GitHub Pages branch")
flags.Bool("push", false, "Push index.yaml to the GitHub Pages branch (must not be set if --pr is set)")
flags.Bool("pr", false, "Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set)")
}
3 changes: 2 additions & 1 deletion cr/cmd/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cmd

import (
"github.com/helm/chart-releaser/pkg/config"
"github.com/helm/chart-releaser/pkg/git"
"github.com/helm/chart-releaser/pkg/github"
"github.com/helm/chart-releaser/pkg/releaser"
"github.com/spf13/cobra"
Expand All @@ -32,7 +33,7 @@ var uploadCmd = &cobra.Command{
return err
}
ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL)
releaser := releaser.NewReleaser(config, ghc)
releaser := releaser.NewReleaser(config, ghc, &git.Git{})
return releaser.CreateReleases()
},
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type Options struct {
GitBaseURL string `mapstructure:"git-base-url"`
GitUploadURL string `mapstructure:"git-upload-url"`
Commit string `mapstructure:"commit"`
PagesBranch string `mapstructure:"pages-branch"`
Push bool `mapstructure:"push"`
PR bool `mapstructure:"pr"`
Remote string `mapstructure:"remote"`
}

func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []string) (*Options, error) {
Expand Down Expand Up @@ -89,6 +93,10 @@ func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []strin
return nil, errors.Wrap(err, "Error unmarshaling configuration")
}

if opts.Push && opts.PR {
return nil, errors.New("specify either --push or --pr, but not both")
}

elem := reflect.ValueOf(opts).Elem()
for _, requiredFlag := range requiredFlags {
fieldName := kebabCaseToTitleCamelCase(requiredFlag)
Expand Down
62 changes: 62 additions & 0 deletions pkg/git/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package git

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
)

type Git struct{}

// AddWorktree creates a new Git worktree with a detached HEAD for the given committish and returns its path.
func (g *Git) AddWorktree(workingDir string, committish string) (string, error) {
dir, err := ioutil.TempDir("", "chart-releaser-")
if err != nil {
return "", err
}
command := exec.Command("git", "worktree", "add", "--detach", dir, committish)

if err := runCommand(workingDir, command); err != nil {
return "", err
}
return dir, nil
}

// RemoveWorktree removes the Git worktree with the given path.
func (g *Git) RemoveWorktree(workingDir string, path string) error {
command := exec.Command("git", "worktree", "remove", path, "--force")
return runCommand(workingDir, command)
}

// Add runs 'git add' with the given args.
func (g *Git) Add(workingDir string, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("no args specified")
}
addArgs := []string{"add"}
addArgs = append(addArgs, args...)
command := exec.Command("git", addArgs...)
return runCommand(workingDir, command)
}

// Commit runs 'git commit' with the given message. the commit is signed off.
func (g *Git) Commit(workingDir string, message string) error {
command := exec.Command("git", "commit", "--message", message, "--signoff")
return runCommand(workingDir, command)
}

// Push runs 'git push' with the given args.
func (g *Git) Push(workingDir string, args ...string) error {
pushArgs := []string{"push"}
pushArgs = append(pushArgs, args...)
command := exec.Command("git", pushArgs...)
return runCommand(workingDir, command)
}

func runCommand(workingDir string, command *exec.Cmd) error {
command.Dir = workingDir
command.Stdout = os.Stdout
command.Stderr = os.Stderr
return command.Run()
}
23 changes: 23 additions & 0 deletions pkg/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,29 @@ func (c *Client) CreateRelease(ctx context.Context, input *Release) error {
return nil
}

// CreatePullRequest creates a pull request in the repository specified by repoURL.
// The return value is the pull request URL.
func (c *Client) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) {
split := strings.SplitN(message, "\n", 2)
title := split[0]

pr := &github.NewPullRequest{
Title: &title,
Head: &head,
Base: &base,
}
if len(split) == 2 {
body := strings.TrimSpace(split[1])
pr.Body = &body
}

pullRequest, _, err := c.PullRequests.Create(context.Background(), owner, repo, pr)
if err != nil {
return "", err
}
return *pullRequest.HTMLURL, nil
}

// UploadAsset uploads specified assets to a given release object
func (c *Client) uploadReleaseAsset(ctx context.Context, releaseID int64, filename string) error {

Expand Down
122 changes: 106 additions & 16 deletions pkg/releaser/releaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import (
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"

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

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

type DefaultHttpClient struct {
type Git interface {
AddWorktree(workingDir string, committish string) (string, error)
RemoveWorktree(workingDir string, path string) error
Add(workingDir string, args ...string) error
Commit(workingDir string, message string) error
Push(workingDir string, args ...string) error
}

type DefaultHttpClient struct{}

var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")

func init() {
rand.Seed(time.Now().UnixNano())
}

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

func NewReleaser(config *config.Options, github GitHub) *Releaser {
func NewReleaser(config *config.Options, github GitHub, git Git) *Releaser {
return &Releaser{
config: config,
github: github,
httpClient: &DefaultHttpClient{},
git: git,
}
}

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

fmt.Printf("====> Using existing index at %s\n", r.config.IndexPath)
fmt.Printf("Using existing index at %s\n", r.config.IndexPath)
indexFile, err = repo.LoadIndexFile(r.config.IndexPath)
if err != nil {
return false, err
}
} else {
fmt.Printf("====> UpdateIndexFile new index at %s\n", r.config.IndexPath)
fmt.Printf("UpdateIndexFile new index at %s\n", r.config.IndexPath)
indexFile = repo.NewIndexFile()
}

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

if update {
fmt.Printf("--> Updating index %s\n", r.config.IndexPath)
indexFile.SortEntries()
return true, indexFile.WriteFile(r.config.IndexPath, 0644)
} else {
fmt.Printf("--> Index %s did not change\n", r.config.IndexPath)
if !update {
fmt.Printf("Index %s did not change\n", r.config.IndexPath)
return false, nil
}

fmt.Printf("Updating index %s\n", r.config.IndexPath)
indexFile.SortEntries()

if err := indexFile.WriteFile(r.config.IndexPath, 0644); err != nil {
return false, err
}

if !r.config.Push && !r.config.PR {
return true, nil
}

worktree, err := r.git.AddWorktree("", r.config.Remote+"/"+r.config.PagesBranch)
if err != nil {
return false, err
}
defer r.git.RemoveWorktree("", worktree)

indexYamlPath := filepath.Join(worktree, "index.yaml")
if err := copyFile(r.config.IndexPath, indexYamlPath); err != nil {
return false, err
}
if err := r.git.Add(worktree, indexYamlPath); err != nil {
return false, err
}
if err := r.git.Commit(worktree, "Update index.yaml"); err != nil {
return false, err
}

pushURL := fmt.Sprintf("https://x-access-token:%s@github.com/%s/%s", r.config.Token, r.config.Owner, r.config.GitRepo)

if r.config.Push {
fmt.Printf("Pushing to branch %q\n", r.config.PagesBranch)
if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+r.config.PagesBranch); err != nil {
return false, err
}
} else if r.config.PR {
branch := fmt.Sprintf("chart-releaser-%s", randomString(16))

fmt.Printf("Pushing to branch %q\n", branch)
if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+branch); err != nil {
return false, err
}
fmt.Printf("Creating pull request against branch %q\n", r.config.PagesBranch)
prURL, err := r.github.CreatePullRequest(r.config.Owner, r.config.GitRepo, "Update index.yaml", branch, r.config.PagesBranch)
if err != nil {
return false, err
}
fmt.Println("Pull request created:", prURL)
}

return false, nil
return true, nil
}

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

// extract chart metadata
fmt.Printf("====> Extracting chart metadata from %s\n", arch)
fmt.Printf("Extracting chart metadata from %s\n", arch)
c, err := loader.LoadFile(arch)
if err != nil {
return errors.Wrapf(err, "%s is not a helm chart package", arch)
}
// calculate hash
fmt.Printf("====> Calculating Hash for %s\n", arch)
fmt.Printf("Calculating Hash for %s\n", arch)
hash, err := provenance.DigestFile(arch)
if err != nil {
return err
Expand All @@ -187,7 +252,7 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error {
return nil
}

// CreateReleases finds and uploads helm chart packages to github
// CreateReleases finds and uploads Helm chart packages to GitHub
func (r *Releaser) CreateReleases() error {
packages, err := r.getListOfPackages(r.config.PackagePath)
if err != nil {
Expand Down Expand Up @@ -230,3 +295,28 @@ func (r *Releaser) CreateReleases() error {
func (r *Releaser) getListOfPackages(dir string) ([]string, error) {
return filepath.Glob(filepath.Join(dir, "*.tgz"))
}

func copyFile(srcFile string, dstFile string) error {
source, err := os.Open(srcFile)
if err != nil {
return err
}
defer source.Close()

destination, err := os.Create(dstFile)
if err != nil {
return err
}
defer destination.Close()

_, err = io.Copy(destination, source)
return err
}

func randomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
5 changes: 5 additions & 0 deletions pkg/releaser/releaser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func (f *FakeGitHub) GetRelease(ctx context.Context, tag string) (*github.Releas
return release, nil
}

func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) {
f.Called(owner, repo, message, head, base)
return "https://github.com/owner/repo/pull/42", nil
}

func TestReleaser_UpdateIndexFile(t *testing.T) {
indexDir, _ := ioutil.TempDir(".", "index")
defer os.RemoveAll(indexDir)
Expand Down