From b93f04ea2efef365aeb02479d56e8f948f30dfdb Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Fri, 12 Jun 2026 07:13:10 +0530 Subject: [PATCH] Normalize forge statuses --- bitbucket/helpers_test.go | 4 +- bitbucket/issues.go | 8 +- bitbucket/prs.go | 9 +- gitea/ci.go | 8 +- gitea/collaborators.go | 17 +++- gitea/commit_statuses.go | 6 +- gitea/helpers_test.go | 4 +- gitea/issues.go | 9 +- gitea/prs.go | 6 +- github/ci.go | 8 +- github/collaborators.go | 17 +++- github/commit_statuses.go | 7 +- github/helpers_test.go | 4 +- github/issues.go | 2 +- github/prs.go | 4 +- gitlab/ci.go | 6 +- gitlab/collaborators.go | 23 +++-- gitlab/commit_statuses.go | 6 +- gitlab/helpers_test.go | 4 +- gitlab/issues.go | 7 +- gitlab/prs.go | 13 +-- internal/cli/ci.go | 12 +-- internal/cli/collaborator.go | 4 +- internal/cli/status.go | 4 +- statuses.go | 192 +++++++++++++++++++++++++++++++++++ statuses_test.go | 82 +++++++++++++++ types.go | 64 ++++++------ 27 files changed, 403 insertions(+), 127 deletions(-) create mode 100644 statuses.go create mode 100644 statuses_test.go diff --git a/bitbucket/helpers_test.go b/bitbucket/helpers_test.go index 4398f8c..743e891 100644 --- a/bitbucket/helpers_test.go +++ b/bitbucket/helpers_test.go @@ -2,9 +2,9 @@ package bitbucket import "testing" -func assertEqual(t *testing.T, field, want, got string) { +func assertEqual[T ~string](t *testing.T, field, want string, got T) { t.Helper() - if want != got { + if want != string(got) { t.Errorf("%s: want %q, got %q", field, want, got) } } diff --git a/bitbucket/issues.go b/bitbucket/issues.go index ad1a8b5..f5798f9 100644 --- a/bitbucket/issues.go +++ b/bitbucket/issues.go @@ -107,13 +107,7 @@ func convertBitbucketIssue(bb bbIssue) forge.Issue { HTMLURL: bb.Links.HTML.Href, } - // Normalize Bitbucket states to open/closed - switch bb.State { - case "new", stateOpen: - result.State = stateOpen - default: - result.State = stateClosed - } + result.State = forge.NormalizeIssueState(bb.State) if bb.Reporter != nil { result.Author = forge.User{ diff --git a/bitbucket/prs.go b/bitbucket/prs.go index e32d289..008407f 100644 --- a/bitbucket/prs.go +++ b/bitbucket/prs.go @@ -123,14 +123,9 @@ func convertBitbucketPR(bb bbPullRequest) forge.PullRequest { } } - switch bb.State { - case "OPEN": - result.State = "open" - case "MERGED": - result.State = "merged" + result.State = forge.NormalizePRStatus(bb.State) + if result.State == forge.PRStatusMerged { result.Merged = true - default: - result.State = "closed" } if bb.Author != nil { diff --git a/gitea/ci.go b/gitea/ci.go index 2d2d13d..c014f55 100644 --- a/gitea/ci.go +++ b/gitea/ci.go @@ -23,7 +23,7 @@ func convertGiteaWorkflowRun(r *gitea.ActionWorkflowRun) forge.CIRun { result := forge.CIRun{ ID: r.ID, Title: r.DisplayTitle, - Status: r.Status, + Status: forge.NormalizeCIStatus(r.Status), Branch: r.HeadBranch, SHA: r.HeadSha, Event: r.Event, @@ -32,7 +32,7 @@ func convertGiteaWorkflowRun(r *gitea.ActionWorkflowRun) forge.CIRun { } if r.Conclusion != "" { - result.Conclusion = r.Conclusion + result.Conclusion = forge.NormalizeCIConclusion(r.Conclusion) } if r.Actor != nil { @@ -54,8 +54,8 @@ func convertGiteaWorkflowJob(j *gitea.ActionWorkflowJob) forge.CIJob { job := forge.CIJob{ ID: j.ID, Name: j.Name, - Status: j.Status, - Conclusion: j.Conclusion, + Status: forge.NormalizeCIStatus(j.Status), + Conclusion: forge.NormalizeCIConclusion(j.Conclusion), HTMLURL: j.HTMLURL, } if !j.StartedAt.IsZero() { diff --git a/gitea/collaborators.go b/gitea/collaborators.go index b902f92..5d3b397 100644 --- a/gitea/collaborators.go +++ b/gitea/collaborators.go @@ -41,7 +41,7 @@ func (s *giteaCollaboratorService) List(ctx context.Context, owner, repo string, } all = append(all, forge.Collaborator{ Login: u.UserName, - Permission: perm, + Permission: forge.NormalizeAccessLevel(perm), }) } if lastPage(resp, len(users), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { @@ -78,7 +78,7 @@ func (s *giteaCollaboratorService) getPermission(owner, repo, username string) ( func (s *giteaCollaboratorService) Add(ctx context.Context, owner, repo, username string, opts forge.AddCollaboratorOpts) error { var perm *gitea.AccessMode if opts.Permission != "" { - mode := gitea.AccessMode(opts.Permission) + mode := gitea.AccessMode(giteaPermission(opts.Permission)) perm = &mode } @@ -94,6 +94,19 @@ func (s *giteaCollaboratorService) Add(ctx context.Context, owner, repo, usernam return nil } +func giteaPermission(permission forge.AccessLevel) string { + switch forge.NormalizeAccessLevel(string(permission)) { + case forge.AccessLevelRead: + return "read" + case forge.AccessLevelWrite: + return "write" + case forge.AccessLevelAdmin: + return "admin" + default: + return string(permission) + } +} + func (s *giteaCollaboratorService) Remove(ctx context.Context, owner, repo, username string) error { resp, err := s.client.DeleteCollaborator(owner, repo, username) if err != nil { diff --git a/gitea/commit_statuses.go b/gitea/commit_statuses.go index 2dcf6ee..bd26fcb 100644 --- a/gitea/commit_statuses.go +++ b/gitea/commit_statuses.go @@ -57,7 +57,7 @@ func (s *giteaCommitStatusService) List(ctx context.Context, owner, repo, sha st } for _, st := range statuses { cs := forge.CommitStatus{ - State: string(st.State), + State: forge.NormalizeCommitStatusState(string(st.State)), Context: st.Context, Description: st.Description, TargetURL: st.TargetURL, @@ -78,7 +78,7 @@ func (s *giteaCommitStatusService) List(ctx context.Context, owner, repo, sha st func (s *giteaCommitStatusService) Set(ctx context.Context, owner, repo, sha string, opts forge.SetCommitStatusOpts) (*forge.CommitStatus, error) { result, resp, err := s.client.CreateStatus(owner, repo, sha, gitea.CreateStatusOption{ - State: gitea.StatusState(opts.State), + State: gitea.StatusState(string(opts.State)), TargetURL: opts.TargetURL, Description: opts.Description, Context: opts.Context, @@ -91,7 +91,7 @@ func (s *giteaCommitStatusService) Set(ctx context.Context, owner, repo, sha str } cs := &forge.CommitStatus{ - State: string(result.State), + State: forge.NormalizeCommitStatusState(string(result.State)), Context: result.Context, Description: result.Description, TargetURL: result.TargetURL, diff --git a/gitea/helpers_test.go b/gitea/helpers_test.go index c22a1b9..205aa0f 100644 --- a/gitea/helpers_test.go +++ b/gitea/helpers_test.go @@ -2,9 +2,9 @@ package gitea import "testing" -func assertEqual(t *testing.T, field, want, got string) { +func assertEqual[T ~string](t *testing.T, field, want string, got T) { t.Helper() - if want != got { + if want != string(got) { t.Errorf("%s: want %q, got %q", field, want, got) } } diff --git a/gitea/issues.go b/gitea/issues.go index 1df6aa2..32944bc 100644 --- a/gitea/issues.go +++ b/gitea/issues.go @@ -26,14 +26,7 @@ func convertGiteaIssue(i *gitea.Issue) forge.Issue { HTMLURL: i.HTMLURL, } - switch i.State { - case gitea.StateOpen: - result.State = stateOpen - case gitea.StateClosed: - result.State = stateClosed - default: - result.State = string(i.State) - } + result.State = forge.NormalizeIssueState(string(i.State)) if i.Poster != nil { result.Author = forge.User{ diff --git a/gitea/prs.go b/gitea/prs.go index 8cc0106..3b127e4 100644 --- a/gitea/prs.go +++ b/gitea/prs.go @@ -41,11 +41,11 @@ func convertGiteaPR(pr *gitea.PullRequest) forge.PullRequest { switch { case pr.HasMerged: - result.State = "merged" + result.State = forge.PRStatusMerged case pr.State == gitea.StateClosed: - result.State = stateClosed + result.State = forge.PRStatusClosed default: - result.State = stateOpen + result.State = forge.PRStatusOpen } var baseRepoID int64 diff --git a/github/ci.go b/github/ci.go index 2bf6513..6a87795 100644 --- a/github/ci.go +++ b/github/ci.go @@ -23,7 +23,7 @@ func convertGitHubWorkflowRun(r *github.WorkflowRun) forge.CIRun { result := forge.CIRun{ ID: r.GetID(), Title: r.GetName(), - Status: r.GetStatus(), + Status: forge.NormalizeCIStatus(r.GetStatus()), Branch: r.GetHeadBranch(), SHA: r.GetHeadSHA(), Event: r.GetEvent(), @@ -31,7 +31,7 @@ func convertGitHubWorkflowRun(r *github.WorkflowRun) forge.CIRun { } if c := r.GetConclusion(); c != "" { - result.Conclusion = c + result.Conclusion = forge.NormalizeCIConclusion(c) } if a := r.GetActor(); a != nil { @@ -124,8 +124,8 @@ func (s *gitHubCIService) GetRun(ctx context.Context, owner, repo string, runID job := forge.CIJob{ ID: j.GetID(), Name: j.GetName(), - Status: j.GetStatus(), - Conclusion: j.GetConclusion(), + Status: forge.NormalizeCIStatus(j.GetStatus()), + Conclusion: forge.NormalizeCIConclusion(j.GetConclusion()), HTMLURL: j.GetHTMLURL(), } if t := j.GetStartedAt(); !t.IsZero() { diff --git a/github/collaborators.go b/github/collaborators.go index 12e6109..0cbe14b 100644 --- a/github/collaborators.go +++ b/github/collaborators.go @@ -43,7 +43,7 @@ func convertGitHubCollaborator(u *github.User) forge.Collaborator { } return forge.Collaborator{ Login: u.GetLogin(), - Permission: perm, + Permission: forge.NormalizeAccessLevel(perm), } } @@ -89,7 +89,7 @@ func (s *gitHubCollaboratorService) List(ctx context.Context, owner, repo string func (s *gitHubCollaboratorService) Add(ctx context.Context, owner, repo, username string, opts forge.AddCollaboratorOpts) error { ghOpts := &github.RepositoryAddCollaboratorOptions{} if opts.Permission != "" { - ghOpts.Permission = opts.Permission + ghOpts.Permission = githubPermission(opts.Permission) } _, resp, err := s.client.Repositories.AddCollaborator(ctx, owner, repo, username, ghOpts) @@ -102,6 +102,19 @@ func (s *gitHubCollaboratorService) Add(ctx context.Context, owner, repo, userna return nil } +func githubPermission(permission forge.AccessLevel) string { + switch forge.NormalizeAccessLevel(string(permission)) { + case forge.AccessLevelRead: + return "pull" + case forge.AccessLevelWrite: + return "push" + case forge.AccessLevelAdmin: + return "admin" + default: + return string(permission) + } +} + func (s *gitHubCollaboratorService) Remove(ctx context.Context, owner, repo, username string) error { resp, err := s.client.Repositories.RemoveCollaborator(ctx, owner, repo, username) if err != nil { diff --git a/github/commit_statuses.go b/github/commit_statuses.go index a141e6b..69eb8b7 100644 --- a/github/commit_statuses.go +++ b/github/commit_statuses.go @@ -29,7 +29,7 @@ func (s *gitHubCommitStatusService) List(ctx context.Context, owner, repo, sha s } for _, st := range statuses { cs := forge.CommitStatus{ - State: st.GetState(), + State: forge.NormalizeCommitStatusState(st.GetState()), Context: st.GetContext(), Description: st.GetDescription(), TargetURL: st.GetTargetURL(), @@ -51,8 +51,9 @@ func (s *gitHubCommitStatusService) List(ctx context.Context, owner, repo, sha s } func (s *gitHubCommitStatusService) Set(ctx context.Context, owner, repo, sha string, opts forge.SetCommitStatusOpts) (*forge.CommitStatus, error) { + state := string(opts.State) status := github.RepoStatus{ - State: &opts.State, + State: &state, Context: &opts.Context, Description: &opts.Description, TargetURL: &opts.TargetURL, @@ -67,7 +68,7 @@ func (s *gitHubCommitStatusService) Set(ctx context.Context, owner, repo, sha st } cs := &forge.CommitStatus{ - State: result.GetState(), + State: forge.NormalizeCommitStatusState(result.GetState()), Context: result.GetContext(), Description: result.GetDescription(), TargetURL: result.GetTargetURL(), diff --git a/github/helpers_test.go b/github/helpers_test.go index c06cce0..bc9e063 100644 --- a/github/helpers_test.go +++ b/github/helpers_test.go @@ -15,9 +15,9 @@ func parseTime(s string) time.Time { return t } -func assertEqual(t *testing.T, field, want, got string) { +func assertEqual[T ~string](t *testing.T, field, want string, got T) { t.Helper() - if want != got { + if want != string(got) { t.Errorf("%s: want %q, got %q", field, want, got) } } diff --git a/github/issues.go b/github/issues.go index 0f687c7..e87f57d 100644 --- a/github/issues.go +++ b/github/issues.go @@ -27,7 +27,7 @@ func convertGitHubIssue(i *github.Issue) forge.Issue { Number: i.GetNumber(), Title: i.GetTitle(), Body: i.GetBody(), - State: i.GetState(), + State: forge.NormalizeIssueState(i.GetState()), Locked: i.GetLocked(), HTMLURL: i.GetHTMLURL(), } diff --git a/github/prs.go b/github/prs.go index b4f97f3..70751dd 100644 --- a/github/prs.go +++ b/github/prs.go @@ -24,7 +24,7 @@ func convertGitHubPR(pr *github.PullRequest) forge.PullRequest { Number: pr.GetNumber(), Title: pr.GetTitle(), Body: pr.GetBody(), - State: pr.GetState(), + State: forge.NormalizePRStatus(pr.GetState()), Draft: pr.GetDraft(), Mergeable: pr.GetMergeable(), Merged: pr.GetMerged(), @@ -37,7 +37,7 @@ func convertGitHubPR(pr *github.PullRequest) forge.PullRequest { } if pr.GetMerged() { - result.State = "merged" + result.State = forge.PRStatusMerged } if u := pr.GetUser(); u != nil { diff --git a/gitlab/ci.go b/gitlab/ci.go index f6369f9..344481a 100644 --- a/gitlab/ci.go +++ b/gitlab/ci.go @@ -20,7 +20,7 @@ func (f *gitLabForge) CI() forge.CIService { func convertGitLabPipeline(p *gitlab.PipelineInfo) forge.CIRun { result := forge.CIRun{ ID: int64(p.ID), - Status: p.Status, + Status: forge.NormalizeCIStatus(p.Status), Branch: p.Ref, SHA: p.SHA, HTMLURL: p.WebURL, @@ -37,7 +37,7 @@ func convertGitLabPipeline(p *gitlab.PipelineInfo) forge.CIRun { func convertGitLabPipelineDetail(p *gitlab.Pipeline) forge.CIRun { result := forge.CIRun{ ID: int64(p.ID), - Status: p.Status, + Status: forge.NormalizeCIStatus(p.Status), Branch: p.Ref, SHA: p.SHA, HTMLURL: p.WebURL, @@ -127,7 +127,7 @@ func (s *gitLabCIService) GetRun(ctx context.Context, owner, repo string, runID job := forge.CIJob{ ID: int64(j.ID), Name: j.Name, - Status: j.Status, + Status: forge.NormalizeCIStatus(j.Status), HTMLURL: j.WebURL, } if j.StartedAt != nil { diff --git a/gitlab/collaborators.go b/gitlab/collaborators.go index 123408e..c3a7bcf 100644 --- a/gitlab/collaborators.go +++ b/gitlab/collaborators.go @@ -17,22 +17,22 @@ func (f *gitLabForge) Collaborators() forge.CollaboratorService { return &gitLabCollaboratorService{client: f.client} } -func convertGitLabAccessLevel(level gitlab.AccessLevelValue) string { +func convertGitLabAccessLevel(level gitlab.AccessLevelValue) forge.AccessLevel { switch { case level >= gitlab.OwnerPermissions: - return "admin" + return forge.AccessLevelAdmin case level >= gitlab.MaintainerPermissions: - return "admin" + return forge.AccessLevelAdmin case level >= gitlab.DeveloperPermissions: - return "write" + return forge.AccessLevelWrite default: - return "read" + return forge.AccessLevelRead } } -func parseGitLabAccessLevel(permission string) *gitlab.AccessLevelValue { +func parseGitLabAccessLevel(permission forge.AccessLevel) *gitlab.AccessLevelValue { var level gitlab.AccessLevelValue - switch permission { + switch string(permission) { case "guest": level = gitlab.GuestPermissions case "reporter": @@ -44,7 +44,14 @@ func parseGitLabAccessLevel(permission string) *gitlab.AccessLevelValue { case "owner": level = gitlab.OwnerPermissions default: - level = gitlab.DeveloperPermissions + switch forge.NormalizeAccessLevel(string(permission)) { + case forge.AccessLevelRead: + level = gitlab.ReporterPermissions + case forge.AccessLevelAdmin: + level = gitlab.MaintainerPermissions + default: + level = gitlab.DeveloperPermissions + } } return &level } diff --git a/gitlab/commit_statuses.go b/gitlab/commit_statuses.go index 8643ed8..0ad1943 100644 --- a/gitlab/commit_statuses.go +++ b/gitlab/commit_statuses.go @@ -34,7 +34,7 @@ func (s *gitLabCommitStatusService) List(ctx context.Context, owner, repo, sha s } for _, st := range statuses { cs := forge.CommitStatus{ - State: st.Status, + State: forge.NormalizeCommitStatusState(st.Status), Context: st.Name, Description: st.Description, TargetURL: st.TargetURL, @@ -56,7 +56,7 @@ func (s *gitLabCommitStatusService) List(ctx context.Context, owner, repo, sha s func (s *gitLabCommitStatusService) Set(ctx context.Context, owner, repo, sha string, opts forge.SetCommitStatusOpts) (*forge.CommitStatus, error) { pid := owner + "/" + repo glOpts := &gitlab.SetCommitStatusOptions{ - State: gitlab.BuildStateValue(opts.State), + State: gitlab.BuildStateValue(string(opts.State)), Name: gitlab.Ptr(opts.Context), Description: gitlab.Ptr(opts.Description), TargetURL: gitlab.Ptr(opts.TargetURL), @@ -71,7 +71,7 @@ func (s *gitLabCommitStatusService) Set(ctx context.Context, owner, repo, sha st } cs := &forge.CommitStatus{ - State: result.Status, + State: forge.NormalizeCommitStatusState(result.Status), Context: result.Name, Description: result.Description, TargetURL: result.TargetURL, diff --git a/gitlab/helpers_test.go b/gitlab/helpers_test.go index 02bd67e..814640f 100644 --- a/gitlab/helpers_test.go +++ b/gitlab/helpers_test.go @@ -2,9 +2,9 @@ package gitlab import "testing" -func assertEqual(t *testing.T, field, want, got string) { +func assertEqual[T ~string](t *testing.T, field, want string, got T) { t.Helper() - if want != got { + if want != string(got) { t.Errorf("%s: want %q, got %q", field, want, got) } } diff --git a/gitlab/issues.go b/gitlab/issues.go index 89df02e..481381d 100644 --- a/gitlab/issues.go +++ b/gitlab/issues.go @@ -27,16 +27,11 @@ func convertGitLabIssue(i *gitlab.Issue) forge.Issue { Number: int(i.IID), Title: i.Title, Body: i.Description, - State: i.State, + State: forge.NormalizeIssueState(i.State), Locked: i.DiscussionLocked, HTMLURL: i.WebURL, } - // Normalize "opened" to "open" - if result.State == stateOpened { - result.State = stateOpen - } - if i.Author != nil { result.Author = forge.User{ Login: i.Author.Username, diff --git a/gitlab/prs.go b/gitlab/prs.go index 6251ab9..1a1b9eb 100644 --- a/gitlab/prs.go +++ b/gitlab/prs.go @@ -27,7 +27,7 @@ func convertGitLabMR(mr *gitlab.MergeRequest) forge.PullRequest { Number: int(mr.IID), Title: mr.Title, Body: mr.Description, - State: mr.State, // "opened", "closed", "merged" + State: forge.NormalizePRStatus(mr.State), Draft: mr.Draft, Head: forge.PRBranch{Ref: mr.SourceBranch, SHA: mr.SHA}, Base: forge.PRBranch{Ref: mr.TargetBranch}, @@ -37,11 +37,6 @@ func convertGitLabMR(mr *gitlab.MergeRequest) forge.PullRequest { HTMLURL: mr.WebURL, } - // Normalize "opened" to "open" - if result.State == stateOpened { - result.State = stateOpen - } - if mr.Author != nil { result.Author = forge.User{ Login: mr.Author.Username, @@ -116,7 +111,7 @@ func convertBasicGitLabMR(mr *gitlab.BasicMergeRequest) forge.PullRequest { Number: int(mr.IID), Title: mr.Title, Body: mr.Description, - State: mr.State, + State: forge.NormalizePRStatus(mr.State), Draft: mr.Draft, Head: forge.PRBranch{Ref: mr.SourceBranch}, Base: forge.PRBranch{Ref: mr.TargetBranch}, @@ -124,10 +119,6 @@ func convertBasicGitLabMR(mr *gitlab.BasicMergeRequest) forge.PullRequest { HTMLURL: mr.WebURL, } - if result.State == stateOpened { - result.State = stateOpen - } - if mr.Author != nil { result.Author = forge.User{ Login: mr.Author.Username, diff --git a/internal/cli/ci.go b/internal/cli/ci.go index 9b7d81e..c1667ce 100644 --- a/internal/cli/ci.go +++ b/internal/cli/ci.go @@ -78,9 +78,9 @@ func ciListCmd() *cobra.Command { headers := []string{"ID", "TITLE", "STATUS", "BRANCH", "EVENT", "CREATED"} rows := make([][]string, len(runs)) for i, r := range runs { - status := r.Status + status := string(r.Status) if r.Conclusion != "" { - status = r.Conclusion + status = string(r.Conclusion) } created := "" if !r.CreatedAt.IsZero() { @@ -134,9 +134,9 @@ func ciViewCmd() *cobra.Command { return p.PrintJSON(run) } - status := run.Status + status := string(run.Status) if run.Conclusion != "" { - status = run.Conclusion + status = string(run.Conclusion) } _, _ = fmt.Fprintf(os.Stdout, "#%d %s\n", run.ID, run.Title) @@ -153,9 +153,9 @@ func ciViewCmd() *cobra.Command { _, _ = fmt.Fprintln(os.Stdout) _, _ = fmt.Fprintln(os.Stdout, "Jobs:") for _, j := range run.Jobs { - jStatus := j.Status + jStatus := string(j.Status) if j.Conclusion != "" { - jStatus = j.Conclusion + jStatus = string(j.Conclusion) } _, _ = fmt.Fprintf(os.Stdout, " %d %s %s\n", j.ID, j.Name, jStatus) } diff --git a/internal/cli/collaborator.go b/internal/cli/collaborator.go index dda2be7..89e48ea 100644 --- a/internal/cli/collaborator.go +++ b/internal/cli/collaborator.go @@ -61,7 +61,7 @@ func collaboratorListCmd() *cobra.Command { headers := []string{"LOGIN", "PERMISSION"} rows := make([][]string, len(collabs)) for i, c := range collabs { - rows[i] = []string{c.Login, c.Permission} + rows[i] = []string{c.Login, string(c.Permission)} } p.PrintTable(headers, rows) return nil @@ -88,7 +88,7 @@ func collaboratorAddCmd() *cobra.Command { } opts := forges.AddCollaboratorOpts{ - Permission: flagPermission, + Permission: forges.AccessLevel(flagPermission), } if err := forge.Collaborators().Add(cmd.Context(), owner, repoName, username, opts); err != nil { diff --git a/internal/cli/status.go b/internal/cli/status.go index 3469460..4bf1527 100644 --- a/internal/cli/status.go +++ b/internal/cli/status.go @@ -56,7 +56,7 @@ func statusListCmd() *cobra.Command { rows := make([][]string, len(statuses)) for i, s := range statuses { rows[i] = []string{ - s.State, + string(s.State), s.Context, s.Description, s.TargetURL, @@ -90,7 +90,7 @@ func statusSetCmd() *cobra.Command { } opts := forges.SetCommitStatusOpts{ - State: flagState, + State: forges.CommitStatusState(flagState), Context: flagContext, Description: flagDescription, TargetURL: flagURL, diff --git a/statuses.go b/statuses.go new file mode 100644 index 0000000..df90857 --- /dev/null +++ b/statuses.go @@ -0,0 +1,192 @@ +package forges + +import "strings" + +// IssueState is the normalized lifecycle state for issues. +type IssueState string + +const ( + IssueStateOpen IssueState = "open" + IssueStateClosed IssueState = "closed" + IssueStateUnknown IssueState = "unknown" +) + +// PRStatus is the normalized lifecycle state for pull or merge requests. +type PRStatus string + +const ( + PRStatusOpen PRStatus = "open" + PRStatusClosed PRStatus = "closed" + PRStatusMerged PRStatus = "merged" + PRStatusUnknown PRStatus = "unknown" +) + +// CIStatus is the normalized execution state for CI runs and jobs. +type CIStatus string + +const ( + CIStatusQueued CIStatus = "queued" + CIStatusRunning CIStatus = "running" + CIStatusCompleted CIStatus = "completed" + CIStatusSuccess CIStatus = "success" + CIStatusFailed CIStatus = "failed" + CIStatusCancelled CIStatus = "cancelled" + CIStatusSkipped CIStatus = "skipped" + CIStatusManual CIStatus = "manual" + CIStatusUnknown CIStatus = "unknown" +) + +// CIConclusion is the normalized result for completed CI runs and jobs. +type CIConclusion string + +const ( + CIConclusionSuccess CIConclusion = "success" + CIConclusionFailure CIConclusion = "failure" + CIConclusionCancelled CIConclusion = "cancelled" + CIConclusionSkipped CIConclusion = "skipped" + CIConclusionNeutral CIConclusion = "neutral" + CIConclusionTimedOut CIConclusion = "timed_out" + CIConclusionActionRequired CIConclusion = "action_required" + CIConclusionUnknown CIConclusion = "unknown" +) + +// CommitStatusState is the normalized state for commit status checks. +type CommitStatusState string + +const ( + CommitStatusSuccess CommitStatusState = "success" + CommitStatusFailure CommitStatusState = "failure" + CommitStatusPending CommitStatusState = "pending" + CommitStatusError CommitStatusState = "error" + CommitStatusCancelled CommitStatusState = "cancelled" + CommitStatusSkipped CommitStatusState = "skipped" + CommitStatusUnknown CommitStatusState = "unknown" +) + +// AccessLevel is the normalized permission level for repository collaborators. +type AccessLevel string + +const ( + AccessLevelNone AccessLevel = "none" + AccessLevelRead AccessLevel = "read" + AccessLevelWrite AccessLevel = "write" + AccessLevelAdmin AccessLevel = "admin" + AccessLevelUnknown AccessLevel = "unknown" +) + +// NormalizeIssueState maps forge-specific issue states to common states. +func NormalizeIssueState(state string) IssueState { + switch normalizeToken(state) { + case "open", "opened", "new", "reopened": + return IssueStateOpen + case "closed", "resolved", "declined", "rejected", "done": + return IssueStateClosed + default: + return IssueStateUnknown + } +} + +// NormalizePRStatus maps forge-specific pull request states to common states. +func NormalizePRStatus(state string) PRStatus { + switch normalizeToken(state) { + case "open", "opened", "new", "reopened": + return PRStatusOpen + case "closed", "declined", "rejected", "superseded": + return PRStatusClosed + case "merged": + return PRStatusMerged + default: + return PRStatusUnknown + } +} + +// NormalizeCIStatus maps forge-specific CI run and job statuses to common states. +func NormalizeCIStatus(status string) CIStatus { + switch normalizeToken(status) { + case "queued", "pending", "created", "waiting", "requested", "scheduled": + return CIStatusQueued + case "running", "in_progress", "inprogress": + return CIStatusRunning + case "completed", "complete", "done", "finished": + return CIStatusCompleted + case "success", "successful", "succeeded", "passed", "passing", "approved": + return CIStatusSuccess + case "failed", "failure", "fail", "failing": + return CIStatusFailed + case "cancelled", "canceled", "canceling", "cancelling": + return CIStatusCancelled + case "skipped", "skip": + return CIStatusSkipped + case "manual", "blocked": + return CIStatusManual + default: + return CIStatusUnknown + } +} + +// NormalizeCIConclusion maps forge-specific CI conclusions to common results. +func NormalizeCIConclusion(conclusion string) CIConclusion { + switch normalizeToken(conclusion) { + case "": + return "" + case "success", "successful", "succeeded", "passed", "passing", "approved": + return CIConclusionSuccess + case "failure", "failed", "fail", "failing", "declined", "rejected": + return CIConclusionFailure + case "cancelled", "canceled": + return CIConclusionCancelled + case "skipped", "skip": + return CIConclusionSkipped + case "neutral": + return CIConclusionNeutral + case "timed_out", "timedout", "timeout": + return CIConclusionTimedOut + case "action_required", "actionrequired": + return CIConclusionActionRequired + default: + return CIConclusionUnknown + } +} + +// NormalizeCommitStatusState maps forge-specific commit status states to common states. +func NormalizeCommitStatusState(state string) CommitStatusState { + switch normalizeToken(state) { + case "success", "successful", "succeeded", "passed", "passing", "approved", "ok": + return CommitStatusSuccess + case "failure", "failed", "fail", "failing", "declined", "rejected": + return CommitStatusFailure + case "pending", "queued", "running", "in_progress", "created", "waiting": + return CommitStatusPending + case "error", "errored", "broken": + return CommitStatusError + case "cancelled", "canceled": + return CommitStatusCancelled + case "skipped", "skip": + return CommitStatusSkipped + default: + return CommitStatusUnknown + } +} + +// NormalizeAccessLevel maps forge-specific collaborator permission names to common levels. +func NormalizeAccessLevel(permission string) AccessLevel { + switch normalizeToken(permission) { + case "", "none", "no_access": + return AccessLevelNone + case "read", "pull", "guest", "reporter": + return AccessLevelRead + case "write", "push", "developer": + return AccessLevelWrite + case "admin", "owner", "maintainer": + return AccessLevelAdmin + default: + return AccessLevelUnknown + } +} + +func normalizeToken(v string) string { + v = strings.TrimSpace(strings.ToLower(v)) + v = strings.ReplaceAll(v, "-", "_") + v = strings.ReplaceAll(v, " ", "_") + return v +} diff --git a/statuses_test.go b/statuses_test.go new file mode 100644 index 0000000..8e13d48 --- /dev/null +++ b/statuses_test.go @@ -0,0 +1,82 @@ +package forges + +import "testing" + +func TestNormalizePRStatus(t *testing.T) { + tests := []struct { + in string + want PRStatus + }{ + {"open", PRStatusOpen}, + {"opened", PRStatusOpen}, + {"MERGED", PRStatusMerged}, + {"DECLINED", PRStatusClosed}, + {"superseded", PRStatusClosed}, + {"unexpected", PRStatusUnknown}, + } + + for _, tt := range tests { + if got := NormalizePRStatus(tt.in); got != tt.want { + t.Errorf("NormalizePRStatus(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestNormalizeCommitStatusState(t *testing.T) { + tests := []struct { + in string + want CommitStatusState + }{ + {"success", CommitStatusSuccess}, + {"passing", CommitStatusSuccess}, + {"approved", CommitStatusSuccess}, + {"failed", CommitStatusFailure}, + {"in_progress", CommitStatusPending}, + {"errored", CommitStatusError}, + {"canceled", CommitStatusCancelled}, + {"unexpected", CommitStatusUnknown}, + } + + for _, tt := range tests { + if got := NormalizeCommitStatusState(tt.in); got != tt.want { + t.Errorf("NormalizeCommitStatusState(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestNormalizeAccessLevel(t *testing.T) { + tests := []struct { + in string + want AccessLevel + }{ + {"pull", AccessLevelRead}, + {"reporter", AccessLevelRead}, + {"push", AccessLevelWrite}, + {"developer", AccessLevelWrite}, + {"maintainer", AccessLevelAdmin}, + {"owner", AccessLevelAdmin}, + {"", AccessLevelNone}, + {"custom", AccessLevelUnknown}, + } + + for _, tt := range tests { + if got := NormalizeAccessLevel(tt.in); got != tt.want { + t.Errorf("NormalizeAccessLevel(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestNormalizeCIStatusAndConclusion(t *testing.T) { + if got := NormalizeCIStatus("in_progress"); got != CIStatusRunning { + t.Errorf("NormalizeCIStatus(in_progress) = %q", got) + } + if got := NormalizeCIStatus("canceled"); got != CIStatusCancelled { + t.Errorf("NormalizeCIStatus(canceled) = %q", got) + } + if got := NormalizeCIConclusion("timed-out"); got != CIConclusionTimedOut { + t.Errorf("NormalizeCIConclusion(timed-out) = %q", got) + } + if got := NormalizeCIConclusion(""); got != "" { + t.Errorf("NormalizeCIConclusion(empty) = %q", got) + } +} diff --git a/types.go b/types.go index 8e55e19..8f2afb1 100644 --- a/types.go +++ b/types.go @@ -175,7 +175,7 @@ type Issue struct { Number int `json:"number"` Title string `json:"title"` Body string `json:"body"` - State string `json:"state"` // "open" or "closed" + State IssueState `json:"state"` Author User `json:"author"` Assignees []User `json:"assignees,omitempty"` Labels []Label `json:"labels,omitempty"` @@ -250,7 +250,7 @@ type PullRequest struct { Number int `json:"number"` Title string `json:"title"` Body string `json:"body"` - State string `json:"state"` // "open", "closed", or "merged" + State PRStatus `json:"state"` Draft bool `json:"draft"` Author User `json:"author"` Assignees []User `json:"assignees,omitempty"` @@ -437,30 +437,30 @@ type ListBranchOpts struct { // CIRun holds normalized metadata about a CI pipeline or workflow run. type CIRun struct { - ID int64 `json:"id"` - Title string `json:"title"` - Status string `json:"status"` // queued, running, completed, failed, success, cancelled - Conclusion string `json:"conclusion"` // success, failure, cancelled, skipped (GitHub-specific) - Branch string `json:"branch"` - SHA string `json:"sha"` - Event string `json:"event,omitempty"` // push, pull_request, etc. - Author User `json:"author"` - HTMLURL string `json:"html_url"` - Jobs []CIJob `json:"jobs,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - FinishedAt *time.Time `json:"finished_at,omitempty"` + ID int64 `json:"id"` + Title string `json:"title"` + Status CIStatus `json:"status"` + Conclusion CIConclusion `json:"conclusion"` + Branch string `json:"branch"` + SHA string `json:"sha"` + Event string `json:"event,omitempty"` // push, pull_request, etc. + Author User `json:"author"` + HTMLURL string `json:"html_url"` + Jobs []CIJob `json:"jobs,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + FinishedAt *time.Time `json:"finished_at,omitempty"` } // CIJob holds normalized metadata about a CI job. type CIJob struct { - ID int64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Conclusion string `json:"conclusion,omitempty"` - HTMLURL string `json:"html_url,omitempty"` - StartedAt *time.Time `json:"started_at,omitempty"` - FinishedAt *time.Time `json:"finished_at,omitempty"` + ID int64 `json:"id"` + Name string `json:"name"` + Status CIStatus `json:"status"` + Conclusion CIConclusion `json:"conclusion,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` } // ListCIRunOpts holds options for listing CI runs. @@ -622,13 +622,13 @@ type FileEntry struct { // Collaborator holds normalized metadata about a repository collaborator. type Collaborator struct { - Login string `json:"login"` - Permission string `json:"permission"` // read, write, admin + Login string `json:"login"` + Permission AccessLevel `json:"permission"` } // AddCollaboratorOpts holds options for adding a collaborator. type AddCollaboratorOpts struct { - Permission string // pull, push, admin (GitHub/Gitea); guest, reporter, developer, maintainer, owner (GitLab) + Permission AccessLevel } // ListCollaboratorOpts holds options for listing collaborators. @@ -648,17 +648,17 @@ type Contributor struct { // CommitStatus holds normalized metadata about a commit status. type CommitStatus struct { - State string `json:"state"` // success, failure, pending, error - Context string `json:"context"` // e.g. "my-check" - Description string `json:"description"` // short summary - TargetURL string `json:"target_url"` // link to details - Creator string `json:"creator"` // login of who created it - CreatedAt time.Time `json:"created_at"` + State CommitStatusState `json:"state"` + Context string `json:"context"` // e.g. "my-check" + Description string `json:"description"` // short summary + TargetURL string `json:"target_url"` // link to details + Creator string `json:"creator"` // login of who created it + CreatedAt time.Time `json:"created_at"` } // SetCommitStatusOpts holds options for creating a commit status. type SetCommitStatusOpts struct { - State string // success, failure, pending, error + State CommitStatusState Context string // e.g. "my-check" Description string TargetURL string