From eab9104ffbed8656922b6847b5dac523a597a2cf Mon Sep 17 00:00:00 2001 From: finn Date: Tue, 19 Dec 2023 13:43:57 -0800 Subject: [PATCH] open PRs on known SDKs when the vectors change --- .github/workflows/build-report.yaml | 4 + .github/workflows/sync-vectors.yaml | 23 +++ reports/cmd/sync-vectors/main.go | 33 ++++ reports/github.go | 52 ++++++ reports/go.mod | 8 +- reports/go.sum | 14 +- reports/reports.go | 1 + reports/sdks.go | 30 ++-- reports/sync.go | 258 ++++++++++++++++++++++++++++ 9 files changed, 402 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/sync-vectors.yaml create mode 100644 reports/cmd/sync-vectors/main.go create mode 100644 reports/github.go create mode 100644 reports/sync.go diff --git a/.github/workflows/build-report.yaml b/.github/workflows/build-report.yaml index 6338353..b0254e7 100644 --- a/.github/workflows/build-report.yaml +++ b/.github/workflows/build-report.yaml @@ -21,6 +21,10 @@ jobs: mv _site ../ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CICD_ROBOT_GITHUB_APP_NAME: ${{ secrets.CICD_ROBOT_GITHUB_APP_NAME }} + CICD_ROBOT_GITHUB_APP_PRIVATE_KEY: ${{ secrets.CICD_ROBOT_GITHUB_APP_PRIVATE_KEY }} + CICD_ROBOT_GITHUB_APP_ID: ${{ secrets.CICD_ROBOT_GITHUB_APP_ID }} + CICD_ROBOT_GITHUB_APP_INSTALLATION_ID: ${{ secrets.CICD_ROBOT_GITHUB_APP_INSTALLATION_ID }} - uses: actions/upload-pages-artifact@v2 - name: deploy GitHub Pages uses: actions/deploy-pages@v3 diff --git a/.github/workflows/sync-vectors.yaml b/.github/workflows/sync-vectors.yaml new file mode 100644 index 0000000..d02b6bd --- /dev/null +++ b/.github/workflows/sync-vectors.yaml @@ -0,0 +1,23 @@ +name: sync vectors + +on: + push: + branches: [main] + paths: + - 'web5-test-vectors/**/*.json' + workflow_dispatch: + +jobs: + build-report: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + - name: sync vectors + run: cd reports && go run ./cmd/sync-vectors + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CICD_ROBOT_GITHUB_APP_NAME: ${{ secrets.CICD_ROBOT_GITHUB_APP_NAME }} + CICD_ROBOT_GITHUB_APP_PRIVATE_KEY: ${{ secrets.CICD_ROBOT_GITHUB_APP_PRIVATE_KEY }} + CICD_ROBOT_GITHUB_APP_ID: ${{ secrets.CICD_ROBOT_GITHUB_APP_ID }} + CICD_ROBOT_GITHUB_APP_INSTALLATION_ID: ${{ secrets.CICD_ROBOT_GITHUB_APP_INSTALLATION_ID }} \ No newline at end of file diff --git a/reports/cmd/sync-vectors/main.go b/reports/cmd/sync-vectors/main.go new file mode 100644 index 0000000..abeedfe --- /dev/null +++ b/reports/cmd/sync-vectors/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "os" + + "github.com/TBD54566975/sdk-development/reports" + "golang.org/x/exp/slog" +) + +func main() { + defer reports.CleanupGitAuth() + if err := reports.ConfigureGitAuth(); err != nil { + panic(err) + } + + errs := make(map[string]error) + for _, sdk := range reports.SDKs { + if err := reports.SyncSDK(sdk); err != nil { + errs[sdk.Name] = err + } + } + + if err := reports.CleanupGitAuth(); err != nil { + panic(err) + } + + if len(errs) > 0 { + for sdk, err := range errs { + slog.Error("error", "sdk", sdk, "error", err) + } + os.Exit(1) + } +} diff --git a/reports/github.go b/reports/github.go new file mode 100644 index 0000000..633d53c --- /dev/null +++ b/reports/github.go @@ -0,0 +1,52 @@ +package reports + +import ( + "context" + "fmt" + "net/http" + "os" + "strconv" + + ghinstallation "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/google/go-github/v57/github" + "golang.org/x/exp/slog" +) + +var ( + gh *github.Client + ghTransport *ghinstallation.Transport + ghAppName = os.Getenv("CICD_ROBOT_GITHUB_APP_NAME") + ghUserName = fmt.Sprintf("%s[bot]", ghAppName) + ghAppPrivateKey = os.Getenv("CICD_ROBOT_GITHUB_APP_PRIVATE_KEY") +) + +func init() { + ghAppID, err := strconv.ParseInt(os.Getenv("CICD_ROBOT_GITHUB_APP_ID"), 10, 32) + if err != nil { + slog.Error("invalid or unset app ID. Please set environment variable CICD_ROBOT_GITHUB_APP_ID to a valid integer") + panic(err) + } + + ghInstallationID, err := strconv.ParseInt(os.Getenv("CICD_ROBOT_GITHUB_APP_INSTALLATION_ID"), 10, 32) + if err != nil { + slog.Error("invalid or unset installation ID. Please set environment variable CICD_ROBOT_GITHUB_APP_INSTALLATION_ID to a valid integer") + panic(err) + } + + ghTransport, err = ghinstallation.New(http.DefaultTransport, ghAppID, ghInstallationID, []byte(ghAppPrivateKey)) + if err != nil { + slog.Error("error initializing github auth transport.") + panic(err) + } + + gh = github.NewClient(&http.Client{Transport: ghTransport}) + + user, _, err := gh.Users.Get(context.Background(), ghUserName) + if err != nil { + slog.Error("error getting own (app) user info") + panic(err) + } + + gitConfig["user.email"] = fmt.Sprintf("%d+%s@users.noreply.github.com", user.GetID(), ghUserName) + gitConfig["user.name"] = ghUserName +} diff --git a/reports/go.mod b/reports/go.mod index 08c5cd7..62b77be 100644 --- a/reports/go.mod +++ b/reports/go.mod @@ -3,9 +3,15 @@ module github.com/TBD54566975/sdk-development/reports go 1.20 require ( + github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 github.com/google/go-github/v57 v57.0.0 github.com/joshdk/go-junit v1.0.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/google/go-github/v56 v56.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect +) diff --git a/reports/go.sum b/reports/go.sum index b5eab79..31b35c3 100644 --- a/reports/go.sum +++ b/reports/go.sum @@ -1,7 +1,14 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 h1:yUmoVv70H3J4UOqxqsee39+KlXxNEDfTbAp8c/qULKk= +github.com/bradleyfalzon/ghinstallation/v2 v2.8.0/go.mod h1:fmPmvCiBWhJla3zDv9ZTQSZc8AbwyRnGW1yg5ep1Pcs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -11,11 +18,12 @@ github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/reports/reports.go b/reports/reports.go index 1caf3f8..caa596b 100644 --- a/reports/reports.go +++ b/reports/reports.go @@ -36,6 +36,7 @@ type SDKMeta struct { ArtifactName string FeatureRegex *regexp.Regexp VectorRegex *regexp.Regexp + VectorPath string } type Report struct { diff --git a/reports/sdks.go b/reports/sdks.go index e343621..6d4051d 100644 --- a/reports/sdks.go +++ b/reports/sdks.go @@ -5,24 +5,21 @@ import ( "fmt" "io" "net/http" - "os" "regexp" "strings" - "github.com/google/go-github/v57/github" "golang.org/x/exp/slog" ) var ( - ghToken = os.Getenv("GITHUB_TOKEN") - gh = github.NewClient(nil).WithAuthToken(ghToken) - SDKs = []SDKMeta{ + SDKs = []SDKMeta{ { Name: "web5-js", Repo: "TBD54566975/web5-js", ArtifactName: "junit-results", FeatureRegex: regexp.MustCompile(`Web5TestVectors(\w+)`), VectorRegex: regexp.MustCompile(`\w+ \w+ (\w+)`), + VectorPath: "test-vectors", }, { Name: "web5-kt", @@ -30,16 +27,11 @@ var ( ArtifactName: "test-results", FeatureRegex: regexp.MustCompile(`web5\.sdk\.\w+.Web5TestVectors(\w+)`), VectorRegex: regexp.MustCompile(`(\w+)\(\)`), + VectorPath: "test-vectors", }, } ) -func init() { - if ghToken == "" { - panic("please set environment variable GITHUB_TOKEN to a valid github token (generate one at https://github.com/settings/tokens?type=beta)") - } -} - func GetAllReports() ([]Report, error) { ctx := context.TODO() @@ -47,17 +39,17 @@ func GetAllReports() ([]Report, error) { for _, sdk := range SDKs { artifact, err := downloadArtifact(ctx, sdk) if err != nil { - return nil, err + return nil, fmt.Errorf("error downloading artifact from %s: %v", sdk.Repo, err) } suites, err := readArtifactZip(artifact) if err != nil { - return nil, err + return nil, fmt.Errorf("error parsing artifact from %s: %v", sdk.Repo, err) } report, err := sdk.buildReport(suites) if err != nil { - return nil, err + return nil, fmt.Errorf("error processing data from %s: %v", sdk.Repo, err) } reports = append(reports, report) @@ -90,16 +82,20 @@ func downloadArtifact(ctx context.Context, sdk SDKMeta) ([]byte, error) { if err != nil { return nil, err } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ghToken)) + bearer, err := ghTransport.Token(ctx) + if err != nil { + return nil, fmt.Errorf("error getting github token: %v", err) + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", bearer)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, fmt.Errorf("error making http request to %s: %v", artifactURL, err) } defer resp.Body.Close() artifact, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("error reading response body: %v", err) } slog.Info("downloaded artifact", "sdk", sdk.Repo, "size", len(artifact)) diff --git a/reports/sync.go b/reports/sync.go new file mode 100644 index 0000000..1a372d0 --- /dev/null +++ b/reports/sync.go @@ -0,0 +1,258 @@ +package reports + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/google/go-github/v57/github" + "golang.org/x/exp/slog" +) + +const ( + vectorUpdateBranch = "vector-update" + gitConfigCredentialHelper = "credential.helper" + vectorUpdateCommitMessage = "update test vectors" +) + +// these should be consts but the library expects a pointer to a string, which cannot be done with a const +var ( + vectorUpdatePRTitle = "update test vectors" + vectorUpdatePRBody = "some test vectors were changed or added to the sdk-development repo, so they need to be updated in this repo" + vectorUpdatePRBaseBranch = "main" +) + +var gitCredentialStoreFile string + +var gitConfig = map[string]string{} + +func SyncSDK(sdk SDKMeta) error { + slog.Info("syncing vectors", "repo", sdk.Repo) + + tmpdir, err := os.MkdirTemp("", "vector-update") + if err != nil { + return fmt.Errorf("error making a temp dir: %v", err) + } + defer os.RemoveAll(tmpdir) + + // clone sdk.Repo + // check if a vector update branch already exists. + // If vector update branch exists, check it out + rebase on default branch + // if vector update branch does not exist, make it + err = clone(fmt.Sprintf("https://github.com/%s", sdk.Repo), tmpdir) + if err != nil { + return fmt.Errorf("error cloning repo %s: %v", sdk.Repo, err) + } + + // copy ../../web5-test-vectors/* to sdk.VectorPath + err = copyDir("../web5-test-vectors", filepath.Join(tmpdir, sdk.VectorPath)) + if err != nil { + return fmt.Errorf("error copying current vectors to cloned repo: %v", err) + } + + // check if git says the repo has changed - return if it hasn't + err = git("-C", tmpdir, "diff-index", "--quiet", "HEAD") + if err != nil { + exitError := &exec.ExitError{} + if !errors.As(err, &exitError) { + return fmt.Errorf("error checking if repo changed: %v", err) + } + + slog.Info("repo changed after copying current vectors in") + } else { + slog.Info("repo did not change after copying current vectors in, not taking further action") + return nil + } + + // commit + if err := git("-C", tmpdir, "commit", "-a", "-m", vectorUpdateCommitMessage); err != nil { + return fmt.Errorf("error committing changes: %v", err) + } + + // push + if err := git("-C", tmpdir, "push", "origin", vectorUpdateBranch, "--force"); err != nil { + return fmt.Errorf("error pushing changes: %v", err) + } + + // open a pull request if one isn't already open + if err := openPRIfNeeded(sdk.Repo); err != nil { + return fmt.Errorf("error opening PR: %v", err) + } + return nil +} + +// clone the repo and checkout the correct branch and rebase it on main +func clone(url string, dest string) error { + if err := git("clone", url, dest); err != nil { + return err + } + + if err := git("-C", dest, "checkout", vectorUpdateBranch); err != nil { + exitError := &exec.ExitError{} + if !errors.As(err, &exitError) { + return err + } + + err = git("-C", dest, "checkout", "-b", vectorUpdateBranch) + if err != nil { + return err + } + } + + if err := git("-C", dest, "rebase", "main"); err != nil { + slog.Warn("rebase failed") + return err + } + + return nil +} + +func git(args ...string) error { + cmd := exec.Command("git", args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Env = []string{fmt.Sprintf("GIT_CONFIG_COUNT=%d", len(gitConfig))} + i := 0 + for k, v := range gitConfig { + cmd.Env = append(cmd.Env, + fmt.Sprintf("GIT_CONFIG_KEY_%d=%s", i, k), + fmt.Sprintf("GIT_CONFIG_VALUE_%d=%s", i, v), + ) + i = i + 1 + } + + slog.Info("invoking", "git", args) + if err := cmd.Run(); err != nil { + exitError := &exec.ExitError{} + if !errors.As(err, &exitError) { + return err + } + slog.Info("command did not succeed", "exit_code", exitError.ExitCode()) + return err + } + + return nil +} + +func copyDir(src, dest string) error { + return filepath.WalkDir(src, func(path string, _ fs.DirEntry, err error) error { + if err != nil { + slog.Error("error from walkdir") + return err + } + + if !strings.HasSuffix(path, ".json") { + return nil + } + + relativePath, _ := filepath.Rel(src, path) + destPath := filepath.Join(dest, relativePath) + + slog.Info("mkdir", "dir", filepath.Dir(destPath)) + err = os.MkdirAll(filepath.Dir(destPath), 0755) + if err != nil && !errors.Is(err, os.ErrExist) { + slog.Error("error creating dir", "dir", destPath) + return err + } + + s, err := os.Open(path) + if err != nil { + slog.Error("error opening source vector", "file", path) + return err + } + defer s.Close() + + d, err := os.Create(destPath) + if err != nil { + slog.Error("error opening dest vector", "file", destPath) + return err + } + defer d.Close() + + _, err = io.Copy(d, s) + if err != nil { + slog.Error("error copying vector contents", "src", path, "dest", destPath) + return err + } + + if err := git("-C", dest, "add", destPath); err != nil { + slog.Error("error git add'ing vector", "file", destPath) + return err + } + + slog.Info("copied vector", "file", relativePath) + return nil + }) +} + +func ConfigureGitAuth() error { + slog.Info("telling git about our github token") + + f, err := os.CreateTemp("", "git-credentials") + if err != nil { + return err + } + gitCredentialStoreFile = f.Name() + + authToken, err := ghTransport.Token(context.TODO()) + if err != nil { + slog.Error("error getting github auth token") + return err + } + + cmd := exec.Command("git", "credential-store", "--file", gitCredentialStoreFile, "store") + cmd.Stdin = strings.NewReader(fmt.Sprintf("protocol=https\nhost=github.com\nusername=%s\npassword=%s", fmt.Sprintf("%s[bot]", ghAppName), authToken)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + + gitConfig[gitConfigCredentialHelper] = fmt.Sprintf("store --file %s", gitCredentialStoreFile) + + return nil +} + +func CleanupGitAuth() error { + return os.Remove(gitCredentialStoreFile) +} + +func openPRIfNeeded(repo string) error { + ctx := context.TODO() + owner, repo, _ := strings.Cut(repo, "/") + head := fmt.Sprintf("%s:%s", owner, vectorUpdateBranch) + existing, _, err := gh.PullRequests.List(ctx, owner, repo, &github.PullRequestListOptions{ + State: "open", + Head: head, + }) + if err != nil { + slog.Error("error checking for existing PR") + return err + } + + if len(existing) > 0 { + slog.Info("a PR for that branch already exists, not opening a new one", "pr", existing[0].GetURL()) + return nil + } + + pr, _, err := gh.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{ + Title: &vectorUpdatePRTitle, + Body: &vectorUpdatePRBody, + Head: &head, + Base: &vectorUpdatePRBaseBranch, + }) + if err != nil { + slog.Error("error creating PR") + return err + } + + slog.Info("opened PR", "pr", pr.GetURL()) + + return nil +}