Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Generated by `make release` command.
# DO NOT EDIT.
tag: v0.50.1
tag: v0.50.2

releaseNoteGenerator:
showCommitter: false
Expand Down
10 changes: 9 additions & 1 deletion docs/content/en/docs-dev/user-guide/plan-preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ pipectl plan-preview \
--repo-remote-url={ REPO_REMOTE_GIT_SSH_URL } \
--head-branch={ HEAD_BRANCH } \
--head-commit={ HEAD_COMMIT } \
--base-branch={ BASE_BRANCH }
--base-branch={ BASE_BRANCH } \
--sort-label-keys={ SORT_LABEL_KEYS }
```

You can run it locally or integrate it to your CI system to run automatically when a new pull request is opened/updated. Use `--help` to see more options.
Expand All @@ -47,6 +48,13 @@ You can run it locally or integrate it to your CI system to run automatically wh
pipectl plan-preview --help
```

### Order of the results

By default, the results are sorted by PipedID and Application Name.

If you want to sort the results by labels, add `--sort-label-keys` option. For example, when you run with `--sort-label-keys=env,team`, the results will be sorted by PipedID, `env` label, `team` label, and then Application Name.


## GitHub Actions

If you are using GitHub Actions, you can seamlessly integrate our prepared [actions-plan-preview](https://github.com/pipe-cd/actions-plan-preview) to your workflows. This automatically comments the plan-preview result on the pull request when it is opened or updated. You can also trigger to run plan-preview manually by leave a comment `/pipecd plan-preview` on the pull request.
10 changes: 9 additions & 1 deletion docs/content/en/docs-v0.50.x/user-guide/plan-preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ pipectl plan-preview \
--repo-remote-url={ REPO_REMOTE_GIT_SSH_URL } \
--head-branch={ HEAD_BRANCH } \
--head-commit={ HEAD_COMMIT } \
--base-branch={ BASE_BRANCH }
--base-branch={ BASE_BRANCH } \
--sort-label-keys={ SORT_LABEL_KEYS }
```

You can run it locally or integrate it to your CI system to run automatically when a new pull request is opened/updated. Use `--help` to see more options.
Expand All @@ -47,6 +48,13 @@ You can run it locally or integrate it to your CI system to run automatically wh
pipectl plan-preview --help
```

### Order of the results

By default, the results are sorted by PipedID and Application Name.

If you want to sort the results by labels, add `--sort-label-keys` option. For example, when you run with `--sort-label-keys=env,team`, the results will be sorted by PipedID, `env` label, `team` label, and then Application Name.


## GitHub Actions

If you are using GitHub Actions, you can seamlessly integrate our prepared [actions-plan-preview](https://github.com/pipe-cd/actions-plan-preview) to your workflows. This automatically comments the plan-preview result on the pull request when it is opened or updated. You can also trigger to run plan-preview manually by leave a comment `/pipecd plan-preview` on the pull request.
24 changes: 24 additions & 0 deletions pkg/app/pipectl/cmd/planpreview/planpreview.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"fmt"
"io"
"os"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -49,6 +50,7 @@
timeout time.Duration
pipedHandleTimeout time.Duration
checkInterval time.Duration
sortLabelKeys []string

clientOptions *client.Options
}
Expand All @@ -75,6 +77,7 @@
cmd.Flags().StringVar(&c.out, "out", c.out, "Write planpreview result to the given path.")
cmd.Flags().DurationVar(&c.timeout, "timeout", c.timeout, "Maximum amount of time this command has to complete. Default is 10m.")
cmd.Flags().DurationVar(&c.pipedHandleTimeout, "piped-handle-timeout", c.pipedHandleTimeout, "Maximum amount of time that a Piped can take to handle. Default is 5m.")
cmd.Flags().StringSliceVar(&c.sortLabelKeys, "sort-label-keys", c.sortLabelKeys, "The application label keys to sort the results by. If not specified, the results will be sorted by only PipedID and ApplicationName.")

Check warning on line 80 in pkg/app/pipectl/cmd/planpreview/planpreview.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/planpreview/planpreview.go#L80

Added line #L80 was not covered by tests

cmd.MarkFlagRequired("repo-remote-url")
cmd.MarkFlagRequired("head-branch")
Expand Down Expand Up @@ -147,11 +150,32 @@
fmt.Printf("Failed to retrieve plan-preview results: %v\n", err)
return err
}
sortResults(results, c.sortLabelKeys)

Check warning on line 153 in pkg/app/pipectl/cmd/planpreview/planpreview.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/planpreview/planpreview.go#L153

Added line #L153 was not covered by tests
return printResults(results, os.Stdout, c.out)
}
}
}

// sortResults sorts the given results by pipedID and the given sortLabelKeys.
// If sortLabelKeys is not specified or the all values of sortLabelKeys are the same, it sorts by pipedID and ApplicationName.
func sortResults(allResults []*model.PlanPreviewCommandResult, sortLabelKeys []string) {
sort.SliceStable(allResults, func(i, j int) bool {
return allResults[i].PipedId < allResults[j].PipedId
})
for _, resultsPerPiped := range allResults {
results := resultsPerPiped.Results
sort.SliceStable(results, func(i, j int) bool {
a, b := results[i], results[j]
for _, key := range sortLabelKeys {
if a.Labels[key] != b.Labels[key] {
return a.Labels[key] < b.Labels[key]
}
}
return a.ApplicationName < b.ApplicationName
})
}
}

func printResults(results []*model.PlanPreviewCommandResult, stdout io.Writer, outFile string) error {
r := convert(results)

Expand Down
120 changes: 120 additions & 0 deletions pkg/app/pipectl/cmd/planpreview/planpreview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,123 @@ NOTE: An error occurred while building plan-preview for applications of the foll
})
}
}
func TestSortResults(t *testing.T) {
t.Parallel()
testcases := []struct {
name string
results []*model.PlanPreviewCommandResult
sortLabelKeys []string
expected []*model.PlanPreviewCommandResult
}{
{
name: "sort by pipedID and application name",
results: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-2",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-2"},
{ApplicationName: "app-1"},
},
},
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-2"},
{ApplicationName: "app-1"},
},
},
},
sortLabelKeys: []string{},
expected: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1"},
{ApplicationName: "app-2"},
},
},
{
PipedId: "piped-2",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1"},
{ApplicationName: "app-2"},
},
},
},
},
{
name: "sort by label keys",
results: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
},
},
{
PipedId: "piped-2",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
},
},
},
sortLabelKeys: []string{"env"},
expected: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
},
},
{
PipedId: "piped-2",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
},
},
},
},
{
name: "sort by multiple label keys",
results: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
},
},
},
sortLabelKeys: []string{"env", "team"},
expected: []*model.PlanPreviewCommandResult{
{
PipedId: "piped-1",
Results: []*model.ApplicationPlanPreviewResult{
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
},
},
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
sortResults(tc.results, tc.sortLabelKeys)
assert.Equal(t, tc.expected, tc.results)
})
}
}
45 changes: 35 additions & 10 deletions pkg/app/piped/eventwatcher/eventwatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
case <-ticker.C:
err := repo.Pull(ctx, repo.GetClonedBranch())
if err != nil {
w.logger.Error("failed to perform git pull",
w.logger.Error("failed to perform git pull. will retry in the next loop",

Check warning on line 186 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L186

Added line #L186 was not covered by tests
zap.String("repo-id", repoCfg.RepoID),
zap.String("branch", repo.GetClonedBranch()),
zap.Error(err),
Expand Down Expand Up @@ -233,6 +233,7 @@
if err := w.updateValues(ctx, repo, repoCfg.RepoID, cfg.Events, commitMsg); err != nil {
w.logger.Error("failed to update the values",
zap.String("repo-id", repoCfg.RepoID),
zap.String("branch", repo.GetClonedBranch()),

Check warning on line 236 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L236

Added line #L236 was not covered by tests
zap.Error(err),
)
}
Expand Down Expand Up @@ -294,6 +295,7 @@
if err := w.execute(ctx, repo, repoCfg.RepoID, cfgs); err != nil {
w.logger.Error("failed to execute the event from application configuration",
zap.String("repo-id", repoCfg.RepoID),
zap.String("branch", repo.GetClonedBranch()),

Check warning on line 298 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L298

Added line #L298 was not covered by tests
zap.Error(err),
)
}
Expand Down Expand Up @@ -456,28 +458,40 @@
var responseError error
retry := backoff.NewRetry(retryPushNum, backoff.NewConstant(retryPushInterval))
for branch, events := range branchHandledEvents {
eventIDs := make([]string, 0, len(events))
for _, e := range events {
eventIDs = append(eventIDs, e.Id)
}
zlogger := w.logger.With(
zap.String("repo-id", repoID),
zap.String("branch", tmpRepo.GetClonedBranch()),
zap.Strings("event-ids", eventIDs),
)

Check warning on line 470 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L461-L470

Added lines #L461 - L470 were not covered by tests
_, err = retry.Do(ctx, func() (interface{}, error) {
if err := tmpRepo.Push(ctx, branch); err != nil {
w.logger.Error("failed to push commits", zap.String("repo-id", repoID), zap.String("branch", branch), zap.Error(err))
zlogger.Warn(fmt.Sprintf("failed to push commits. retry attempt %d/%d", retry.Calls(), retryPushNum), zap.Error(err))

Check warning on line 473 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L473

Added line #L473 was not covered by tests
return nil, err
}
return nil, nil
})

if err == nil {
if _, err := w.apiClient.ReportEventStatuses(ctx, &pipedservice.ReportEventStatusesRequest{Events: events}); err != nil {
w.logger.Error("failed to report event statuses", zap.Error(err))
zlogger.Error("failed to report event statuses", zap.Error(err))

Check warning on line 481 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L481

Added line #L481 was not covered by tests
}
w.executionMilestoneMap.Store(repoID, maxTimestamp)
continue
}

// If push fails because the local branch was not fresh, exit to retry again in the next interval.
if err == git.ErrBranchNotFresh {
w.logger.Warn("failed to push commits", zap.Error(err))
zlogger.Warn("failed to push commits. local branch was not up-to-date. will retry in the next loop", zap.Error(err))

Check warning on line 489 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L489

Added line #L489 was not covered by tests
continue
}

zlogger.Error("failed to push commits", zap.Error(err))

Check warning on line 494 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L493-L494

Added lines #L493 - L494 were not covered by tests
// If push fails because of the other reason, re-set all statuses to FAILURE.
for i := range events {
if events[i].Status == model.EventStatus_EVENT_FAILURE {
Expand All @@ -487,7 +501,7 @@
events[i].StatusDescription = fmt.Sprintf("Failed to push changed files: %v", err)
}
if _, err := w.apiClient.ReportEventStatuses(ctx, &pipedservice.ReportEventStatusesRequest{Events: events}); err != nil {
w.logger.Error("failed to report event statuses", zap.Error(err))
zlogger.Error("failed to report event statuses", zap.Error(err))

Check warning on line 504 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L504

Added line #L504 was not covered by tests
}
w.executionMilestoneMap.Store(repoID, maxTimestamp)
responseError = errors.Join(responseError, err)
Expand Down Expand Up @@ -600,17 +614,27 @@
return nil
}

eventIDs := make([]string, 0, len(handledEvents))
for _, e := range handledEvents {
eventIDs = append(eventIDs, e.Id)
}
zlogger := w.logger.With(
zap.String("repo-id", repoID),
zap.String("branch", tmpRepo.GetClonedBranch()),
zap.Strings("event-ids", eventIDs),
)

Check warning on line 626 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L617-L626

Added lines #L617 - L626 were not covered by tests
retry := backoff.NewRetry(retryPushNum, backoff.NewConstant(retryPushInterval))
_, err = retry.Do(ctx, func() (interface{}, error) {
if err := tmpRepo.Push(ctx, tmpRepo.GetClonedBranch()); err != nil {
w.logger.Error("failed to push commits", zap.String("repo-id", repoID), zap.String("branch", tmpRepo.GetClonedBranch()), zap.Error(err))
zlogger.Warn(fmt.Sprintf("failed to push commits. retry attempt %d/%d", retry.Calls(), retryPushNum), zap.Error(err))

Check warning on line 630 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L630

Added line #L630 was not covered by tests
return nil, err
}
return nil, nil
})
if err == nil {
if _, err := w.apiClient.ReportEventStatuses(ctx, &pipedservice.ReportEventStatusesRequest{Events: handledEvents}); err != nil {
w.logger.Error("failed to report event statuses", zap.Error(err))
zlogger.Error("failed to report event statuses", zap.Error(err))

Check warning on line 637 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L637

Added line #L637 was not covered by tests
return err
}
w.milestoneMap.Store(repoID, maxTimestamp)
Expand All @@ -619,10 +643,12 @@

// If push fails because the local branch was not fresh, exit to retry again in the next interval.
if err == git.ErrBranchNotFresh {
w.logger.Warn("failed to push commits", zap.Error(err))
zlogger.Warn("failed to push commits. local branch was not up-to-date. will retry in the next loop", zap.Error(err))

Check warning on line 646 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L646

Added line #L646 was not covered by tests
return nil
}

zlogger.Error("failed to push commits", zap.Error(err))

Check warning on line 651 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L650-L651

Added lines #L650 - L651 were not covered by tests
// If push fails because of the other reason, re-set all statuses to FAILURE.
for i := range handledEvents {
if handledEvents[i].Status == model.EventStatus_EVENT_FAILURE {
Expand All @@ -632,11 +658,10 @@
handledEvents[i].StatusDescription = fmt.Sprintf("Failed to push changed files: %v", err)
}
if _, err := w.apiClient.ReportEventStatuses(ctx, &pipedservice.ReportEventStatusesRequest{Events: handledEvents}); err != nil {
w.logger.Error("failed to report event statuses: %w", zap.Error(err))
zlogger.Error("failed to report event statuses: %w", zap.Error(err))

Check warning on line 661 in pkg/app/piped/eventwatcher/eventwatcher.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/eventwatcher/eventwatcher.go#L661

Added line #L661 was not covered by tests
return err
}
w.milestoneMap.Store(repoID, maxTimestamp)
w.logger.Error("failed to push commits", zap.Error(err))
return err
}

Expand Down
Loading
Loading