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  }