From f6db7a5f2e1505565293a9e85c2670ae1f1279e4 Mon Sep 17 00:00:00 2001 From: Jordan McClintock Date: Wed, 16 Oct 2024 23:50:36 +0000 Subject: [PATCH 1/5] feat: add github release command --- README.md | 4 ++ go.mod | 1 + go.sum | 2 + src/cmd/release.go | 25 ++++++++ src/github/release.go | 120 +++++++++++++++++++++++++++++++++++++ src/github/release_test.go | 42 +++++++++++++ src/gitlab/release.go | 12 +--- 7 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 src/github/release.go create mode 100644 src/github/release_test.go diff --git a/README.md b/README.md index 0810206..ce10128 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ uds-releaser release When running `uds-releaser release gitlab ` you are expected to have an environment variable set to a GitLab token that has write permissions for your current project. This defaults to `GITLAB_RELEASE_TOKEN` but can be changed with the `--token-var-name` flag. +### GitHub + +When running `uds-releaser release github ` you are expected to have an environment variable set to a GitHub token that has write permissions for your current project. This defaults to `GITHUB_TOKEN` but can be changed with the `--token-var-name` flag. + ## Configuration UDS-Releaser can be configured using a YAML file named uds-releaser.yaml in your project's root directory. diff --git a/go.mod b/go.mod index 1ae115e..dd40927 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/defenseunicorns/uds-cli v0.16.0 github.com/go-git/go-git/v5 v5.12.0 github.com/goccy/go-yaml v1.12.0 + github.com/google/go-github/v66 v66.0.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/xanzy/go-gitlab v0.109.0 diff --git a/go.sum b/go.sum index 6b64910..a751ca6 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= diff --git a/src/cmd/release.go b/src/cmd/release.go index ecc1509..a732c75 100644 --- a/src/cmd/release.go +++ b/src/cmd/release.go @@ -16,6 +16,7 @@ limitations under the License. package cmd import ( + "github.com/defenseunicorns/uds-releaser/src/github" "github.com/defenseunicorns/uds-releaser/src/gitlab" "github.com/defenseunicorns/uds-releaser/src/utils" "github.com/spf13/cobra" @@ -45,6 +46,28 @@ var gitlabCmd = &cobra.Command{ }, } +// githubCmd represents the github command +var githubCmd = &cobra.Command{ + Use: "github flavor", + Short: "Create a tag and release on GitHub based on flavor", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + releaserConfig, err := utils.LoadReleaserConfig(releaserDir) + if err != nil { + return err + } + + currentFlavor, err := utils.GetFlavorConfig(args[0], releaserConfig) + if err != nil { + return err + } + + rootCmd.SilenceUsage = true + + return github.TagAndRelease(currentFlavor, tokenVarName) + }, +} + // releaseCmd represents the release command var releaseCmd = &cobra.Command{ Use: "release platform", @@ -54,5 +77,7 @@ var releaseCmd = &cobra.Command{ func init() { rootCmd.AddCommand(releaseCmd) releaseCmd.AddCommand(gitlabCmd) + releaseCmd.AddCommand(githubCmd) gitlabCmd.Flags().StringVarP(&tokenVarName, "token-var-name", "t", "GITLAB_RELEASE_TOKEN", "Environment variable name for GitLab token") + githubCmd.Flags().StringVarP(&tokenVarName, "token-var-name", "t", "GITHUB_TOKEN", "Environment variable name for GitHub token") } diff --git a/src/github/release.go b/src/github/release.go new file mode 100644 index 0000000..3a28956 --- /dev/null +++ b/src/github/release.go @@ -0,0 +1,120 @@ +package github + +import ( + "context" + "fmt" + "os" + "regexp" + "time" + + "github.com/defenseunicorns/uds-releaser/src/types" + "github.com/defenseunicorns/uds-releaser/src/utils" + github "github.com/google/go-github/v66/github" +) + +func TagAndRelease(flavor types.Flavor, tokenVarName string) error { + repo, err := utils.OpenRepo() + if err != nil { + return err + } + + remote, err := repo.Remote("origin") + if err != nil { + return err + } + + // Get the default branch of the current repository + ref, err := repo.Head() + if err != nil { + return err + } + + // Create a new GitHub client + githubClient := github.NewClient(nil) + + // Set the authentication token + githubClient = githubClient.WithAuthToken(os.Getenv(tokenVarName)) + + // Get the repository owner and name + + remoteURL := remote.Config().URLs[0] + + owner, repoName, err := getGithubOwnerAndRepo(remoteURL) + if err != nil { + return err + } + + // Create the tag + zarfPackageName, err := utils.GetPackageName() + if err != nil { + return err + } + + tagName := fmt.Sprintf("%s-%s", flavor.Version, flavor.Name) + releaseName := fmt.Sprintf("%s %s", zarfPackageName, tagName) + + tag := createGitHubTag(tagName, releaseName, ref.Hash().String()) + + createdTag, _, err := githubClient.Git.CreateTag(context.Background(), owner, repoName, tag) + if err != nil { + return err + } + + // Create a reference for the tag + tagRef := &github.Reference{ + Ref: github.String("refs/tags/" + tagName), + Object: &github.GitObject{ + SHA: createdTag.SHA, + }, + } + + _, _, err = githubClient.Git.CreateRef(context.Background(), owner, repoName, tagRef) + if err != nil { + return err + } + + // Create the release + release := &github.RepositoryRelease{ + TagName: github.String(tagName), + Name: github.String(releaseName), + Body: github.String(releaseName), //TODO @corang release notes + GenerateReleaseNotes: github.Bool(true), + } + + _, _, err = githubClient.Repositories.CreateRelease(context.Background(), owner, repoName, release) + if err != nil { + return err + } + return nil +} + +func createGitHubTag(tagName string, releaseName string, hash string) *github.Tag { + tag := &github.Tag{ + Tag: github.String(tagName), + Message: github.String(releaseName), + Object: &github.GitObject{ + SHA: github.String(hash), + Type: github.String("commit"), + }, + Tagger: &github.CommitAuthor{ + Name: github.String(os.Getenv("GITHUB_ACTOR")), + Email: github.String(os.Getenv("GITHUB_ACTOR") + "@users.noreply.github.com"), + Date: &github.Timestamp{Time: time.Now()}, + }, + } + return tag +} + +func getGithubOwnerAndRepo(remoteURL string) (string, string, error) { + // Parse the GitHub owner and repository name from the remote URL + ownerRepoRegex := regexp.MustCompile(`github.com[:/](.*)/(.*).git`) + matches := ownerRepoRegex.FindStringSubmatch(remoteURL) + if len(matches) != 3 { + return "", "", fmt.Errorf("could not parse GitHub owner and repository name from remote URL: %s", remoteURL) + } + + owner := matches[1] + repo := matches[2] + + return owner, repo, nil +} diff --git a/src/github/release_test.go b/src/github/release_test.go new file mode 100644 index 0000000..f71d49c --- /dev/null +++ b/src/github/release_test.go @@ -0,0 +1,42 @@ +package github + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func testCreateGitHubTag(t *testing.T) { + // Create a new tag + tagName := "v1.0.0-uds.0-unicorn" + releaseName := "testing-package v1.0.0-uds.0-unicorn" + hash := "1234567890" + + tag := createGitHubTag(tagName, releaseName, hash) + + assert.Equal(t, tagName, *tag.Tag) + assert.Equal(t, releaseName, *tag.Message) + assert.Equal(t, hash, *tag.Object.SHA) +} + +func testgetGithubOwnerAndRepo(t *testing.T) { + // Get the owner and repo from a remote URL + httpsRemoteURL := "https://github.com/defenseunicorns/uds-releaser.git" + sshRemoteURL := "git@github.com:defenseunicorns/uds-releaser.git" + + owner, repo, err := getGithubOwnerAndRepo(httpsRemoteURL) + assert.NoError(t, err) + assert.Equal(t, "defenseunicorns", owner) + assert.Equal(t, "uds-releaser", repo) + + owner, repo, err = getGithubOwnerAndRepo(sshRemoteURL) + assert.NoError(t, err) + assert.Equal(t, "defenseunicorns", owner) + assert.Equal(t, "uds-releaser", repo) + + gitlabRemoteURL := "https://gitlab.com/defenseunicorns/uds-releaser.git" + owner, repo, err = getGithubOwnerAndRepo(gitlabRemoteURL) + assert.Error(t, err) + assert.Empty(t, owner) + assert.Empty(t, repo) +} diff --git a/src/gitlab/release.go b/src/gitlab/release.go index 4dd474f..3fd2ebb 100644 --- a/src/gitlab/release.go +++ b/src/gitlab/release.go @@ -11,14 +11,8 @@ import ( gitlab "github.com/xanzy/go-gitlab" ) -var newGitlabClient = gitlab.NewClient - -var openRepo = utils.OpenRepo - -var getPackageName = utils.GetPackageName - func TagAndRelease(flavor types.Flavor, tokenVarName string) error { - repo, err := openRepo() + repo, err := utils.OpenRepo() if err != nil { return err } @@ -47,12 +41,12 @@ func TagAndRelease(flavor types.Flavor, tokenVarName string) error { fmt.Printf("Default branch: %s\n", defaultBranch) // Create a new GitLab client - gitlabClient, err := newGitlabClient(os.Getenv(tokenVarName), gitlab.WithBaseURL(gitlabBaseURL)) + gitlabClient, err := gitlab.NewClient(os.Getenv(tokenVarName), gitlab.WithBaseURL(gitlabBaseURL)) if err != nil { return err } - zarfPackageName, err := getPackageName() + zarfPackageName, err := utils.GetPackageName() if err != nil { return err } From 008ed3be60a58a111845b9003d603fea13c1fd43 Mon Sep 17 00:00:00 2001 From: Jordan McClintock Date: Wed, 16 Oct 2024 19:53:48 -0400 Subject: [PATCH 2/5] Fix code scanning alert no. 1: Incomplete regular expression for hostnames Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/github/release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github/release.go b/src/github/release.go index 3a28956..805d1bb 100644 --- a/src/github/release.go +++ b/src/github/release.go @@ -107,7 +107,7 @@ func createGitHubTag(tagName string, releaseName string, hash string) *github.Ta func getGithubOwnerAndRepo(remoteURL string) (string, string, error) { // Parse the GitHub owner and repository name from the remote URL - ownerRepoRegex := regexp.MustCompile(`github.com[:/](.*)/(.*).git`) + ownerRepoRegex := regexp.MustCompile(`github\.com[:/](.*)/(.*)\.git`) matches := ownerRepoRegex.FindStringSubmatch(remoteURL) if len(matches) != 3 { return "", "", fmt.Errorf("could not parse GitHub owner and repository name from remote URL: %s", remoteURL) From 31da5e04268ab3f094172232c381a727e357874b Mon Sep 17 00:00:00 2001 From: Jordan McClintock Date: Thu, 17 Oct 2024 17:26:17 +0000 Subject: [PATCH 3/5] dedupe git code in releases --- src/github/release.go | 17 +---------------- src/gitlab/release.go | 17 +---------------- src/utils/git.go | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/github/release.go b/src/github/release.go index 805d1bb..cd12b68 100644 --- a/src/github/release.go +++ b/src/github/release.go @@ -13,18 +13,7 @@ import ( ) func TagAndRelease(flavor types.Flavor, tokenVarName string) error { - repo, err := utils.OpenRepo() - if err != nil { - return err - } - - remote, err := repo.Remote("origin") - if err != nil { - return err - } - - // Get the default branch of the current repository - ref, err := repo.Head() + remoteURL, _, ref, err := utils.GetRepoInfo() if err != nil { return err } @@ -35,10 +24,6 @@ func TagAndRelease(flavor types.Flavor, tokenVarName string) error { // Set the authentication token githubClient = githubClient.WithAuthToken(os.Getenv(tokenVarName)) - // Get the repository owner and name - - remoteURL := remote.Config().URLs[0] - owner, repoName, err := getGithubOwnerAndRepo(remoteURL) if err != nil { return err diff --git a/src/gitlab/release.go b/src/gitlab/release.go index 3fd2ebb..24497a2 100644 --- a/src/gitlab/release.go +++ b/src/gitlab/release.go @@ -12,32 +12,17 @@ import ( ) func TagAndRelease(flavor types.Flavor, tokenVarName string) error { - repo, err := utils.OpenRepo() + remoteURL, defaultBranch, _, err := utils.GetRepoInfo() if err != nil { return err } - remote, err := repo.Remote("origin") - if err != nil { - return err - } - - remoteURL := remote.Config().URLs[0] - // Parse the GitLab base URL from the remote URL gitlabBaseURL, err := getGitlabBaseUrl(remoteURL) if err != nil { return err } - // Get the default branch of the current repository - ref, err := repo.Head() - if err != nil { - return err - } - - defaultBranch := ref.Name().Short() - fmt.Printf("Default branch: %s\n", defaultBranch) // Create a new GitLab client diff --git a/src/utils/git.go b/src/utils/git.go index d26041f..634008a 100644 --- a/src/utils/git.go +++ b/src/utils/git.go @@ -29,3 +29,25 @@ func DoesTagExist(tag string) (bool, error) { func OpenRepo() (*git.Repository, error) { return git.PlainOpen(".") } + +func GetRepoInfo() (remoteURL string, defaultBranch string, ref *plumbing.Reference, err error) { + repo, err := OpenRepo() + if err != nil { + return "", "", ref, err + } + + remote, err := repo.Remote("origin") + if err != nil { + return "", "", ref, err + } + + remoteURL = remote.Config().URLs[0] + + ref, err = repo.Head() + if err != nil { + return "", "", ref, err + } + + defaultBranch = ref.Name().Short() + return remoteURL, defaultBranch, ref, nil +} \ No newline at end of file From 3b24bd32475dd80a20d2e799f5758edc97184268 Mon Sep 17 00:00:00 2001 From: Jordan McClintock Date: Thu, 17 Oct 2024 19:23:05 +0000 Subject: [PATCH 4/5] move releases under platform interface and simplify calling --- src/cmd/release.go | 34 ++++------------------ src/{ => platforms}/github/release.go | 4 ++- src/{ => platforms}/github/release_test.go | 0 src/{ => platforms}/gitlab/release.go | 4 ++- src/{ => platforms}/gitlab/release_test.go | 0 src/platforms/platform.go | 24 +++++++++++++++ 6 files changed, 35 insertions(+), 31 deletions(-) rename src/{ => platforms}/github/release.go (96%) rename src/{ => platforms}/github/release_test.go (100%) rename src/{ => platforms}/gitlab/release.go (95%) rename src/{ => platforms}/gitlab/release_test.go (100%) create mode 100644 src/platforms/platform.go diff --git a/src/cmd/release.go b/src/cmd/release.go index a732c75..6c1ec60 100644 --- a/src/cmd/release.go +++ b/src/cmd/release.go @@ -16,9 +16,9 @@ limitations under the License. package cmd import ( - "github.com/defenseunicorns/uds-releaser/src/github" - "github.com/defenseunicorns/uds-releaser/src/gitlab" - "github.com/defenseunicorns/uds-releaser/src/utils" + "github.com/defenseunicorns/uds-releaser/src/platforms" + "github.com/defenseunicorns/uds-releaser/src/platforms/github" + "github.com/defenseunicorns/uds-releaser/src/platforms/gitlab" "github.com/spf13/cobra" ) @@ -30,19 +30,7 @@ var gitlabCmd = &cobra.Command{ Short: "Create a tag and release on GitLab based on flavor", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - releaserConfig, err := utils.LoadReleaserConfig(releaserDir) - if err != nil { - return err - } - - currentFlavor, err := utils.GetFlavorConfig(args[0], releaserConfig) - if err != nil { - return err - } - - rootCmd.SilenceUsage = true - - return gitlab.TagAndRelease(currentFlavor, tokenVarName) + return platforms.LoadAndTag(releaserDir, args[0], tokenVarName, gitlab.Platform{}) }, } @@ -52,19 +40,7 @@ var githubCmd = &cobra.Command{ Short: "Create a tag and release on GitHub based on flavor", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - releaserConfig, err := utils.LoadReleaserConfig(releaserDir) - if err != nil { - return err - } - - currentFlavor, err := utils.GetFlavorConfig(args[0], releaserConfig) - if err != nil { - return err - } - - rootCmd.SilenceUsage = true - - return github.TagAndRelease(currentFlavor, tokenVarName) + return platforms.LoadAndTag(releaserDir, args[0], tokenVarName, github.Platform{}) }, } diff --git a/src/github/release.go b/src/platforms/github/release.go similarity index 96% rename from src/github/release.go rename to src/platforms/github/release.go index cd12b68..9dc9254 100644 --- a/src/github/release.go +++ b/src/platforms/github/release.go @@ -12,7 +12,9 @@ import ( github "github.com/google/go-github/v66/github" ) -func TagAndRelease(flavor types.Flavor, tokenVarName string) error { +type Platform struct{} + +func (Platform) TagAndRelease(flavor types.Flavor, tokenVarName string) error { remoteURL, _, ref, err := utils.GetRepoInfo() if err != nil { return err diff --git a/src/github/release_test.go b/src/platforms/github/release_test.go similarity index 100% rename from src/github/release_test.go rename to src/platforms/github/release_test.go diff --git a/src/gitlab/release.go b/src/platforms/gitlab/release.go similarity index 95% rename from src/gitlab/release.go rename to src/platforms/gitlab/release.go index 24497a2..599c821 100644 --- a/src/gitlab/release.go +++ b/src/platforms/gitlab/release.go @@ -11,7 +11,9 @@ import ( gitlab "github.com/xanzy/go-gitlab" ) -func TagAndRelease(flavor types.Flavor, tokenVarName string) error { +type Platform struct{} + +func (Platform) TagAndRelease(flavor types.Flavor, tokenVarName string) error { remoteURL, defaultBranch, _, err := utils.GetRepoInfo() if err != nil { return err diff --git a/src/gitlab/release_test.go b/src/platforms/gitlab/release_test.go similarity index 100% rename from src/gitlab/release_test.go rename to src/platforms/gitlab/release_test.go diff --git a/src/platforms/platform.go b/src/platforms/platform.go new file mode 100644 index 0000000..19ab7a7 --- /dev/null +++ b/src/platforms/platform.go @@ -0,0 +1,24 @@ +package platforms + +import ( + "github.com/defenseunicorns/uds-releaser/src/types" + "github.com/defenseunicorns/uds-releaser/src/utils" +) + +type Platform interface { + TagAndRelease(flavor types.Flavor, tokenVarName string) error +} + +func LoadAndTag(releaserDir, flavor, tokenVarName string, platform Platform) error { + releaserConfig, err := utils.LoadReleaserConfig(releaserDir) + if err != nil { + return err + } + + currentFlavor, err := utils.GetFlavorConfig(flavor, releaserConfig) + if err != nil { + return err + } + + return platform.TagAndRelease(currentFlavor, tokenVarName) +} \ No newline at end of file From 33375e5c86703eed42ad6c2ec8ddbc08aeab29e1 Mon Sep 17 00:00:00 2001 From: Jordan McClintock Date: Thu, 17 Oct 2024 19:25:04 +0000 Subject: [PATCH 5/5] newlines lint --- src/platforms/platform.go | 2 +- src/utils/git.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platforms/platform.go b/src/platforms/platform.go index 19ab7a7..27b1ffb 100644 --- a/src/platforms/platform.go +++ b/src/platforms/platform.go @@ -21,4 +21,4 @@ func LoadAndTag(releaserDir, flavor, tokenVarName string, platform Platform) err } return platform.TagAndRelease(currentFlavor, tokenVarName) -} \ No newline at end of file +} diff --git a/src/utils/git.go b/src/utils/git.go index 634008a..5b06997 100644 --- a/src/utils/git.go +++ b/src/utils/git.go @@ -50,4 +50,4 @@ func GetRepoInfo() (remoteURL string, defaultBranch string, ref *plumbing.Refere defaultBranch = ref.Name().Short() return remoteURL, defaultBranch, ref, nil -} \ No newline at end of file +}