Files
zooqueen 5577c95c38 deps: consume gh, xfail, and wire from upstream
These three are generic upstream helper libraries that carry no local
changes: the hashes recorded for them are byte-identical to the upstream
modules, and their pinned go.mod bytes declare the upstream path. A
github.com/hanzoai requirement therefore has nothing to resolve against
from a cold cache. Point go.mod, go.sum, and imports at upstream, which
is what the root module already does for wire.

Pin the local docdb replace at v1.0.0; the prior v2 placeholder failed
go.mod parsing.
2026-08-19 22:53:24 -07:00

255 lines
5.6 KiB
Go

// Copyright 2021 Hanzo AI Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main contains script that generates changes for the latest version.
package main
import (
"bytes"
"context"
_ "embed"
"errors"
"flag"
"fmt"
"io"
"log"
"log/slog"
"os"
"strconv"
"text/template"
"github.com/FerretDB/gh"
"github.com/google/go-github/v70/github"
)
//go:embed changelog.md.template
var templateFile string
// PRData represents template information about a single pull request.
type PRData struct {
Title string
Author string
URL string
}
// Data represents template information about the release.
type Data struct {
PrevVersion string
Version string
Date string
Milestone int
NewFeatures []PRData
FixedBugs []PRData
Enhancements []PRData
Documentation []PRData
OtherChanges []PRData
}
// getMilestone returns milestone by title (which matches DocDB version and Git tag).
func getMilestone(ctx context.Context, client *github.Client, title string) (*github.Milestone, error) {
opts := &github.MilestoneListOptions{
State: "all",
Sort: "due_on",
Direction: "desc",
ListOptions: github.ListOptions{
Page: 1,
PerPage: 100,
},
}
for {
milestones, resp, err := client.Issues.ListMilestones(ctx, "DocDB", "DocDB", opts)
if err != nil {
return nil, err
}
for _, milestone := range milestones {
if *milestone.Title == title {
return milestone, nil
}
}
if resp.NextPage == 0 {
return nil, fmt.Errorf("no milestone found with the title %q", title)
}
opts.ListOptions.Page = resp.NextPage
}
}
// getPRs returns all pull requests for the given milestone.
func getPRs(ctx context.Context, client *github.Client, milestone *github.Milestone) ([]*github.Issue, error) {
opts := &github.IssueListByRepoOptions{
Milestone: strconv.Itoa(*milestone.Number),
State: "all",
Sort: "created",
Direction: "asc",
ListOptions: github.ListOptions{
Page: 1,
PerPage: 100,
},
}
var prs []*github.Issue
for {
issues, resp, err := client.Issues.ListByRepo(ctx, "DocDB", "DocDB", opts)
if err != nil {
return nil, err
}
for _, issue := range issues {
if issue.IsPullRequest() {
prs = append(prs, issue)
}
}
if resp.NextPage == 0 {
return prs, nil
}
opts.ListOptions.Page = resp.NextPage
}
}
// makeData creates template data.
func makeData(milestone *github.Milestone, prev string, prs []*github.Issue, l *slog.Logger) (*Data, error) {
if milestone.ClosedAt != nil {
l.Warn("milestone is closed")
}
d := &Data{
PrevVersion: prev,
Version: *milestone.Title,
Milestone: *milestone.Number,
}
if milestone.DueOn == nil {
return nil, fmt.Errorf("milestone %q does not have a due date", *milestone.Title)
}
d.Date = milestone.DueOn.Format("2006-01-02")
var errs []error
for _, pr := range prs {
if pr.ClosedAt == nil {
l.Warn(fmt.Sprintf("PR is not closed: %s", *pr.HTMLURL))
}
prData := PRData{
Title: *pr.Title,
Author: *pr.User.Login,
URL: *pr.HTMLURL,
}
var found bool
for _, label := range pr.Labels {
switch *label.Name {
case "code/feature":
d.NewFeatures = append(d.NewFeatures, prData)
case "code/bug", "code/bug-regression":
d.FixedBugs = append(d.FixedBugs, prData)
case "code/enhancement":
d.Enhancements = append(d.Enhancements, prData)
case "blog/engineering", "blog/marketing", "documentation":
d.Documentation = append(d.Documentation, prData)
case "code/chore", "project", "deps":
d.OtherChanges = append(d.OtherChanges, prData)
default:
continue
}
if !found {
found = true
continue
}
errs = append(errs, fmt.Errorf("multiple possible categories for %s", prData.URL))
break
}
if !found {
errs = append(errs, fmt.Errorf("no category found for %s", prData.URL))
}
}
return d, errors.Join(errs...)
}
// run generates the changelog.
func run(w io.Writer, l *slog.Logger, prev, next string) error {
ctx := context.Background()
client, err := gh.NewRESTClient(os.Getenv("GITHUB_TOKEN"), gh.SLogPrintf(l))
if err != nil {
return err
}
_, err = getMilestone(ctx, client, prev)
if err != nil {
return err
}
milestone, err := getMilestone(ctx, client, next)
if err != nil {
return err
}
l.Info("Received milestones", slog.Int("next", *milestone.Number))
prs, err := getPRs(ctx, client, milestone)
if err != nil {
return err
}
l.Info("Received PRs", slog.Int("count", len(prs)))
d, err := makeData(milestone, prev, prs, l)
if err != nil {
return err
}
t, err := template.New("").Option("missingkey=error").Parse(templateFile)
if err != nil {
return err
}
if err = t.Execute(w, d); err != nil {
return err
}
return nil
}
func main() {
prevF := flag.String("prev", "", "Previous version")
nextF := flag.String("next", "", "Next version")
flag.Parse()
if *prevF == "" || *nextF == "" {
log.Fatal("Both -prev and -next must be specified.")
}
var buf bytes.Buffer
if err := run(&buf, slog.Default(), *prevF, *nextF); err != nil {
log.Fatal(err)
}
if _, err := io.Copy(os.Stdout, &buf); err != nil {
log.Fatal(err)
}
}