diff --git a/cli/pipeline/purge.go b/cli/pipeline/purge.go index c71b6ee79..3781f8458 100644 --- a/cli/pipeline/purge.go +++ b/cli/pipeline/purge.go @@ -1,4 +1,4 @@ -// Copyright 2022 Woodpecker Authors +// Copyright 2024 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package pipeline import ( "context" + "errors" "fmt" + "net/http" "time" "github.com/rs/zerolog/log" @@ -121,6 +123,11 @@ func pipelinePurge(c *cli.Command, client woodpecker.Client) (err error) { err := client.PipelineDelete(repoID, p.Number) if err != nil { + var clientErr *woodpecker.ClientError + if errors.As(err, &clientErr) && clientErr.StatusCode == http.StatusUnprocessableEntity { + log.Error().Err(err).Msgf("failed to delete pipeline %d", p.Number) + continue + } return err } } diff --git a/cli/pipeline/purge_test.go b/cli/pipeline/purge_test.go index 12356dd0b..728a947d4 100644 --- a/cli/pipeline/purge_test.go +++ b/cli/pipeline/purge_test.go @@ -16,13 +16,14 @@ import ( func TestPipelinePurge(t *testing.T) { tests := []struct { - name string - repoID int64 - args []string - pipelinesKeep []*woodpecker.Pipeline - pipelines []*woodpecker.Pipeline - wantDelete int - wantErr error + name string + repoID int64 + args []string + pipelinesKeep []*woodpecker.Pipeline + pipelines []*woodpecker.Pipeline + mockDeleteError error + wantDelete int + wantErr error }{ { name: "success with no pipelines to purge", @@ -53,6 +54,24 @@ func TestPipelinePurge(t *testing.T) { args: []string{"purge", "--older-than", "invalid", "repo/name"}, wantErr: errors.New("time: invalid duration \"invalid\""), }, + { + name: "continue on 422 error", + repoID: 1, + args: []string{"purge", "--older-than", "1h", "repo/name"}, + pipelinesKeep: []*woodpecker.Pipeline{ + {Number: 1}, + }, + pipelines: []*woodpecker.Pipeline{ + {Number: 1}, + {Number: 2}, + {Number: 3}, + }, + wantDelete: 2, + mockDeleteError: &woodpecker.ClientError{ + StatusCode: 422, + Message: "test error", + }, + }, } for _, tt := range tests { @@ -80,7 +99,9 @@ func TestPipelinePurge(t *testing.T) { return []*woodpecker.Pipeline{}, nil }).Maybe() - if tt.wantDelete > 0 { + if tt.mockDeleteError != nil { + mockClient.On("PipelineDelete", tt.repoID, mock.Anything).Return(tt.mockDeleteError) + } else if tt.wantDelete > 0 { mockClient.On("PipelineDelete", tt.repoID, mock.Anything).Return(nil).Times(tt.wantDelete) } diff --git a/woodpecker-go/woodpecker/client.go b/woodpecker-go/woodpecker/client.go index 3a26f5dc2..8871f654a 100644 --- a/woodpecker-go/woodpecker/client.go +++ b/woodpecker-go/woodpecker/client.go @@ -34,6 +34,15 @@ const ( // pathVersion = "%s/version" ) +type ClientError struct { + StatusCode int + Message string +} + +func (e *ClientError) Error() string { + return fmt.Sprintf("client error %d: %s", e.StatusCode, e.Message) +} + type client struct { client *http.Client addr string @@ -140,7 +149,10 @@ func (c *client) open(rawURL, method string, in any) (io.ReadCloser, error) { if resp.StatusCode > http.StatusPartialContent { defer resp.Body.Close() out, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("client error %d: %s", resp.StatusCode, string(out)) + return nil, &ClientError{ + StatusCode: resp.StatusCode, + Message: string(out), + } } return resp.Body, nil }