Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing changelog generation tool #264

Merged
merged 28 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
97a64c0
Introducing changelog
doggydogworld Jul 1, 2024
47c7d92
Moving things around
doggydogworld Jul 1, 2024
7d016de
Fixing typo
doggydogworld Jul 2, 2024
d8046e7
Changing licenses on new files
doggydogworld Jul 2, 2024
2bfafc1
Moving to multi-module
doggydogworld Jul 3, 2024
62d5e67
Moved to github SDK
doggydogworld Jul 5, 2024
eb0adee
Removing some unecessary code
doggydogworld Jul 5, 2024
9f091be
Requiring dir to be passed in to simplify code
doggydogworld Jul 5, 2024
cc60d0d
Initial refactor to go-git lib
doggydogworld Jul 5, 2024
d604039
Removing more git execs for native go git
doggydogworld Jul 5, 2024
de6e47e
Fixing some typos
doggydogworld Jul 5, 2024
6d34444
More refactors for git module
doggydogworld Jul 5, 2024
7ce3d02
Moving another function to git module
doggydogworld Jul 5, 2024
3d7f69d
Fixing typo
doggydogworld Jul 5, 2024
6b00945
Updating an error string
doggydogworld Jul 5, 2024
7ffa993
Restoring bot module deps
doggydogworld Jul 5, 2024
d3a4a52
Moving around a file
doggydogworld Jul 5, 2024
f7c537f
Updating docs for git module
doggydogworld Jul 5, 2024
9acb082
Using v3 API for search
doggydogworld Jul 6, 2024
6e67e79
Cleaning up multi-module and adding links back to ent cl
doggydogworld Jul 9, 2024
d48641f
Using git CLI
doggydogworld Jul 9, 2024
207e590
Even prettier changelog
doggydogworld Jul 9, 2024
a6692fb
Go moduling changelog and libs
doggydogworld Jul 10, 2024
c94b5c0
Using cl for template name
doggydogworld Jul 10, 2024
f4a6950
Fixing changelog mod
doggydogworld Jul 10, 2024
62ba564
Updating govulncheck to use latest go version
doggydogworld Jul 15, 2024
2c75aae
Using more secure version of go-github
doggydogworld Jul 15, 2024
86a40ae
New dependencies
doggydogworld Jul 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/govulncheck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.18'
go-version: '1.22'
check-latest: true

- name: govulncheck
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go.work*
50 changes: 50 additions & 0 deletions libs/git/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2024 Gravitational, 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 git

import (
"bytes"
"os/exec"
"strings"

"github.com/gravitational/trace"
)

// IsAvailable returns status of git
func IsAvailable() error {
_, err := exec.LookPath("git")
return err
}

// RunCmd runs git and returns output (stdout/stderr, depends on the cmd result) and error
func (r *Repo) RunCmd(args ...string) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer

cmd := exec.Command("git", args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Dir = r.dir

err := cmd.Run()

if err != nil {
return strings.TrimSpace(stderr.String()), trace.Wrap(err)
}

return strings.TrimSpace(stdout.String()), nil
}
98 changes: 98 additions & 0 deletions libs/git/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2024 Gravitational, 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 git

import (
"strings"
"time"

"github.com/gravitational/trace"
)

// Repo provides a collection of functions to query and modify a single git repository.
// Wrapper around the go-git library that also includes methods to execute git commands
// on the system for missing compatability.
type Repo struct {
dir string
}

const (
gitTimeFormat = "Mon Jan 02 15:04:05 2006 -0700"
)

// NewRepoFromDirectory initializes [Repo] from a directory.
func NewRepoFromDirectory(dir string) (*Repo, error) {
return &Repo{
dir: dir,
}, nil
}

// BranchNameForHead will get the name of branch currently on.
// If not on a branch will return an error.
func (r *Repo) BranchNameForHead() (string, error) {
// get ref
ref, err := r.RunCmd("symbolic-ref", "HEAD")
if err != nil {
return "", trace.Wrap(err, "not on a branch")
}

// remove prefix and ensure that branch is in expected format
branch, _ := strings.CutPrefix(ref, "refs/heads/")
if branch == ref {
return "", trace.BadParameter("not on a branch: %s", ref)
}
return branch, nil
}

// TimestampForRef will get the timestamp for the given reference.
// Will work for symbolic references such as tags, HEAD, branches
func (r *Repo) TimestampForRef(ref string) (time.Time, error) {
t, err := r.RunCmd("show", "-s", "--format=%cd", ref)
if err != nil {
return time.Time{}, trace.Wrap(err, "can't get timestamp for ref")
}
return time.Parse(gitTimeFormat, t)
}

// TimestampForLatestCommit will get the timestamp for the last commit.
func (r *Repo) TimestampForLatestCommit() (time.Time, error) {
t, err := r.RunCmd("log", "-n", "1", "--format=%cd")
if err != nil {
return time.Time{}, trace.Wrap(err, "can't get timestamp for latest commit")
}
return time.Parse(gitTimeFormat, t)
}

// GetParentReleaseBranch will attempt to find a parent branch for HEAD.
// This will also work if HEAD has branched from an earlier commit in a release branch e.g.
//
// o---o---HEAD
// /---o---o---branch/v16
func (r *Repo) GetParentReleaseBranch() (string, error) {
forkPointRef, err := r.RunCmd("merge-base", "--fork-point", "HEAD")
doggydogworld marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", trace.Wrap(err)
}
fbranch, err := r.RunCmd("branch", "--list", "branch/v*", "--contains", forkPointRef, "--format", "%(refname:short)")
if err != nil {
return "", trace.Wrap(err)
}
if fbranch == "" { // stdout is empty indicating the search failed
return "", trace.Errorf("could not find a valid root branch")
}
return fbranch, nil
}
37 changes: 37 additions & 0 deletions libs/github/gh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright 2024 Gravitational, 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 github

import (
"context"
"errors"

"github.com/cli/go-gh/v2/pkg/auth"
)

var ErrTokenNotFound = errors.New("could not find a GitHub token configured on system")

// NewClientFromGHAuth will use the gh credential chain to initialize the client.
// Useful for initialization both in CI and in user environments.
// Will check in order: GITHUB_TOKEN env var, gh config file, gh system keyring (gh auth login).
func NewClientFromGHAuth(ctx context.Context) (*Client, error) {
token, _ := auth.TokenForHost("github.com")
if token == "" {
return &Client{}, ErrTokenNotFound
}

return New(ctx, token)
}
45 changes: 45 additions & 0 deletions libs/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2024 Gravitational, 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 github

import (
"context"
"time"

go_github "github.com/google/go-github/v63/github"
"golang.org/x/oauth2"
)

const (
OutputEnv = "GITHUB_OUTPUT"
ClientTimeout = 30 * time.Second
)

type Client struct {
client *go_github.Client
}

// New returns a new GitHub Client.
func New(ctx context.Context, token string) (*Client, error) {
clt := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))

clt.Timeout = ClientTimeout

return &Client{
client: go_github.NewClient(clt),
}, nil
}
89 changes: 89 additions & 0 deletions libs/github/pull_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Copyright 2024 Gravitational, 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 github

import (
"context"
"fmt"
"time"

"github.com/google/go-github/v63/github"
"github.com/gravitational/trace"
)

var SearchTimeNow = time.Unix(0, 0)

// %Y-%m-%dT%H:%M:%S%z
const searchTimeLayout = "2006-01-02T15:04:05-0700"

// ChangelogPR contains all the data necessary for a changelog from the PR
type ChangelogPR struct {
Body string `json:"body,omitempty"`
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty" graphql:"url"`
}

// ListChangelogPullRequestsOpts contains options for searching for changelog pull requests.
type ListChangelogPullRequestsOpts struct {
Branch string
FromDate time.Time
ToDate time.Time
}

// ListChangelogPullRequests will search for pull requests that provide changelog information.
func (c *Client) ListChangelogPullRequests(ctx context.Context, org, repo string, opts *ListChangelogPullRequestsOpts) ([]ChangelogPR, error) {
var prs []ChangelogPR
query := fmt.Sprintf(`repo:%s/%s base:%s merged:%s -label:no-changelog`,
org, repo, opts.Branch, dateRangeFormat(opts.FromDate, opts.ToDate))
page, _, err := c.client.Search.Issues(
ctx,
query,
&github.SearchOptions{
Sort: "",
Order: "",
TextMatch: false,
ListOptions: github.ListOptions{
Page: 0,
PerPage: 100,
},
},
)

if err != nil {
return prs, trace.Wrap(err)
}

for _, pull := range page.Issues {
prs = append(prs, ChangelogPR{
Body: pull.GetBody(),
Number: pull.GetNumber(),
Title: pull.GetTitle(),
URL: pull.GetURL(),
})
}

return prs, nil
}

// dateRangeFormat takes in a date range and will format it for GitHub search syntax.
// to can be empty and the format will be to search everything after from
func dateRangeFormat(from, to time.Time) string {
if to == SearchTimeNow {
return fmt.Sprintf(">%s", from.Format(searchTimeLayout))
}
return fmt.Sprintf("%s..%s", from.Format(searchTimeLayout), to.Format(searchTimeLayout))
}
17 changes: 17 additions & 0 deletions libs/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/gravitational/shared-workflows/libs

go 1.22.4

require (
github.com/cli/go-gh/v2 v2.9.0
github.com/google/go-github/v63 v63.0.0
github.com/gravitational/trace v1.4.0
golang.org/x/oauth2 v0.21.0
)

require (
github.com/cli/safeexec v1.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
golang.org/x/net v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading