supabase-cli/tools/changelog/main.go

184 lines
4.6 KiB
Go

package main
import (
"context"
_ "embed"
"fmt"
"log"
"os"
"os/signal"
"regexp"
"sort"
"strings"
"time"
"github.com/google/go-github/v62/github"
"github.com/slack-go/slack"
"github.com/supabase/cli/internal/utils"
)
func main() {
slackChannel := ""
if len(os.Args) > 1 {
slackChannel = os.Args[1]
}
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
if err := showChangeLog(ctx, slackChannel); err != nil {
log.Fatalln(err)
}
}
func showChangeLog(ctx context.Context, slackChannel string) error {
client := utils.GetGitHubClient(ctx)
releases, _, err := client.Repositories.ListReleases(ctx, utils.CLI_OWNER, utils.CLI_REPO, &github.ListOptions{})
if err != nil {
return err
}
opts := github.GenerateNotesOptions{}
n := getLatestRelease(releases)
if n < len(releases) {
opts.TagName = *releases[n].TagName
if m := getLatestRelease(releases[n+1:]) + n + 1; m < len(releases) {
opts.PreviousTagName = releases[m].TagName
}
} else {
branch := "main"
opts.TargetCommitish = &branch
opts.TagName = "v1.0.0"
}
fmt.Fprintln(os.Stderr, "Generating changelog for", opts.TagName)
notes, _, err := client.Repositories.GenerateReleaseNotes(ctx, utils.CLI_OWNER, utils.CLI_REPO, &opts)
if err != nil {
return err
}
title := Title(releases[n])
body := Body(notes)
fmt.Println(title)
fmt.Println(body)
if len(slackChannel) == 0 {
return nil
}
title = slackFormat(title)
body = slackFormat(body)
return slackAnnounce(ctx, slackChannel, title, body)
}
func getLatestRelease(releases []*github.RepositoryRelease) int {
for i, r := range releases {
if !*r.Draft && !*r.Prerelease {
return i
}
}
return len(releases)
}
func Title(r *github.RepositoryRelease) string {
timestamp := r.PublishedAt.GetTime()
if timestamp == nil {
now := time.Now().UTC()
timestamp = &now
}
return fmt.Sprintf("# %s (%s)\n", timestamp.Format("2 Jan 2006"), *r.TagName)
}
var logPattern = regexp.MustCompile(`^\* (.*): (.*) by @(.*) in (https:.*)$`)
type ChangeGroup struct {
Prefix string
Header string
Messages []string
}
func (g ChangeGroup) Markdown() string {
result := make([]string, len(g.Messages)+2)
result[1] = "### " + g.Header
for i, m := range g.Messages {
result[i+2] = logPattern.ReplaceAllString(m, "* [$1]($4): $2")
}
return strings.Join(result, "\n")
}
func Body(n *github.RepositoryReleaseNotes) string {
lines := strings.Split(n.Body, "\n")
// Group features, fixes, dependencies, and chores
groups := []ChangeGroup{
{Prefix: "feat", Header: "Features"},
{Prefix: "fix", Header: "Bug fixes"},
{Prefix: "chore(deps)", Header: "Dependencies"},
{Header: "Others"},
}
footer := []string{}
for _, msg := range lines[1:] {
matches := logPattern.FindStringSubmatch(msg)
if len(matches) != 5 {
footer = append(footer, msg)
continue
}
cat := strings.ToLower(matches[1])
for i, g := range groups {
if strings.HasPrefix(cat, g.Prefix) {
groups[i].Messages = append(g.Messages, msg)
break
}
}
}
// Concatenate output
result := []string{lines[0]}
for _, g := range groups {
if len(g.Messages) > 0 && g.Header != "Dependencies" {
sort.Strings(g.Messages)
result = append(result, g.Markdown())
}
}
result = append(result, footer...)
return strings.Join(result, "\n")
}
var linkPattern = regexp.MustCompile(`^(.*)\[(.*?)\]\((.*?)\)(.*)$`)
func toSlack(md string) string {
// Change link format
line := linkPattern.ReplaceAllString(md, "$1<$3|$2>$4")
// Change first header to plain text
if strings.HasPrefix(line, "# ") {
return line[2:]
}
// Change second header to italics
if strings.HasPrefix(line, "## ") {
return fmt.Sprintf("_%s_", line[3:])
}
// Change third header to bold
if strings.HasPrefix(line, "### ") {
return fmt.Sprintf("*%s*", line[4:])
}
// Keep original list style
if strings.HasPrefix(line, "* ") {
return "• " + line[2:]
}
// Keep original bold style
return strings.ReplaceAll(line, "**", "*")
}
func slackFormat(md string) string {
lines := strings.Split(md, "\n")
for i, md := range lines {
lines[i] = toSlack(md)
}
return strings.Join(lines, "\n")
}
func slackAnnounce(ctx context.Context, channel, title, body string) error {
api := slack.New(os.Getenv("SLACK_TOKEN"), slack.OptionDebug(true))
msg := slack.MsgOptionBlocks(
slack.NewHeaderBlock(&slack.TextBlockObject{Type: slack.PlainTextType, Text: title}),
slack.NewSectionBlock(&slack.TextBlockObject{Type: slack.MarkdownType, Text: body}, nil, nil),
)
_, timestamp, err := api.PostMessageContext(ctx, channel, msg)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Announced changelog", timestamp)
return nil
}