From ca61b796afb4f7114be093781162604067fc25ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Tue, 8 Oct 2024 19:12:23 +0200 Subject: [PATCH] feat: use semantic versioning --- .github/workflows/distro.yml | 17 ++----- README.md | 2 +- build.go | 59 +++++++++++++---------- cmd/action.go | 13 +++-- cmd/cmd.go | 34 ++++++------- internal/registry/registry.go | 90 ++++++++++++++++++++++++++++++----- options.go | 5 +- release.go | 33 +++++++++---- releases/v0.1.8.md | 11 +++++ template.go | 11 +++-- 10 files changed, 185 insertions(+), 90 deletions(-) create mode 100644 releases/v0.1.8.md diff --git a/.github/workflows/distro.yml b/.github/workflows/distro.yml index 4333ed3..4d1620f 100644 --- a/.github/workflows/distro.yml +++ b/.github/workflows/distro.yml @@ -4,10 +4,6 @@ # A new release is made if a new version of k6 or one or more of the extensions # has been released since the last distribution release. # -# The distro release workflow should be scheduled once a day. -# More than one distribution release cannot be made in one day, -# because the release version is the date. -# # Usage # ----- # @@ -21,7 +17,7 @@ # ``` # on: # schedule: -# - cron: "0 0 * * *" +# - cron: "10 */2 * * *" # # jobs: # distro: @@ -134,17 +130,12 @@ jobs: - name: Latest Release Notes id: latest run: | - if gh api /repos/${{github.repository}}/releases/tag/$(date +%y.%m.%d) > /dev/null 2>/dev/null; then - echo "today=true" >> "$GITHUB_OUTPUT" - else - if gh api --jq .body /repos/${{github.repository}}/releases/latest > ${{env.NOTES_LATEST}} 2>/dev/null; then - echo "notes=${{env.NOTES_LATEST}}" >> "$GITHUB_OUTPUT" - fi + if gh api --jq .body /repos/${{github.repository}}/releases/latest > ${{env.NOTES_LATEST}} 2>/dev/null; then + echo "notes=${{env.NOTES_LATEST}}" >> "$GITHUB_OUTPUT" fi - name: Build Distro - uses: grafana/k6dist@v0.1.7 - if: ${{ steps.latest.outputs.today != 'true' }} + uses: grafana/k6dist@v0.1.8 id: build with: args: "${{ inputs.args }}" diff --git a/README.md b/README.md index b69bcc0..de7db4e 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ k6dist [flags] [registry-location] ``` --distro-name string distro name (default detect) - --distro-version string distro version (default current date in YY.MM.DD format) + --distro-version string distro version (default generated) --platform strings target platforms (default [linux/amd64,darwin/amd64,windows/amd64]) --executable string executable file name (default "dist/{{.Name}}_{{.OS}}_{{.Arch}}/k6{{.ExeExt}}") --archive string archive file name (default "dist/{{.Name}}_{{.Version}}_{{.OS}}_{{.Arch}}{{.ZipExt}}") diff --git a/build.go b/build.go index 0496863..e1b2ba8 100644 --- a/build.go +++ b/build.go @@ -11,30 +11,35 @@ import ( "path/filepath" "strings" + "github.com/Masterminds/semver/v3" "github.com/grafana/k6dist/internal/registry" "github.com/grafana/k6foundry" ) -func detectChange(registry registry.Registry, latest string) (bool, error) { +var initialVersion = semver.MustParse("v0.1.0") //nolint:gochecknoglobals + +func detectChange(reg registry.Registry, latest string) (bool, *semver.Version, error) { if len(latest) == 0 { - return true, nil + return true, initialVersion, nil } contents, err := os.ReadFile(filepath.Clean(latest)) //nolint:forbidigo if err != nil { - return false, err + return false, nil, err } - found, modules, err := parseNotes(contents) + found, version, modules, err := parseNotes(contents) if err != nil { - return false, err + return false, nil, err } if !found { - return false, nil + return true, initialVersion, nil } - return registry.AddLatest(modules), nil + bumped := reg.AddLatest(modules, version) + + return !bumped.Equal(version), bumped, nil } func newBuilder(ctx context.Context, modules registry.Modules) (k6foundry.Builder, error) { @@ -58,35 +63,39 @@ func newBuilder(ctx context.Context, modules registry.Modules) (k6foundry.Builde } // Build builds k6 binaries and archives based on opts parameter. -func Build(ctx context.Context, opts *Options) (bool, error) { +func Build(ctx context.Context, opts *Options) (bool, *semver.Version, error) { opts.setDefaults() registry, err := registry.LoadRegistry(ctx, opts.Registry) if err != nil { - return false, err + return false, nil, err } - changed, err := detectChange(registry, opts.NotesLatest) + changed, version, err := detectChange(registry, opts.NotesLatest) if err != nil { - return false, err + return false, nil, err } if !changed { - return false, nil + return false, nil, nil + } + + if opts.Version != nil { + version = opts.Version } - notes, err := expandNotes(opts.Name, opts.Version, registry, opts.NotesTemplate) + notes, err := expandNotes(opts.Name, version, registry, opts.NotesTemplate) if err != nil { - return false, err + return false, nil, err } - filename, err := expandAsTargetPath("notes", opts.Notes, newInstsanceData(opts.Name, opts.Version, &Platform{})) + filename, err := expandAsTargetPath("notes", opts.Notes, newInstsanceData(opts.Name, version, &Platform{})) if err != nil { - return false, err + return false, nil, err } if err := os.WriteFile(filename, []byte(notes), 0o600); err != nil { //nolint:forbidigo - return false, err + return false, nil, err } modules := registry.ToModules() @@ -94,39 +103,39 @@ func Build(ctx context.Context, opts *Options) (bool, error) { builder, err := newBuilder(ctx, modules) if err != nil { - return false, err + return false, nil, err } for _, platform := range opts.Platforms { - data := newInstsanceData(opts.Name, opts.Version, platform) + data := newInstsanceData(opts.Name, version, platform) filename, err := expandAsTargetPath("executable", opts.Executable, data) if err != nil { - return false, err + return false, nil, err } err = buildExecutable(ctx, builder, platform, k6Version, mods, filename) if err != nil { - return false, err + return false, nil, err } err = createDockerfile(filename, opts.DockerfileTemplate, opts.Dockerfile) if err != nil { - return false, err + return false, nil, err } archive, err := expandAsTargetPath("archive", opts.Archive, data) if err != nil { - return false, err + return false, nil, err } err = buildArchive(archive, filename, opts.Readme, opts.License) if err != nil { - return false, err + return false, nil, err } } - return true, nil + return true, version, nil } func buildExecutable( diff --git a/cmd/action.go b/cmd/action.go index 37222fc..1d084ee 100644 --- a/cmd/action.go +++ b/cmd/action.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/Masterminds/semver/v3" "github.com/google/shlex" "github.com/spf13/pflag" ) @@ -57,7 +58,7 @@ func ghinput(args []string, flag *pflag.Flag) []string { } //nolint:forbidigo -func emitOutput(changed bool, version string) error { +func emitOutput(changed bool, version *semver.Version) error { ghOutput := os.Getenv("GITHUB_OUTPUT") if len(ghOutput) == 0 { return nil @@ -75,11 +76,13 @@ func emitOutput(changed bool, version string) error { return err } - slog.Debug("Emit version", "version", version) + if version != nil { + slog.Debug("Emit version", "version", version.Original()) - _, err = fmt.Fprintf(file, "version=%s\n", version) - if err != nil { - return err + _, err = fmt.Fprintf(file, "version=%s\n", version.Original()) + if err != nil { + return err + } } return file.Close() diff --git a/cmd/cmd.go b/cmd/cmd.go index d7db2d5..5fb4791 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -10,8 +10,8 @@ import ( "path/filepath" "runtime" "strings" - "time" + "github.com/Masterminds/semver/v3" "github.com/grafana/k6dist" "github.com/spf13/cobra" ) @@ -25,6 +25,7 @@ type options struct { verbose bool single bool platforms []string + version string } var defaultPlatforms = []string{ //nolint:gochecknoglobals @@ -50,12 +51,12 @@ func New(levelVar *slog.LevelVar) *cobra.Command { return preRun(args, levelVar, opts) }, RunE: func(cmd *cobra.Command, _ []string) error { - changed, err := run(cmd, opts) + changed, version, err := run(cmd, opts) if err != nil { return err } - return emitOutput(changed, opts.Version) + return emitOutput(changed, version) }, } @@ -66,7 +67,7 @@ func New(levelVar *slog.LevelVar) *cobra.Command { flags.SortFlags = false flags.StringVar(&opts.Name, "distro-name", "", "distro name (default detect)") - flags.StringVar(&opts.Version, "distro-version", "", "distro version (default current date in YY.MM.DD format)") + flags.StringVar(&opts.version, "distro-version", "", "distro version (default generated)") flags.StringSliceVar(&opts.platforms, "platform", defaultPlatforms, "target platforms") flags.StringVar(&opts.Executable, "executable", k6dist.DefaultExecutableTemplate, "executable file name") flags.StringVar(&opts.Archive, "archive", k6dist.DefaultArchiveTemplate, "archive file name") @@ -107,8 +108,13 @@ func preRun(args []string, levelVar *slog.LevelVar, opts *options) error { opts.Name = guessName(opts.Registry) } - if len(opts.Version) == 0 { - opts.Version = defaultDate() + if len(opts.version) > 0 { + ver, err := semver.NewVersion(opts.version) + if err != nil { + return err + } + + opts.Version = ver } if len(opts.Readme) == 0 { @@ -133,22 +139,22 @@ func preRun(args []string, levelVar *slog.LevelVar, opts *options) error { return nil } -func run(_ *cobra.Command, opts *options) (bool, error) { +func run(_ *cobra.Command, opts *options) (bool, *semver.Version, error) { if len(opts.Name) == 0 { cwd, err := os.Getwd() //nolint:forbidigo if err != nil { - return false, err + return false, nil, err } opts.Name = filepath.Base(cwd) } - changed, err := k6dist.Build(context.TODO(), &opts.Options) + changed, version, err := k6dist.Build(context.TODO(), &opts.Options) if err != nil { - return false, err + return false, nil, err } - return changed, nil + return changed, version, nil } func guessName(source string) string { @@ -196,12 +202,6 @@ func findLicense() string { return findTextFile("LICENSE") } -func defaultDate() string { - now := time.Now() - - return now.Format("06.01.02") -} - func parsePlatforms(values []string) ([]*k6dist.Platform, error) { platforms := make([]*k6dist.Platform, 0, len(values)) diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 804eda5..26e9112 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -10,6 +10,8 @@ import ( "path/filepath" "sort" "strings" + + "github.com/Masterminds/semver/v3" ) // ToModules creates a module list from the registry. @@ -107,28 +109,92 @@ func loadRegistryHTTP(ctx context.Context, source string) (Registry, error) { return reg, nil } +type bumpType int + +const ( + bumpNone bumpType = iota + bumpPatch + bumpMinor + bumpMajor +) + +func (b bumpType) max(val bumpType) bumpType { + if val > b { + return val + } + + return b +} + +func (b bumpType) apply(version *semver.Version) *semver.Version { + var ver semver.Version + + switch b { + case bumpMajor: + ver = version.IncMajor() + case bumpMinor: + ver = version.IncMinor() + case bumpPatch: + ver = version.IncPatch() + case bumpNone: + fallthrough + default: + return version + } + + return &ver +} + +func detectBump(current, previous string) bumpType { + curr, err := semver.NewVersion(current) + if err != nil { + return bumpNone + } + + prev, err := semver.NewVersion(previous) + if err != nil { + return bumpNone + } + + if curr.Equal(prev) { + return bumpNone + } + + if curr.Major() != prev.Major() { + return bumpMajor + } + + if curr.Minor() != prev.Minor() { + return bumpMinor + } + + if curr.Patch() != prev.Patch() { + return bumpPatch + } + + return bumpNone +} + // AddLatest adds latest versions to extensions. -func (reg Registry) AddLatest(modules Modules) bool { +func (reg Registry) AddLatest(modules Modules, version *semver.Version) *semver.Version { regAsMap := make(map[string]*Extension, len(reg)) for idx := range reg { regAsMap[reg[idx].Module] = ®[idx] } - changed := false + bump := bumpNone for _, mod := range modules { ext, found := regAsMap[mod.Path] - if found { - ext.Versions[1] = mod.Version - changed = changed || (ext.Versions[0] != mod.Version) + if !found { // module removed + bump = bump.max(bumpMajor) + continue } - changed = changed || !found - } + ext.Versions[1] = mod.Version - if changed { - return true + bump = bump.max(detectBump(ext.Versions[0], ext.Versions[1])) } modulesAsMap := make(map[string]*Module, len(modules)) @@ -138,10 +204,10 @@ func (reg Registry) AddLatest(modules Modules) bool { } for _, ext := range reg { - if _, found := modulesAsMap[ext.Module]; !found { - return true + if _, found := modulesAsMap[ext.Module]; !found { // module added + bump = bump.max(bumpMinor) } } - return false + return bump.apply(version) } diff --git a/options.go b/options.go index 18767cb..bb6e8d8 100644 --- a/options.go +++ b/options.go @@ -3,6 +3,8 @@ package k6dist import ( "runtime" + + "github.com/Masterminds/semver/v3" ) // Options contains the optional parameters of the Build function. @@ -11,8 +13,7 @@ type Options struct { // Templating is not supported. Name string // Version contains distribution version. - // Templating is not supported. - Version string + Version *semver.Version // Executable is the name of the k6 executable file to be built. // Templating is supported. // It defaults to DefaultExecutableTemplate. diff --git a/release.go b/release.go index 7e10d8d..2c3a122 100644 --- a/release.go +++ b/release.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" + "github.com/Masterminds/semver/v3" "github.com/grafana/k6dist/internal/registry" ) @@ -19,7 +20,12 @@ const ( notesFooterEnd = "```-->" ) -func notesFooter(registry registry.Registry) (string, error) { +type footerData struct { + Version string `json:"version,omitempty"` + Modules registry.Modules `json:"modules,omitempty"` +} + +func notesFooter(version *semver.Version, registry registry.Registry) (string, error) { var buff bytes.Buffer buff.WriteString(notesFooterBegin) @@ -29,7 +35,9 @@ func notesFooter(registry registry.Registry) (string, error) { encoder.SetEscapeHTML(false) - if err := encoder.Encode(registry.ToModules()); err != nil { + data := &footerData{Version: version.Original(), Modules: registry.ToModules()} + + if err := encoder.Encode(data); err != nil { return "", err } @@ -39,7 +47,7 @@ func notesFooter(registry registry.Registry) (string, error) { return buff.String(), nil } -func expandNotes(name, version string, reg registry.Registry, tmplfile string) (string, error) { +func expandNotes(name string, version *semver.Version, reg registry.Registry, tmplfile string) (string, error) { var tmplsrc string if len(tmplfile) != 0 { @@ -61,20 +69,25 @@ func expandNotes(name, version string, reg registry.Registry, tmplfile string) ( return expandTemplate("notes", tmplsrc, data) } -var reModules = regexp.MustCompile("(?ms:^" + notesFooterBegin + "(?P.*)" + notesFooterEnd + ")") +var reModules = regexp.MustCompile("(?ms:^" + notesFooterBegin + "(?P.*)" + notesFooterEnd + ")") -func parseNotes(notes []byte) (bool, registry.Modules, error) { +func parseNotes(notes []byte) (bool, *semver.Version, registry.Modules, error) { match := reModules.FindSubmatch(notes) if match == nil { - return false, nil, nil + return false, nil, nil, nil } - var modules registry.Modules + var data footerData - if err := json.Unmarshal(match[reModules.SubexpIndex("modules")], &modules); err != nil { - return false, nil, err + if err := json.Unmarshal(match[reModules.SubexpIndex("state")], &data); err != nil { + return false, nil, nil, err + } + + version, err := semver.NewVersion(data.Version) + if err != nil { + return false, nil, nil, err } - return true, modules, nil + return true, version, data.Modules, nil } diff --git a/releases/v0.1.8.md b/releases/v0.1.8.md new file mode 100644 index 0000000..f1471a0 --- /dev/null +++ b/releases/v0.1.8.md @@ -0,0 +1,11 @@ +🎉 k6dist `v0.1.8` is here! + +**Use semantic versioning** + +Semantic versioning is widespread and well-known and has many advantages. It is advisable to switch from the date-based version to the use of the semantic version. + +Assuming that k6 and its extensions use semantic versioning, the semantic version of the distribution will be generated as follows: + +- if a major version of a module (k6 or extension) is bumped, or a module is removed, the major version will be bumped (potential breaking change) +- if the minor version of any module (k6 or extension) is bumped or a new module is added, the minor version will be bumped (backward compatible feature change) +- otherwise, the patch version will be bumped \ No newline at end of file diff --git a/template.go b/template.go index 17712a2..004385e 100644 --- a/template.go +++ b/template.go @@ -6,6 +6,7 @@ import ( "path/filepath" "text/template" + "github.com/Masterminds/semver/v3" sprig "github.com/go-task/slim-sprig/v3" "github.com/grafana/k6dist/internal/registry" @@ -27,7 +28,7 @@ type releaseData struct { Footer string } -func newInstsanceData(name, version string, platform *Platform) *instanceData { +func newInstsanceData(name string, version *semver.Version, platform *Platform) *instanceData { exe := "" zip := ".tar.gz" @@ -38,7 +39,7 @@ func newInstsanceData(name, version string, platform *Platform) *instanceData { return &instanceData{ Name: name, - Version: version, + Version: version.Original(), OS: platform.OS, Arch: platform.Arch, ExeExt: exe, @@ -46,13 +47,13 @@ func newInstsanceData(name, version string, platform *Platform) *instanceData { } } -func newReleaseData(name, version string, reg registry.Registry) (*releaseData, error) { - footer, err := notesFooter(reg) +func newReleaseData(name string, version *semver.Version, reg registry.Registry) (*releaseData, error) { + footer, err := notesFooter(version, reg) if err != nil { return nil, err } - return &releaseData{Name: name, Version: version, Registry: reg, Footer: footer}, nil + return &releaseData{Name: name, Version: version.Original(), Registry: reg, Footer: footer}, nil } func expandTemplate(name string, tmplsrc string, data interface{}) (string, error) {