github.com/supabase/cli@v1.168.1/tools/changelog/main.go (about) 1 package main 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "log" 8 "os" 9 "os/signal" 10 "regexp" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/google/go-github/v62/github" 16 "github.com/slack-go/slack" 17 "github.com/supabase/cli/internal/utils" 18 ) 19 20 func main() { 21 slackChannel := "" 22 if len(os.Args) > 1 { 23 slackChannel = os.Args[1] 24 } 25 26 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) 27 if err := showChangeLog(ctx, slackChannel); err != nil { 28 log.Fatalln(err) 29 } 30 } 31 32 func showChangeLog(ctx context.Context, slackChannel string) error { 33 client := utils.GetGtihubClient(ctx) 34 releases, _, err := client.Repositories.ListReleases(ctx, utils.CLI_OWNER, utils.CLI_REPO, &github.ListOptions{}) 35 if err != nil { 36 return err 37 } 38 opts := github.GenerateNotesOptions{} 39 n := getLatestRelease(releases) 40 if n < len(releases) { 41 opts.TagName = *releases[n].TagName 42 if m := getLatestRelease(releases[n+1:]) + n + 1; m < len(releases) { 43 opts.PreviousTagName = releases[m].TagName 44 } 45 } else { 46 branch := "main" 47 opts.TargetCommitish = &branch 48 opts.TagName = "v1.0.0" 49 } 50 fmt.Fprintln(os.Stderr, "Generating changelog for", opts.TagName) 51 notes, _, err := client.Repositories.GenerateReleaseNotes(ctx, utils.CLI_OWNER, utils.CLI_REPO, &opts) 52 if err != nil { 53 return err 54 } 55 title := Title(releases[n]) 56 body := Body(notes) 57 fmt.Println(title) 58 fmt.Println(body) 59 if len(slackChannel) == 0 { 60 return nil 61 } 62 title = slackFormat(title) 63 body = slackFormat(body) 64 return slackAnnounce(ctx, slackChannel, title, body) 65 } 66 67 func getLatestRelease(releases []*github.RepositoryRelease) int { 68 for i, r := range releases { 69 if !*r.Draft && !*r.Prerelease { 70 return i 71 } 72 } 73 return len(releases) 74 } 75 76 func Title(r *github.RepositoryRelease) string { 77 timestamp := r.PublishedAt.GetTime() 78 if timestamp == nil { 79 now := time.Now().UTC() 80 timestamp = &now 81 } 82 return fmt.Sprintf("# %s (%s)\n", timestamp.Format("2 Jan 2006"), *r.TagName) 83 } 84 85 var logPattern = regexp.MustCompile(`^\* (.*): (.*) by @(.*) in (https:.*)$`) 86 87 type ChangeGroup struct { 88 Prefix string 89 Header string 90 Messages []string 91 } 92 93 func (g ChangeGroup) Markdown() string { 94 result := make([]string, len(g.Messages)+2) 95 result[1] = "### " + g.Header 96 for i, m := range g.Messages { 97 result[i+2] = logPattern.ReplaceAllString(m, "* [$1]($4): $2") 98 } 99 return strings.Join(result, "\n") 100 } 101 102 func Body(n *github.RepositoryReleaseNotes) string { 103 lines := strings.Split(n.Body, "\n") 104 // Group features, fixes, dependencies, and chores 105 groups := []ChangeGroup{ 106 {Prefix: "feat", Header: "Features"}, 107 {Prefix: "fix", Header: "Bug fixes"}, 108 {Prefix: "chore(deps)", Header: "Dependencies"}, 109 {Header: "Others"}, 110 } 111 footer := []string{} 112 for _, msg := range lines[1:] { 113 matches := logPattern.FindStringSubmatch(msg) 114 if len(matches) != 5 { 115 footer = append(footer, msg) 116 continue 117 } 118 cat := strings.ToLower(matches[1]) 119 for i, g := range groups { 120 if strings.HasPrefix(cat, g.Prefix) { 121 groups[i].Messages = append(g.Messages, msg) 122 break 123 } 124 } 125 } 126 // Concatenate output 127 result := []string{lines[0]} 128 for _, g := range groups { 129 if len(g.Messages) > 0 && g.Header != "Dependencies" { 130 sort.Strings(g.Messages) 131 result = append(result, g.Markdown()) 132 } 133 } 134 result = append(result, footer...) 135 return strings.Join(result, "\n") 136 } 137 138 var linkPattern = regexp.MustCompile(`^(.*)\[(.*?)\]\((.*?)\)(.*)$`) 139 140 func toSlack(md string) string { 141 // Change link format 142 line := linkPattern.ReplaceAllString(md, "$1<$3|$2>$4") 143 // Change first header to plain text 144 if strings.HasPrefix(line, "# ") { 145 return line[2:] 146 } 147 // Change second header to italics 148 if strings.HasPrefix(line, "## ") { 149 return fmt.Sprintf("_%s_", line[3:]) 150 } 151 // Change third header to bold 152 if strings.HasPrefix(line, "### ") { 153 return fmt.Sprintf("*%s*", line[4:]) 154 } 155 // Keep original list style 156 if strings.HasPrefix(line, "* ") { 157 return "• " + line[2:] 158 } 159 // Keep original bold style 160 return strings.ReplaceAll(line, "**", "*") 161 } 162 163 func slackFormat(md string) string { 164 lines := strings.Split(md, "\n") 165 for i, md := range lines { 166 lines[i] = toSlack(md) 167 } 168 return strings.Join(lines, "\n") 169 } 170 171 func slackAnnounce(ctx context.Context, channel, title, body string) error { 172 api := slack.New(os.Getenv("SLACK_TOKEN"), slack.OptionDebug(true)) 173 msg := slack.MsgOptionBlocks( 174 slack.NewHeaderBlock(&slack.TextBlockObject{Type: slack.PlainTextType, Text: title}), 175 slack.NewSectionBlock(&slack.TextBlockObject{Type: slack.MarkdownType, Text: body}, nil, nil), 176 ) 177 _, timestamp, err := api.PostMessageContext(ctx, channel, msg) 178 if err != nil { 179 return err 180 } 181 fmt.Fprintln(os.Stderr, "Announced changelog", timestamp) 182 return nil 183 }