1
0
mirror of https://github.com/helm/chart-releaser.git synced 2026-02-05 09:45:23 +01:00

Add option to host the chart package files in the GitHub Pages branch (#123)

* add packages-with-index flag

Signed-off-by: Steven Barnes <Steven.Barnes@topgolf.com>

* Add unit tests

Signed-off-by: Steven Barnes <Steven.Barnes@topgolf.com>

* delete files created by test

Signed-off-by: Steven Barnes <Steven.Barnes@topgolf.com>

* Authenticate to get existing index.yaml

Signed-off-by: Steven Barnes <Steven.Barnes@topgolf.com>

* update docs

Signed-off-by: cpanato <ctadeu@gmail.com>

* fix lints

Signed-off-by: cpanato <ctadeu@gmail.com>

* add git pull function

Signed-off-by: cpanato <ctadeu@gmail.com>

* update help text for upload command

Signed-off-by: cpanato <ctadeu@gmail.com>

---------

Signed-off-by: Steven Barnes <Steven.Barnes@topgolf.com>
Signed-off-by: cpanato <ctadeu@gmail.com>
Co-authored-by: cpanato <ctadeu@gmail.com>
This commit is contained in:
Steven Barnes
2023-06-20 04:26:09 -05:00
committed by GitHub
parent 9630f429d4
commit 378f5275a6
9 changed files with 267 additions and 91 deletions

View File

@@ -86,6 +86,7 @@ Flags:
--skip-existing Skip upload if release exists
-t, --token string GitHub Auth Token
--make-release-latest bool Mark the created GitHub release as 'latest' (default "true")
--packages-with-index Host the package files in the GitHub Pages branch
Global Flags:
--config string Config file (default is $HOME/.cr.yaml)
@@ -118,6 +119,7 @@ Flags:
--release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}")
--remote string The Git remote used when creating a local worktree for the GitHub Pages branch (default "origin")
-t, --token string GitHub Auth Token (only needed for private repos)
--packages-with-index Host the package files in the GitHub Pages branch
Global Flags:
--config string Config file (default is $HOME/.cr.yaml)

View File

@@ -76,4 +76,5 @@ func init() {
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)")
flags.String("release-name-template", "{{ .Name }}-{{ .Version }}", "Go template for computing release names, using chart metadata")
flags.Bool("packages-with-index", false, "Host the package files in the GitHub Pages branch")
}

View File

@@ -57,4 +57,9 @@ func init() {
"If it is set to empty string, or the file is not found, the chart description will be used instead. The file is read from the chart package")
uploadCmd.Flags().Bool("generate-release-notes", false, "Whether to automatically generate the name and body for this release. See https://docs.github.com/en/rest/releases/releases")
uploadCmd.Flags().Bool("make-release-latest", true, "Mark the created GitHub release as 'latest'")
uploadCmd.Flags().String("pages-branch", "gh-pages", "The GitHub pages branch")
uploadCmd.Flags().String("remote", "origin", "The Git remote used when creating a local worktree for the GitHub Pages branch")
uploadCmd.Flags().Bool("push", false, "Push the chart package to the GitHub Pages branch (must not be set if --pr is set)")
uploadCmd.Flags().Bool("pr", false, "Create a pull request for the chart package against the GitHub Pages branch (must not be set if --push is set)")
uploadCmd.Flags().Bool("packages-with-index", false, "Host the package files in the GitHub Pages branch")
}

View File

@@ -23,6 +23,7 @@ cr index [flags]
-i, --index-path string Path to index file (default ".cr-index/index.yaml")
-o, --owner string GitHub username or organization
-p, --package-path string Path to directory with chart packages (default ".cr-release-packages")
--packages-with-index Host the package files in the GitHub Pages branch
--pages-branch string The GitHub pages branch (default "gh-pages")
--pages-index-path string The GitHub pages index path (default "index.yaml")
--pr Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set)

View File

@@ -22,8 +22,13 @@ cr upload [flags]
--make-release-latest Mark the created GitHub release as 'latest' (default true)
-o, --owner string GitHub username or organization
-p, --package-path string Path to directory with chart packages (default ".cr-release-packages")
--packages-with-index Host the package files in the GitHub Pages branch
--pages-branch string The GitHub pages branch (default "gh-pages")
--pr Create a pull request for the chart package against the GitHub Pages branch (must not be set if --push is set)
--push Push the chart package to the GitHub Pages branch (must not be set if --pr is set)
--release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}")
--release-notes-file string Markdown file with chart release notes. If it is set to empty string, or the file is not found, the chart description will be used instead. The file is read from the chart package
--remote string The Git remote used when creating a local worktree for the GitHub Pages branch (default "origin")
--skip-existing Skip upload if release exists
-t, --token string GitHub Auth Token
```

View File

@@ -61,6 +61,7 @@ type Options struct {
ReleaseNotesFile string `mapstructure:"release-notes-file"`
GenerateReleaseNotes bool `mapstructure:"generate-release-notes"`
MakeReleaseLatest bool `mapstructure:"make-release-latest"`
PackagesWithIndex bool `mapstructure:"packages-with-index"`
}
func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []string) (*Options, error) {

View File

@@ -60,6 +60,14 @@ func (g *Git) Commit(workingDir string, message string) error {
return runCommand(workingDir, command)
}
// UpdateBranch runs 'git pull' with the given args.
func (g *Git) Pull(workingDir string, args ...string) error {
pullArgs := []string{"pull"}
pullArgs = append(pullArgs, args...)
command := exec.Command("git", pullArgs...)
return runCommand(workingDir, command)
}
// Push runs 'git push' with the given args.
func (g *Git) Push(workingDir string, args ...string) error {
pushArgs := []string{"push"}

View File

@@ -55,6 +55,7 @@ type GitHub interface {
type HTTPClient interface {
Get(url string) (*http.Response, error)
GetWithToken(url string, token string) (*http.Response, error)
}
type Git interface {
@@ -63,6 +64,7 @@ type Git interface {
Add(workingDir string, args ...string) error
Commit(workingDir string, message string) error
Push(workingDir string, args ...string) error
Pull(workingDir string, args ...string) error
GetPushURL(remote string, token string) (string, error)
}
@@ -213,38 +215,23 @@ func (r *Releaser) UpdateIndexFile() (bool, error) {
if err := copyFile(r.config.IndexPath, indexYamlPath); err != nil {
return false, err
}
if err := r.git.Pull(worktree, r.config.Remote, r.config.PagesBranch); err != nil {
return false, err
}
if err := r.git.Add(worktree, indexYamlPath); err != nil {
return false, err
}
if err := r.git.Commit(worktree, fmt.Sprintf("Update %s", r.config.PagesIndexPath)); err != nil {
return false, err
}
pushURL, err := r.git.GetPushURL(r.config.Remote, r.config.Token)
if err != nil {
if err := r.pushToPagesBranch(worktree); err != nil {
return false, err
}
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 true, nil
}
@@ -302,6 +289,12 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error {
s := strings.Split(url, "/")
s = s[:len(s)-1]
if r.config.PackagesWithIndex {
// the chart will be stored in the same repo as
// the index file so let's make the path relative
s = s[:0]
}
// Add to index
if err := indexFile.MustAdd(c.Metadata, filepath.Base(arch), strings.Join(s, "/"), hash); err != nil {
return err
@@ -354,6 +347,31 @@ func (r *Releaser) CreateReleases() error {
if err := r.github.CreateRelease(context.TODO(), release); err != nil {
return errors.Wrapf(err, "error creating GitHub release %s", releaseName)
}
if r.config.PackagesWithIndex {
worktree, err := r.git.AddWorktree("", r.config.Remote+"/"+r.config.PagesBranch)
if err != nil {
return err
}
defer r.git.RemoveWorktree("", worktree) //nolint: errcheck
pkgTargetPath := filepath.Join(worktree, filepath.Base(p))
if err := copyFile(p, pkgTargetPath); err != nil {
return err
}
if err := r.git.Add(worktree, pkgTargetPath); err != nil {
return err
}
if err := r.git.Commit(worktree, fmt.Sprintf("Publishing chart package for %s", releaseName)); err != nil {
return err
}
if err := r.pushToPagesBranch(worktree); err != nil {
return err
}
}
}
return nil
@@ -363,6 +381,35 @@ func (r *Releaser) getListOfPackages(dir string) ([]string, error) {
return filepath.Glob(filepath.Join(dir, "*.tgz"))
}
func (r *Releaser) pushToPagesBranch(worktree string) error {
pushURL, err := r.git.GetPushURL(r.config.Remote, r.config.Token)
if err != nil {
return err
}
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 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 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 err
}
fmt.Println("Pull request created:", prURL)
}
return nil
}
func copyFile(srcFile string, dstFile string) error {
source, err := os.Open(srcFile)
if err != nil {

View File

@@ -37,6 +37,7 @@ type FakeGitHub struct {
type FakeGit struct {
indexFile string
mock.Mock
}
func (f *FakeGit) AddWorktree(workingDir string, committish string) (string, error) {
@@ -52,23 +53,37 @@ func (f *FakeGit) AddWorktree(workingDir string, committish string) (string, err
}
func (f *FakeGit) RemoveWorktree(workingDir string, path string) error {
return nil
f.Called(workingDir, path)
return os.RemoveAll(workingDir)
}
func (f *FakeGit) Add(workingDir string, args ...string) error {
panic("implement me")
f.Called(workingDir, args)
if len(args) == 0 {
return fmt.Errorf("no args specified")
}
return nil
}
func (f *FakeGit) Commit(workingDir string, message string) error {
panic("implement me")
f.Called(workingDir, message)
return nil
}
func (f *FakeGit) Pull(workingDir string, args ...string) error {
f.Called(workingDir, args)
return nil
}
func (f *FakeGit) Push(workingDir string, args ...string) error {
panic("implement me")
f.Called(workingDir, args)
return nil
}
func (f *FakeGit) GetPushURL(remote string, token string) (string, error) {
panic("implement me")
f.Called(remote, token)
pushURLWithToken := fmt.Sprintf("https://x-access-token:%s@github.com/owner/repo", token)
return pushURLWithToken, nil
}
func (f *FakeGitHub) CreateRelease(ctx context.Context, input *github.Release) error {
@@ -107,59 +122,60 @@ func TestReleaser_UpdateIndexFile(t *testing.T) {
fakeGitHub := new(FakeGitHub)
tests := []struct {
name string
exists bool
releaser *Releaser
name string
exists bool
releaser *Releaser
indexFile string
}{
{
"index-file-exists",
true,
&Releaser{
name: "index-file-exists",
exists: true,
releaser: &Releaser{
config: &config.Options{
IndexPath: "testdata/index/index.yaml",
PackagePath: "testdata/release-packages",
},
github: fakeGitHub,
git: &FakeGit{"testdata/repo/index.yaml"},
},
indexFile: "testdata/repo/index.yaml",
},
{
"index-file-exists-pages-index-path",
true,
&Releaser{
name: "index-file-exists-pages-index-path",
exists: true,
releaser: &Releaser{
config: &config.Options{
IndexPath: "testdata/index/index.yaml",
PackagePath: "testdata/release-packages",
PagesIndexPath: "./",
},
github: fakeGitHub,
git: &FakeGit{"testdata/repo/index.yaml"},
},
indexFile: "testdata/repo/index.yaml",
},
{
"index-file-does-not-exist",
false,
&Releaser{
name: "index-file-does-not-exist",
exists: false,
releaser: &Releaser{
config: &config.Options{
IndexPath: filepath.Join(indexDir, "index.yaml"),
PackagePath: "testdata/release-packages",
},
github: fakeGitHub,
git: &FakeGit{""},
},
indexFile: "",
},
{
"index-file-does-not-exist-pages-index-path",
false,
&Releaser{
name: "index-file-does-not-exist-pages-index-path",
exists: false,
releaser: &Releaser{
config: &config.Options{
IndexPath: filepath.Join(indexDir, "index.yaml"),
PackagePath: "testdata/release-packages",
PagesIndexPath: "./",
},
github: fakeGitHub,
git: &FakeGit{""},
},
indexFile: "",
},
}
for _, tt := range tests {
@@ -168,6 +184,11 @@ func TestReleaser_UpdateIndexFile(t *testing.T) {
if tt.exists {
sha256, _ = provenance.DigestFile(tt.releaser.config.IndexPath)
}
fakeGit := new(FakeGit)
fakeGit.indexFile = tt.indexFile
fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil)
tt.releaser.git = fakeGit
update, err := tt.releaser.UpdateIndexFile()
assert.NoError(t, err)
assert.Equal(t, update, !tt.exists)
@@ -189,23 +210,29 @@ func TestReleaser_UpdateIndexFileGenerated(t *testing.T) {
fakeGitHub := new(FakeGitHub)
tests := []struct {
name string
releaser *Releaser
name string
releaser *Releaser
indexFile string
}{
{
"index-file-exists",
&Releaser{
name: "index-file-exists",
releaser: &Releaser{
config: &config.Options{
IndexPath: filepath.Join(indexDir, "index.yaml"),
PackagePath: "testdata/release-packages",
},
github: fakeGitHub,
git: &FakeGit{indexFile: "testdata/empty-repo/index.yaml"},
},
indexFile: "testdata/empty-repo/index.yaml",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeGit := new(FakeGit)
fakeGit.indexFile = tt.indexFile
fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil)
tt.releaser.git = fakeGit
indexFile, _ := repo.LoadIndexFile("testdata/empty-repo/index.yaml")
generated := indexFile.Generated
update, err := tt.releaser.UpdateIndexFile()
@@ -258,37 +285,70 @@ func TestReleaser_splitPackageNameAndVersion(t *testing.T) {
func TestReleaser_addToIndexFile(t *testing.T) {
tests := []struct {
name string
chart string
version string
error bool
name string
chart string
version string
releaser *Releaser
packagesWithIndex bool
error bool
}{
{
"invalid-package",
"does-not-exist",
"0.1.0",
&Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
PackagesWithIndex: false,
},
},
false,
true,
},
{
"valid-package",
"test-chart",
"0.1.0",
&Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
PackagesWithIndex: false,
},
},
false,
false,
},
{
"valid-package-with-index",
"test-chart",
"0.1.0",
&Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
PackagesWithIndex: true,
},
},
true,
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Releaser{
config: &config.Options{PackagePath: "testdata/release-packages"},
}
indexFile := repo.NewIndexFile()
url := fmt.Sprintf("https://myrepo/charts/%s-%s.tgz", tt.chart, tt.version)
err := r.addToIndexFile(indexFile, url)
err := tt.releaser.addToIndexFile(indexFile, url)
if tt.error {
assert.Error(t, err)
assert.False(t, indexFile.Has(tt.chart, tt.version))
} else {
assert.True(t, indexFile.Has(tt.chart, tt.version))
indexEntry, _ := indexFile.Get(tt.chart, tt.version)
if tt.packagesWithIndex {
assert.Equal(t, filepath.Base(url), indexEntry.URLs[0])
} else {
assert.Equal(t, url, indexEntry.URLs[0])
}
}
})
}
@@ -302,50 +362,95 @@ func TestReleaser_CreateReleases(t *testing.T) {
version string
commit string
latest string
Releaser *Releaser
error bool
}{
{
"invalid-package-path",
"testdata/does-not-exist",
"test-chart",
"0.1.0",
"",
"true",
true,
name: "invalid-package-path",
packagePath: "testdata/does-not-exist",
chart: "test-chart",
version: "0.1.0",
commit: "",
latest: "true",
Releaser: &Releaser{
config: &config.Options{
PackagePath: "testdata/does-not-exist",
Commit: "",
PackagesWithIndex: false,
MakeReleaseLatest: true,
},
},
error: true,
},
{
"valid-package-path",
"testdata/release-packages",
"test-chart",
"0.1.0",
"",
"true",
false,
name: "valid-package-path",
packagePath: "testdata/release-packages",
chart: "test-chart",
version: "0.1.0",
commit: "",
latest: "true",
Releaser: &Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
Commit: "",
PackagesWithIndex: false,
MakeReleaseLatest: true,
},
},
error: false,
},
{
"valid-package-path-with-commit",
"testdata/release-packages",
"test-chart",
"0.1.0",
"5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c",
"true",
false,
name: "valid-package-path-with-commit",
packagePath: "testdata/release-packages",
chart: "test-chart",
version: "0.1.0",
commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c",
latest: "true",
Releaser: &Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
Commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c",
PackagesWithIndex: false,
MakeReleaseLatest: true,
},
},
error: false,
},
{
name: "valid-package-with-index",
packagePath: "testdata/release-packages",
chart: "test-chart",
version: "0.1.0",
commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c",
latest: "true",
Releaser: &Releaser{
config: &config.Options{
PackagePath: "testdata/release-packages",
Commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c",
PackagesWithIndex: true,
Push: true,
MakeReleaseLatest: true,
},
},
error: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeGitHub := new(FakeGitHub)
r := &Releaser{
config: &config.Options{
PackagePath: tt.packagePath,
Commit: tt.commit,
ReleaseNameTemplate: "{{ .Name }}-{{ .Version }}",
MakeReleaseLatest: true,
},
github: fakeGitHub,
}
fakeGitHub.On("CreateRelease", mock.Anything, mock.Anything).Return(nil)
err := r.CreateReleases()
tt.Releaser.github = fakeGitHub
fakeGit := new(FakeGit)
fakeGit.On("AddWorktree", mock.Anything, mock.Anything).Return("/tmp/chart-releaser-012345678", nil)
fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil)
fakeGit.On("Add", mock.Anything, mock.Anything).Return(nil)
fakeGit.On("Commit", mock.Anything, mock.Anything).Return(nil)
fakeGit.On("Push", mock.Anything, mock.Anything).Return(nil)
pushURL := fmt.Sprintf("https://x-access-token:%s@github.com/owner/repo", tt.Releaser.config.Token)
fakeGit.On("GetPushURL", mock.Anything, mock.Anything).Return(pushURL, nil)
tt.Releaser.git = fakeGit
tt.Releaser.config.ReleaseNameTemplate = "{{ .Name }}-{{ .Version }}"
err := tt.Releaser.CreateReleases()
if tt.error {
assert.Error(t, err)
assert.Nil(t, fakeGitHub.release)
@@ -353,7 +458,7 @@ func TestReleaser_CreateReleases(t *testing.T) {
} else {
assert.NoError(t, err)
releaseName := fmt.Sprintf("%s-%s", tt.chart, tt.version)
assetPath := fmt.Sprintf("%s/%s-%s.tgz", r.config.PackagePath, tt.chart, tt.version)
assetPath := fmt.Sprintf("%s/%s-%s.tgz", tt.Releaser.config.PackagePath, tt.chart, tt.version)
releaseDescription := "A Helm chart for Kubernetes"
assert.Equal(t, releaseName, fakeGitHub.release.Name)
assert.Equal(t, releaseDescription, fakeGitHub.release.Description)
@@ -361,6 +466,7 @@ func TestReleaser_CreateReleases(t *testing.T) {
assert.Equal(t, assetPath, fakeGitHub.release.Assets[0].Path)
assert.Equal(t, tt.commit, fakeGitHub.release.Commit)
assert.Equal(t, tt.latest, fakeGitHub.release.MakeLatest)
assert.Equal(t, tt.Releaser.config.Commit, fakeGitHub.release.Commit)
fakeGitHub.AssertNumberOfCalls(t, "CreateRelease", 1)
}
})