Skip to content

Commit

Permalink
Merge pull request #71 from thepwagner/batched-push
Browse files Browse the repository at this point in the history
Batched push
  • Loading branch information
Pete Wagner authored Oct 1, 2020
2 parents 5716f2f + fbd7ebd commit ff272ec
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 99 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ linters:
- unused
- varcheck
- whitespace

issues:
exclude:
- G101
- G306
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ inputs:
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }}
required: true
signing_key:
default: "i deserve this"
description: >
Unique key to use for maintaining trusted metadata in PR body.
required: false
log_level:
description: 'Control debug/info/warn/error output'
required: false
Expand Down
2 changes: 1 addition & 1 deletion actions/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func getRepoUpdater(env *cmd.Environment) (updater.Repo, *updater.RepoUpdater, e

var modRepo updater.Repo
if env.GitHubRepository != "" && env.GitHubToken != "" {
modRepo, err = gitrepo.NewGitHubRepo(gitRepo, env.GitHubRepository, env.GitHubToken)
modRepo, err = gitrepo.NewGitHubRepo(gitRepo, env.InputSigningKey, env.GitHubRepository, env.GitHubToken)
if err != nil {
return nil, nil, err
}
Expand Down
11 changes: 6 additions & 5 deletions cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ type Environment struct {
GitHubEventPath string `env:"GITHUB_EVENT_PATH"`
GitHubRepository string `env:"GITHUB_REPOSITORY"`

InputBatches string `env:"INPUT_BATCHES"`
InputBranches string `env:"INPUT_BRANCHES"`
GitHubToken string `env:"INPUT_TOKEN"`
InputLogLevel string `env:"INPUT_LOG_LEVEL" envDefault:"debug"`
InputUpdater string `env:"INPUT_UPDATER"`
InputBatches string `env:"INPUT_BATCHES"`
InputBranches string `env:"INPUT_BRANCHES"`
GitHubToken string `env:"INPUT_TOKEN"`
InputLogLevel string `env:"INPUT_LOG_LEVEL" envDefault:"debug"`
InputUpdater string `env:"INPUT_UPDATER"`
InputSigningKey []byte `env:"INPUT_SIGNING_KEY"`
}

func ParseEnvironment() (*Environment, error) {
Expand Down
23 changes: 23 additions & 0 deletions repo/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package repo

import (
"fmt"
"strings"

"github.com/thepwagner/action-update-go/updater"
)

type commitMessageGen func(...updater.Update) string

var defaultCommitMessage = func(updates ...updater.Update) string {
if len(updates) == 1 {
update := updates[0]
return fmt.Sprintf("%s@%s", update.Path, update.Next)
}
var s strings.Builder
s.WriteString("dependency updates\n\n")
for _, u := range updates {
_, _ = fmt.Fprintf(&s, "%s@%s\n", u.Path, u.Next)
}
return s.String()
}
27 changes: 14 additions & 13 deletions repo/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ const RemoteName = "origin"

// GitRepo is a Repo that synchronizes access to a single git working tree.
type GitRepo struct {
repo *git.Repository
wt *git.Worktree
branch string
author GitIdentity
remotes bool
repo *git.Repository
wt *git.Worktree
branch string
author GitIdentity
remotes bool
commitMessage commitMessageGen
}

var _ updater.Repo = (*GitRepo)(nil)
Expand Down Expand Up @@ -65,11 +66,12 @@ func NewGitRepo(repo *git.Repository) (*GitRepo, error) {
branch = head.Name().Short()
}
return &GitRepo{
repo: repo,
wt: wt,
branch: branch,
remotes: len(remotes) > 0,
author: DefaultGitIdentity,
repo: repo,
wt: wt,
branch: branch,
remotes: len(remotes) > 0,
author: DefaultGitIdentity,
commitMessage: defaultCommitMessage,
}, nil
}

Expand Down Expand Up @@ -165,9 +167,8 @@ func (t *GitRepo) Root() string {
return t.wt.Filesystem.Root()
}

func (t *GitRepo) Push(ctx context.Context, update updater.Update) error {
// TODO: dependency inject this?
commitMessage := fmt.Sprintf("update %s to %s", update.Path, update.Next)
func (t *GitRepo) Push(ctx context.Context, update ...updater.Update) error {
commitMessage := t.commitMessage(update...)
if err := t.commit(commitMessage); err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion repo/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestGitRepo_Push(t *testing.T) {
commit, err := log.Next()
require.NoError(t, err)
t.Logf("inspecting commit %s", commit.Hash)
assert.Equal(t, "update github.com/test to v1.0.0", commit.Message)
assert.Equal(t, "github.com/test@v1.0.0", commit.Message)
assert.Equal(t, repo.DefaultGitIdentity.Name, commit.Author.Name)
assert.Equal(t, repo.DefaultGitIdentity.Email, commit.Author.Email)

Expand Down
16 changes: 8 additions & 8 deletions repo/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ type GitHubRepo struct {
var _ updater.Repo = (*GitHubRepo)(nil)

type PullRequestContent interface {
Generate(context.Context, updater.Update) (title, body string, err error)
Generate(context.Context, ...updater.Update) (title, body string, err error)
}

func NewGitHubRepo(repo *GitRepo, repoNameOwner, token string) (*GitHubRepo, error) {
func NewGitHubRepo(repo *GitRepo, hmacKey []byte, repoNameOwner, token string) (*GitHubRepo, error) {
ghRepoSplit := strings.Split(repoNameOwner, "/")
if len(ghRepoSplit) != 2 {
return nil, fmt.Errorf("expected repo in OWNER/NAME format")
Expand All @@ -39,7 +39,7 @@ func NewGitHubRepo(repo *GitRepo, repoNameOwner, token string) (*GitHubRepo, err
owner: ghRepoSplit[0],
repoName: ghRepoSplit[1],
github: ghClient,
prContent: NewGitHubPullRequestContent(ghClient),
prContent: NewGitHubPullRequestContent(ghClient, hmacKey),
}, nil
}

Expand All @@ -56,18 +56,18 @@ func (g *GitHubRepo) SetBranch(branch string) error { return g.repo.SetBra
func (g *GitHubRepo) NewBranch(base, branch string) error { return g.repo.NewBranch(base, branch) }

// Push follows the git push with opening a pull request
func (g *GitHubRepo) Push(ctx context.Context, update updater.Update) error {
if err := g.repo.Push(ctx, update); err != nil {
func (g *GitHubRepo) Push(ctx context.Context, updates ...updater.Update) error {
if err := g.repo.Push(ctx, updates...); err != nil {
return err
}
if err := g.createPR(ctx, update); err != nil {
if err := g.createPR(ctx, updates); err != nil {
return err
}
return nil
}

func (g *GitHubRepo) createPR(ctx context.Context, update updater.Update) error {
title, body, err := g.prContent.Generate(ctx, update)
func (g *GitHubRepo) createPR(ctx context.Context, updates []updater.Update) error {
title, body, err := g.prContent.Generate(ctx, updates...)
if err != nil {
return fmt.Errorf("generating PR prContent: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion repo/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func TestNewGitHubRepo(t *testing.T) {
gr := initGitRepo(t, plumbing.NewBranchReferenceName(branchName))

gh, err := repo.NewGitHubRepo(gr, "foo/bar", "")
gh, err := repo.NewGitHubRepo(gr, testKey, "foo/bar", "")
require.NoError(t, err)
assert.NotNil(t, gh)
}
Expand Down
141 changes: 116 additions & 25 deletions repo/pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,99 @@ package repo

import (
"context"
"crypto/hmac"
"crypto/sha512"
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"

"github.com/dependabot/gomodules-extracted/cmd/go/_internal_/semver"
"github.com/google/go-github/v32/github"
"github.com/thepwagner/action-update-go/updater"
)

type GitHubPullRequestContent struct {
github *github.Client
key []byte
}

var _ PullRequestContent = (*GitHubPullRequestContent)(nil)

func NewGitHubPullRequestContent(gh *github.Client) *GitHubPullRequestContent {
return &GitHubPullRequestContent{github: gh}
func NewGitHubPullRequestContent(gh *github.Client, key []byte) *GitHubPullRequestContent {
return &GitHubPullRequestContent{
github: gh,
key: key,
}
}

func (d *GitHubPullRequestContent) Generate(ctx context.Context, update updater.Update) (title, body string, err error) {
title = fmt.Sprintf("Update %s from %s to %s", update.Path, update.Previous, update.Next)
body, err = d.prBody(ctx, update)
func (d *GitHubPullRequestContent) Generate(ctx context.Context, updates ...updater.Update) (title, body string, err error) {
if len(updates) == 1 {
update := updates[0]
title = fmt.Sprintf("Update %s from %s to %s", update.Path, update.Previous, update.Next)
body, err = d.bodySingle(ctx, update)
} else {
title = "Dependency Updates"
body, err = d.bodyMulti(ctx, updates)
}
return
}

func (d *GitHubPullRequestContent) prBody(ctx context.Context, update updater.Update) (string, error) {
const (
openToken = "<!--::action-update-go::"
closeToken = "-->"
)

func (d *GitHubPullRequestContent) ParseBody(s string) []updater.Update {
lastOpen := strings.LastIndex(s, openToken)
if lastOpen == -1 {
return nil
}
closeAfterOpen := strings.Index(s[lastOpen:], closeToken)
raw := s[lastOpen+len(openToken) : lastOpen+closeAfterOpen]

var signed SignedUpdateDescriptor
if err := json.Unmarshal([]byte(raw), &signed); err != nil {
return nil
}

updates, _ := VerifySignedUpdateDescriptor(d.key, signed)
return updates
}

func (d *GitHubPullRequestContent) bodySingle(ctx context.Context, update updater.Update) (string, error) {
var body strings.Builder
_, _ = fmt.Fprintf(&body, "Here is %s %s, I hope it works.\n", update.Path, update.Next)

if err := d.writeGitHubChangelog(ctx, &body, update); err != nil {
return "", err
}
writePatchBlob(&body, update)
if err := d.writeUpdateSignature(&body, update); err != nil {
return "", fmt.Errorf("writing update signature: %w", err)
}
return body.String(), nil
}

func (d *GitHubPullRequestContent) bodyMulti(ctx context.Context, updates []updater.Update) (string, error) {
var body strings.Builder
body.WriteString("Here are some updates, I hope they work.\n\n")

for _, update := range updates {
_, _ = fmt.Fprintf(&body, "#### %s@%s\n", update.Path, update.Next)
before := body.Len()
if err := d.writeGitHubChangelog(ctx, &body, update); err != nil {
return "", err
}
if body.Len() != before {
body.WriteString("\n")
}
}

if err := d.writeUpdateSignature(&body, updates...); err != nil {
return "", fmt.Errorf("writing update signature: %w", err)
}
return body.String(), nil
}

Expand All @@ -61,21 +119,54 @@ func (d *GitHubPullRequestContent) writeGitHubChangelog(ctx context.Context, out
return nil
}

func writePatchBlob(out io.Writer, update updater.Update) {
major := semver.Major(update.Previous) != semver.Major(update.Next)
minor := !major && semver.MajorMinor(update.Previous) != semver.MajorMinor(update.Next)
details := struct {
Major bool `json:"major"`
Minor bool `json:"minor"`
Patch bool `json:"patch"`
}{
Major: major,
Minor: minor,
Patch: !major && !minor,
}
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")
_, _ = fmt.Fprintln(out, "\n```json")
_ = encoder.Encode(&details)
_, _ = fmt.Fprint(out, "```\n")
func (d *GitHubPullRequestContent) writeUpdateSignature(out io.Writer, updates ...updater.Update) error {
dsc, err := NewSignedUpdateDescriptor(d.key, updates...)
if err != nil {
return fmt.Errorf("signing updates: %w", err)
}

_, _ = fmt.Fprint(out, "\n", openToken, "\n")
if err := json.NewEncoder(out).Encode(&dsc); err != nil {
return fmt.Errorf("encoding signature: %w", err)
}
_, _ = fmt.Fprint(out, closeToken)
return nil
}

type SignedUpdateDescriptor struct {
Updates []updater.Update `json:"updates"`
Signature []byte `json:"signature"`
}

func NewSignedUpdateDescriptor(key []byte, updates ...updater.Update) (SignedUpdateDescriptor, error) {
signature, err := updatesHash(key, updates)
if err != nil {
return SignedUpdateDescriptor{}, err
}
return SignedUpdateDescriptor{
Updates: updates,
Signature: signature,
}, nil
}

func updatesHash(key []byte, updates []updater.Update) ([]byte, error) {
sort.Slice(updates, func(i, j int) bool {
return updates[i].Path < updates[j].Path
})
hash := hmac.New(sha512.New, key)
if err := json.NewEncoder(hash).Encode(updates); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}

func VerifySignedUpdateDescriptor(key []byte, descriptor SignedUpdateDescriptor) ([]updater.Update, error) {
calculated, err := updatesHash(key, descriptor.Updates)
if err != nil {
return nil, fmt.Errorf("calculating signature: %w", err)
}
if subtle.ConstantTimeCompare(calculated, descriptor.Signature) != 1 {
return nil, fmt.Errorf("invalid signature")
}
return descriptor.Updates, nil
}
Loading

0 comments on commit ff272ec

Please sign in to comment.