mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-02-07 10:17:02 +00:00
feat: parse existing release pr
This commit is contained in:
parent
a06bbec1f6
commit
8199918903
3 changed files with 219 additions and 15 deletions
|
|
@ -4,6 +4,7 @@ Copyright © 2024 Julian Tölle
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -11,6 +12,10 @@ import (
|
||||||
rp "github.com/apricote/releaser-pleaser"
|
rp "github.com/apricote/releaser-pleaser"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RELEASER_PLEASER_BRANCH = "releaser-pleaser--branches--%s"
|
||||||
|
)
|
||||||
|
|
||||||
// runCmd represents the run command
|
// runCmd represents the run command
|
||||||
var runCmd = &cobra.Command{
|
var runCmd = &cobra.Command{
|
||||||
Use: "run",
|
Use: "run",
|
||||||
|
|
@ -56,34 +61,65 @@ func run(cmd *cobra.Command, args []string) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tag, err := f.LatestTag(ctx)
|
changesets, err := getChangesetsFromForge(ctx, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to get changesets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = reconcileReleasePR(ctx, f, changesets)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reconcile release pr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChangesetsFromForge(ctx context.Context, forge rp.Forge) ([]rp.Changeset, error) {
|
||||||
|
tag, err := forge.LatestTag(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoContext(ctx, "Latest Tag", "tag.hash", tag.Hash, "tag.name", tag.Name)
|
logger.InfoContext(ctx, "Latest Tag", "tag.hash", tag.Hash, "tag.name", tag.Name)
|
||||||
|
|
||||||
releaseableCommits, err := f.CommitsSince(ctx, tag)
|
releasableCommits, err := forge.CommitsSince(ctx, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoContext(ctx, "Found releasable commits", "length", len(releaseableCommits))
|
logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
|
||||||
|
|
||||||
changesets, err := f.Changesets(ctx, releaseableCommits)
|
changesets, err := forge.Changesets(ctx, releasableCommits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoContext(ctx, "Found changesets", "length", len(changesets))
|
logger.InfoContext(ctx, "Found changesets", "length", len(changesets))
|
||||||
|
|
||||||
for _, changeset := range changesets {
|
return changesets, nil
|
||||||
fmt.Printf("%s %s\n", changeset.Identifier, changeset.URL)
|
}
|
||||||
for _, entry := range changeset.ChangelogEntries {
|
|
||||||
fmt.Printf(" - %s %s\n", entry.Hash, entry.Description)
|
func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset) error {
|
||||||
|
// Check Forge for open PR
|
||||||
|
// Get any modifications from open PR
|
||||||
|
// Clone Repo
|
||||||
|
// Run Updaters + Changelog
|
||||||
|
// Upsert PR
|
||||||
|
pr, err := forge.PullRequestForBranch(ctx, fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pr != nil {
|
||||||
|
logger.InfoContext(ctx, "found existing release pull request: %d: %s", pr.ID, pr.Title)
|
||||||
}
|
}
|
||||||
fmt.Printf("Previous Tag: %s\n", tag.Name)
|
|
||||||
|
releaseOverrides, err := pr.GetOverrides()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
forge.go
50
forge.go
|
|
@ -8,6 +8,11 @@ import (
|
||||||
"github.com/google/go-github/v63/github"
|
"github.com/google/go-github/v63/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GITHUB_PER_PAGE_MAX = 100
|
||||||
|
GITHUB_PR_STATE_OPEN = "open"
|
||||||
|
)
|
||||||
|
|
||||||
type Changeset struct {
|
type Changeset struct {
|
||||||
URL string
|
URL string
|
||||||
Identifier string
|
Identifier string
|
||||||
|
|
@ -26,6 +31,10 @@ type Forge interface {
|
||||||
|
|
||||||
// Changesets looks up the Pull/Merge Requests for each commit, returning its parsed metadata.
|
// Changesets looks up the Pull/Merge Requests for each commit, returning its parsed metadata.
|
||||||
Changesets(context.Context, []Commit) ([]Changeset, error)
|
Changesets(context.Context, []Commit) ([]Changeset, error)
|
||||||
|
|
||||||
|
// PullRequestForBranch returns the open pull request between the branch and ForgeOptions.BaseBranch. If no open PR
|
||||||
|
// exists, it returns nil.
|
||||||
|
PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForgeOptions struct {
|
type ForgeOptions struct {
|
||||||
|
|
@ -106,7 +115,7 @@ func (g *GitHub) commitsSinceTag(ctx context.Context, tag *Tag) ([]*github.Repos
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
tag.Hash, head, &github.ListOptions{
|
tag.Hash, head, &github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: 100,
|
PerPage: GITHUB_PER_PAGE_MAX,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -149,7 +158,7 @@ func (g *GitHub) Changesets(ctx context.Context, commits []Commit) ([]Changeset,
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
commit.Hash, &github.ListOptions{
|
commit.Hash, &github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: 100,
|
PerPage: GITHUB_PER_PAGE_MAX,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -199,6 +208,43 @@ func (g *GitHub) Changesets(ctx context.Context, commits []Commit) ([]Changeset,
|
||||||
return changesets, nil
|
return changesets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*ReleasePullRequest, error) {
|
||||||
|
page := 1
|
||||||
|
|
||||||
|
for {
|
||||||
|
prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(ctx, g.options.Owner, g.options.Repo, branch, &github.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PerPage: GITHUB_PER_PAGE_MAX,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pr := range prs {
|
||||||
|
if pr.Base.GetLabel() == g.options.BaseBranch && pr.Head.GetLabel() == branch && pr.GetState() == GITHUB_PR_STATE_OPEN {
|
||||||
|
labels := make([]string, 0, len(pr.Labels))
|
||||||
|
for _, label := range pr.Labels {
|
||||||
|
labels = append(labels, label.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReleasePullRequest{
|
||||||
|
ID: pr.GetNumber(),
|
||||||
|
Title: pr.GetTitle(),
|
||||||
|
Description: pr.GetBody(),
|
||||||
|
Labels: labels,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if page == resp.LastPage || resp.LastPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GitHubOptions) autodiscover() {
|
func (g *GitHubOptions) autodiscover() {
|
||||||
// TODO: Read settings from GitHub Actions env vars
|
// TODO: Read settings from GitHub Actions env vars
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
releasepr.go
Normal file
122
releasepr.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleasePullRequest struct {
|
||||||
|
ID int
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Labels []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseOverrides struct {
|
||||||
|
Prefix string
|
||||||
|
Suffix string
|
||||||
|
// TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the changesets
|
||||||
|
NextVersionType NextVersionType
|
||||||
|
}
|
||||||
|
|
||||||
|
type NextVersionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NextVersionTypeUndefined NextVersionType = iota
|
||||||
|
NextVersionTypeNormal
|
||||||
|
NextVersionTypeRC
|
||||||
|
NextVersionTypeBeta
|
||||||
|
NextVersionTypeAlpha
|
||||||
|
)
|
||||||
|
|
||||||
|
// PR Labels
|
||||||
|
const (
|
||||||
|
LabelNextVersionTypeNormal = "rp-next-version::normal"
|
||||||
|
LabelNextVersionTypeRC = "rp-next-version::rc"
|
||||||
|
LabelNextVersionTypeBeta = "rp-next-version::beta"
|
||||||
|
LabelNextVersionTypeAlpha = "rp-next-version::alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DescriptionLanguagePrefix = "rp-prefix"
|
||||||
|
DescriptionLanguageSuffix = "rp-suffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) {
|
||||||
|
overrides := ReleaseOverrides{}
|
||||||
|
overrides = pr.parseVersioningFlags(overrides)
|
||||||
|
overrides, err := pr.parseDescription(overrides)
|
||||||
|
if err != nil {
|
||||||
|
return ReleaseOverrides{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) ReleaseOverrides {
|
||||||
|
for _, label := range pr.Labels {
|
||||||
|
switch label {
|
||||||
|
// Versioning
|
||||||
|
case LabelNextVersionTypeNormal:
|
||||||
|
overrides.NextVersionType = NextVersionTypeNormal
|
||||||
|
case LabelNextVersionTypeRC:
|
||||||
|
overrides.NextVersionType = NextVersionTypeRC
|
||||||
|
case LabelNextVersionTypeBeta:
|
||||||
|
overrides.NextVersionType = NextVersionTypeBeta
|
||||||
|
case LabelNextVersionTypeAlpha:
|
||||||
|
overrides.NextVersionType = NextVersionTypeAlpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
|
||||||
|
source := []byte(pr.Description)
|
||||||
|
descriptionAST := parser.NewParser().Parse(text.NewReader(source))
|
||||||
|
|
||||||
|
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if !entering {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Type() != ast.TypeBlock || n.Kind() != ast.KindFencedCodeBlock {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBlock, ok := n.(*ast.FencedCodeBlock)
|
||||||
|
if !ok {
|
||||||
|
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(codeBlock.Language(source)) {
|
||||||
|
case DescriptionLanguagePrefix:
|
||||||
|
overrides.Prefix = textFromLines(source, codeBlock)
|
||||||
|
case DescriptionLanguageSuffix:
|
||||||
|
overrides.Suffix = textFromLines(source, codeBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ReleaseOverrides{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFromLines(source []byte, n ast.Node) string {
|
||||||
|
content := make([]byte, 0)
|
||||||
|
|
||||||
|
l := n.Lines().Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
line := n.Lines().At(i)
|
||||||
|
content = append(content, line.Value(source)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content)
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue