diff --git a/cmd/changelog-check/main.go b/cmd/changelog-check/main.go new file mode 100644 index 0000000..8b3fcef --- /dev/null +++ b/cmd/changelog-check/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "io" + "log" + "os" + + "github.com/hashicorp/go-changelog" +) + +func main() { + var err error + + // default to reading from stdin + r := os.Stdin + + // read from file arg instead if provided + // TODO: add --help text for [file] arg handling + filepath := "" + if len(os.Args) > 1 { + filepath = os.Args[1] + r, err = os.Open(filepath) + if err != nil { + log.Fatalf("error opening %s", filepath) + os.Exit(1) + } + } + + b, err := io.ReadAll(r) + if err != nil { + if filepath != "" { + log.Fatalf("error reading from %s", filepath) + } else { + log.Fatalf("error reading from stdin") + } + os.Exit(1) + } + + entry := changelog.Entry{ + Body: string(b), + } + + if err := entry.Validate(); err != nil { + log.Fatalf(err.Error()) + os.Exit(1) + } +} diff --git a/cmd/changelog-pr-body-check/main.go b/cmd/changelog-pr-body-check/main.go index e2c701c..0f59cea 100644 --- a/cmd/changelog-pr-body-check/main.go +++ b/cmd/changelog-pr-body-check/main.go @@ -48,60 +48,57 @@ func main() { log.Fatalf("Error retrieving pull request github.com/"+ "%s/%s/%d: %s", owner, repo, prNo, err) } + entry := changelog.Entry{ Issue: pr, Body: pullRequest.GetBody(), } - notes := changelog.NotesFromEntry(entry) - if len(notes) < 1 { - log.Printf("no changelog entry found in %s: %s", entry.Issue, - string(entry.Body)) - body := "Oops! It looks like no changelog entry is attached to" + - " this PR. Please include a release note block" + - " in the PR body, as described in https://github.com/GoogleCloudPlatform/magic-modules/blob/master/.ci/RELEASE_NOTES_GUIDE.md:" + - "\n\n~~~\n```release-note:TYPE\nRelease note" + - "\n```\n~~~" - _, _, err = client.Issues.CreateComment(ctx, owner, repo, - prNo, &github.IssueComment{ - Body: &body, - }) - if err != nil { - log.Fatalf("Error creating pull request comment on"+ - " github.com/%s/%s/%d: %s", owner, repo, prNo, - err) - } - os.Exit(1) - } - var unknownTypes []string - for _, note := range notes { - if !changelog.TypeValid(note.Type) { - unknownTypes = append(unknownTypes, note.Type) - } - } - if len(unknownTypes) > 0 { - log.Printf("unknown changelog types %v", unknownTypes) - body := "Oops! It looks like you're using" - if len(unknownTypes) == 1 { - body += " an" - } - body += " unknown release-note type" - if len(unknownTypes) > 1 { - body += "s" - } - body += " in your changelog entries:" - for _, t := range unknownTypes { - body += "\n* " + t - } - body += "\n\nPlease only use the types listed in https://github.com/GoogleCloudPlatform/magic-modules/blob/master/.ci/RELEASE_NOTES_GUIDE.md." - _, _, err = client.Issues.CreateComment(ctx, owner, repo, - prNo, &github.IssueComment{ - Body: &body, - }) - if err != nil { - log.Fatalf("Error creating pull request comment on"+ - " github.com/%s/%s/%d: %s", owner, repo, prNo, - err) + + if err := entry.Validate(); err != nil { + log.Printf("error parsing changelog entry in %s: %s", entry.Issue, err) + switch err.Code { + case changelog.EntryErrorNotFound: + body := "Oops! It looks like no changelog entry is attached to" + + " this PR. Please include a release note block" + + " in the PR body, as described in https://github.com/GoogleCloudPlatform/magic-modules/blob/master/.ci/RELEASE_NOTES_GUIDE.md:" + + "\n\n~~~\n```release-note:TYPE\nRelease note" + + "\n```\n~~~" + _, _, err := client.Issues.CreateComment(ctx, owner, repo, + prNo, &github.IssueComment{ + Body: &body, + }) + if err != nil { + log.Fatalf("Error creating pull request comment on"+ + " github.com/%s/%s/%d: %s", owner, repo, prNo, + err) + } + os.Exit(1) + case changelog.EntryErrorUnknownTypes: + unknownTypes := err.Details["unknownTypes"].([]string) + + body := "Oops! It looks like you're using" + if len(unknownTypes) == 1 { + body += " an" + } + body += " unknown release-note type" + if len(unknownTypes) > 1 { + body += "s" + } + body += " in your changelog entries:" + for _, t := range unknownTypes { + body += "\n* " + t + } + body += "\n\nPlease only use the types listed in https://github.com/GoogleCloudPlatform/magic-modules/blob/master/.ci/RELEASE_NOTES_GUIDE.md." + _, _, err := client.Issues.CreateComment(ctx, owner, repo, + prNo, &github.IssueComment{ + Body: &body, + }) + if err != nil { + log.Fatalf("Error creating pull request comment on"+ + " github.com/%s/%s/%d: %s", owner, repo, prNo, + err) + } + os.Exit(1) } - os.Exit(1) } } diff --git a/entry.go b/entry.go index 4a0a59f..aef06b3 100644 --- a/entry.go +++ b/entry.go @@ -39,6 +39,54 @@ type EntryList struct { es []*Entry } +type EntryErrorCode string + +const ( + EntryErrorNotFound EntryErrorCode = "NOT_FOUND" + EntryErrorUnknownTypes EntryErrorCode = "UNKNOWN_TYPES" +) + +type EntryValidationError struct { + message string + Code EntryErrorCode + Details map[string]interface{} +} + +func (e *EntryValidationError) Error() string { + return e.message +} + +// Validates that an Entry body contains properly formatted changelog notes +func (e *Entry) Validate() *EntryValidationError { + notes := NotesFromEntry(*e) + + if len(notes) < 1 { + return &EntryValidationError{ + message: fmt.Sprintf("no changelog entry found in: %s", string(e.Body)), + Code: EntryErrorNotFound, + } + } + + var unknownTypes []string + for _, note := range notes { + if !TypeValid(note.Type) { + unknownTypes = append(unknownTypes, note.Type) + } + } + + if len(unknownTypes) > 0 { + return &EntryValidationError{ + message: fmt.Sprintf("unknown changelog types %v: please use only the configured changelog entry types: %v", unknownTypes, TypeValues), + Code: EntryErrorUnknownTypes, + Details: map[string]interface{}{ + "unknownTypes": unknownTypes, + }, + } + } + + return nil +} + // NewEntryList returns an EntryList with capacity c func NewEntryList(c int) *EntryList { return &EntryList{ @@ -139,7 +187,7 @@ func Diff(repo, ref1, ref2, dir string) (*EntryList, error) { if err := wt.Checkout(&git.CheckoutOptions{ Hash: *rev2, Force: true, - }); err != nil { + }); err != nil { return nil, fmt.Errorf("could not checkout repository at %s: %w", ref2, err) } entriesAfterFI, err := wt.Filesystem.ReadDir(dir)