Skip to content

Commit 33c66aa

Browse files
authored
Merge pull request #240 from grafana/234-produce-a-change-log-for-the-sync-command
Added xk6 sync output in various formats
2 parents 773e70e + 50f0397 commit 33c66aa

File tree

6 files changed

+229
-34
lines changed

6 files changed

+229
-34
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@ xk6 sync [flags]
437437
```
438438
-k, --k6-version string The k6 version to use for synchronization (default from go.mod)
439439
-n, --dry-run Do not make any changes, only log them
440+
-o, --out string Write output to file instead of stdout
441+
--json Generate JSON output
442+
-c, --compact Compact instead of pretty-printed JSON output
443+
-m, --markdown Generate Markdown output
440444
```
441445

442446
## Global Flags

internal/cmd/lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func lintRunE(ctx context.Context, args []string, opts *options) (result error)
172172
return nil
173173
}
174174

175-
func jsonOutput(compliance *lint.Compliance, output io.Writer, compact bool) error {
175+
func jsonOutput(compliance any, output io.Writer, compact bool) error {
176176
encoder := json.NewEncoder(output)
177177

178178
if !compact {

internal/cmd/sync.go

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ package cmd
33
import (
44
"context"
55
_ "embed"
6+
"fmt"
7+
"io"
8+
"os"
69

10+
"github.com/Masterminds/semver/v3"
11+
"github.com/fatih/color"
12+
"github.com/mattn/go-colorable"
713
"github.com/spf13/cobra"
814
"go.k6.io/xk6/internal/sync"
915
)
@@ -14,6 +20,11 @@ var syncHelp string
1420
type syncOptions struct {
1521
k6version string
1622
dryRun bool
23+
out string
24+
compact bool
25+
json bool
26+
markdown bool
27+
quiet bool
1728
}
1829

1930
func syncCmd() *cobra.Command {
@@ -24,11 +35,12 @@ func syncCmd() *cobra.Command {
2435
Short: shortHelp(syncHelp),
2536
Long: syncHelp,
2637
Args: cobra.NoArgs,
38+
PreRun: func(cmd *cobra.Command, _ []string) {
39+
opts.json = opts.json || opts.compact
40+
opts.quiet = cmd.Flags().Lookup("quiet").Changed
41+
},
2742
RunE: func(cmd *cobra.Command, _ []string) error {
28-
return sync.Sync(cmd.Context(), ".", &sync.Options{
29-
DryRun: opts.dryRun,
30-
K6Version: opts.k6version,
31-
})
43+
return syncRunE(cmd.Context(), opts)
3244
},
3345
DisableAutoGenTag: true,
3446
}
@@ -43,6 +55,120 @@ func syncCmd() *cobra.Command {
4355
"The k6 version to use for synchronization (default from go.mod)")
4456
flags.BoolVarP(&opts.dryRun, "dry-run", "n", false,
4557
"Do not make any changes, only log them")
58+
flags.StringVarP(&opts.out, "out", "o", "",
59+
"Write output to file instead of stdout")
60+
flags.BoolVar(&opts.json, "json", false,
61+
"Generate JSON output")
62+
flags.BoolVarP(&opts.compact, "compact", "c", false,
63+
"Compact instead of pretty-printed JSON output")
64+
flags.BoolVarP(&opts.markdown, "markdown", "m", false,
65+
"Generate Markdown output")
4666

4767
return cmd
4868
}
69+
70+
func syncRunE(ctx context.Context, opts *syncOptions) (problem error) {
71+
result, err := sync.Sync(ctx, ".", &sync.Options{
72+
DryRun: opts.dryRun,
73+
K6Version: opts.k6version,
74+
})
75+
if err != nil {
76+
return err
77+
}
78+
79+
output := colorable.NewColorableStdout()
80+
81+
if len(opts.out) > 0 {
82+
file, err := os.Create(opts.out)
83+
if err != nil {
84+
return err
85+
}
86+
87+
defer func() {
88+
err := file.Close()
89+
if problem == nil && err != nil {
90+
problem = err
91+
}
92+
}()
93+
94+
output = file
95+
}
96+
97+
if opts.quiet {
98+
return nil
99+
}
100+
101+
if opts.json {
102+
return jsonOutput(result, output, opts.compact)
103+
}
104+
105+
if opts.markdown {
106+
return markdownSyncOutput(result, output)
107+
}
108+
109+
textSyncOutput(result, output)
110+
111+
return nil
112+
}
113+
114+
func textSyncOutput(result *sync.Result, output io.Writer) {
115+
bold := color.New(color.FgHiWhite, color.Bold).SprintfFunc()
116+
117+
downgrade := color.New(color.FgYellow).FprintfFunc()
118+
upgrade := color.New(color.FgGreen).FprintfFunc()
119+
plain := color.New(color.FgWhite).FprintfFunc()
120+
121+
plain(output, "Dependencies have been synchronized with k6 %s.\n\n", bold(result.K6Version))
122+
123+
plain(output, "Changes\n───────\n")
124+
125+
for _, change := range result.Changes {
126+
fprintf := downgrade
127+
symbol := "▼"
128+
129+
if isUpgrade(change) {
130+
fprintf = upgrade
131+
symbol = "▲"
132+
}
133+
134+
fprintf(output, "%s %s\n", symbol, change.Module)
135+
plain(output, " %s => %s\n", change.From, change.To)
136+
}
137+
138+
plain(output, "\n")
139+
}
140+
141+
func isUpgrade(change *sync.Change) bool {
142+
to, err := semver.NewVersion(change.To)
143+
if err != nil {
144+
return false
145+
}
146+
147+
from, err := semver.NewVersion(change.From)
148+
if err != nil {
149+
return false
150+
}
151+
152+
return to.GreaterThan(from)
153+
}
154+
155+
func markdownSyncOutput(result *sync.Result, output io.Writer) error {
156+
_, err := fmt.Fprintf(output, "Dependencies have been synchronized with k6 `%s`.\n\n", result.K6Version)
157+
if err != nil {
158+
return err
159+
}
160+
161+
_, err = fmt.Fprintf(output, "**Changes**\n\n")
162+
if err != nil {
163+
return err
164+
}
165+
166+
for _, change := range result.Changes {
167+
_, err = fmt.Fprintf(output, "- %s\n `%s` => `%s`\n", change.Module, change.From, change.To)
168+
if err != nil {
169+
return err
170+
}
171+
}
172+
173+
return nil
174+
}

internal/sync/sync.go

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,76 +25,111 @@ const (
2525

2626
var errHTTP = errors.New("HTTP error")
2727

28+
// Change represents a change in a module dependency.
29+
type Change struct {
30+
// Module is the module path.
31+
Module string `json:"module,omitempty"`
32+
// From is the version being replaced.
33+
From string `json:"from,omitempty"`
34+
// To is the version being replaced with.
35+
To string `json:"to,omitempty"`
36+
}
37+
38+
// Result represents the result of a synchronization operation.
39+
type Result struct {
40+
// The k6 version used for synchronization.
41+
K6Version string `json:"k6_version,omitempty"`
42+
// Changes is a list of changes made to the module dependencies.
43+
Changes []*Change `json:"changes,omitempty"`
44+
}
45+
2846
// Sync synchronizes the versions of the module dependencies in the specified directory with k6.
29-
func Sync(ctx context.Context, dir string, opts *Options) error {
30-
slog.Info("Syncing dependencies with k6")
47+
func Sync(ctx context.Context, dir string, opts *Options) (*Result, error) {
48+
slog.Debug("Syncing dependencies with k6")
3149

3250
extModfile, err := loadModfile(dir)
3351
if err != nil {
34-
return err
52+
return nil, err
3553
}
3654

3755
k6Version, err := getK6Version(ctx, opts, extModfile)
3856
if err != nil {
39-
return err
57+
return nil, err
4058
}
4159

42-
slog.Info("Target k6", "version", k6Version)
60+
slog.Debug("Target k6", "version", k6Version)
4361

4462
k6Modfile, err := getModule(ctx, k6Module, k6Version)
4563
if err != nil {
46-
return err
64+
return nil, err
65+
}
66+
67+
result := &Result{
68+
K6Version: k6Version,
69+
Changes: diffRequires(extModfile, k6Modfile),
4770
}
4871

49-
patch := diffRequires(extModfile, k6Modfile)
50-
if len(patch) == 0 {
51-
slog.Info("No changes needed")
72+
if len(result.Changes) == 0 {
73+
slog.Debug("No changes needed")
5274

53-
return nil
75+
return result, nil
5476
}
5577

5678
if opts.DryRun {
57-
slog.Warn("Not saving changes, dry run")
79+
slog.Debug("Not saving changes, dry run")
5880

59-
return nil
81+
return result, nil
6082
}
6183

62-
patch = append(patch, "") // make space for the "get" command
63-
copy(patch[1:], patch[0:])
64-
patch[0] = "get"
84+
patch := make([]string, 0, len(result.Changes)+1) // +1 for the "get" command
85+
86+
// Prepare the patch command to update go.mod
87+
patch = append(patch, "get")
88+
89+
// Add each change to the patch command
90+
for _, change := range result.Changes {
91+
slog.Debug("Updating dependency", "module", change.Module, "from", change.From, "to", change.To)
92+
patch = append(patch, fmt.Sprintf("%s@%s", change.Module, change.To))
93+
}
6594

66-
slog.Info("Updating go.mod")
95+
slog.Debug("Updating go.mod")
6796

6897
cmd := exec.Command("go", patch...) // #nosec G204
6998

7099
cmd.Stdout = os.Stdout
71100
cmd.Stderr = os.Stderr
72101

73-
return cmd.Run()
102+
if err := cmd.Run(); err != nil {
103+
return nil, err
104+
}
105+
106+
return result, nil
74107
}
75108

76109
// GetLatestK6Version retrieves the latest version of k6 from the Go proxy.
77110
func GetLatestK6Version(ctx context.Context) (string, error) {
78111
return getLatestVersion(ctx, k6Module)
79112
}
80113

81-
func diffRequires(extModfile, k6Modfile *modfile.File) []string {
82-
patch := make([]string, 0)
114+
func diffRequires(extModfile, k6Modfile *modfile.File) []*Change {
115+
changes := make([]*Change, 0)
83116

84117
for _, k6Require := range k6Modfile.Require {
85118
k6Modpath, k6Modversion := k6Require.Mod.Path, k6Require.Mod.Version
86119

87120
for _, extRequire := range extModfile.Require {
88121
extModpath, extModversion := extRequire.Mod.Path, extRequire.Mod.Version
89122
if k6Modpath == extModpath && k6Modversion != extModversion {
90-
slog.Info("Sync", "module", k6Modpath, "from", extModversion, "to", k6Modversion)
91-
92-
patch = append(patch, fmt.Sprintf("%s@%s", k6Modpath, k6Modversion))
123+
changes = append(changes, &Change{
124+
Module: k6Modpath,
125+
From: extModversion,
126+
To: k6Modversion,
127+
})
93128
}
94129
}
95130
}
96131

97-
return patch
132+
return changes
98133
}
99134

100135
func getK6Version(ctx context.Context, opts *Options, mf *modfile.File) (string, error) {

internal/sync/sync_internal_test.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sync
22

33
import (
4+
"reflect"
45
"testing"
56

67
"golang.org/x/mod/modfile"
@@ -45,10 +46,16 @@ func TestDiffRequires_OneDifference(t *testing.T) {
4546
},
4647
}
4748

48-
want := []string{"github.com/foo/[email protected]"}
49+
want := []*Change{
50+
{
51+
Module: "github.com/foo/bar",
52+
From: "v1.2.3",
53+
To: "v1.2.4",
54+
},
55+
}
4956

5057
got := diffRequires(ext, k6)
51-
if len(got) != 1 || got[0] != want[0] {
58+
if reflect.DeepEqual(got, want) == false {
5259
t.Errorf("expected %v, got %v", want, got)
5360
}
5461
}
@@ -70,13 +77,22 @@ func TestDiffRequires_MultipleDifferences(t *testing.T) {
7077
},
7178
}
7279

73-
want := []string{
74-
"github.com/foo/[email protected]",
75-
"github.com/baz/[email protected]",
80+
want := []*Change{
81+
{
82+
Module: "github.com/foo/bar",
83+
84+
From: "v1.2.3",
85+
To: "v1.2.4",
86+
},
87+
{
88+
Module: "github.com/baz/qux",
89+
From: "v2.0.0",
90+
To: "v2.1.0",
91+
},
7692
}
7793

7894
got := diffRequires(ext, k6)
79-
if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
95+
if !reflect.DeepEqual(got, want) {
8096
t.Errorf("expected %v, got %v", want, got)
8197
}
8298
}

releases/v1.1.2.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Grafana **xk6** `v1.1.2` is here! 🎉
2+
3+
## Bug Fixes
4+
5+
- The `xk6 build` command now correctly logs the **k6 version** being used. The version number is emphasized, and a warning is displayed if it's not the latest.
6+
- `xk6 build` now respects `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables, allowing it to work in proxied environments.
7+
8+
## New Feature
9+
10+
The **`xk6 sync`** command can now generate results in multiple formats:
11+
* **Terminal text**: The default, colored output.
12+
* **JSON**: Use the `--json` flag for standard JSON output or combine it with `--compact` for unindented JSON.
13+
* **Markdown**: Use the `--markdown` flag to generate a Markdown report, which is useful for changelogs.
14+

0 commit comments

Comments
 (0)