Skip to content

Commit 26b69a1

Browse files
kj455pipecd-bot
authored andcommitted
Sort results of plan-preview (#5540)
* Sort results of plan-preview Signed-off-by: kj455 <[email protected]> * Ensure the order of list piped Signed-off-by: kj455 <[email protected]> * fix: lint Signed-off-by: kj455 <[email protected]> * fix: move sorting to pipectl Signed-off-by: kj455 <[email protected]> * fix: add testcase Signed-off-by: kj455 <[email protected]> * fix: dev docs Signed-off-by: kj455 <[email protected]> * add docs Signed-off-by: kj455 <[email protected]> --------- Signed-off-by: kj455 <[email protected]> Signed-off-by: pipecd-bot <[email protected]>
1 parent edcc703 commit 26b69a1

File tree

3 files changed

+153
-1
lines changed

3 files changed

+153
-1
lines changed

docs/content/en/docs-dev/user-guide/plan-preview.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ pipectl plan-preview \
3838
--repo-remote-url={ REPO_REMOTE_GIT_SSH_URL } \
3939
--head-branch={ HEAD_BRANCH } \
4040
--head-commit={ HEAD_COMMIT } \
41-
--base-branch={ BASE_BRANCH }
41+
--base-branch={ BASE_BRANCH } \
42+
--sort-label-keys={ SORT_LABEL_KEYS }
4243
```
4344

4445
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.
@@ -47,6 +48,13 @@ You can run it locally or integrate it to your CI system to run automatically wh
4748
pipectl plan-preview --help
4849
```
4950

51+
### Order of the results
52+
53+
By default, the results are sorted by PipedID and Application Name.
54+
55+
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.
56+
57+
5058
## GitHub Actions
5159

5260
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.

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"os"
23+
"sort"
2324
"strings"
2425
"time"
2526

@@ -49,6 +50,7 @@ type command struct {
4950
timeout time.Duration
5051
pipedHandleTimeout time.Duration
5152
checkInterval time.Duration
53+
sortLabelKeys []string
5254

5355
clientOptions *client.Options
5456
}
@@ -75,6 +77,7 @@ func NewCommand() *cobra.Command {
7577
cmd.Flags().StringVar(&c.out, "out", c.out, "Write planpreview result to the given path.")
7678
cmd.Flags().DurationVar(&c.timeout, "timeout", c.timeout, "Maximum amount of time this command has to complete. Default is 10m.")
7779
cmd.Flags().DurationVar(&c.pipedHandleTimeout, "piped-handle-timeout", c.pipedHandleTimeout, "Maximum amount of time that a Piped can take to handle. Default is 5m.")
80+
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.")
7881

7982
cmd.MarkFlagRequired("repo-remote-url")
8083
cmd.MarkFlagRequired("head-branch")
@@ -147,11 +150,32 @@ func (c *command) run(ctx context.Context, _ cli.Input) error {
147150
fmt.Printf("Failed to retrieve plan-preview results: %v\n", err)
148151
return err
149152
}
153+
sortResults(results, c.sortLabelKeys)
150154
return printResults(results, os.Stdout, c.out)
151155
}
152156
}
153157
}
154158

159+
// sortResults sorts the given results by pipedID and the given sortLabelKeys.
160+
// If sortLabelKeys is not specified or the all values of sortLabelKeys are the same, it sorts by pipedID and ApplicationName.
161+
func sortResults(allResults []*model.PlanPreviewCommandResult, sortLabelKeys []string) {
162+
sort.SliceStable(allResults, func(i, j int) bool {
163+
return allResults[i].PipedId < allResults[j].PipedId
164+
})
165+
for _, resultsPerPiped := range allResults {
166+
results := resultsPerPiped.Results
167+
sort.SliceStable(results, func(i, j int) bool {
168+
a, b := results[i], results[j]
169+
for _, key := range sortLabelKeys {
170+
if a.Labels[key] != b.Labels[key] {
171+
return a.Labels[key] < b.Labels[key]
172+
}
173+
}
174+
return a.ApplicationName < b.ApplicationName
175+
})
176+
}
177+
}
178+
155179
func printResults(results []*model.PlanPreviewCommandResult, stdout io.Writer, outFile string) error {
156180
r := convert(results)
157181

pkg/app/pipectl/cmd/planpreview/planpreview_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,123 @@ NOTE: An error occurred while building plan-preview for applications of the foll
244244
})
245245
}
246246
}
247+
func TestSortResults(t *testing.T) {
248+
t.Parallel()
249+
testcases := []struct {
250+
name string
251+
results []*model.PlanPreviewCommandResult
252+
sortLabelKeys []string
253+
expected []*model.PlanPreviewCommandResult
254+
}{
255+
{
256+
name: "sort by pipedID and application name",
257+
results: []*model.PlanPreviewCommandResult{
258+
{
259+
PipedId: "piped-2",
260+
Results: []*model.ApplicationPlanPreviewResult{
261+
{ApplicationName: "app-2"},
262+
{ApplicationName: "app-1"},
263+
},
264+
},
265+
{
266+
PipedId: "piped-1",
267+
Results: []*model.ApplicationPlanPreviewResult{
268+
{ApplicationName: "app-2"},
269+
{ApplicationName: "app-1"},
270+
},
271+
},
272+
},
273+
sortLabelKeys: []string{},
274+
expected: []*model.PlanPreviewCommandResult{
275+
{
276+
PipedId: "piped-1",
277+
Results: []*model.ApplicationPlanPreviewResult{
278+
{ApplicationName: "app-1"},
279+
{ApplicationName: "app-2"},
280+
},
281+
},
282+
{
283+
PipedId: "piped-2",
284+
Results: []*model.ApplicationPlanPreviewResult{
285+
{ApplicationName: "app-1"},
286+
{ApplicationName: "app-2"},
287+
},
288+
},
289+
},
290+
},
291+
{
292+
name: "sort by label keys",
293+
results: []*model.PlanPreviewCommandResult{
294+
{
295+
PipedId: "piped-1",
296+
Results: []*model.ApplicationPlanPreviewResult{
297+
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
298+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
299+
},
300+
},
301+
{
302+
PipedId: "piped-2",
303+
Results: []*model.ApplicationPlanPreviewResult{
304+
{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
305+
{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
306+
{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
307+
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
308+
},
309+
},
310+
},
311+
sortLabelKeys: []string{"env"},
312+
expected: []*model.PlanPreviewCommandResult{
313+
{
314+
PipedId: "piped-1",
315+
Results: []*model.ApplicationPlanPreviewResult{
316+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
317+
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
318+
},
319+
},
320+
{
321+
PipedId: "piped-2",
322+
Results: []*model.ApplicationPlanPreviewResult{
323+
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
324+
{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
325+
{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
326+
{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
327+
},
328+
},
329+
},
330+
},
331+
{
332+
name: "sort by multiple label keys",
333+
results: []*model.PlanPreviewCommandResult{
334+
{
335+
PipedId: "piped-1",
336+
Results: []*model.ApplicationPlanPreviewResult{
337+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
338+
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
339+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
340+
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
341+
},
342+
},
343+
},
344+
sortLabelKeys: []string{"env", "team"},
345+
expected: []*model.PlanPreviewCommandResult{
346+
{
347+
PipedId: "piped-1",
348+
Results: []*model.ApplicationPlanPreviewResult{
349+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
350+
{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
351+
{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
352+
{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
353+
},
354+
},
355+
},
356+
},
357+
}
358+
359+
for _, tc := range testcases {
360+
t.Run(tc.name, func(t *testing.T) {
361+
t.Parallel()
362+
sortResults(tc.results, tc.sortLabelKeys)
363+
assert.Equal(t, tc.expected, tc.results)
364+
})
365+
}
366+
}

0 commit comments

Comments
 (0)