diff --git a/README.md b/README.md index 1d9698b..a3bacff 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ # Gitea action to Create a Pull Request + +Gitea action to create a pull request. + +Inspired by https://github.com/peter-evans/create-pull-request, but without the ability to commit the changes. diff --git a/action.yml b/action.yml index 1f063be..d5d383c 100644 --- a/action.yml +++ b/action.yml @@ -3,3 +3,28 @@ description: 'Create pull requests in gitea' runs: using: 'go' main: 'main.go' +inputs: + title: + required: false + description: The title of the pull request + default: Changes by create-pull-request action + body: + required: false + description: The body of the pull request + default: Automated changes by create-pull-request Gitea action + labels: + description: 'A comma or newline separated list of labels.' + assignees: + description: 'A comma or newline separated list of assignees (GitHub usernames).' + base: + description: > + The pull request base branch (the one into which the new code will be pushed). + required: true + head: + description: > + The pull request head branch (the one within the new code is developed). + outputs: + pull-request-number: + description: 'The pull request number' + pull-request-url: + description: 'The URL of the pull request.' diff --git a/go.mod b/go.mod index 1aaf635..2009e8f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/davidmz/go-pageant v1.0.2 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect + github.com/sethvargo/go-githubactions v1.2.0 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 8f62e9a..d18d077 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7 github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-githubactions v1.2.0 h1:Gbr36trCAj6uq7Rx1DolY1NTIg0wnzw3/N5WHdKIjME= +github.com/sethvargo/go-githubactions v1.2.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/main.go b/main.go index 40bfc6d..5685df3 100644 --- a/main.go +++ b/main.go @@ -2,32 +2,238 @@ package main import ( "code.gitea.io/sdk/gitea" + "errors" "fmt" + "github.com/sethvargo/go-githubactions" "os" + "slices" + "strconv" "strings" ) -func main() { - fmt.Println("hello") - - client, err := gitea.NewClient("https://gitea.champs-libres.be") - - if nil != err { - fmt.Println("could not create a client, {}", err) +func ParseActionConfig(ctx githubactions.GitHubContext) (*CreatePrConfig, error) { + base := githubactions.GetInput("base") + if base == "" { + return nil, errors.New("base branch name cannot be empty") } - repos, _, err := client.ListOrgRepos("Chill-project", gitea.ListOrgReposOptions{}) + repo := strings.Split(ctx.Repository, "/")[1] + fmt.Printf("The repository is %v\n", repo) - if nil != err { - fmt.Printf("could not list repos, %#v\n", err.Error()) + title := githubactions.GetInput("title") + if title == "" { + return nil, errors.New("title cannot be empty") } - for _, repo := range repos { - fmt.Printf("repository: %v\n", repo.FullName) + body := githubactions.GetInput("body") + + assigneesRaw := githubactions.GetInput("assignees") + assignees := []string{} + for _, a := range strings.Split(assigneesRaw, ",") { + assignees = append(assignees, strings.TrimSpace(a)) } - for _, e := range os.Environ() { - pair := strings.SplitN(e, "=", 2) - fmt.Println(pair[0]) + labelsRaw := githubactions.GetInput("labels") + labels := []string{} + for _, l := range strings.Split(labelsRaw, ",") { + labels = append(labels, strings.TrimSpace(l)) } + + var head string + headRaw := githubactions.GetInput("head") + if headRaw == "" { + if os.Getenv("GITHUB_REF_TYPE") != "branch" { + return nil, fmt.Errorf("set the \"head\" parameter or work from a branch: only branch can create a pull request: %v given as parameter GITHUB_REF_TYPE", githubactions.GetInput("GITHUB_REF_TYPE")) + } + head = os.Getenv("GITHUB_REF_NAME") + } else { + head = headRaw + } + + return &CreatePrConfig{ + Org: ctx.RepositoryOwner, + Repo: repo, + HeadBranch: head, + BaseBranch: base, + Title: title, + Body: body, + Assignees: assignees, + Labels: labels, + }, nil +} + +func main() { + fmt.Println("Starting result CreatePullRequest, main") + ctx, err := githubactions.Context() + if err != nil { + githubactions.Fatalf("could not get context: %v", err.Error()) + } + + token := os.Getenv("GITHUB_TOKEN") + fmt.Printf("Api url is %v\n", ctx.ServerURL) + + config, err := ParseActionConfig(*ctx) + if err != nil { + githubactions.Fatalf("%v", err.Error()) + } + + pr, result, err := createPullRequest(ctx.ServerURL, token, *config) + if err != nil { + githubactions.Fatalf("Error while creating pr: %v", err.Error()) + } + + fmt.Printf("Created PR with id %d\n", pr.ID) + + githubactions.SetOutput("pull-request-number", strconv.FormatInt(pr.Index, 10)) + githubactions.SetOutput("pull-request-result", result) + githubactions.SetOutput("pull-request-url", pr.URL) + + fmt.Println("Ending result CreatePullRequest, main") +} + +// Agent which will perform changes on gitea +type Agent struct { + client *gitea.Client +} + +// CreatePrConfig represents the configuration for creating a pull request +type CreatePrConfig struct { + // the organization where the PR is created + Org string + // the repository where the PR is created + Repo string + // the branch where the changes are made + HeadBranch string + // the branch where the changes will be added + BaseBranch string + // the title of the pull requests + Title string + // the body of the pull requests + Body string + // the list of assignees + Assignees []string + // the list of requested labels + Labels []string +} + +// ExistingOpenPullRequest checks if there is an existing pull request for the same head branch and return it +func (a *Agent) ExistingOpenPullRequest(config CreatePrConfig) (bool, *gitea.PullRequest, error) { + currentPage := 1 + + for currentPage != 0 { + pulls, response, err := a.client.ListRepoPullRequests(config.Org, config.Repo, + gitea.ListPullRequestsOptions{State: gitea.StateOpen, ListOptions: gitea.ListOptions{Page: currentPage}}) + + if err != nil { + return false, nil, err + } + + for _, pull := range pulls { + if pull.Head.Name == config.HeadBranch { + return true, pull, nil + } + } + + currentPage = response.NextPage + } + + return false, nil, nil +} + +// labelsFromString retrieves a list of labels from a repository based on the provided configuration. +// It uses the Gitea client to list all labels in the repository and filters them based on the provided label names. +// The method takes a CreatePrConfig struct as a parameter which contains the organization, repository, and label details. +// It returns a slice of gitea.Label that matches the provided label names and an error if any occurred. +func (a *Agent) labelsFromString(config CreatePrConfig) ([]gitea.Label, error) { + foundLabels := []gitea.Label{} + + currentPage := 1 + for currentPage != 0 { + labels, response, err := a.client.ListRepoLabels(config.Org, config.Repo, gitea.ListLabelsOptions{ListOptions: gitea.ListOptions{Page: currentPage}}) + + if err != nil { + return nil, err + } + + for _, label := range labels { + if slices.Contains(config.Labels, label.Name) { + foundLabels = append(foundLabels, *label) + } + } + + currentPage = response.NextPage + } + + return foundLabels, nil +} + +// createPullRequestGitea creates a pull request in a Gitea repository based on the provided configuration. +// It retrieves the label IDs for the given labels and uses them when creating the pull request. +// The method takes a CreatePrConfig struct as a parameter which contains the organization, repository, and pull request details. +// It returns the created pull request and any error encountered during the process. +func (a *Agent) createPullRequestGitea(config CreatePrConfig) (*gitea.PullRequest, error) { + labelIds := []int64{} + labels, err := a.labelsFromString(config) + if err != nil { + return nil, err + } + + for _, label := range labels { + labelIds = append(labelIds, label.ID) + } + + if err != nil { + return nil, err + } + + pr, _, err := a.client.CreatePullRequest(config.Org, config.Repo, gitea.CreatePullRequestOption{ + Head: config.HeadBranch, + Base: config.BaseBranch, + Title: config.Title, + Body: config.Body, + Assignees: config.Assignees, + Labels: labelIds, + Deadline: nil, + }) + if err != nil { + return nil, err + } + + return pr, nil +} + +// createPullRequest takes in the API URL, access token, and configuration for creating a pull request. +// It initializes a Gitea client using the API URL and access token. +// It creates an instance of the Agent struct using the Gitea client. +// It checks if there is an open pull request with the given branch name in the repository using the ExistingOpenPullRequest method of Agent. +// If an open pull request already exists, it returns nil as the pull request and nil error. +// If no open pull request exists, it creates a new pull request using the createPullRequestGitea method of Agent. +// It returns the created pull request and nil error if successful. +// If there is an error while checking for existing pull requests or creating a new pull request, it returns nil as the pull request and the error. +// The function takes in the following parameters: +// - apiUrl: The URL of the Gitea API. +// - token: The access token for authenticating with the Gitea API. +// - config: The configuration for creating the pull request, including the organization, repository, branch details, title, body, assignees, and labels. +// It returns a pointer to the created pull request and an error. +func createPullRequest(apiUrl string, token string, config CreatePrConfig) (*gitea.PullRequest, string, error) { + + client, _ := gitea.NewClient(apiUrl, gitea.SetToken(token)) + agent := &Agent{client: client} + + has, pull, err := agent.ExistingOpenPullRequest(config) + if err != nil { + return nil, "error", err + } + + if has { + return pull, "existing", nil + } + + pr, err := agent.createPullRequestGitea(config) + + if err != nil { + return nil, "error", err + } + + return pr, "created", nil }