diff --git a/cmd/release-notes/generate.go b/cmd/release-notes/generate.go index 89d7666e6a4..16863aa99de 100644 --- a/cmd/release-notes/generate.go +++ b/cmd/release-notes/generate.go @@ -243,6 +243,13 @@ func addGenerateFlags(subcommand *cobra.Command) { []string{}, "specify a location to recursively look for release notes *.y[a]ml file mappings", ) + + subcommand.PersistentFlags().StringVar( + &opts.ReleaseNoteRegex, + "release-note-regex", + "", + "Optional regex to extract release note from PR body. Must have a named group 'note'. If unset, default ```release-note blocks are used.", + ) } // addGenerate adds the generate subcomand to the main release notes cobra cmd. diff --git a/pkg/notes/document/manifest.json b/pkg/notes/document/manifest.json new file mode 100644 index 00000000000..403f47625af --- /dev/null +++ b/pkg/notes/document/manifest.json @@ -0,0 +1 @@ +[{"RepoTags": ["registry.k8s.io/conformance-amd64:v1.16.0"]}] \ No newline at end of file diff --git a/pkg/notes/notes.go b/pkg/notes/notes.go index e400d457a07..7241dbc842b 100644 --- a/pkg/notes/notes.go +++ b/pkg/notes/notes.go @@ -234,6 +234,29 @@ func NewGatherer(ctx context.Context, opts *options.Options) (*Gatherer, error) return nil, fmt.Errorf("unable to create notes client: %w", err) } + if opts.ReleaseNoteRegex != "" { + re, err := regexp.Compile(opts.ReleaseNoteRegex) + if err != nil { + return nil, fmt.Errorf("invalid --release-note-regex: %w", err) + } + + hasNote := false + + for _, n := range re.SubexpNames() { + if n == "note" { + hasNote = true + + break + } + } + + if !hasNote { + return nil, errors.New("--release-note-regex must define a named capture group 'note', e.g. (?P...)") + } + + opts.ReleaseNoteRegexCompiled = re + } + return &Gatherer{ client: client, context: ctx, @@ -394,27 +417,28 @@ func (g *Gatherer) ListReleaseNotes() (*ReleaseNotes, error) { return notes, nil } -// noteTextFromString returns the text of the release note given a string which -// may contain the commit message, the PR description, etc. -// This is generally the content inside the ```release-note ``` stanza. -func noteTextFromString(s string) (string, error) { - // check release note is not empty - // Matches "release-notes" block with no meaningful content (ex. only whitespace, empty, just newlines) - emptyExps := []*regexp.Regexp{ - regexp.MustCompile("(?i)```release-notes?\\s*```\\s*"), - } +// noteTextFromString returns the text of the release note from a string (e.g. PR body). +// If customRegex is non-nil, exps is set to just that; otherwise exps are the default +// ```release-note / ```dev-release-note patterns. All patterns must capture a "note" group. +func noteTextFromString(s string, customRegex *regexp.Regexp) (string, error) { + var exps []*regexp.Regexp + if customRegex != nil { + exps = []*regexp.Regexp{customRegex} + } else { + emptyExps := []*regexp.Regexp{ + regexp.MustCompile("(?i)```release-notes?\\s*```\\s*"), + } - if matchesFilter(s, emptyExps) { - return "", errors.New("empty release note") - } + if matchesFilter(s, emptyExps) { + return "", errors.New("empty release note") + } - exps := []*regexp.Regexp{ - // (?s) is needed for '.' to be matching on newlines, by default that's disabled - // we need to match ungreedy 'U', because after the notes a `docs` block can occur - regexp.MustCompile("(?sU)```release-notes?\\r\\n(?P.+)\\r\\n```"), - regexp.MustCompile("(?sU)```dev-release-notes?\\r\\n(?P.+)"), - regexp.MustCompile("(?sU)```\\r\\n(?P.+)\\r\\n```"), - regexp.MustCompile("(?sU)```release-notes?\n(?P.+)\n```"), + exps = []*regexp.Regexp{ + regexp.MustCompile("(?sU)```release-notes?\\r\\n(?P.+)\\r\\n```"), + regexp.MustCompile("(?sU)```dev-release-notes?\\r\\n(?P.+)"), + regexp.MustCompile("(?sU)```\\r\\n(?P.+)\\r\\n```"), + regexp.MustCompile("(?sU)```release-notes?\n(?P.+)\n```"), + } } for _, exp := range exps { @@ -509,7 +533,7 @@ func (g *Gatherer) ReleaseNoteFromCommit(result *Result) (*ReleaseNote, error) { prBody := pr.GetBody() - text, err := noteTextFromString(prBody) + text, err := noteTextFromString(prBody, g.options.ReleaseNoteRegexCompiled) if err != nil { return nil, err } @@ -791,7 +815,7 @@ func (g *Gatherer) ReleaseNoteForPullRequest(prNr int) (*ReleaseNote, error) { // If we didn't match the exclusion filter, try to extract the release note from the PR. // If we can't extract the release note, consider that the PR is invalid and take the next one - s, err := noteTextFromString(prBody) + s, err := noteTextFromString(prBody, g.options.ReleaseNoteRegexCompiled) if err != nil && !doNotPublish { return nil, fmt.Errorf("PR #%d does not seem to contain a valid release note: %w", pr.GetNumber(), err) } @@ -867,7 +891,7 @@ func (g *Gatherer) notesForCommit(commit *gogithub.RepositoryCommit) (*Result, e // If we didn't match the exclusion filter, try to extract the release note from the PR. // If we can't extract the release note, consider that the PR is invalid and take the next one - s, err := noteTextFromString(prBody) + s, err := noteTextFromString(prBody, g.options.ReleaseNoteRegexCompiled) if err != nil { logrus.Infof("PR #%d does not seem to contain a valid release note, skipping", pr.GetNumber()) diff --git a/pkg/notes/notes_test.go b/pkg/notes/notes_test.go index 990a628dc3f..5f78c8f764f 100644 --- a/pkg/notes/notes_test.go +++ b/pkg/notes/notes_test.go @@ -321,7 +321,7 @@ func TestNoteTextFromString(t *testing.T) { }, }, } { - tc.expect(noteTextFromString(tc.input)) + tc.expect(noteTextFromString(tc.input, nil)) } } diff --git a/pkg/notes/notes_v2.go b/pkg/notes/notes_v2.go index 5d4e394c439..1804d444b31 100644 --- a/pkg/notes/notes_v2.go +++ b/pkg/notes/notes_v2.go @@ -162,7 +162,7 @@ func (g *Gatherer) buildReleaseNote(pair *commitPrPair) (*ReleaseNote, error) { return nil, nil } - text, err := noteTextFromString(prBody) + text, err := noteTextFromString(prBody, g.options.ReleaseNoteRegexCompiled) if err != nil { logrus.WithFields(logrus.Fields{ "sha": pair.Commit.Hash.String(), diff --git a/pkg/notes/options/options.go b/pkg/notes/options/options.go index d00fd16ec56..b557a333f88 100644 --- a/pkg/notes/options/options.go +++ b/pkg/notes/options/options.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/sirupsen/logrus" @@ -136,6 +137,13 @@ type Options struct { // IncludeLabels can be used to filter PRs by labels so only PRs with one or more specified labels are included. IncludeLabels []string + + // ReleaseNoteRegex optionally overrides how release note text is extracted from PR bodies. + // When set, this regex is used instead of the default ```release-note blocks. Must define a named capture "note". + ReleaseNoteRegex string + + // ReleaseNoteRegexCompiled is the compiled form of ReleaseNoteRegex, set when a gatherer is created. + ReleaseNoteRegexCompiled *regexp.Regexp } type RevisionDiscoveryMode string