From 2077b8fba868e6cced309c1268bd95a155f5b98a Mon Sep 17 00:00:00 2001 From: corverroos Date: Tue, 5 Apr 2022 16:17:36 +0200 Subject: [PATCH] testutil: implement merge-pr action (#356) Implements a simple PR merge action that: - Is triggered when a label is added to the PR - Checks if all actions/checks have passed - The `merge when ready` is applied. - Then merges the PR using our guidelines. category: feature ticket: #325 --- .github/workflows/merge-pr.yml | 17 ++ go.mod | 4 + go.sum | 11 + testutil/mergepr/mergepr.go | 257 ++++++++++++++++ testutil/mergepr/mergepr_internal_test.go | 37 +++ testutil/mergepr/testdata/pr.json | 350 ++++++++++++++++++++++ 6 files changed, 676 insertions(+) create mode 100644 .github/workflows/merge-pr.yml create mode 100644 testutil/mergepr/mergepr.go create mode 100644 testutil/mergepr/mergepr_internal_test.go create mode 100644 testutil/mergepr/testdata/pr.json diff --git a/.github/workflows/merge-pr.yml b/.github/workflows/merge-pr.yml new file mode 100644 index 000000000..033008a9e --- /dev/null +++ b/.github/workflows/merge-pr.yml @@ -0,0 +1,17 @@ +name: merge-pr +on: + pull_request: + types: [labeled] + +jobs: + merge-pr: + runs-on: ubuntu-latest + env: + GITHUB_PR: ${{ toJSON(github.event.pull_request) }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: '^1.17.1' + - run: go run testutil/mergepr/mergepr.go diff --git a/go.mod b/go.mod index 510dfd70f..ef046f07a 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/coinbase/kryptology v1.5.6-0.20220316191335-269410e1b06b github.com/ethereum/go-ethereum v1.10.16 github.com/ferranbt/fastssz v0.0.0-20220103083642-bc5fefefa28b + github.com/google/go-github/v43 v43.0.0 github.com/gorilla/mux v1.8.0 github.com/jonboulle/clockwork v0.2.3 github.com/jsternberg/zap-logfmt v1.2.0 @@ -32,6 +33,7 @@ require ( go.opentelemetry.io/otel/trace v1.6.1 go.uber.org/automaxprocs v1.4.0 go.uber.org/zap v1.21.0 + golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/tools v0.1.10 ) @@ -65,6 +67,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect @@ -162,6 +165,7 @@ require ( golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/grpc v1.44.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 5ddd72f9c..ec797bb32 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0= github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= @@ -338,6 +339,7 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -397,8 +399,14 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= +github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= +github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= @@ -1392,6 +1400,8 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1660,6 +1670,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/testutil/mergepr/mergepr.go b/testutil/mergepr/mergepr.go new file mode 100644 index 000000000..94440a64b --- /dev/null +++ b/testutil/mergepr/mergepr.go @@ -0,0 +1,257 @@ +// Copyright © 2021 Obol Technologies 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. + +//nolint:revive // Nested structs are ok since read-only. +package main + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/google/go-github/v43/github" + "golang.org/x/oauth2" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +const ( + DoNotMerge = "do not merge" + WIP = "wip" + MergeWhenReady = "merge when ready" +) + +func main() { + ctx := context.Background() + if err := run(ctx); err != nil { + log.Error(ctx, "Run error", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + pr, err := getPRFromEnv() + if err != nil { + return err + } + + t, err := getTokenFromEnv() + if err != nil { + return err + } + + client := github.NewClient(oauth2.NewClient(ctx, t)) + + return attemptMerge(ctx, client, pr) +} + +func getTokenFromEnv() (token, error) { + const tokenEnv = "GITHUB_TOKEN" //nolint:gosec + + t, ok := os.LookupEnv(tokenEnv) + if !ok { + return "", errors.New("environments variable not set", z.Str("var", tokenEnv)) + } else if strings.TrimSpace(t) == "" { + return "", errors.New("environments variable empty", z.Str("var", tokenEnv)) + } + + return token(t), nil +} + +func getPRFromEnv() (PR, error) { + const prenv = "GITHUB_PR" + + prJSON, ok := os.LookupEnv(prenv) + if !ok { + return PR{}, errors.New("environments variable unset", z.Str("var", prenv)) + } else if strings.TrimSpace(prJSON) == "" { + return PR{}, errors.New("environments variable empty", z.Str("var", prenv)) + } + + var pr PR + err := json.Unmarshal([]byte(prJSON), &pr) + if err != nil { + return PR{}, errors.Wrap(err, "unmarshal pr") + } + + return pr, nil +} + +type PR struct { + Base struct { + Repo struct { + Name string + Owner struct { + Login string `json:"login"` + } `json:"owner"` + } `json:"repo"` + } `json:"base"` + Number int `json:"number"` + Head struct { + SHA string `json:"sha"` + } `json:"head"` + Labels []struct { + Name string `json:"name"` + } `json:"labels"` + Body string `json:"body"` + Title string `json:"title"` + State string `json:"state"` + Mergeable bool `json:"mergeable"` + Merged bool `json:"merged"` +} + +func (pr PR) Owner() string { + return pr.Base.Repo.Owner.Login +} + +func (pr PR) Repo() string { + return pr.Base.Repo.Name +} + +func attemptMerge(ctx context.Context, client *github.Client, pr PR) error { + if pr.State != "open" { + log.Warn(ctx, "PR not open") + return nil + } else if pr.Merged { + log.Warn(ctx, "PR already merged") + return nil + } else if !pr.Mergeable { + log.Warn(ctx, "PR not mergeable") + return nil + } + + if ok, err := allChecksPassed(ctx, client, pr); err != nil { + return err + } else if !ok { + return nil + } + + log.Info(ctx, "All checks have passed") + + if !readyToMerge(ctx, pr) { + return nil + } + + log.Info(ctx, "Ready to merge") + + return merge(ctx, client, pr) +} + +func merge(ctx context.Context, client *github.Client, pr PR) error { + opts := &github.PullRequestOptions{ + MergeMethod: "squash", + } + + res, _, err := client.PullRequests.Merge(ctx, pr.Owner(), pr.Repo(), pr.Number, pr.Body, opts) + if err != nil { + return errors.Wrap(err, "merge") + } + + if !res.GetMerged() { + log.Warn(ctx, "Merging failed", z.Str("msg", res.GetMessage())) + return nil + } + + log.Info(ctx, "Merged PR", z.Str("sha", res.GetSHA())) + + return nil +} + +func readyToMerge(ctx context.Context, pr PR) bool { + body := strings.ToLower(pr.Body) + if strings.Contains(body, WIP) || strings.Contains(body, DoNotMerge) { + log.Warn(ctx, "Body contains 'wip' or 'do not merge'") + return false + } + + var ready bool + for _, label := range pr.Labels { + if label.Name == WIP || label.Name == DoNotMerge { + log.Warn(ctx, "Labels contains 'wip' or 'do not merge'") + return false + } + if label.Name == MergeWhenReady { + ready = true + } + } + + if !ready { + log.Warn(ctx, "Labels do not contain 'merge when ready'") + return false + } + + return true +} + +func allChecksPassed(ctx context.Context, client *github.Client, pr PR) (bool, error) { + var notOKChecks []string + + sl, _, err := client.Repositories.GetCombinedStatus(ctx, pr.Owner(), pr.Repo(), pr.Head.SHA, nil) + if err != nil { + return false, errors.Wrap(err, "get combined status") + } + + for _, s := range sl.Statuses { + if s.GetState() != "success" { + log.Warn(ctx, "Failed status detected", + z.Str("context", s.GetContext()), + z.Str("state", s.GetState()), + ) + notOKChecks = append(notOKChecks, s.GetContext()) + } + } + + okConclusions := map[string]bool{ + "success": true, + "neutral": true, + "skipped": true, + } + + checkRuns, _, err := client.Checks.ListCheckRunsForRef(ctx, pr.Owner(), pr.Repo(), pr.Head.SHA, nil) + if err != nil { + return false, errors.Wrap(err, "list check runs") + } + + for _, check := range checkRuns.CheckRuns { + if check.GetName() == "merge-pr" { + // Skip our selves + continue + } + if !okConclusions[check.GetConclusion()] { + notOKChecks = append(notOKChecks, check.GetName()) + log.Warn(ctx, "Non-ok check detected", + z.Str("name", check.GetName()), + z.Str("conclusion", check.GetConclusion()), + ) + } + } + + if len(notOKChecks) > 0 { + return false, nil + } + + return true, nil +} + +type token string + +func (t token) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: string(t), + TokenType: "Bearer", + }, nil +} diff --git a/testutil/mergepr/mergepr_internal_test.go b/testutil/mergepr/mergepr_internal_test.go new file mode 100644 index 000000000..b158ccad0 --- /dev/null +++ b/testutil/mergepr/mergepr_internal_test.go @@ -0,0 +1,37 @@ +// Copyright © 2021 Obol Technologies 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 main + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestIntegration tests merge pr action if a GITHUB_TOKEN env var is found. +func TestIntegration(t *testing.T) { + if _, ok := os.LookupEnv("GITHUB_TOKEN"); !ok { + return + } + + b, err := os.ReadFile("testdata/pr.json") + require.NoError(t, err) + require.NoError(t, os.Setenv("GITHUB_PR", string(b))) + + err = run(context.Background()) + require.NoError(t, err) +} diff --git a/testutil/mergepr/testdata/pr.json b/testutil/mergepr/testdata/pr.json new file mode 100644 index 000000000..f326ca9d2 --- /dev/null +++ b/testutil/mergepr/testdata/pr.json @@ -0,0 +1,350 @@ +{ + "url": "https://api.github.com/repos/ObolNetwork/charon/pulls/348", + "id": 898891401, + "node_id": "PR_kwDOGGrHP841k_6J", + "html_url": "https://github.com/ObolNetwork/charon/pull/348", + "diff_url": "https://github.com/ObolNetwork/charon/pull/348.diff", + "patch_url": "https://github.com/ObolNetwork/charon/pull/348.patch", + "issue_url": "https://api.github.com/repos/ObolNetwork/charon/issues/348", + "number": 348, + "state": "open", + "locked": false, + "title": ".github/workflows: add multi platform support for docker image", + "user": { + "login": "dB2510", + "id": 37813203, + "node_id": "MDQ6VXNlcjM3ODEzMjAz", + "avatar_url": "https://avatars.githubusercontent.com/u/37813203?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/dB2510", + "html_url": "https://github.com/dB2510", + "followers_url": "https://api.github.com/users/dB2510/followers", + "following_url": "https://api.github.com/users/dB2510/following{/other_user}", + "gists_url": "https://api.github.com/users/dB2510/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dB2510/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dB2510/subscriptions", + "organizations_url": "https://api.github.com/users/dB2510/orgs", + "repos_url": "https://api.github.com/users/dB2510/repos", + "events_url": "https://api.github.com/users/dB2510/events{/privacy}", + "received_events_url": "https://api.github.com/users/dB2510/received_events", + "type": "User", + "site_admin": false + }, + "body": "Adds multi-platform support for docker image using https://github.com/docker/build-push-action\r\n\r\ncategory: bug \r\nticket: #339 \r\n", + "created_at": "2022-04-04T11:19:46Z", + "updated_at": "2022-04-05T12:45:41Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "a33bc86f695f92a5e0cdf1fb0e85c7a1b4ddec84", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/ObolNetwork/charon/pulls/348/commits", + "review_comments_url": "https://api.github.com/repos/ObolNetwork/charon/pulls/348/comments", + "review_comment_url": "https://api.github.com/repos/ObolNetwork/charon/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/ObolNetwork/charon/issues/348/comments", + "statuses_url": "https://api.github.com/repos/ObolNetwork/charon/statuses/52ddd0c3b8370f344b9768aba8d7fc67a53a50fd", + "head": { + "label": "ObolNetwork:dhruv/docker_multi", + "ref": "dhruv/docker_multi", + "sha": "52ddd0c3b8370f344b9768aba8d7fc67a53a50fd", + "user": { + "login": "ObolNetwork", + "id": 85748921, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg1NzQ4OTIx", + "avatar_url": "https://avatars.githubusercontent.com/u/85748921?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ObolNetwork", + "html_url": "https://github.com/ObolNetwork", + "followers_url": "https://api.github.com/users/ObolNetwork/followers", + "following_url": "https://api.github.com/users/ObolNetwork/following{/other_user}", + "gists_url": "https://api.github.com/users/ObolNetwork/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ObolNetwork/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ObolNetwork/subscriptions", + "organizations_url": "https://api.github.com/users/ObolNetwork/orgs", + "repos_url": "https://api.github.com/users/ObolNetwork/repos", + "events_url": "https://api.github.com/users/ObolNetwork/events{/privacy}", + "received_events_url": "https://api.github.com/users/ObolNetwork/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 409651007, + "node_id": "R_kgDOGGrHPw", + "name": "charon", + "full_name": "ObolNetwork/charon", + "private": true, + "owner": { + "login": "ObolNetwork", + "id": 85748921, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg1NzQ4OTIx", + "avatar_url": "https://avatars.githubusercontent.com/u/85748921?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ObolNetwork", + "html_url": "https://github.com/ObolNetwork", + "followers_url": "https://api.github.com/users/ObolNetwork/followers", + "following_url": "https://api.github.com/users/ObolNetwork/following{/other_user}", + "gists_url": "https://api.github.com/users/ObolNetwork/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ObolNetwork/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ObolNetwork/subscriptions", + "organizations_url": "https://api.github.com/users/ObolNetwork/orgs", + "repos_url": "https://api.github.com/users/ObolNetwork/repos", + "events_url": "https://api.github.com/users/ObolNetwork/events{/privacy}", + "received_events_url": "https://api.github.com/users/ObolNetwork/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/ObolNetwork/charon", + "description": "A repository for the Obol distributed validator middleware", + "fork": false, + "url": "https://api.github.com/repos/ObolNetwork/charon", + "forks_url": "https://api.github.com/repos/ObolNetwork/charon/forks", + "keys_url": "https://api.github.com/repos/ObolNetwork/charon/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/ObolNetwork/charon/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/ObolNetwork/charon/teams", + "hooks_url": "https://api.github.com/repos/ObolNetwork/charon/hooks", + "issue_events_url": "https://api.github.com/repos/ObolNetwork/charon/issues/events{/number}", + "events_url": "https://api.github.com/repos/ObolNetwork/charon/events", + "assignees_url": "https://api.github.com/repos/ObolNetwork/charon/assignees{/user}", + "branches_url": "https://api.github.com/repos/ObolNetwork/charon/branches{/branch}", + "tags_url": "https://api.github.com/repos/ObolNetwork/charon/tags", + "blobs_url": "https://api.github.com/repos/ObolNetwork/charon/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/ObolNetwork/charon/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/ObolNetwork/charon/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/ObolNetwork/charon/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/ObolNetwork/charon/statuses/{sha}", + "languages_url": "https://api.github.com/repos/ObolNetwork/charon/languages", + "stargazers_url": "https://api.github.com/repos/ObolNetwork/charon/stargazers", + "contributors_url": "https://api.github.com/repos/ObolNetwork/charon/contributors", + "subscribers_url": "https://api.github.com/repos/ObolNetwork/charon/subscribers", + "subscription_url": "https://api.github.com/repos/ObolNetwork/charon/subscription", + "commits_url": "https://api.github.com/repos/ObolNetwork/charon/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/ObolNetwork/charon/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/ObolNetwork/charon/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/ObolNetwork/charon/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/ObolNetwork/charon/contents/{+path}", + "compare_url": "https://api.github.com/repos/ObolNetwork/charon/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/ObolNetwork/charon/merges", + "archive_url": "https://api.github.com/repos/ObolNetwork/charon/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/ObolNetwork/charon/downloads", + "issues_url": "https://api.github.com/repos/ObolNetwork/charon/issues{/number}", + "pulls_url": "https://api.github.com/repos/ObolNetwork/charon/pulls{/number}", + "milestones_url": "https://api.github.com/repos/ObolNetwork/charon/milestones{/number}", + "notifications_url": "https://api.github.com/repos/ObolNetwork/charon/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/ObolNetwork/charon/labels{/name}", + "releases_url": "https://api.github.com/repos/ObolNetwork/charon/releases{/id}", + "deployments_url": "https://api.github.com/repos/ObolNetwork/charon/deployments", + "created_at": "2021-09-23T15:42:01Z", + "updated_at": "2022-04-01T07:23:57Z", + "pushed_at": "2022-04-05T11:21:53Z", + "git_url": "git://github.com/ObolNetwork/charon.git", + "ssh_url": "git@github.com:ObolNetwork/charon.git", + "clone_url": "https://github.com/ObolNetwork/charon.git", + "svn_url": "https://github.com/ObolNetwork/charon", + "homepage": "https://obol.tech/", + "size": 20241, + "stargazers_count": 5, + "watchers_count": 5, + "language": "Go", + "has_issues": true, + "has_projects": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 22, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": false, + "is_template": false, + "topics": [], + "visibility": "private", + "forks": 0, + "open_issues": 22, + "watchers": 5, + "default_branch": "main" + } + }, + "base": { + "label": "ObolNetwork:main", + "ref": "main", + "sha": "d7330a3c3feec9e8c191dd02272ea220d93a51c3", + "user": { + "login": "ObolNetwork", + "id": 85748921, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg1NzQ4OTIx", + "avatar_url": "https://avatars.githubusercontent.com/u/85748921?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ObolNetwork", + "html_url": "https://github.com/ObolNetwork", + "followers_url": "https://api.github.com/users/ObolNetwork/followers", + "following_url": "https://api.github.com/users/ObolNetwork/following{/other_user}", + "gists_url": "https://api.github.com/users/ObolNetwork/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ObolNetwork/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ObolNetwork/subscriptions", + "organizations_url": "https://api.github.com/users/ObolNetwork/orgs", + "repos_url": "https://api.github.com/users/ObolNetwork/repos", + "events_url": "https://api.github.com/users/ObolNetwork/events{/privacy}", + "received_events_url": "https://api.github.com/users/ObolNetwork/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 409651007, + "node_id": "R_kgDOGGrHPw", + "name": "charon", + "full_name": "ObolNetwork/charon", + "private": true, + "owner": { + "login": "ObolNetwork", + "id": 85748921, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjg1NzQ4OTIx", + "avatar_url": "https://avatars.githubusercontent.com/u/85748921?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ObolNetwork", + "html_url": "https://github.com/ObolNetwork", + "followers_url": "https://api.github.com/users/ObolNetwork/followers", + "following_url": "https://api.github.com/users/ObolNetwork/following{/other_user}", + "gists_url": "https://api.github.com/users/ObolNetwork/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ObolNetwork/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ObolNetwork/subscriptions", + "organizations_url": "https://api.github.com/users/ObolNetwork/orgs", + "repos_url": "https://api.github.com/users/ObolNetwork/repos", + "events_url": "https://api.github.com/users/ObolNetwork/events{/privacy}", + "received_events_url": "https://api.github.com/users/ObolNetwork/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/ObolNetwork/charon", + "description": "A repository for the Obol distributed validator middleware", + "fork": false, + "url": "https://api.github.com/repos/ObolNetwork/charon", + "forks_url": "https://api.github.com/repos/ObolNetwork/charon/forks", + "keys_url": "https://api.github.com/repos/ObolNetwork/charon/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/ObolNetwork/charon/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/ObolNetwork/charon/teams", + "hooks_url": "https://api.github.com/repos/ObolNetwork/charon/hooks", + "issue_events_url": "https://api.github.com/repos/ObolNetwork/charon/issues/events{/number}", + "events_url": "https://api.github.com/repos/ObolNetwork/charon/events", + "assignees_url": "https://api.github.com/repos/ObolNetwork/charon/assignees{/user}", + "branches_url": "https://api.github.com/repos/ObolNetwork/charon/branches{/branch}", + "tags_url": "https://api.github.com/repos/ObolNetwork/charon/tags", + "blobs_url": "https://api.github.com/repos/ObolNetwork/charon/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/ObolNetwork/charon/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/ObolNetwork/charon/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/ObolNetwork/charon/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/ObolNetwork/charon/statuses/{sha}", + "languages_url": "https://api.github.com/repos/ObolNetwork/charon/languages", + "stargazers_url": "https://api.github.com/repos/ObolNetwork/charon/stargazers", + "contributors_url": "https://api.github.com/repos/ObolNetwork/charon/contributors", + "subscribers_url": "https://api.github.com/repos/ObolNetwork/charon/subscribers", + "subscription_url": "https://api.github.com/repos/ObolNetwork/charon/subscription", + "commits_url": "https://api.github.com/repos/ObolNetwork/charon/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/ObolNetwork/charon/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/ObolNetwork/charon/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/ObolNetwork/charon/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/ObolNetwork/charon/contents/{+path}", + "compare_url": "https://api.github.com/repos/ObolNetwork/charon/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/ObolNetwork/charon/merges", + "archive_url": "https://api.github.com/repos/ObolNetwork/charon/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/ObolNetwork/charon/downloads", + "issues_url": "https://api.github.com/repos/ObolNetwork/charon/issues{/number}", + "pulls_url": "https://api.github.com/repos/ObolNetwork/charon/pulls{/number}", + "milestones_url": "https://api.github.com/repos/ObolNetwork/charon/milestones{/number}", + "notifications_url": "https://api.github.com/repos/ObolNetwork/charon/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/ObolNetwork/charon/labels{/name}", + "releases_url": "https://api.github.com/repos/ObolNetwork/charon/releases{/id}", + "deployments_url": "https://api.github.com/repos/ObolNetwork/charon/deployments", + "created_at": "2021-09-23T15:42:01Z", + "updated_at": "2022-04-01T07:23:57Z", + "pushed_at": "2022-04-05T11:21:53Z", + "git_url": "git://github.com/ObolNetwork/charon.git", + "ssh_url": "git@github.com:ObolNetwork/charon.git", + "clone_url": "https://github.com/ObolNetwork/charon.git", + "svn_url": "https://github.com/ObolNetwork/charon", + "homepage": "https://obol.tech/", + "size": 20241, + "stargazers_count": 5, + "watchers_count": 5, + "language": "Go", + "has_issues": true, + "has_projects": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 22, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": false, + "is_template": false, + "topics": [], + "visibility": "private", + "forks": 0, + "open_issues": 22, + "watchers": 5, + "default_branch": "main" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/ObolNetwork/charon/pulls/348" + }, + "html": { + "href": "https://github.com/ObolNetwork/charon/pull/348" + }, + "issue": { + "href": "https://api.github.com/repos/ObolNetwork/charon/issues/348" + }, + "comments": { + "href": "https://api.github.com/repos/ObolNetwork/charon/issues/348/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/ObolNetwork/charon/pulls/348/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/ObolNetwork/charon/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/ObolNetwork/charon/pulls/348/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/ObolNetwork/charon/statuses/52ddd0c3b8370f344b9768aba8d7fc67a53a50fd" + } + }, + "author_association": "CONTRIBUTOR", + "auto_merge": null, + "active_lock_reason": null, + "merged": false, + "mergeable": true, + "rebaseable": true, + "mergeable_state": "blocked", + "merged_by": null, + "comments": 2, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 1, + "additions": 21, + "deletions": 7, + "changed_files": 1 +} \ No newline at end of file