From 4452c010dfa0a95a26df0cceda0ed97a1201128f Mon Sep 17 00:00:00 2001 From: Guillermo Gaston Date: Fri, 20 Oct 2023 22:48:05 +0000 Subject: [PATCH] Make release notes tool not dependent on local git Now all the date is retrieved through GitHub APIs, making the tool more portable and easier to use. It should not increase the rate limiting chances since it now performs less API requests (by getting the label from all PRs at once instead of with one request per PR). --- hack/tools/go.mod | 28 - hack/tools/go.sum | 72 --- hack/tools/release/notes/generator.go | 82 +++ hack/tools/release/notes/github.go | 205 +++++++ hack/tools/release/notes/list.go | 154 +++++ hack/tools/release/notes/list_test.go | 67 ++ hack/tools/release/notes/main.go | 578 ++++-------------- hack/tools/release/notes/main_test.go | 67 +- hack/tools/release/notes/print.go | 172 ++++++ hack/tools/release/notes/process.go | 253 ++++++++ hack/tools/release/notes/ref.go | 54 ++ .../notes/release_notes_integration_test.go | 72 +-- .../tools/release/notes/test/golden/v1.5.0.md | 8 +- 13 files changed, 1196 insertions(+), 616 deletions(-) create mode 100644 hack/tools/release/notes/generator.go create mode 100644 hack/tools/release/notes/github.go create mode 100644 hack/tools/release/notes/list.go create mode 100644 hack/tools/release/notes/list_test.go create mode 100644 hack/tools/release/notes/print.go create mode 100644 hack/tools/release/notes/process.go create mode 100644 hack/tools/release/notes/ref.go diff --git a/hack/tools/go.mod b/hack/tools/go.mod index bb2d69db4f5a..5f933eb89a4c 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -20,7 +20,6 @@ require ( k8s.io/client-go v0.28.4 k8s.io/klog/v2 v2.100.1 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 - k8s.io/kubectl v0.28.4 k8s.io/utils v0.0.0-20231127182322-b307cd553661 sigs.k8s.io/cluster-api v0.0.0-00010101000000-000000000000 sigs.k8s.io/cluster-api/test v0.0.0-00010101000000-000000000000 @@ -36,7 +35,6 @@ require ( cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.5 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect @@ -48,10 +46,8 @@ require ( github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/daviddengcn/go-colortext v1.0.0 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v24.0.7+incompatible // indirect @@ -61,11 +57,8 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect - github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fatih/camelcase v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fvbommel/sortorder v1.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -76,7 +69,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/btree v1.0.1 // indirect github.com/google/cel-go v0.16.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -84,61 +76,45 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.4.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.13 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/lithammer/dedent v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/viper v1.18.1 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/xlab/treeprint v1.2.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect - go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect @@ -164,13 +140,9 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.28.4 // indirect - k8s.io/cli-runtime v0.28.4 // indirect k8s.io/cluster-bootstrap v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect - k8s.io/component-helpers v0.28.4 // indirect - k8s.io/metrics v0.28.4 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) diff --git a/hack/tools/go.sum b/hack/tools/go.sum index 04f8c4219b6d..8960321be5c4 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -10,7 +10,6 @@ cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXE cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= @@ -28,7 +27,6 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -39,11 +37,6 @@ github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= @@ -52,15 +45,11 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= github.com/coredns/corefile-migration v1.0.21 h1:W/DCETrHDiFo0Wj03EyMkaQ9fwsmSgqTCQDHpceaSsE= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE= -github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -84,17 +73,11 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -133,13 +116,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= -github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A= -github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE= -github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ= -github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.16.1 h1:3hZfSNiAU3KOiNtxuFXVp5WFy4hf/Ly3Sa4/7F8SXNo= github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -149,7 +125,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -167,8 +142,6 @@ github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdf github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= @@ -177,9 +150,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= @@ -187,10 +157,6 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -203,10 +169,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= -github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -216,29 +178,20 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= @@ -250,8 +203,6 @@ github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7X github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -266,13 +217,10 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -284,8 +232,6 @@ github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNo github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM= @@ -294,7 +240,6 @@ github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ai github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -309,8 +254,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -326,8 +269,6 @@ go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ3 go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -385,18 +326,15 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= @@ -491,24 +429,16 @@ k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= -k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q= -k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k= k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q= k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc= k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= -k8s.io/component-helpers v0.28.4 h1:+X9VXT5+jUsRdC26JyMZ8Fjfln7mSjgumafocE509C4= -k8s.io/component-helpers v0.28.4/go.mod h1:8LzMalOQ0K10tkBJWBWq8h0HTI9HDPx4WT3QvTFn9Ro= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= -k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= -k8s.io/metrics v0.28.4 h1:u36fom9+6c8jX2sk8z58H0hFaIUfrPWbXIxN7GT2blk= -k8s.io/metrics v0.28.4/go.mod h1:bBqAJxH20c7wAsTQxDXOlVqxGMdce49d7WNr1WeaLac= k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= @@ -521,8 +451,6 @@ sigs.k8s.io/kubebuilder/docs/book/utils v0.0.0-20211028165026-57688c578b5d h1:KL sigs.k8s.io/kubebuilder/docs/book/utils v0.0.0-20211028165026-57688c578b5d/go.mod h1:NRdZafr4zSCseLQggdvIMXa7umxf+Q+PJzrj3wFwiGE= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= -sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3 h1:vq2TtoDcQomhy7OxXLUOzSbHMuMYq0Bjn93cDtJEdKw= -sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/hack/tools/release/notes/generator.go b/hack/tools/release/notes/generator.go new file mode 100644 index 000000000000..49e6e92e7e8b --- /dev/null +++ b/hack/tools/release/notes/generator.go @@ -0,0 +1,82 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +// notesGenerator orchestrates the release notes generation. +// Lists the selected PRs for this collection of notes, +// process them to generate one entry per PR and then +// formats and prints the results. +type notesGenerator struct { + lister prLister + processor prProcessor + printer entriesPrinter +} + +func newNotesGenerator(lister prLister, processor prProcessor, printer entriesPrinter) *notesGenerator { + return ¬esGenerator{ + lister: lister, + processor: processor, + printer: printer, + } +} + +// PR is a GitHub PR. +type pr struct { + number uint64 + title string + labels []string +} + +// prLister returns a list of PRs. +type prLister interface { + listPRs() ([]pr, error) +} + +// notesEntry represents a line item for the release notes. +type notesEntry struct { + title string + section string + prNumber string +} + +// prProcessor generates notes entries for a list of PRs. +type prProcessor interface { + process([]pr) []notesEntry +} + +// entriesPrinter formats and outputs to stdout the notes +// based on a list of entries. +type entriesPrinter interface { + print([]notesEntry) +} + +// run generates and prints the notes. +func (g *notesGenerator) run() error { + prs, err := g.lister.listPRs() + if err != nil { + return err + } + + entries := g.processor.process(prs) + + g.printer.print(entries) + + return nil +} diff --git a/hack/tools/release/notes/github.go b/hack/tools/release/notes/github.go new file mode 100644 index 000000000000..8a93d3803706 --- /dev/null +++ b/hack/tools/release/notes/github.go @@ -0,0 +1,205 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "fmt" + "log" + "math" + "os/exec" + "strings" + "time" +) + +// githubClient uses the gh CLI to make API request to GitHub. +type githubClient struct { + // repo is full [org]/[repo_name] + repo string +} + +// githubDiff is the API response for the "compare" endpoint. +type githubDiff struct { + // MergeBaseCommit points to most recent common ancestor between two references. + MergeBaseCommit githubCommitNode `json:"merge_base_commit"` + Commits []githubCommitNode `json:"commits"` + Total int `json:"total_commits"` +} + +type githubCommitNode struct { + Commit githubCommit `json:"commit"` +} + +type githubCommitter struct { + Date time.Time `json:"date"` +} + +// getDiffAllCommits calls the `compare` endpoint, iterating over all pages and aggregating results. +func (c githubClient) getDiffAllCommits(base, head string) (*githubDiff, error) { + pageSize := 250 + url := fmt.Sprintf("repos/%s/compare/%s...%s", c.repo, base, head) + + diff, commits, err := iterate(c, url, pageSize, nil, func(page *githubDiff) ([]githubCommitNode, int) { + return page.Commits, page.Total + }) + if err != nil { + return nil, err + } + + diff.Commits = commits + + return diff, nil +} + +// githubRef is the API response for the "ref" endpoint. +type githubRef struct { + Object githubObject `json:"object"` +} + +type objectType string + +const ( + commitType objectType = "commit" + tagType objectType = "tag" +) + +type githubObject struct { + ObjectType objectType `json:"type"` + SHA string `json:"sha"` +} + +// getRef calls the `git/ref` endpoint. +func (c githubClient) getRef(ref string) (githubRef, error) { + refResponse := githubRef{} + if err := c.runGHAPICommand(fmt.Sprintf("repos/%s/git/ref/%s", c.repo, ref), &refResponse); err != nil { + return githubRef{}, err + } + return refResponse, nil +} + +// githubTag is the API response for the "tags" endpoint. +type githubTag struct { + Object githubObject `json:"object"` +} + +// getTag calls the `tags` endpoint. +func (c githubClient) getTag(tagSHA string) (githubTag, error) { + tagResponse := githubTag{} + if err := c.runGHAPICommand(fmt.Sprintf("repos/%s/git/tags/%s", c.repo, tagSHA), &tagResponse); err != nil { + return githubTag{}, err + } + return tagResponse, nil +} + +// githubCommit is the API response for a "git/commits" request. +type githubCommit struct { + Message string `json:"message"` + Committer githubCommitter `json:"committer"` +} + +// getCommit calls the `commits` endpoint. +func (c githubClient) getCommit(sha string) (githubCommit, error) { + commit := githubCommit{} + if err := c.runGHAPICommand(fmt.Sprintf("repos/%s/git/commits/%s", c.repo, sha), &commit); err != nil { + return githubCommit{}, err + } + return commit, nil +} + +// githubPRList is the API response for the "search" endpoint. +type githubPRList struct { + Total int `json:"total_count"` + Items []githubPR `json:"items"` +} + +// githubPR is the API object included in a "search" query response when the +// return item is a PR. +type githubPR struct { + Number uint64 `json:"number"` + Title string `json:"title"` + Labels []githubLabel `json:"labels"` +} + +type githubLabel struct { + Name string `json:"name"` +} + +// listMergedPRs calls the `search` endpoint and queries for PRs. +func (c githubClient) listMergedPRs(after, before time.Time, baseBranches ...string) ([]githubPR, error) { + pageSize := 100 + searchQuery := fmt.Sprintf("repo:%s+is:pr+is:merged+merged:%s..%s", c.repo, after.Format(time.RFC3339), before.Format(time.RFC3339)) + if len(baseBranches) != 0 { + searchQuery += "+base:" + strings.Join(baseBranches, "+base:") + } + + _, prs, err := iterate(c, "search/issues", pageSize, []string{"q=" + searchQuery}, func(page *githubPRList) ([]githubPR, int) { + return page.Items, page.Total + }) + if err != nil { + return nil, err + } + + return prs, nil +} + +func (c githubClient) runGHAPICommand(url string, response any) error { + cmd := exec.Command("gh", "api", url) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %v", string(out), err) + } + + return json.Unmarshal(out, response) +} + +type extractFunc[T, C any] func(page *T) (pageElements []C, totalElements int) + +func iterate[T, C any](client githubClient, url string, pageSize int, extraQueryArgs []string, extract extractFunc[T, C]) (*T, []C, error) { + page := 0 + totalElements := math.MaxInt + elementsRead := 0 + + var firstPage *T + var collection []C + + for elementsRead < totalElements { + page++ + requestURL := fmt.Sprintf("%s?per_page=%d&page=%d&%s", url, pageSize, page, strings.Join(extraQueryArgs, "&")) + log.Printf("Calling endpoint %s", requestURL) + pageResult := new(T) + if err := client.runGHAPICommand(requestURL, pageResult); err != nil { + return nil, nil, err + } + + pageRead, t := extract(pageResult) + collection = append(collection, pageRead...) + elementsRead += len(pageRead) + totalElements = t + + if firstPage == nil { + firstPage = pageResult + } + } + + log.Printf("Total of %d pages and %d elements read", page, len(collection)) + + return firstPage, collection, nil +} diff --git a/hack/tools/release/notes/list.go b/hack/tools/release/notes/list.go new file mode 100644 index 000000000000..2c0afa1b1ea6 --- /dev/null +++ b/hack/tools/release/notes/list.go @@ -0,0 +1,154 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/pkg/errors" +) + +// githubFromToPRLister lists PRs from GitHub contained between two refs. +type githubFromToPRLister struct { + client *githubClient + fromRef, toRef ref + // branch is optional. It helps optimize the PR query by restricting + // the results to PRs merged in the selected branch and in main + branch string +} + +func newGithubFromToPRLister(repo string, fromRef, toRef ref, branch string) *githubFromToPRLister { + return &githubFromToPRLister{ + client: &githubClient{repo: repo}, + fromRef: fromRef, + toRef: toRef, + branch: branch, + } +} + +// listPRs returns the PRs merged between `fromRef` and `toRef` (included). +// It lists all PRs merged in main in the configured branch in the date +// range between fromRef and toRef (we include main because minor releases +// include PRs both in main and the release branch). +// Then it crosschecks them with the PR numbers found in the commits +// between fromRef and toRef, discarding any PR not seeing in the commits list. +// This ensures we don't include any PR merged in the same date range that +// doesn't belong to our git timeline. +func (l *githubFromToPRLister) listPRs() ([]pr, error) { + log.Printf("Computing diff between %s and %s", l.fromRef, l.toRef) + diff, err := l.client.getDiffAllCommits(l.fromRef.value, l.toRef.value) + if err != nil { + return nil, err + } + + log.Printf("Reading ref %s for upper limit", l.toRef) + toRef, err := l.client.getRef(l.toRef.String()) + if err != nil { + return nil, err + } + + var toCommitSHA string + if toRef.Object.ObjectType == tagType { + log.Printf("Reading tag info %s for upper limit", toRef.Object.SHA) + toTag, err := l.client.getTag(toRef.Object.SHA) + if err != nil { + return nil, err + } + toCommitSHA = toTag.Object.SHA + } else { + toCommitSHA = toRef.Object.SHA + } + + log.Printf("Reading commit %s for upper limit", toCommitSHA) + toCommit, err := l.client.getCommit(toCommitSHA) + if err != nil { + return nil, err + } + + fromDate := diff.MergeBaseCommit.Commit.Committer.Date + // We add an extra minute to avoid errors by 1 (observed during testing) + // We cross check the list of PRs against the list of commits, so we will filter out + // any PRs entries not belonging to the computed diff + toDate := toCommit.Committer.Date.Add(1 * time.Minute) + + log.Printf("Listing PRs from %s to %s", fromDate, toDate) + // We include both the configured branch and `main` as the base branches because when + // cutting a new minor version, there will be PRs merged in both main and the release branch. + // This is just an optimization to avoid listing PRs over all branches, since we know we don't + // need the PRs from other release branches. + gPRs, err := l.client.listMergedPRs(fromDate, toDate, l.branch, "main") + if err != nil { + return nil, err + } + + log.Printf("Found %d PRs in github", len(gPRs)) + + selectedPRNumbers := buildSetOfPRNumbers(diff.Commits) + + prs := make([]pr, 0, len(gPRs)) + for _, p := range gPRs { + if _, ok := selectedPRNumbers[fmt.Sprintf("%d", p.Number)]; !ok { + continue + } + labels := make([]string, 0, len(p.Labels)) + for _, l := range p.Labels { + labels = append(labels, l.Name) + } + prs = append(prs, pr{ + number: p.Number, + title: p.Title, + labels: labels, + }) + } + + log.Printf("%d PRs match the commits from the git diff", len(prs)) + + if len(prs) != len(selectedPRNumbers) { + return nil, errors.Errorf("expected %d PRs from commit list but only found %d", len(selectedPRNumbers), len(prs)) + } + + return prs, nil +} + +var ( + mergeCommitMessage = regexp.MustCompile(`(?m)^Merge pull request #(\d+) .*$`) + tideSquashedCommitMessage = regexp.MustCompile(`(?m)^.+\(#(?P\d+)\)$`) +) + +func buildSetOfPRNumbers(commits []githubCommitNode) map[string]struct{} { + prNumbers := make(map[string]struct{}) + for _, commit := range commits { + match := mergeCommitMessage.FindStringSubmatch(commit.Commit.Message) + if len(match) == 2 { + prNumbers[match[1]] = struct{}{} + continue + } + + match = tideSquashedCommitMessage.FindStringSubmatch(commit.Commit.Message) + if len(match) == 2 { + prNumbers[match[1]] = struct{}{} + } + } + + return prNumbers +} diff --git a/hack/tools/release/notes/list_test.go b/hack/tools/release/notes/list_test.go new file mode 100644 index 000000000000..688abc9a1c0e --- /dev/null +++ b/hack/tools/release/notes/list_test.go @@ -0,0 +1,67 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func Test_buildSetOfPRNumbers(t *testing.T) { + tests := []struct { + name string + commits []githubCommitNode + want map[string]struct{} + }{ + { + name: "merge commit", + commits: []githubCommitNode{ + { + Commit: githubCommit{ + Message: "Merge pull request #9072 from k8s-infra-cherrypick-robot/cherry-pick-9070-to-release-1.5\n\n[release-1.5] :bug: Change tilt debug base image to golang", + }, + }, + }, + want: map[string]struct{}{ + "9072": {}, + }, + }, + { + name: "squashed commit by tide", + commits: []githubCommitNode{ + { + Commit: githubCommit{ + Message: ":seedling: Add dependabot groups. Allow additional patch updates (#9263)\n\n* Allow patch updates on dependabot ignore list\n\nSigned-off-by: user \n\n* Add dependency groups for dependabot\n\nSigned-off-by: user \n\n---------\n\nSigned-off-by: user ", + }, + }, + }, + want: map[string]struct{}{ + "9263": {}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(buildSetOfPRNumbers(tt.commits)).To(Equal(tt.want)) + }) + } +} diff --git a/hack/tools/release/notes/main.go b/hack/tools/release/notes/main.go index eeeed683e93d..2417667dc974 100644 --- a/hack/tools/release/notes/main.go +++ b/hack/tools/release/notes/main.go @@ -21,414 +21,100 @@ limitations under the License. package main import ( - "bytes" - "encoding/json" - "errors" "flag" "fmt" - "os" + "log" "os/exec" - "regexp" - "sort" - "strings" - "sync" - "time" - release "sigs.k8s.io/cluster-api/hack/tools/release/internal" + "github.com/blang/semver/v4" + "github.com/pkg/errors" ) /* -This tool prints all the titles of all PRs from previous release to HEAD. -This needs to be run *before* a tag is created. +This tool prints all the titles of all PRs in between to references. Use these as the base of your release notes. */ -var ( - outputOrder = []string{ - release.Proposals, - release.Warning, - release.Features, - release.Bugs, - release.Other, - release.Documentation, - release.Unknown, - } - - repo = flag.String("repository", "kubernetes-sigs/cluster-api", "The repo to run the tool from.") - - fromTag = flag.String("from", "", "The tag or commit to start from.") - - since = flag.String("since", "", "Include commits starting from and including this date. Accepts format: YYYY-MM-DD") - until = flag.String("until", "", "Include commits up to and including this date. Accepts format: YYYY-MM-DD") - numWorkers = flag.Int("workers", 10, "Number of concurrent routines to process PR entries. If running into GitHub rate limiting, use 1.") - - prefixAreaLabel = flag.Bool("prefix-area-label", true, "If enabled, will prefix the area label.") - - preReleaseVersion = flag.Bool("pre-release-version", false, "If enabled, will add a pre-release warning header. (default false)") - deprecation = flag.Bool("deprecation", true, "If enabled, will add a templated deprecation warning header.") - addKubernetesVersionSupport = flag.Bool("add-kubernetes-version-support", true, "If enabled, will add the Kubernetes version support header.") - - tagRegex = regexp.MustCompile(`^\[release-[\w-\.]*\]`) - - userFriendlyAreas = map[string]string{ - "e2e-testing": "e2e", - "provider/control-plane-kubeadm": "KCP", - "provider/infrastructure-docker": "CAPD", - "dependency": "Dependency", - "devtools": "Devtools", - "machine": "Machine", - "api": "API", - "machinepool": "MachinePool", - "clustercachetracker": "ClusterCacheTracker", - "clusterclass": "ClusterClass", - "testing": "Testing", - "release": "Release", - "machineset": "MachineSet", - "clusterresourceset": "ClusterResourceSet", - "machinedeployment": "MachineDeployment", - "ipam": "IPAM", - "provider/bootstrap-kubeadm": "CABPK", - "provider/infrastructure-in-memory": "CAPIM", - "provider/core": "Core", - "runtime-sdk": "Runtime SDK", - "ci": "CI", - "machinehealthcheck": "MachineHealthCheck", - "clusterctl": "clusterctl", // Preserve lowercase - "util": "util", // Preserve lowercase - "community-meeting": "Community meeting", - } - - releaseBackportMarker = regexp.MustCompile(`(?m)^\[release-\d\.\d\]\s*`) -) - func main() { - flag.Parse() - os.Exit(run()) -} - -func lastTag() string { - if fromTag != nil && *fromTag != "" { - return *fromTag - } - cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") - out, err := cmd.Output() - if err != nil { - return firstCommit() + cmd := newNotesCmd() + if err := cmd.run(); err != nil { + log.Fatal(err) } - return string(bytes.TrimSpace(out)) } -func firstCommit() string { - cmd := exec.Command("git", "rev-list", "--max-parents=0", "HEAD") - out, err := cmd.Output() - if err != nil { - return "UNKNOWN" - } - return string(bytes.TrimSpace(out)) +type notesCmdConfig struct { + repo string + fromRef string + toRef string + newTag string + branch string + prefixAreaLabel bool + preReleaseVersion bool + deprecation bool + addKubernetesVersionSupport bool } -// Since git doesn't include the last day in rev-list we want to increase 1 day to include it in the interval. -func increaseDateByOneDay(date string) (string, error) { - layout := "2006-01-02" - datetime, err := time.Parse(layout, date) - if err != nil { - return "", err - } - datetime = datetime.Add(time.Hour * 24) - return datetime.Format(layout), nil -} - -const ( - missingAreaLabelPrefix = "MISSING_AREA" - areaLabelPrefix = "area/" - multipleAreaLabelsPrefix = "MULTIPLE_AREAS[" - documentationAreaLabel = "Documentation" -) - -type githubPullRequest struct { - Labels []githubLabel `json:"labels"` -} - -type githubLabel struct { - Name string `json:"name"` -} - -func getAreaLabel(merge string) (string, error) { - // Get pr id from merge commit - prID := strings.Replace(strings.TrimSpace(strings.Split(merge, " ")[3]), "#", "", -1) - - cmd := exec.Command("gh", "api", fmt.Sprintf("repos/%s/pulls/%s", *repo, prID)) //nolint:gosec - - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("%s: %v", string(out), err) - } +func readCmdConfig() *notesCmdConfig { + config := ¬esCmdConfig{} - pr := &githubPullRequest{} - if err := json.Unmarshal(out, pr); err != nil { - return "", err - } + flag.StringVar(&config.repo, "repository", "kubernetes-sigs/cluster-api", "The repo to run the tool from.") + flag.StringVar(&config.fromRef, "from", "", "The tag or commit to start from. It must be formatted as heads/ for branches and tags/ for tags. If not set, it will be calculated from release.") + flag.StringVar(&config.toRef, "to", "", "The ref (tag, branch or commit to stop at. It must be formatted as heads/ for branches and tags/ for tags. If not set, it will default to branch.") + flag.StringVar(&config.branch, "branch", "", "The branch to generate the notes from. If not set, it will be calculated from release.") + flag.StringVar(&config.newTag, "release", "", "The tag for the new release.") - var areaLabels []string - for _, label := range pr.Labels { - if area, ok := trimAreaLabel(label.Name); ok { - if userFriendlyArea, ok := userFriendlyAreas[area]; ok { - area = userFriendlyArea - } else { - area = capitalize(area) - } + flag.BoolVar(&config.prefixAreaLabel, "prefix-area-label", true, "If enabled, will prefix the area label.") + flag.BoolVar(&config.preReleaseVersion, "pre-release-version", false, "If enabled, will add a pre-release warning header. (default false)") + flag.BoolVar(&config.deprecation, "deprecation", true, "If enabled, will add a templated deprecation warning header.") + flag.BoolVar(&config.addKubernetesVersionSupport, "add-kubernetes-version-support", true, "If enabled, will add the Kubernetes version support header.") - areaLabels = append(areaLabels, area) - } - } + flag.Parse() - switch len(areaLabels) { - case 0: - return missingAreaLabelPrefix, nil - case 1: - return areaLabels[0], nil - default: - return multipleAreaLabelsPrefix + strings.Join(areaLabels, "/") + "]", nil - } + return config } -// trimAreaLabel removes the "area/" prefix from area labels and returns it. -// If the label is an area label, the second return value is true, otherwise false. -func trimAreaLabel(label string) (string, bool) { - trimmed := strings.TrimPrefix(label, areaLabelPrefix) - if len(trimmed) < len(label) { - return trimmed, true - } - - return label, false +type notesCmd struct { + config *notesCmdConfig } -func run() int { - if err := ensureInstalledDependencies(); err != nil { - fmt.Println(err) - return 1 - } - - var commitRange string - var cmd *exec.Cmd - - if *since != "" && *until != "" { - commitRange = fmt.Sprintf("%s - %s", *since, *until) - - lastDay, err := increaseDateByOneDay(*until) - if err != nil { - fmt.Println(err) - return 1 - } - cmd = exec.Command("git", "rev-list", "HEAD", "--since=\""+*since+"\"", "--until=\""+lastDay+"\"", "--merges", "--pretty=format:%B") //nolint:gosec - } else if *since != "" || *until != "" { - fmt.Println("--since and --until are required together or both unset") - return 1 - } else { - commitRange = lastTag() - cmd = exec.Command("git", "rev-list", commitRange+"..HEAD", "--merges", "--pretty=format:%B") //nolint:gosec - } - - merges := map[string][]string{ - release.Features: {}, - release.Bugs: {}, - release.Documentation: {}, - release.Warning: {}, - release.Other: {}, - release.Unknown: {}, - } - out, err := cmd.CombinedOutput() - if err != nil { - fmt.Println("Error") - fmt.Println(string(out)) - return 1 - } - - commits := []*commit{} - outLines := strings.Split(string(out), "\n") - for _, line := range outLines { - line = strings.TrimSpace(line) - last := len(commits) - 1 - switch { - case strings.HasPrefix(line, "commit"): - commits = append(commits, &commit{}) - case strings.HasPrefix(line, "Merge"): - commits[last].merge = line - continue - case line == "": - default: - commits[last].body = line - } - } - - results := make(chan releaseNoteEntryResult) - commitCh := make(chan *commit) - var wg sync.WaitGroup - - wg.Add(*numWorkers) - for i := 0; i < *numWorkers; i++ { - go func() { - for commit := range commitCh { - processed := releaseNoteEntryResult{} - processed.prEntry, processed.err = generateReleaseNoteEntry(commit) - results <- processed - } - wg.Done() - }() - } - - go func() { - for _, c := range commits { - commitCh <- c - } - close(commitCh) - }() - - go func() { - wg.Wait() - close(results) - }() - - for result := range results { - if result.err != nil { - fmt.Println(result.err) - return -1 - } - - if result.prEntry == nil || result.prEntry.title == "" { - continue - } - - if result.prEntry.section == release.Documentation { - merges[result.prEntry.section] = append(merges[result.prEntry.section], result.prEntry.prNumber) - } else { - merges[result.prEntry.section] = append(merges[result.prEntry.section], result.prEntry.title) - } - } - - if *preReleaseVersion { - fmt.Printf("🚨 This is a RELEASE CANDIDATE. Use it only for testing purposes. If you find any bugs, file an [issue](https://github.com/%s/issues/new).\n", *repo) - } - - if *addKubernetesVersionSupport { - fmt.Print(`## 👌 Kubernetes version support - -- Management Cluster: v1.**X**.x -> v1.**X**.x -- Workload Cluster: v1.**X**.x -> v1.**X**.x - -[More information about version support can be found here](https://cluster-api.sigs.k8s.io/reference/versions.html) - -`) +func newNotesCmd() *notesCmd { + config := readCmdConfig() + return ¬esCmd{ + config: config, } +} - fmt.Print(`## Highlights - -* REPLACE ME - -`) - - if *deprecation { - fmt.Print(`## Deprecation Warning - -REPLACE ME: A couple sentences describing the deprecation, including links to docs. - -* [GitHub issue #REPLACE ME](REPLACE ME) - -`) +func (cmd *notesCmd) run() error { + if err := validateConfig(cmd.config); err != nil { + return err } - fmt.Printf("## Changes since %v\n", commitRange) - - fmt.Printf("## :chart_with_upwards_trend: Overview\n") - if count := len(commits); count == 1 { - fmt.Println("- 1 new commit merged") - } else if count > 1 { - fmt.Printf("- %d new commits merged\n", count) - } - if count := len(merges[release.Warning]); count == 1 { - fmt.Println("- 1 breaking change :warning:") - } else if count > 1 { - fmt.Printf("- %d breaking changes :warning:\n", count) - } - if count := len(merges[release.Features]); count == 1 { - fmt.Println("- 1 feature addition ✨") - } else if count > 1 { - fmt.Printf("- %d feature additions ✨\n", count) + if err := computeConfigDefaults(cmd.config); err != nil { + return err } - if count := len(merges[release.Bugs]); count == 1 { - fmt.Println("- 1 bug fixed 🐛") - } else if count > 1 { - fmt.Printf("- %d bugs fixed 🐛\n", count) - } - fmt.Println() - - for _, key := range outputOrder { - mergeslice := merges[key] - if len(mergeslice) == 0 { - continue - } - switch key { - case release.Documentation: - sort.Strings(mergeslice) - if len(mergeslice) == 1 { - fmt.Printf( - ":book: Additionally, there has been 1 contribution to our documentation and book. (%s) \n\n", - mergeslice[0], - ) - } else { - fmt.Printf( - ":book: Additionally, there have been %d contributions to our documentation and book. (%s) \n\n", - len(mergeslice), - strings.Join(mergeslice, ", "), - ) - } - default: - fmt.Println("## " + key) - sort.Slice(mergeslice, func(i int, j int) bool { - str1 := strings.ToLower(mergeslice[i]) - str2 := strings.ToLower(mergeslice[j]) - return str1 < str2 - }) - - for _, merge := range mergeslice { - fmt.Println(merge) - } - fmt.Println() - } + if err := ensureInstalledDependencies(); err != nil { + return err } - fmt.Println("") - fmt.Println("_Thanks to all our contributors!_ 😊") - - return 0 -} + from, to := parseRef(cmd.config.fromRef), parseRef(cmd.config.toRef) -func trimTitle(title string) string { - // Remove a tag prefix if found. - title = tagRegex.ReplaceAllString(title, "") + printer := newReleaseNotesPrinter(cmd.config.repo, from.value) + printer.isPreRelease = cmd.config.preReleaseVersion + printer.printDeprecation = cmd.config.deprecation + printer.printKubernetesSupport = cmd.config.addKubernetesVersionSupport - return strings.TrimSpace(title) -} + generator := newNotesGenerator( + newGithubFromToPRLister(cmd.config.repo, from, to, cmd.config.branch), + newPREntryProcessor(cmd.config.prefixAreaLabel), + printer, + ) -type commit struct { - merge string - body string -} - -func formatMerge(line, prNumber string) string { - if prNumber == "" { - return line - } - return fmt.Sprintf("%s (%s)", line, prNumber) + return generator.run() } func ensureInstalledDependencies() error { - if !commandExists("git") { - return errors.New("git not available. Git is required to be present in the PATH") - } - if !commandExists("gh") { return errors.New("gh GitHub CLI not available. GitHub CLI is required to be present in the PATH. Refer to https://cli.github.com/ for installation") } @@ -441,121 +127,85 @@ func commandExists(cmd string) bool { return err == nil } -// releaseNoteEntryResult is the result of processing a PR to create a release note item. -// Used to aggregate the line item and error when processing concurrently. -type releaseNoteEntryResult struct { - prEntry *releaseNoteEntry - err error -} +func validateConfig(config *notesCmdConfig) error { + if config.fromRef == "" && config.newTag == "" { + return errors.New("at least one of --from or --release need to be set") + } -// releaseNoteEntry represents a line item in the release notes. -type releaseNoteEntry struct { - title string - section string - prNumber string -} + if config.branch == "" && config.newTag == "" { + return errors.New("at least one of --branch or --release need to be set") + } -// removePrefixes removes the specified prefixes from the title. -func removePrefixes(title string, prefixes []string) string { - entryWithoutTag := title - for _, prefix := range prefixes { - entryWithoutTag = strings.TrimLeft(strings.TrimPrefix(entryWithoutTag, prefix), " ") + if config.fromRef != "" { + if err := validateRef(config.fromRef); err != nil { + return err + } } - return entryWithoutTag -} + if config.toRef != "" { + if err := validateRef(config.toRef); err != nil { + return err + } + } -// trimAreaFromTitle removes the prefixed area from title to avoid duplication. -func trimAreaFromTitle(title, area string) string { - titleWithoutArea := title - pattern := `(?i)^` + regexp.QuoteMeta(area+":") - re := regexp.MustCompile(pattern) - titleWithoutArea = re.ReplaceAllString(titleWithoutArea, "") - titleWithoutArea = strings.TrimSpace(titleWithoutArea) - return titleWithoutArea + return nil } -func capitalize(str string) string { - return strings.ToUpper(string(str[0])) + str[1:] -} +// computeConfigDefaults calculates the value the non specified configuration fields +// based on the set fields. +func computeConfigDefaults(config *notesCmdConfig) error { + if config.fromRef != "" && config.branch != "" { + return nil + } -// generateReleaseNoteEntry processes a commit into a PR line item for the release notes. -func generateReleaseNoteEntry(c *commit) (*releaseNoteEntry, error) { - entry := &releaseNoteEntry{} - if c.body == "" { - c.body = "ERROR: BODY MISSING. FIX MANUALLY" + newTag, err := semver.ParseTolerant(config.newTag) + if err != nil { + return errors.Wrap(err, "invalid --release, is not a semver") } - entry.title = trimTitle(c.body) - var fork string - - var area string - if *prefixAreaLabel { - var err error - area, err = getAreaLabel(c.merge) - if err != nil { - return nil, err + + if config.fromRef == "" { + if newTag.Patch == 0 { + // If patch = 0, this a new minor release + // Hence we want to read commits from + config.fromRef = "tags/" + fmt.Sprintf("v%d.%d.0", newTag.Major, newTag.Minor-1) + } else { + // if not new minor release, this is a new patch, just decrease the patch + config.fromRef = "tags/" + fmt.Sprintf("v%d.%d.%d", newTag.Major, newTag.Minor, newTag.Patch-1) } } - switch { - case strings.HasPrefix(entry.title, ":sparkles:"), strings.HasPrefix(entry.title, "✨"): - entry.section = release.Features - entry.title = removePrefixes(entry.title, []string{":sparkles:", "✨"}) - case strings.HasPrefix(entry.title, ":bug:"), strings.HasPrefix(entry.title, "🐛"): - entry.section = release.Bugs - entry.title = removePrefixes(entry.title, []string{":bug:", "🐛"}) - case strings.HasPrefix(entry.title, ":book:"), strings.HasPrefix(entry.title, "📖"): - entry.section = release.Documentation - entry.title = removePrefixes(entry.title, []string{":book:", "📖"}) - if strings.Contains(entry.title, "CAEP") || strings.Contains(entry.title, "proposal") { - entry.section = release.Proposals - } - case strings.HasPrefix(entry.title, ":warning:"), strings.HasPrefix(entry.title, "⚠️"): - entry.section = release.Warning - entry.title = removePrefixes(entry.title, []string{":warning:", "⚠️"}) - case strings.HasPrefix(entry.title, "🚀"), strings.HasPrefix(entry.title, "🌱 Release v1."): - // TODO(g-gaston): remove the second condition using 🌱 prefix once 1.6 is released - // Release trigger PRs from previous releases are not included in the release notes - return nil, nil - case strings.HasPrefix(entry.title, ":seedling:"), strings.HasPrefix(entry.title, "🌱"): - entry.section = release.Other - entry.title = removePrefixes(entry.title, []string{":seedling:", "🌱"}) - default: - entry.section = release.Unknown + if config.branch == "" { + config.branch = defaultBranchForNewTag(newTag) } - // If the area label indicates documentation, use documentation as the section - // no matter what was the emoji used. This takes into account that the area label - // tends to be more accurate than the emoji (data point observed by the release team). - // We handle this after the switch statement to make sure we remove all emoji prefixes. - if area == documentationAreaLabel { - entry.section = release.Documentation + if config.toRef == "" { + config.toRef = "heads/" + config.branch } - entry.title = strings.TrimSpace(entry.title) - entry.title = trimReleaseBackportMarker(entry.title) + return nil +} - if entry.title == "" { - return entry, nil - } +// defaultBranchForNewTag calculates the branch to cut a release +// based on the new release tag. +func defaultBranchForNewTag(newTag semver.Version) string { + if newTag.Patch == 0 { + if len(newTag.Pre) == 0 { + // for new minor releases, use the release branch + return releaseBranchForVersion(newTag) + } else if len(newTag.Pre) == 2 && newTag.Pre[0].VersionStr == "rc" && newTag.Pre[1].VersionNum >= 1 { + // for the second or later RCs, we use the release branch since we cut this branch with the first RC + return releaseBranchForVersion(newTag) + } - if *prefixAreaLabel { - entry.title = trimAreaFromTitle(entry.title, area) - entry.title = capitalize(entry.title) - entry.title = fmt.Sprintf("- %s: %s", area, entry.title) - } else { - entry.title = capitalize(entry.title) - entry.title = fmt.Sprintf("- %s", entry.title) + // for any other pre release, we always cut from main + // this includes all beta releases and the first RC + return "main" } - _, _ = fmt.Sscanf(c.merge, "Merge pull request %s from %s", &entry.prNumber, &fork) - entry.title = formatMerge(entry.title, entry.prNumber) - - return entry, nil + // If it's a patch, we use the release branch + return releaseBranchForVersion(newTag) } -// trimReleaseBackportMarker removes the `[release-x.x]` prefix from a PR title if present. -// These are mostly used for back-ported PRs in release branches. -func trimReleaseBackportMarker(title string) string { - return releaseBackportMarker.ReplaceAllString(title, "${1}") +func releaseBranchForVersion(version semver.Version) string { + return fmt.Sprintf("release-%d.%d", version.Major, version.Minor) } diff --git a/hack/tools/release/notes/main_test.go b/hack/tools/release/notes/main_test.go index 83c7193f93f7..7bc1405720c9 100644 --- a/hack/tools/release/notes/main_test.go +++ b/hack/tools/release/notes/main_test.go @@ -19,7 +19,12 @@ limitations under the License. package main -import "testing" +import ( + "testing" + + "github.com/blang/semver/v4" + . "github.com/onsi/gomega" +) func Test_trimTitle(t *testing.T) { tests := []struct { @@ -103,3 +108,63 @@ func Test_trimAreaFromTitle(t *testing.T) { }) } } + +func Test_defaultBranchForNewTag(t *testing.T) { + tests := []struct { + name string + newVersion string + want string + }{ + { + name: "new minor", + newVersion: "v1.5.0", + want: "release-1.5", + }, + { + name: "new patch", + newVersion: "v1.6.1", + want: "release-1.6", + }, + { + name: "first RC", + newVersion: "v1.6.0-rc.0", + want: "main", + }, + { + name: "second RC", + newVersion: "v1.6.0-rc.1", + want: "release-1.6", + }, + { + name: "third RC", + newVersion: "v1.6.0-rc.2", + want: "release-1.6", + }, + { + name: "first Beta", + newVersion: "v1.6.0-beta.0", + want: "main", + }, + { + name: "second Beta", + newVersion: "v1.6.0-beta.1", + want: "main", + }, + { + name: "third Beta", + newVersion: "v1.6.0-beta.2", + want: "main", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + version, err := semver.ParseTolerant(tt.newVersion) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(defaultBranchForNewTag(version)).To(Equal(tt.want)) + }) + } +} diff --git a/hack/tools/release/notes/print.go b/hack/tools/release/notes/print.go new file mode 100644 index 000000000000..4c2819577b7a --- /dev/null +++ b/hack/tools/release/notes/print.go @@ -0,0 +1,172 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "sort" + "strings" + + release "sigs.k8s.io/cluster-api/hack/tools/release/internal" +) + +var defaultOutputOrder = []string{ + release.Proposals, + release.Warning, + release.Features, + release.Bugs, + release.Other, + release.Documentation, + release.Unknown, +} + +// releaseNotesPrinter outputs the PR entries following +// the right format for the release notes. +type releaseNotesPrinter struct { + outputOrder []string + isPreRelease bool + printKubernetesSupport bool + printDeprecation bool + fromTag string + repo string +} + +func newReleaseNotesPrinter(repo, fromTag string) *releaseNotesPrinter { + return &releaseNotesPrinter{ + repo: repo, + fromTag: fromTag, + outputOrder: defaultOutputOrder, + } +} + +// print outputs to stdout the release notes. +func (p *releaseNotesPrinter) print(entries []notesEntry) { + merges := map[string][]string{ + release.Features: {}, + release.Bugs: {}, + release.Documentation: {}, + release.Warning: {}, + release.Other: {}, + release.Unknown: {}, + } + + for _, entry := range entries { + if entry.section == release.Documentation { + merges[entry.section] = append(merges[entry.section], "#"+entry.prNumber) + } else { + merges[entry.section] = append(merges[entry.section], entry.title) + } + } + + if p.isPreRelease { + fmt.Printf("🚨 This is a RELEASE CANDIDATE. Use it only for testing purposes. If you find any bugs, file an [issue](https://github.com/%s/issues/new).\n", p.repo) + } + + if p.printKubernetesSupport { + fmt.Print(`## 👌 Kubernetes version support + +- Management Cluster: v1.**X**.x -> v1.**X**.x +- Workload Cluster: v1.**X**.x -> v1.**X**.x + +[More information about version support can be found here](https://cluster-api.sigs.k8s.io/reference/versions.html) + +`) + } + + fmt.Print(`## Highlights + +* REPLACE ME + +`) + + if p.printDeprecation { + fmt.Print(`## Deprecation Warning + +REPLACE ME: A couple sentences describing the deprecation, including links to docs. + +* [GitHub issue #REPLACE ME](REPLACE ME) + +`) + } + + fmt.Printf("## Changes since %s\n", p.fromTag) + + fmt.Printf("## :chart_with_upwards_trend: Overview\n") + if count := len(entries); count == 1 { + fmt.Println("- 1 new commit merged") + } else if count > 1 { + fmt.Printf("- %d new commits merged\n", count) + } + if count := len(merges[release.Warning]); count == 1 { + fmt.Println("- 1 breaking change :warning:") + } else if count > 1 { + fmt.Printf("- %d breaking changes :warning:\n", count) + } + if count := len(merges[release.Features]); count == 1 { + fmt.Println("- 1 feature addition ✨") + } else if count > 1 { + fmt.Printf("- %d feature additions ✨\n", count) + } + if count := len(merges[release.Bugs]); count == 1 { + fmt.Println("- 1 bug fixed 🐛") + } else if count > 1 { + fmt.Printf("- %d bugs fixed 🐛\n", count) + } + fmt.Println() + + for _, key := range p.outputOrder { + mergeslice := merges[key] + if len(mergeslice) == 0 { + continue + } + + switch key { + case release.Documentation: + sort.Strings(mergeslice) + if len(mergeslice) == 1 { + fmt.Printf( + ":book: Additionally, there has been 1 contribution to our documentation and book. (%s) \n\n", + mergeslice[0], + ) + } else { + fmt.Printf( + ":book: Additionally, there have been %d contributions to our documentation and book. (%s) \n\n", + len(mergeslice), + strings.Join(mergeslice, ", "), + ) + } + default: + fmt.Println("## " + key) + sort.Slice(mergeslice, func(i int, j int) bool { + str1 := strings.ToLower(mergeslice[i]) + str2 := strings.ToLower(mergeslice[j]) + return str1 < str2 + }) + + for _, merge := range mergeslice { + fmt.Println(merge) + } + fmt.Println() + } + } + + fmt.Println("") + fmt.Println("_Thanks to all our contributors!_ 😊") +} diff --git a/hack/tools/release/notes/process.go b/hack/tools/release/notes/process.go new file mode 100644 index 000000000000..7b5da18e6792 --- /dev/null +++ b/hack/tools/release/notes/process.go @@ -0,0 +1,253 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "log" + "regexp" + "strings" + + release "sigs.k8s.io/cluster-api/hack/tools/release/internal" +) + +const ( + missingAreaLabelPrefix = "MISSING_AREA" + areaLabelPrefix = "area/" + multipleAreaLabelsPrefix = "MULTIPLE_AREAS[" + documentationArea = "Documentation" +) + +var ( + defaultUserFriendlyAreas = map[string]string{ + "e2e-testing": "e2e", + "provider/control-plane-kubeadm": "KCP", + "provider/infrastructure-docker": "CAPD", + "dependency": "Dependency", + "devtools": "Devtools", + "machine": "Machine", + "api": "API", + "machinepool": "MachinePool", + "clustercachetracker": "ClusterCacheTracker", + "clusterclass": "ClusterClass", + "testing": "Testing", + "release": "Release", + "machineset": "MachineSet", + "clusterresourceset": "ClusterResourceSet", + "machinedeployment": "MachineDeployment", + "ipam": "IPAM", + "provider/bootstrap-kubeadm": "CABPK", + "provider/infrastructure-in-memory": "CAPIM", + "provider/core": "Core", + "runtime-sdk": "Runtime SDK", + "ci": "CI", + "machinehealthcheck": "MachineHealthCheck", + "clusterctl": "clusterctl", // Preserve lowercase + "util": "util", // Preserve lowercase + "community-meeting": "Community meeting", + } + + tagRegex = regexp.MustCompile(`^\[release-[\w-\.]*\]`) + releaseBackportMarker = regexp.MustCompile(`(?m)^\[release-\d\.\d\]\s*`) +) + +type prEntriesProcessor struct { + userFriendlyAreas map[string]string + addAreaPrefix bool +} + +func newPREntryProcessor(addAreaPrefix bool) prEntriesProcessor { + return prEntriesProcessor{ + userFriendlyAreas: defaultUserFriendlyAreas, + addAreaPrefix: addAreaPrefix, + } +} + +// process generates a PR entry ready for printing per PR. It extracts the area +// from the PR labels and appends it as a prefix to the title. +// It might skip some PRs depending on the title. +func (g prEntriesProcessor) process(prs []pr) []notesEntry { + entries := make([]notesEntry, 0, len(prs)) + for i := range prs { + pr := &prs[i] + + entry := g.generateNoteEntry(pr) + + if entry == nil || entry.title == "" { + log.Printf("Ignoring PR [%s (#%d)]", pr.title, pr.number) + continue + } + + entries = append(entries, *entry) + } + + return entries +} + +func (g prEntriesProcessor) generateNoteEntry(p *pr) *notesEntry { + entry := ¬esEntry{} + + entry.title = trimTitle(p.title) + + var area string + if g.addAreaPrefix { + area = g.extractArea(p) + } + + switch { + case strings.HasPrefix(entry.title, ":sparkles:"), strings.HasPrefix(entry.title, "✨"): + entry.section = release.Features + entry.title = removePrefixes(entry.title, []string{":sparkles:", "✨"}) + case strings.HasPrefix(entry.title, ":bug:"), strings.HasPrefix(entry.title, "🐛"): + entry.section = release.Bugs + entry.title = removePrefixes(entry.title, []string{":bug:", "🐛"}) + case strings.HasPrefix(entry.title, ":book:"), strings.HasPrefix(entry.title, "📖"): + entry.section = release.Documentation + entry.title = removePrefixes(entry.title, []string{":book:", "📖"}) + if strings.Contains(entry.title, "CAEP") || strings.Contains(entry.title, "proposal") { + entry.section = release.Proposals + } + case strings.HasPrefix(entry.title, ":warning:"), strings.HasPrefix(entry.title, "⚠️"): + entry.section = release.Warning + entry.title = removePrefixes(entry.title, []string{":warning:", "⚠️"}) + case strings.HasPrefix(entry.title, "🚀"), strings.HasPrefix(entry.title, "🌱 Release v1."): + // TODO(g-gaston): remove the second condition using 🌱 prefix once 1.6 is released + // Release trigger PRs from previous releases are not included in the release notes + return nil + case strings.HasPrefix(entry.title, ":seedling:"), strings.HasPrefix(entry.title, "🌱"): + entry.section = release.Other + entry.title = removePrefixes(entry.title, []string{":seedling:", "🌱"}) + default: + entry.section = release.Unknown + } + + // If the area label indicates documentation, use documentation as the section + // no matter what was the emoji used. This takes into account that the area label + // tends to be more accurate than the emoji (data point observed by the release team). + // We handle this after the switch statement to make sure we remove all emoji prefixes. + if area == documentationArea { + entry.section = release.Documentation + } + + entry.title = strings.TrimSpace(entry.title) + entry.title = trimReleaseBackportMarker(entry.title) + + if entry.title == "" { + return nil + } + + if g.addAreaPrefix { + entry.title = trimAreaFromTitle(entry.title, area) + entry.title = capitalize(entry.title) + entry.title = fmt.Sprintf("- %s: %s", area, entry.title) + } else { + entry.title = capitalize(entry.title) + entry.title = fmt.Sprintf("- %s", entry.title) + } + + entry.prNumber = fmt.Sprintf("%d", p.number) + entry.title = formatPREntry(entry.title, entry.prNumber) + + return entry +} + +// extractArea processes the PR labels to extract the area. +func (g prEntriesProcessor) extractArea(pr *pr) string { + var areaLabels []string + for _, label := range pr.labels { + if area, ok := trimAreaLabel(label); ok { + if userFriendlyArea, ok := g.userFriendlyAreas[area]; ok { + area = userFriendlyArea + } else { + area = capitalize(area) + } + + areaLabels = append(areaLabels, area) + } + } + + switch len(areaLabels) { + case 0: + return missingAreaLabelPrefix + case 1: + return areaLabels[0] + default: + return multipleAreaLabelsPrefix + strings.Join(areaLabels, "/") + "]" + } +} + +// trimAreaLabel removes the "area/" prefix from area labels and returns it. +// If the label is an area label, the second return value is true, otherwise false. +func trimAreaLabel(label string) (string, bool) { + trimmed := strings.TrimPrefix(label, areaLabelPrefix) + if len(trimmed) < len(label) { + return trimmed, true + } + + return label, false +} + +// trimTitle removes release tags and white space from +// PR titles. +func trimTitle(title string) string { + // Remove a tag prefix if found. + title = tagRegex.ReplaceAllString(title, "") + + return strings.TrimSpace(title) +} + +// formatPREntry appends the PR number at the end of the title +// and makes it a valid link in GitHub release notes. +func formatPREntry(line, prNumber string) string { + if prNumber == "" { + return line + } + return fmt.Sprintf("%s (#%s)", line, prNumber) +} + +func capitalize(str string) string { + return strings.ToUpper(string(str[0])) + str[1:] +} + +// trimReleaseBackportMarker removes the `[release-x.x]` prefix from a PR title if present. +// These are mostly used for back-ported PRs in release branches. +func trimReleaseBackportMarker(title string) string { + return releaseBackportMarker.ReplaceAllString(title, "${1}") +} + +// removePrefixes removes the specified prefixes from the title. +func removePrefixes(title string, prefixes []string) string { + entryWithoutTag := title + for _, prefix := range prefixes { + entryWithoutTag = strings.TrimLeft(strings.TrimPrefix(entryWithoutTag, prefix), " ") + } + + return entryWithoutTag +} + +// trimAreaFromTitle removes the prefixed area from title to avoid duplication. +func trimAreaFromTitle(title, area string) string { + titleWithoutArea := title + pattern := `(?i)^` + regexp.QuoteMeta(area+":") + re := regexp.MustCompile(pattern) + titleWithoutArea = re.ReplaceAllString(titleWithoutArea, "") + titleWithoutArea = strings.TrimSpace(titleWithoutArea) + return titleWithoutArea +} diff --git a/hack/tools/release/notes/ref.go b/hack/tools/release/notes/ref.go new file mode 100644 index 000000000000..a0da62833315 --- /dev/null +++ b/hack/tools/release/notes/ref.go @@ -0,0 +1,54 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "strings" + + "github.com/pkg/errors" +) + +// ref represents a git reference. +type ref struct { + // reType is the ref type: tags for a tag, head for a branch, commit for a commit. + reType string + value string +} + +func (r ref) String() string { + return r.reType + "/" + r.value +} + +func parseRef(r string) ref { + split := strings.SplitN(r, "/", 2) + return ref{ + reType: split[0], + value: split[1], + } +} + +func validateRef(r string) error { + split := strings.SplitN(r, "/", 2) + if len(split) != 2 { + return errors.Errorf("invalid ref %s: must follow [type]/[value]", r) + } + + return nil +} diff --git a/hack/tools/release/notes/release_notes_integration_test.go b/hack/tools/release/notes/release_notes_integration_test.go index 52f814737bc5..10ed87c888b3 100644 --- a/hack/tools/release/notes/release_notes_integration_test.go +++ b/hack/tools/release/notes/release_notes_integration_test.go @@ -27,44 +27,36 @@ import ( "testing" . "github.com/onsi/gomega" - "k8s.io/kubectl/pkg/cmd" ) func TestReleaseNotesIntegration(t *testing.T) { testCases := []struct { - name string - previousRelease string - releaseBranchForTest string - head string - expected string + name string + args []string + expected string }{ { // This tests a patch release computing the PR list from previous patch tag // to HEAD. Since v1.3 is out of support, we won't be backporting new PRs // so the branch should remain untouched and the test valid. - name: "new patch", - previousRelease: "v1.3.9", - releaseBranchForTest: "release-1.3", - head: "release-1.3", - expected: "test/golden/v1.3.10.md", + name: "new patch", + args: []string{"--release", "v1.3.10"}, + expected: "test/golden/v1.3.10.md", }, { - // The release notes command computes everything from last tag - // to HEAD. Hence if we use the head of release-1.5, this test will - // become invalid everytime we backport some PR to release branch release-1.5. - // Here we cheat a little by poiting to worktree to v1.5.0, which is the - // release that this test is simulating, so it should not exist yet. But - // it represents accurately the HEAD of release-1.5 when we released v1.5.0. - name: "new minor", - previousRelease: "v1.4.0", - releaseBranchForTest: "v1.5.0-integration-test", - head: "v1.5.0", - expected: "test/golden/v1.5.0.md", + // By default when using the `--release` options, notes command computes + // everything from last tag to HEAD. Hence if we use the head of release-1.5, + // this test will become invalid every time we backport some PR to release + // branch release-1.5. + // Instead, to simulate the v1.5.0 release, we manually set the `--to` flag, + // which sets the upper boundary for the PR search. + name: "new minor", + args: []string{"--from", "tags/v1.4.0", "--to", "tags/v1.5.0", "--branch", "release-1.5"}, + expected: "test/golden/v1.5.0.md", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - setupNotesTest(t, tc.previousRelease, tc.releaseBranchForTest, tc.head) g := NewWithT(t) expectedOutput, err := os.ReadFile(tc.expected) @@ -75,16 +67,20 @@ func TestReleaseNotesIntegration(t *testing.T) { t.Cleanup(func() { g.Expect(os.Chdir(orgCurrentDir)).To(Succeed()) }) - g.Expect(os.Chdir(tc.releaseBranchForTest)).To(Succeed()) // a two workers config is slow but it guarantees no rate limiting - os.Args = []string{os.Args[0], "--from", tc.previousRelease, "--workers", "2"} + os.Args = append([]string{os.Args[0]}, tc.args...) old := os.Stdout // keep backup of the real stdout to restore later r, w, err := os.Pipe() g.Expect(err).To(Succeed()) os.Stdout = w + t.Cleanup(func() { + // Reset defined flags so we can can call cmd.run() again + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + }) + g.Expect(runReleaseNotesCmd()).To(Succeed()) w.Close() @@ -98,34 +94,14 @@ func TestReleaseNotesIntegration(t *testing.T) { } func runReleaseNotesCmd() error { - // we replicate the main function here so we don't get os.Exit - flag.Parse() - if code := run(); code != 0 { - return fmt.Errorf("release notes command exited with code %d", code) + cmd := newNotesCmd() + if err := cmd.run(); err != nil { + return fmt.Errorf("release notes command failed: %w", err) } return nil } -func setupNotesTest(tb testing.TB, previousRelease, releaseBranchForTest, head string) { - g := NewWithT(tb) - - _, err := os.Stat(releaseBranchForTest) - if os.IsNotExist(err) { - runCommand(tb, exec.Command("git", "worktree", "add", releaseBranchForTest)) - } else { - g.Expect(err).To(Succeed()) - } - - pull := exec.Command("git", "checkout", head) - pull.Dir = releaseBranchForTest - runCommand(tb, pull) - - tb.Cleanup(func() { - runCommand(tb, cmd.Command("git", "worktree", "remove", releaseBranchForTest)) - }) -} - func runCommand(tb testing.TB, cmd *exec.Cmd) { out, err := cmd.CombinedOutput() if err != nil { diff --git a/hack/tools/release/notes/test/golden/v1.5.0.md b/hack/tools/release/notes/test/golden/v1.5.0.md index 256b5a723471..01f35fdba2a8 100644 --- a/hack/tools/release/notes/test/golden/v1.5.0.md +++ b/hack/tools/release/notes/test/golden/v1.5.0.md @@ -17,7 +17,7 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do ## Changes since v1.4.0 ## :chart_with_upwards_trend: Overview -- 335 new commits merged +- 339 new commits merged - 4 breaking changes :warning: - 19 feature additions ✨ - 67 bugs fixed 🐛 @@ -57,9 +57,9 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do - CAPD: Add kind mapper (#8880) - CAPD: Change the haproxy entrypoint to prevent getting stopped immediately after start (#8685) - CAPD: Delegate CAPD port selection to the container runtime (#8642) +- CAPD: Fix fail-swap-on=false flag not being part of kind images anymore (#8767) - CAPD: Implement watch filter (#8789) - CAPD: Test/capd: fix kind mapper entry for v1.25.11 (#8914) -- CAPD: Test/e2e fix fail-swap-on=false flag not being part of kind images anymore (#8767) - CAPIM: Fix cluster deletion in the in-memory API server (#8818) - CAPIM: Fix inmemory provider docker build (#8822) - CAPIM: Test/e2e/in-memory: set providerID after VM is provisioned (#8879) @@ -150,6 +150,7 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do - ClusterClass: Cluster/topology: use cached MD list in get current state (#8922) - ClusterClass: Deprecate rolloutAfter in cluster topology (#8324) - ClusterClass: Upgrading control plane should only be blocked if MD are upgrading (not just rolling out) (#8658) +- clusterctl: Add CABPOCNE and CACPOCNE Providers (#9068) - clusterctl: Add labels to OWNERS file (#8342) - clusterctl: Add move annotation on objects for cluster move operation (#8322) - clusterctl: Bump controller-tools to v0.12 (#8581) @@ -179,6 +180,7 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do - Dependency: Bump github.com/onsi/ginkgo/v2 from 2.9.5 to 2.9.7 (#8792) - Dependency: Bump github.com/onsi/ginkgo/v2 from 2.9.7 to 2.10.0 (#8839) - Dependency: Bump github.com/onsi/gomega from 1.27.4 to 1.27.5 (#8390) +- Dependency: Bump github.com/onsi/gomega from 1.27.5 to 1.27.6 (#8460) - Dependency: Bump github.com/onsi/gomega from 1.27.6 to 1.27.7 (#8715) - Dependency: Bump github.com/onsi/gomega from 1.27.7 to 1.27.8 (#8841) - Dependency: Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0 (#8541) @@ -293,7 +295,7 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do - util: Move `internal.labels` to `format` package for use by providers (#9006) - util: Rename internal/util/ssa util functions for better naming consistency (#8423) -:book: Additionally, there have been 72 contributions to our documentation and book. (#8252, #8279, #8284, #8288, #8293, #8307, #8308, #8309, #8319, #8327, #8351, #8355, #8363, #8375, #8383, #8397, #8416, #8419, #8439, #8446, #8447, #8454, #8508, #8509, #8510, #8511, #8520, #8521, #8552, #8554, #8559, #8580, #8587, #8593, #8596, #8597, #8612, #8613, #8630, #8632, #8651, #8661, #8673, #8686, #8699, #8701, #8712, #8719, #8729, #8740, #8753, #8760, #8762, #8763, #8775, #8779, #8781, #8782, #8787, #8798, #8802, #8805, #8812, #8843, #8854, #8901, #8924, #8932, #8955, #8956, #8958, #8960) +:book: Additionally, there have been 74 contributions to our documentation and book. (#8252, #8279, #8284, #8288, #8293, #8307, #8308, #8309, #8319, #8327, #8351, #8355, #8363, #8375, #8383, #8397, #8416, #8419, #8439, #8446, #8447, #8454, #8508, #8509, #8510, #8511, #8520, #8521, #8544, #8552, #8554, #8559, #8580, #8587, #8593, #8596, #8597, #8612, #8613, #8630, #8632, #8651, #8661, #8673, #8686, #8699, #8701, #8712, #8719, #8729, #8740, #8753, #8760, #8762, #8763, #8775, #8779, #8781, #8782, #8787, #8798, #8802, #8805, #8812, #8843, #8854, #8901, #8924, #8932, #8955, #8956, #8958, #8960, #8980) _Thanks to all our contributors!_ 😊