mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-07 07:07:05 +02:00
Add a new `pipeline github sync branches` command that can synchronize two branches. We'll use this to synchronize the `hashicorp/vault-enterprise/ce/*` branches with `hashicorp/vault/*`. As the community repository is effectively a mirror of what is hosted in Enterprise, a scheduled sync cadence is probably fine. Eventually we'll hook the workflow and sync into the release pipeline to ensure that `hashicorp/vault` branches are up-to-date when cutting community releases. As part of this I also fixed a few static analysis issues that popped up when running `golangci-lint` and fixed a few smaller bugs. Signed-off-by: Ryan Cragun <me@ryan.ec>
234 lines
6.2 KiB
Go
234 lines
6.2 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
libgithub "github.com/google/go-github/v68/github"
|
|
libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
slogctx "github.com/veqryn/slog-context"
|
|
)
|
|
|
|
// SyncBranchReq is a request to synchronize two github hosted branches with
|
|
// a git merge from one into another.
|
|
//
|
|
// NOTE: We require that both branches exist for the operation to succeed.
|
|
type SyncBranchReq struct {
|
|
FromOwner string
|
|
FromRepo string
|
|
FromOrigin string
|
|
FromBranch string
|
|
ToOwner string
|
|
ToRepo string
|
|
ToOrigin string
|
|
ToBranch string
|
|
RepoDir string
|
|
}
|
|
|
|
// SyncBranchRes is a copy pull request response.
|
|
type SyncBranchRes struct {
|
|
Error error `json:"error,omitempty"`
|
|
Request *SyncBranchReq `json:"request,omitempty"`
|
|
}
|
|
|
|
// Run runs the request to synchronize a branches via a merge.
|
|
func (r *SyncBranchReq) Run(
|
|
ctx context.Context,
|
|
github *libgithub.Client,
|
|
git *libgit.Client,
|
|
) (*SyncBranchRes, error) {
|
|
var err error
|
|
res := &SyncBranchRes{Request: r}
|
|
|
|
slog.Default().DebugContext(slogctx.Append(ctx,
|
|
slog.String("from-owner", r.FromOwner),
|
|
slog.String("from-repo", r.FromRepo),
|
|
slog.String("from-origin", r.FromOrigin),
|
|
slog.String("from-branch", r.FromBranch),
|
|
slog.String("to-owner", r.ToOwner),
|
|
slog.String("to-repo", r.ToRepo),
|
|
slog.String("to-origin", r.ToOrigin),
|
|
slog.String("to-branch", r.ToBranch),
|
|
slog.String("to-owner", r.ToOwner),
|
|
slog.String("repo-dir", r.RepoDir),
|
|
), "synchronizing branches")
|
|
|
|
// Make sure we have required and valid fields
|
|
err = r.Validate(ctx)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
// Make sure we've been given a valid location for a repo and/or create a
|
|
// temporary one
|
|
var tmpDir bool
|
|
r.RepoDir, err, tmpDir = ensureGitRepoDir(ctx, r.RepoDir)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
if tmpDir {
|
|
defer os.RemoveAll(r.RepoDir)
|
|
}
|
|
|
|
// Clone the remote repository and fetch the branch we're going to merge into.
|
|
// These will change our working directory into RepoDir.
|
|
_, err = os.Stat(filepath.Join(r.RepoDir, ".git"))
|
|
if err == nil {
|
|
err = initializeExistingRepo(
|
|
ctx, git, r.RepoDir, r.ToOrigin, r.ToBranch,
|
|
)
|
|
} else {
|
|
err = initializeNewRepo(
|
|
ctx, git, r.RepoDir, r.ToOwner, r.ToRepo, r.ToOrigin, r.ToBranch,
|
|
)
|
|
}
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
// Check out our branch. Our intialization above will ensure we have a local
|
|
// reference.
|
|
slog.Default().DebugContext(ctx, "checking out to-branch")
|
|
checkoutRes, err := git.Checkout(ctx, &libgit.CheckoutOpts{
|
|
Branch: r.ToBranch,
|
|
})
|
|
if err != nil {
|
|
return res, fmt.Errorf("checking out to-branch: %s: %w", checkoutRes.String(), err)
|
|
}
|
|
|
|
// Add our from upstream as a remote and fetch our from branch.
|
|
slog.Default().DebugContext(ctx, "adding from upstream and fetching from-branch")
|
|
remoteRes, err := git.Remote(ctx, &libgit.RemoteOpts{
|
|
Command: libgit.RemoteCommandAdd,
|
|
Track: []string{r.FromBranch},
|
|
Fetch: true,
|
|
Name: r.FromOrigin,
|
|
URL: fmt.Sprintf("https://github.com/%s/%s.git", r.FromOwner, r.FromRepo),
|
|
})
|
|
if err != nil {
|
|
err = fmt.Errorf("fetching from branch: %s, %w", remoteRes.String(), err)
|
|
return res, err
|
|
}
|
|
|
|
// Use our remote reference as we haven't created a local reference.
|
|
fromBranch := "remotes/" + r.FromOrigin + "/" + r.FromBranch
|
|
slog.Default().DebugContext(ctx, "merging from-branch into to-branch")
|
|
mergeRes, err := git.Merge(ctx, &libgit.MergeOpts{
|
|
NoVerify: true,
|
|
Strategy: libgit.MergeStrategyORT,
|
|
StrategyOptions: []libgit.MergeStrategyOption{
|
|
libgit.MergeStrategyOptionTheirs,
|
|
libgit.MergeStrategyOptionIgnoreSpaceChange,
|
|
},
|
|
IntoName: r.ToBranch,
|
|
Commit: fromBranch,
|
|
})
|
|
if err != nil {
|
|
return res, fmt.Errorf("merging from-branch into to-branch: %s: %w", mergeRes.String(), err)
|
|
}
|
|
|
|
slog.Default().DebugContext(ctx, "pushing to-branch")
|
|
pushRes, err := git.Push(ctx, &libgit.PushOpts{
|
|
Repository: r.ToOrigin,
|
|
Refspec: []string{r.ToBranch},
|
|
})
|
|
if err != nil {
|
|
return res, fmt.Errorf("pushing to-branch: %s: %w", pushRes.String(), err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// validate ensures that we've been given the minimum filter arguments necessary to complete a
|
|
// request. It is always recommended that additional fitlers be given to reduce the response size
|
|
// and not exhaust API limits.
|
|
func (r *SyncBranchReq) Validate(ctx context.Context) error {
|
|
// TODO
|
|
if r == nil {
|
|
return errors.New("failed to initialize request")
|
|
}
|
|
|
|
if r.FromOrigin == "" {
|
|
return errors.New("no github from origin has been provided")
|
|
}
|
|
|
|
if r.FromOwner == "" {
|
|
return errors.New("no github from owner has been provided")
|
|
}
|
|
|
|
if r.FromRepo == "" {
|
|
return errors.New("no github from repository has been provided")
|
|
}
|
|
|
|
if r.FromBranch == "" {
|
|
return errors.New("no github from branch has been provided")
|
|
}
|
|
|
|
if r.ToOrigin == "" {
|
|
return errors.New("no github to origin has been provided")
|
|
}
|
|
|
|
if r.ToOwner == "" {
|
|
return errors.New("no github to owner has been provided")
|
|
}
|
|
|
|
if r.ToRepo == "" {
|
|
return errors.New("no github to repository has been provided")
|
|
}
|
|
|
|
if r.ToBranch == "" {
|
|
return errors.New("no github to branch has been provided")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ToJSON marshals the response to JSON.
|
|
func (r *SyncBranchRes) ToJSON() ([]byte, error) {
|
|
b, err := json.Marshal(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling list changed files to JSON: %w", err)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// ToTable marshals the response to a text table.
|
|
func (r *SyncBranchRes) ToTable(err error) table.Writer {
|
|
t := table.NewWriter()
|
|
t.Style().Options.DrawBorder = false
|
|
t.Style().Options.SeparateColumns = false
|
|
t.Style().Options.SeparateFooter = false
|
|
t.Style().Options.SeparateHeader = false
|
|
t.Style().Options.SeparateRows = false
|
|
t.AppendHeader(table.Row{
|
|
"From", "To", "Error",
|
|
})
|
|
|
|
if r.Request != nil {
|
|
from := r.Request.FromOwner + "/" + r.Request.FromRepo + "/" + r.Request.FromBranch
|
|
to := r.Request.ToOwner + "/" + r.Request.ToRepo + "/" + r.Request.ToBranch
|
|
row := table.Row{from, to}
|
|
if err != nil {
|
|
row = append(row, err.Error())
|
|
} else {
|
|
row = append(row, nil)
|
|
}
|
|
t.AppendRow(row)
|
|
}
|
|
|
|
t.SuppressEmptyColumns()
|
|
t.SuppressTrailingSpaces()
|
|
|
|
return t
|
|
}
|