refactor(services): leave CLI flag and command names to the app layer

Services report ErrRunScanExhausted, ErrMaxPages and plain conflicts,
and the commands add the --branch/--max-pages/--force hints. Unknown
label names use the shared NotFoundError, and the repository-scoped
review and team search refusal moves into tea search's own usage
check.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 18:44:29 +02:00
1 parent f2f1dd1e34
commit af14803b3a
10 files changed
+37 -18

No files matched your search

+5 -2
View File
@@ -100,6 +100,9 @@ type RunListOptions struct {
// RunStatuses are the values Gitea accepts as run status filters.
var RunStatuses = []string{"queued", "in_progress", "waiting", "requested", "pending", "action_required", "completed", "success", "failure", "cancelled", "skipped", "timed_out", "neutral"} //nolint:misspell // Gitea API spelling.
// ErrRunScanExhausted reports time filters that matched too few of the runs a scan may read.
var ErrRunScanExhausted = fmt.Errorf("time filters scanned %d runs without exhausting the history", maxRunScanPages*100)
// Validate rejects unsupported status filters before performing API requests.
func (o RunListOptions) Validate() error {
if o.Limit < 1 || o.Limit > 1000 || o.Page < 1 || o.Page > int(^uint(0)>>1)/o.Limit {
@@ -107,7 +110,7 @@ func (o RunListOptions) Validate() error {
}
for _, status := range o.Statuses {
if !slices.Contains(RunStatuses, status) {
return fmt.Errorf("unsupported run status %q; see tea run list --help", status)
return fmt.Errorf("unsupported run status %q", status)
}
}
return nil
@@ -173,7 +176,7 @@ func (s Actions) runs(ctx context.Context, opts RunListOptions) (Page[Run], erro
return Page[Run]{Items: result}, nil
}
}
return Page[Run]{}, fmt.Errorf("time filters scanned %d runs without exhausting the history; narrow the search with --branch, --workflow or --status", maxRunScanPages*100)
return Page[Run]{}, ErrRunScanExhausted
}
func (s Actions) runsPage(ctx context.Context, opts RunListOptions) (Page[Run], error) {
+1 -1
View File
@@ -275,7 +275,7 @@ func TestRunTimeFilteringStopsAtScanCap(t *testing.T) {
defer server.Close()
service := Actions{Client: transport.New(server.URL, "", false), Repository: "owner/repo"}
_, err := service.Runs(context.Background(), RunListOptions{Limit: 1, Page: 1, Until: &until})
require.ErrorContains(t, err, "narrow the search")
require.ErrorIs(t, err, ErrRunScanExhausted)
require.Equal(t, maxRunScanPages, pages)
}
+4 -1
View File
@@ -29,6 +29,9 @@ type APIOptions struct {
MaxPages int
}
// ErrMaxPages reports a paginated request that has more pages than APIOptions.MaxPages allows.
var ErrMaxPages = errors.New("API pagination exceeded the page limit")
func nextPageLinks(headers []string) ([]string, error) {
result := []string{}
for _, header := range headers {
@@ -141,7 +144,7 @@ func (s API) Visit(ctx context.Context, options APIOptions, visit func(*http.Res
return nil
}
if page+1 >= options.MaxPages {
return errors.New("API pagination exceeded --max-pages")
return ErrMaxPages
}
if _, err := s.Client.URL(next); err != nil {
return err
+3 -3
View File
@@ -55,7 +55,7 @@ func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOp
return Label{}, false, err
}
if !update {
return Label{}, false, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope(), Hint: "pass --force to update it"}
return Label{}, false, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope()}
}
opts.Name = nil
label, err := s.Edit(ctx, current.ID, opts)
@@ -68,7 +68,7 @@ func selectLabelNames(scope string, names []string, matches map[string][]int64)
for _, name := range names {
candidates := matches[name]
if len(candidates) == 0 {
return nil, fmt.Errorf("label %q does not exist; use tea label list", name)
return nil, NotFound("label", strconv.Quote(name), scope)
}
if len(candidates) > 1 {
return nil, &AmbiguousError{Kind: "label", Key: strconv.Quote(name), Scope: scope, IDs: candidates}
@@ -148,7 +148,7 @@ func (s Labels) RequireFilterNames(ctx context.Context, names []string) error {
}
for _, name := range names {
if !known[name] {
return fmt.Errorf("label %q does not exist in %s; issue filters match repository labels only, see tea label list", name, s.Repository)
return fmt.Errorf("label %q does not exist in %s; issue filters match repository labels only", name, s.Repository)
}
}
return nil
+2 -2
View File
@@ -36,7 +36,7 @@ func TestLabelSelectionReadsCappedPagesAndPreservesOrder(t *testing.T) {
require.Equal(t, []int64{2, 1}, ids)
require.Equal(t, 3, reads)
_, err = service.ResolveIssueNames(context.Background(), []string{"missing"})
require.ErrorContains(t, err, "does not exist")
require.ErrorContains(t, err, "not found")
before := reads
_, err = service.ResolveIssueNames(context.Background(), []string{" "})
require.ErrorContains(t, err, "cannot be empty")
@@ -89,5 +89,5 @@ func TestScopedLabelNamesDoNotCollideWithLiteralPrefixes(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []int64{1, 2, 3}, ids)
_, err = service.ResolveIssueNames(context.Background(), []string{"org:bug"})
require.ErrorContains(t, err, "does not exist")
require.ErrorContains(t, err, "not found")
}
+2 -5
View File
@@ -75,11 +75,8 @@ func (s Issues) SearchPage(ctx context.Context, opts DiscussionSearchOptions) (P
}
func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) (Page[*gitea.Issue], error) {
if opts.ReviewRequested || opts.Reviewed {
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped review search", Reason: "Gitea supports review filters on instance-wide search only; omit --repo or TEA_REPO"}
}
if opts.Team != "" {
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped team search", Reason: "Gitea supports team filters on instance-wide search only; omit --repo or TEA_REPO"}
if opts.ReviewRequested || opts.Reviewed || opts.Team != "" {
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped review or team search", Reason: "Gitea supports review and team filters on instance-wide search only"}
}
owner, repo, err := splitRepository(s.Repository)
if err != nil {