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 }