// Command changelog writes docs/site/changelog.json, which the website // renders at carlosframework.com/platform/changelog. // // Run it from a full checkout of carlos/platform with every input // fetched, and with gh and amadan signed in: // // git fetch origin --tags '+refs/amadan/closed/*:refs/amadan/closed/*' // go run ./cmd/changelog // // It reads the published release notes with gh and Amadan's discussions // with the amadan CLI, so it is a release-time tool, not part of the // build. The file it writes is checked by internal/changelog's tests. package main import ( "bytes" "encoding/json" "errors" "flag" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/carlosframework/platform/internal/changelog" "github.com/carlosframework/platform/internal/docsite" ) func main() { out := flag.String("out", "docs/site/changelog.json", "file to write") repo := flag.String("repo", "carlos/platform", "the Amadan repo discussions are read from") releases := flag.String("releases", "carlosframework/releases", "the GitHub repo release notes are read from") flag.Parse() if err := run(*out, *repo, *releases); err != nil { fmt.Fprintln(os.Stderr, "changelog:", err) os.Exit(1) } } func run(out, repo, releasesRepo string) error { people, err := changelog.LoadPeople("docs/site/people.json") if err != nil { return err } links, err := changelog.LoadLinks("docs/site/changelog-links.json") if err != nil { return err } site, err := docsite.Load("docs/site") if err != nil { return err } g, err := changelog.NewExecGit(".") if err != nil { return err } c, err := changelog.Build(changelog.Sources{ Git: g, Releases: ghReleases{repo: releasesRepo}, Discussions: amadanDiscussions{repo: repo}, }, people, links, site) if err != nil { return err } raw, err := changelog.Marshal(c) if err != nil { return err } if err := os.WriteFile(filepath.Clean(out), raw, 0o644); err != nil { return err } for _, r := range c.Releases { notes := "unpublished" if r.Notes != nil { notes = r.Notes.Form } fmt.Printf("%-8s %s %3d changes notes: %s\n", r.Tag, r.Date, len(r.Changes), notes) } return nil } func command(name string, args ...string) (stdout, stderr string, err error) { cmd := exec.Command(name, args...) var o, e bytes.Buffer cmd.Stdout, cmd.Stderr = &o, &e err = cmd.Run() return o.String(), e.String(), err } type ghReleases struct{ repo string } func (g ghReleases) Body(tag string) (string, bool, error) { out, stderr, err := command("gh", "release", "view", tag, "-R", g.repo, "--json", "body", "-q", ".body") if err != nil { if strings.Contains(stderr, "release not found") { return "", false, nil } return "", false, fmt.Errorf("gh release view %s: %v: %s", tag, err, strings.TrimSpace(stderr)) } return out, true, nil } type amadanDiscussions struct{ repo string } type amadanDiscussion struct { ID int `json:"id"` Title string `json:"title"` State string `json:"state"` Created int64 `json:"created"` } func (d amadanDiscussion) public() changelog.Discussion { return changelog.Discussion{ID: d.ID, Title: d.Title, State: d.State, Created: d.Created} } func (a amadanDiscussions) Show(id int) (*changelog.Discussion, error) { out, stderr, err := command("amadan", "discuss", "show", a.repo, fmt.Sprint(id), "-json") if strings.Contains(stderr, "no such discussion") { return nil, nil } if err != nil { return nil, fmt.Errorf("amadan discuss show %d: %v: %s", id, err, strings.TrimSpace(stderr)) } var resp struct { Discussion *amadanDiscussion `json:"discussion"` } if err := json.Unmarshal([]byte(out), &resp); err != nil || resp.Discussion == nil { return nil, fmt.Errorf("amadan discuss show %d: unreadable answer: %v %s", id, err, strings.TrimSpace(stderr)) } d := resp.Discussion.public() return &d, nil } func (a amadanDiscussions) ForBranch(branch string) ([]changelog.Discussion, error) { out, stderr, err := command("amadan", "discuss", "list", a.repo, "-branch", branch, "-state", "all", "-json") if err != nil { return nil, fmt.Errorf("amadan discuss list -branch %s: %v: %s", branch, err, strings.TrimSpace(stderr)) } var list []amadanDiscussion if err := json.Unmarshal([]byte(out), &list); err != nil { return nil, errors.New("amadan discuss list -branch " + branch + ": unreadable answer: " + err.Error()) } res := make([]changelog.Discussion, 0, len(list)) for _, d := range list { res = append(res, d.public()) } return res, nil }