├── .gitignore ├── test_data ├── config_v1_author_in_team.yml ├── config_v1_issues.yml ├── config_v0.yml ├── create_pr_headers ├── reopen_pr_headers ├── config2_v1.yml ├── config_v1_composite_size.yml ├── config_v1.yml ├── create_pr_mergeable_not_clean_payload ├── diff_response ├── issue_open_payload ├── create_pr_payload ├── create_pr_non_owner_payload ├── create_draft_pr_payload └── big_pr_payload ├── .github ├── FUNDING.yml ├── workflows │ ├── apply_labels.yml │ ├── release.yml │ ├── build.yml │ └── codeql-analysis.yml ├── dependabot.yml └── labeler.yml ├── pkg ├── http.go ├── condition_isdraft.go ├── condition_body.go ├── condition_title.go ├── condition_author_in_team.go ├── util_test.go ├── condition_branch.go ├── condition_basebranch.go ├── condition_author.go ├── util.go ├── condition_ismergeable.go ├── condition_type.go ├── condition_author_can_merge.go ├── condition_last_modified.go ├── condition_age.go ├── condition_files.go ├── condition_size.go └── labeler.go ├── Dockerfile ├── dependabot.yml ├── go.mod ├── Makefile ├── action.yml ├── LICENSE ├── go.sum ├── cmd ├── action_test.go └── action.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .idea 3 | coverage.out 4 | action 5 | action.tar.gz 6 | .vscode 7 | .env 8 | -------------------------------------------------------------------------------- /test_data/config_v1_author_in_team.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | labels: 3 | - label: "TestIsAuthorInTeam" 4 | author-in-team: "team-a" 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: srvaroa 4 | custom: ['https://buy.stripe.com/00g8yS1E6eml2t26oo'] 5 | -------------------------------------------------------------------------------- /test_data/config_v1_issues.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | issues: True 3 | labels: 4 | - label: "Test" 5 | authors: 6 | - "Test1" 7 | - "Test2" 8 | -------------------------------------------------------------------------------- /test_data/config_v0.yml: -------------------------------------------------------------------------------- 1 | WIP: 2 | title: "^WIP:.*" 3 | WOP: 4 | title: "^WOP:.*" 5 | S: 6 | size-below: 10 7 | M: 8 | size-above: 9 9 | size-below: 100 10 | L: 11 | size-above: 100 12 | -------------------------------------------------------------------------------- /test_data/create_pr_headers: -------------------------------------------------------------------------------- 1 | Request URL: http://example.com 2 | Request method: POST 3 | content-type: application/json 4 | Expect: 5 | User-Agent: GitHub-Hookshot/aef1084 6 | X-GitHub-Delivery: 5a529a20-8f96-11e9-88e4-07664efbd106 7 | X-GitHub-Event: pull_request 8 | -------------------------------------------------------------------------------- /test_data/reopen_pr_headers: -------------------------------------------------------------------------------- 1 | Request URL: http://example.com 2 | Request method: POST 3 | content-type: application/json 4 | Expect: 5 | User-Agent: GitHub-Hookshot/aef1084 6 | X-GitHub-Delivery: 5a529a20-8f96-11e9-88e4-07664efbd106 7 | X-GitHub-Event: pull_request 8 | -------------------------------------------------------------------------------- /test_data/config2_v1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | labels: 4 | - label: "TestLabel" 5 | title: ".*" 6 | - label: "TestFileMatch" 7 | files: 8 | - "cmd\\/.*.go" 9 | - "pkg\\/.*.go" 10 | - label: "TestTypePullRequest" 11 | type: "pull_request" 12 | - label: "TestTypeIssue" 13 | type: "issue" 14 | -------------------------------------------------------------------------------- /.github/workflows/apply_labels.yml: -------------------------------------------------------------------------------- 1 | name: Label PRs with published action as canary 2 | 3 | on: 4 | - pull_request 5 | - issues 6 | 7 | jobs: 8 | apply_labels: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: srvaroa/labeler@master 14 | env: 15 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 16 | -------------------------------------------------------------------------------- /test_data/config_v1_composite_size.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | labels: 3 | - label: S 4 | size-below: 10 5 | size-above: 1 6 | - label: M 7 | size: 8 | exclude-files: ["test.yaml"] 9 | above: 9 10 | below: 100 11 | - label: L 12 | size: 13 | exclude-files: ["test.yaml", "\\/dir\\/test.+.yaml"] 14 | above: 100 15 | -------------------------------------------------------------------------------- /pkg/http.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import "net/http" 4 | 5 | type HttpClient interface { 6 | Do(req *http.Request) (*http.Response, error) 7 | } 8 | 9 | type DefaultHttpClient struct { 10 | client *http.Client 11 | } 12 | 13 | func NewDefaultHttpClient() HttpClient { 14 | return &DefaultHttpClient{client: &http.Client{}} 15 | } 16 | 17 | func (d *DefaultHttpClient) Do(req *http.Request) (*http.Response, error) { 18 | return d.client.Do(req) 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 3 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17.2 2 | 3 | LABEL "com.github.actions.name"="Condition-based Pull Request labeller" \ 4 | "com.github.actions.description"="Automatically label pull requests based on rules" \ 5 | "com.github.actions.icon"="award" \ 6 | "com.github.actions.color"="blue" \ 7 | "maintainer"="Galo Navarro " \ 8 | "repository"="https://github.com/srvaroa/labeler" 9 | 10 | WORKDIR / 11 | ARG ASSET_URL=https://github.com/srvaroa/labeler/releases/download/v1.13.0/action.tar.gz 12 | RUN wget -q -O- $ASSET_URL | tar xzvf - 13 | ENTRYPOINT ["/action"] 14 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | open-pull-requests-limit: 5 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release Binary 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_tag: 7 | description: 'Tag to publish' 8 | required: true 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: wangyoucao577/go-release-action@v1.40 16 | with: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | goos: linux 19 | goarch: amd64 20 | build_command: make build 21 | overwrite: true 22 | binary_name: action 23 | asset_name: action 24 | release_tag: ${{ inputs.release_tag }} 25 | -------------------------------------------------------------------------------- /pkg/condition_isdraft.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func IsDraftCondition() Condition { 9 | return Condition{ 10 | GetName: func() string { 11 | return "Pull Request is draft" 12 | }, 13 | CanEvaluate: func(target *Target) bool { 14 | return target.ghPR != nil 15 | }, 16 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 17 | b, err := strconv.ParseBool(matcher.Draft) 18 | if err != nil { 19 | return false, fmt.Errorf("draft is not set in config") 20 | } 21 | if b { 22 | return target.ghPR.GetDraft(), nil 23 | } 24 | return !target.ghPR.GetDraft(), nil 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/condition_body.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | ) 8 | 9 | func BodyCondition() Condition { 10 | return Condition{ 11 | GetName: func() string { 12 | return "Body matches regex" 13 | }, 14 | CanEvaluate: func(target *Target) bool { 15 | return true 16 | }, 17 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 18 | if len(matcher.Body) <= 0 { 19 | return false, fmt.Errorf("body is not set in config") 20 | } 21 | log.Printf("Matching `%s` against: `%s`", matcher.Body, target.Body) 22 | isMatched, _ := regexp.Match(matcher.Body, []byte(target.Body)) 23 | return isMatched, nil 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/condition_title.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | ) 8 | 9 | func TitleCondition() Condition { 10 | return Condition{ 11 | GetName: func() string { 12 | return "Title matches regex" 13 | }, 14 | CanEvaluate: func(target *Target) bool { 15 | return true 16 | }, 17 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 18 | if len(matcher.Title) <= 0 { 19 | return false, fmt.Errorf("title is not set in config") 20 | } 21 | log.Printf("Matching `%s` against: `%s`", matcher.Title, target.Title) 22 | isMatched, _ := regexp.Match(matcher.Title, []byte(target.Title)) 23 | return isMatched, nil 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test_data/config_v1.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | labels: 3 | - label: WIP 4 | branch: "wip" 5 | - label: WIP 6 | title: "^WIP:.*" 7 | - label: WOP 8 | title: "^WOP:.*" 9 | - label: S 10 | size-below: 10 11 | - label: M 12 | size-above: 9 13 | size-below: 100 14 | - label: L 15 | size-above: 100 16 | - label: "TestFileMatch" 17 | files: 18 | - "cmd\\/.*.go" 19 | - "pkg\\/.*.go" 20 | - label: "Test" 21 | authors: 22 | - "Test1" 23 | - "Test2" 24 | - label: "TestDraft" 25 | draft: True 26 | - label: "TestMergeable" 27 | mergeable: True 28 | - label: "TestAuthorCanMerge" 29 | author-can-merge: True 30 | - label: "TestIsAuthorInTeam" 31 | author-in-team: "team1" 32 | -------------------------------------------------------------------------------- /pkg/condition_author_in_team.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func AuthorInTeamCondition(l *Labeler) Condition { 8 | return Condition{ 9 | GetName: func() string { 10 | return "Author is member of team" 11 | }, 12 | CanEvaluate: func(target *Target) bool { 13 | return true 14 | }, 15 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 16 | if len(matcher.AuthorInTeam) <= 0 { 17 | return false, fmt.Errorf("author-in-team is not set in config") 18 | } 19 | // check if author is a member of team 20 | return l.GitHubFacade.IsUserMemberOfTeam( 21 | target.Owner, 22 | target.Author, 23 | matcher.AuthorInTeam, // this is the team slug 24 | ) 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/util_test.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestParseExtendedDuration(t *testing.T) { 9 | tests := []struct { 10 | input string 11 | expected time.Duration 12 | }{ 13 | {"1s", 1 * time.Second}, 14 | {"2m", 2 * time.Minute}, 15 | {"3h", 3 * time.Hour}, 16 | {"4d", 4 * 24 * time.Hour}, 17 | {"5w", 5 * 7 * 24 * time.Hour}, 18 | {"6y", 6 * 365 * 24 * time.Hour}, 19 | } 20 | 21 | for _, test := range tests { 22 | result, err := parseExtendedDuration(test.input) 23 | if err != nil { 24 | t.Errorf("failed to parse duration from %s: %v", test.input, err) 25 | } 26 | if result != test.expected { 27 | t.Errorf("expected %v, got %v", test.expected, result) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/condition_branch.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | ) 8 | 9 | func BranchCondition() Condition { 10 | return Condition{ 11 | GetName: func() string { 12 | return "Branch matches regex" 13 | }, 14 | CanEvaluate: func(target *Target) bool { 15 | return target.ghPR != nil 16 | }, 17 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 18 | if len(matcher.Branch) <= 0 { 19 | return false, fmt.Errorf("branch is not set in config") 20 | } 21 | prBranchName := target.ghPR.Head.GetRef() 22 | log.Printf("Matching `%s` against: `%s`", matcher.Branch, prBranchName) 23 | isMatched, _ := regexp.Match(matcher.Branch, []byte(prBranchName)) 24 | return isMatched, nil 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/condition_basebranch.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | ) 8 | 9 | func BaseBranchCondition() Condition { 10 | return Condition{ 11 | GetName: func() string { 12 | return "Base branch matches regex" 13 | }, 14 | CanEvaluate: func(target *Target) bool { 15 | return target.ghPR != nil 16 | }, 17 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 18 | if len(matcher.BaseBranch) <= 0 { 19 | return false, fmt.Errorf("branch is not set in config") 20 | } 21 | prBranchName := target.ghPR.Base.GetRef() 22 | log.Printf("Matching `%s` against: `%s`", matcher.Branch, prBranchName) 23 | isMatched, _ := regexp.Match(matcher.BaseBranch, []byte(prBranchName)) 24 | return isMatched, nil 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/condition_author.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | ) 8 | 9 | func AuthorCondition() Condition { 10 | return Condition{ 11 | GetName: func() string { 12 | return "Author matches" 13 | }, 14 | CanEvaluate: func(target *Target) bool { 15 | return true 16 | }, 17 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 18 | if len(matcher.Authors) <= 0 { 19 | return false, fmt.Errorf("Users are not set in config") 20 | } 21 | 22 | log.Printf("Matching `%s` against: `%v`", matcher.Authors, target.Author) 23 | for _, author := range matcher.Authors { 24 | if strings.ToLower(author) == strings.ToLower(target.Author) { 25 | return true, nil 26 | } 27 | } 28 | return false, nil 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/srvaroa/labeler 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.5 6 | 7 | require ( 8 | github.com/go-yaml/yaml v2.1.0+incompatible 9 | github.com/google/go-cmp v0.7.0 10 | github.com/google/go-github/v50 v50.2.0 11 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d 12 | golang.org/x/oauth2 v0.30.0 13 | ) 14 | 15 | require ( 16 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 17 | github.com/cloudflare/circl v1.6.1 // indirect 18 | github.com/google/go-querystring v1.1.0 // indirect 19 | github.com/kr/pretty v0.1.0 // indirect 20 | golang.org/x/crypto v0.35.0 // indirect 21 | golang.org/x/sys v0.30.0 // indirect 22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 23 | gopkg.in/yaml.v2 v2.4.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/util.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func parseExtendedDuration(s string) (time.Duration, error) { 10 | multiplier := time.Hour * 24 // default to days 11 | 12 | if strings.HasSuffix(s, "w") { 13 | multiplier = time.Hour * 24 * 7 // weeks 14 | s = strings.TrimSuffix(s, "w") 15 | } else if strings.HasSuffix(s, "y") { 16 | multiplier = time.Hour * 24 * 365 // years 17 | s = strings.TrimSuffix(s, "y") 18 | } else if strings.HasSuffix(s, "d") { 19 | s = strings.TrimSuffix(s, "d") // days 20 | } else { 21 | return time.ParseDuration(s) // default to time.ParseDuration for hours, minutes, seconds 22 | } 23 | 24 | value, err := strconv.Atoi(s) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | return time.Duration(value) * multiplier, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/condition_ismergeable.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func IsMergeableCondition() Condition { 9 | return Condition{ 10 | GetName: func() string { 11 | return "Pull Request is mergeable" 12 | }, 13 | CanEvaluate: func(target *Target) bool { 14 | return target.ghPR != nil 15 | }, 16 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 17 | b, err := strconv.ParseBool(matcher.Mergeable) 18 | if err != nil { 19 | return false, fmt.Errorf("mergeable is not set in config") 20 | } 21 | 22 | // Check both the mergeable state and the mergeable flag 23 | isMergeable := target.ghPR.GetMergeable() && target.ghPR.GetMergeableState() == "clean" 24 | 25 | if b { 26 | return isMergeable, nil 27 | } 28 | return !isMergeable, nil 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and test 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out source code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: "1.24" 20 | check-latest: true 21 | cache: true 22 | 23 | - name: Build 24 | run: make build 25 | 26 | - name: Test 27 | run: make test 28 | 29 | - name: Run local action 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | INPUT_CONFIG_PATH: ./.github/labeler.yml 33 | INPUT_FAIL_ON_ERROR: true 34 | run: ./action 35 | 36 | - name: Check that the docker image builds 37 | run: docker build . -t local 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | GO := GO111MODULE=on GO15VENDOREXPERIMENT=1 go 3 | GO_NOMOD := GO111MODULE=off go 4 | 5 | # set dev version unless VERSION is explicitly set via environment 6 | VERSION ?= $(shell echo "$$(git describe --abbrev=0 --tags 2>/dev/null)-dev+$(REV)" | sed 's/^v//') 7 | 8 | GO_VERSION := $(shell $(GO) version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/') 9 | PACKAGE_DIRS := $(shell $(GO) list ./... | grep -v /vendor/ | grep -v e2e) 10 | PEGOMOCK_PACKAGE := github.com/petergtz/pegomock 11 | GO_DEPENDENCIES := $(shell find . -type f -name '*.go') 12 | 13 | BUILDFLAGS := -trimpath 14 | CGO_ENABLED = 0 15 | BUILDTAGS := 16 | 17 | GOPATH1=$(firstword $(subst :, ,$(GOPATH))) 18 | 19 | export PATH := $(PATH):$(GOPATH1)/bin 20 | 21 | build: $(GO_DEPENDENCIES) 22 | CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(BUILDTAGS) $(BUILDFLAGS) -o action cmd/action.go 23 | 24 | test: 25 | DISABLE_SSO=true CGO_ENABLED=$(CGO_ENABLED) $(GO) test -count 1 -coverprofile=coverage.out $(PACKAGE_DIRS) 26 | -------------------------------------------------------------------------------- /test_data/create_pr_mergeable_not_clean_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "number": 1, 4 | "pull_request": { 5 | "number": 1, 6 | "state": "open", 7 | "locked": false, 8 | "title": "Update test", 9 | "user": { 10 | "login": "srvaroa", 11 | "type": "User" 12 | }, 13 | "body": "Test PR", 14 | "base": { 15 | "repo": { 16 | "name": "labeler", 17 | "owner": { 18 | "login": "srvaroa" 19 | } 20 | } 21 | }, 22 | "created_at": "2019-05-24T22:03:59Z", 23 | "updated_at": "2019-05-24T22:03:59Z", 24 | "closed_at": null, 25 | "merged_at": null, 26 | "merge_commit_sha": null, 27 | "assignee": null, 28 | "assignees": [], 29 | "requested_reviewers": [], 30 | "requested_teams": [], 31 | "labels": [], 32 | "milestone": null, 33 | "commits": 1, 34 | "additions": 1, 35 | "deletions": 1, 36 | "changed_files": 1, 37 | "mergeable": true, 38 | "mergeable_state": "blocked", 39 | "author_association": "OWNER" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/condition_type.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func TypeCondition() Condition { 9 | return Condition{ 10 | GetName: func() string { 11 | return "Target type matches defined type" 12 | }, 13 | CanEvaluate: func(target *Target) bool { 14 | return true 15 | }, 16 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 17 | if len(matcher.Type) <= 0 { 18 | return false, fmt.Errorf("type is not set in config") 19 | } else if matcher.Type != "pull_request" && matcher.Type != "issue" { 20 | return false, fmt.Errorf("type musst be of value 'pull_request' or 'issue'") 21 | } 22 | 23 | var targetType string 24 | if target.ghPR != nil { 25 | targetType = "pull_request" 26 | } else if target.ghIssue != nil { 27 | targetType = "issue" 28 | } else { 29 | return false, fmt.Errorf("target is neither pull_request nor issue") 30 | } 31 | 32 | log.Printf("Matching `%s` against: `%s`", matcher.Type, targetType) 33 | return matcher.Type == targetType || matcher.Type == "all", nil 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Label manager for PRs and Issues based on configurable conditions' 2 | description: 'All-in-one action to manage labels in PRs and Issues based on many, extensible conditional rules' 3 | author: 'Galo Navarro ' 4 | inputs: 5 | config_path: 6 | default: '.github/labeler.yml' 7 | description: 'Path for labeling rules' 8 | use_local_config: 9 | default: 'false' 10 | description: 'By default the action will use the configuration file set in the default branch of the repository. When set to true, the action will instead use the configuration found in the local checkout of the repository.' 11 | fail_on_error: 12 | default: 'false' 13 | description: 'By default the action will never fail when an error is found during the evaluation of the labels. This is done in order to avoid disrupting CI pipelines with non-critical tasks. To override this behaviour, set this property to `true` so that any error in the evaluation of labels causes a failure of the workflow.' 14 | runs: 15 | using: 'docker' 16 | image: 'Dockerfile' 17 | branding: 18 | icon: award 19 | color: blue 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Galo Navarro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/condition_author_can_merge.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func AuthorCanMergeCondition() Condition { 9 | return Condition{ 10 | GetName: func() string { 11 | return "Author can merge" 12 | }, 13 | CanEvaluate: func(target *Target) bool { 14 | return target.ghPR != nil 15 | }, 16 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 17 | expected, err := strconv.ParseBool(matcher.AuthorCanMerge) 18 | if err != nil { 19 | return false, fmt.Errorf("author-can-merge doesn't have a valid value in config") 20 | } 21 | 22 | authorAssoc := target.ghPR.GetAuthorAssociation() 23 | canMerge := authorAssoc == "MEMBER" || authorAssoc == "OWNER" || authorAssoc == "COLLABORATOR" 24 | 25 | if expected && canMerge { 26 | fmt.Printf("User: %s can merge, condition matched\n", target.Author) 27 | return true, nil 28 | } 29 | 30 | if !expected && !canMerge { 31 | fmt.Printf("User: %s can not merge, condition matched\n", 32 | target.Author) 33 | return true, nil 34 | } 35 | 36 | fmt.Printf("Condition not matched") 37 | return false, nil 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/condition_last_modified.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/go-github/v50/github" 8 | ) 9 | 10 | func LastModifiedCondition(l *Labeler) Condition { 11 | return Condition{ 12 | GetName: func() string { 13 | return "Last modification of issue/PR" 14 | }, 15 | CanEvaluate: func(target *Target) bool { 16 | return target.ghIssue != nil || target.ghPR != nil 17 | }, 18 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 19 | if matcher.LastModified == nil { 20 | return false, fmt.Errorf("no last modified conditions are set in config") 21 | } 22 | // Determine the last modification time of the issue or PR 23 | var lastModifiedAt *github.Timestamp 24 | if target.ghIssue != nil { 25 | lastModifiedAt = target.ghIssue.UpdatedAt 26 | } else if target.ghPR != nil { 27 | lastModifiedAt = target.ghPR.UpdatedAt 28 | } else { 29 | return false, fmt.Errorf("no issue or PR found in target") 30 | } 31 | duration := time.Since(lastModifiedAt.Time) 32 | 33 | if matcher.LastModified.AtMost != "" { 34 | maxDuration, err := parseExtendedDuration(matcher.LastModified.AtMost) 35 | if err != nil { 36 | return false, fmt.Errorf("failed to parse `last-modified.at-most` parameter in configuration: %v", err) 37 | } 38 | return duration <= maxDuration, nil 39 | } 40 | 41 | if matcher.LastModified.AtLeast != "" { 42 | minDuration, err := parseExtendedDuration(matcher.LastModified.AtLeast) 43 | if err != nil { 44 | return false, fmt.Errorf("failed to parse `last-modified.at-least` parameter in configuration: %v", err) 45 | } 46 | return duration >= minDuration, nil 47 | } 48 | 49 | return false, fmt.Errorf("no last modified conditions are set in config") 50 | 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | labels: 4 | # Type: Recent changes 5 | - label: "@type/new" 6 | last-modified: 7 | at-most: 1d 8 | 9 | # Type: Old changes 10 | - label: "@type/old" 11 | last-modified: 12 | at-least: 30d 13 | 14 | # Type: Build-related changes 15 | - label: "@type/build" 16 | title: '^build(?:\(.+\))?\!?:' 17 | 18 | # Type: CI-related changes 19 | - label: "@type/ci" 20 | title: '^ci(?:\(.+\))?\!?:' 21 | files: 22 | - '\.github/.+' 23 | 24 | # Type: Documentation changes 25 | - label: "@type/docs" 26 | title: '^docs(?:\(.+\))?\!?:' 27 | files: 28 | - "docs/.+" 29 | - "**/*.md" 30 | 31 | # Type: New feature 32 | - label: "@type/feature" 33 | title: '^feat(?:\(.+\))?\!?:' 34 | 35 | # Type: Bug fix 36 | - label: "@type/fix" 37 | title: '^fix(?:\(.+\))?\!?:' 38 | 39 | # Type: Improvements such as style changes, refactoring, or performance improvements 40 | - label: "@type/improve" 41 | title: '^(style|refactor|perf)(?:\(.+\))?\!?:' 42 | 43 | # Type: Dependency changes 44 | - label: "@type/dependency" 45 | title: '^(chore|build)(?:\(deps\))?\!?:' 46 | 47 | # Type: Test-related changes 48 | - label: "@type/test" 49 | title: '^test(?:\(.+\))?\!?:' 50 | files: 51 | - "tests/.+" 52 | - "spec/.+" 53 | 54 | # Type: Security-related changes 55 | - label: "@type/security" 56 | title: '^security(?:\(.+\))?\!?:' 57 | files: 58 | - "**/security/.+" 59 | 60 | # Issue Type Only: Feature Request 61 | - label: "Feature Request" 62 | type: issue 63 | title: "^Feature Request:" 64 | 65 | # Issue Type Only: Documentation 66 | - label: "Documentation" 67 | type: issue 68 | title: "^.*(\b[Dd]ocumentation|doc(s)?\b).*" 69 | 70 | # Issue Type Only: Bug Report 71 | - label: "Bug Report" 72 | type: issue 73 | title: "^.*(\b[Bb]ug|b(u)?g(s)?\b).*" 74 | -------------------------------------------------------------------------------- /pkg/condition_age.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func AgeCondition(l *Labeler) Condition { 9 | return Condition{ 10 | GetName: func() string { 11 | return "Age of issue/PR" 12 | }, 13 | CanEvaluate: func(target *Target) bool { 14 | return target.ghIssue != nil || target.ghPR != nil 15 | }, 16 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 17 | // Backward compatibility: If "age" is provided as a string, treat it as "at-least" 18 | var atLeastDuration, atMostDuration time.Duration 19 | var err error 20 | 21 | // If they have specified a legacy "age" field, use that 22 | // and treat it is as "at-least" 23 | if matcher.Age != "" { 24 | atLeastDuration, err = parseExtendedDuration(matcher.Age) 25 | if err != nil { 26 | return false, fmt.Errorf("failed to parse age parameter in configuration: %v", err) 27 | } 28 | } else if matcher.AgeRange != nil { 29 | // Parse "at-least" if specified 30 | if matcher.AgeRange.AtLeast != "" { 31 | atLeastDuration, err = parseExtendedDuration(matcher.AgeRange.AtLeast) 32 | if err != nil { 33 | return false, fmt.Errorf("failed to parse `age.at-least` parameter in configuration: %v", err) 34 | } 35 | } 36 | 37 | // Parse "at-most" if specified 38 | if matcher.AgeRange.AtMost != "" { 39 | atMostDuration, err = parseExtendedDuration(matcher.AgeRange.AtMost) 40 | if err != nil { 41 | return false, fmt.Errorf("failed to parse `age.at-most` parameter in configuration: %v", err) 42 | } 43 | } 44 | } else { 45 | return false, fmt.Errorf("no age conditions are set in config") 46 | } 47 | 48 | // Determine the creation time of the issue or PR 49 | var createdAt time.Time 50 | if target.ghIssue != nil { 51 | createdAt = target.ghIssue.CreatedAt.Time 52 | } else if target.ghPR != nil { 53 | createdAt = target.ghPR.CreatedAt.Time 54 | } 55 | 56 | age := time.Since(createdAt) 57 | 58 | // Check if the age of the issue/PR is within the specified range 59 | if atLeastDuration != 0 && age < atLeastDuration { 60 | return false, nil 61 | } 62 | if atMostDuration != 0 && age > atMostDuration { 63 | return false, nil 64 | } 65 | 66 | return true, nil 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test_data/diff_response: -------------------------------------------------------------------------------- 1 | diff --git a/README.md b/README.md 2 | index fd6712e..6bc9d15 100644 3 | --- a/README.md 4 | +++ b/README.md 5 | @@ -1,4 +1,4 @@ 6 | -# Label manager for PRs and issues based on configurable conditions 7 | +# Label manager for PRs and issues based on configurable conditions!! 8 | 9 | [![labeler release (latest SemVer)](https://img.shields.io/github/v/release/srvaroa/labeler?sort=semver)](https://github.com/srvaroa/labeler/releases) 10 | 11 | @@ -53,7 +53,7 @@ on: 12 | jobs: 13 | build: 14 | 15 | - runs-on: ubuntu-latest 16 | + runs-on: ubuntu-latest!! 17 | 18 | steps: 19 | - uses: srvaroa/labeler@master 20 | @@ -104,7 +104,7 @@ errors are: 21 | permissions to label the main repository ([issue for 22 | solving this](https://github.com/srvaroa/labeler/issues/3)) 23 | 24 | -## Configuring matching rules 25 | +## Configuring matching ruleA!!s 26 | 27 | Configuration can be stored at `.github/labeler.yml` as a plain list of 28 | label matchers, which consist of a label and a set of conditions for 29 | @@ -379,4 +379,4 @@ This condition is satisfied when the title matches on the given regex. 30 | 31 | ```yaml 32 | title: "^WIP:.*" 33 | -``` 34 | +```!! 35 | diff --git a/dependabot.yml b/dependabot.yml 36 | deleted file mode 100644 37 | index f17f51b..0000000 38 | --- a/dependabot.yml 39 | +++ /dev/null 40 | @@ -1,12 +0,0 @@ 41 | -# To get started with Dependabot version updates, you'll need to specify which 42 | -# package ecosystems to update and where the package manifests are located. 43 | -# Please see the documentation for all configuration options: 44 | -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 45 | - 46 | -version: 2 47 | -updates: 48 | - - package-ecosystem: "" # See documentation for possible values 49 | - directory: "/" # Location of package manifests 50 | - schedule: 51 | - interval: "weekly" 52 | - 53 | diff --git a/pkg/condition_title.go b/pkg/condition_title.go 54 | index 79886f4..2fa4ee1 100644 55 | --- a/pkg/condition_title.go 56 | +++ b/pkg/condition_title.go 57 | @@ -18,6 +18,7 @@ func TitleCondition() Condition { 58 | if len(matcher.Title) <= 0 { 59 | return false, fmt.Errorf("title is not set in config") 60 | } 61 | + log.Printf("A change") 62 | log.Printf("Matching `%s` against: `%s`", matcher.Title, target.Title) 63 | isMatched, _ := regexp.Match(matcher.Title, []byte(target.Title)) 64 | return isMatched, nil 65 | diff --git a/new_file b/new_file 66 | new file mode 100644 67 | index 0000000..ce01362 68 | --- /dev/null 69 | +++ b/new_file 70 | @@ -0,0 +1 @@ 71 | +hello 72 | diff --git a/root/sub/test.md b/root/sub/test.md 73 | index 6c61a60..85aa975 100644 74 | --- a/root/sub/test.md 75 | +++ b/root/sub/test.md 76 | @@ -1 +1 @@ 77 | -# Test File 78 | +# Test File ! 79 | diff --git a/sub/test.md b/sub/test.md 80 | index 6c61a60..85aa975 100644 81 | --- a/sub/test.md 82 | +++ b/sub/test.md 83 | @@ -1 +1 @@ 84 | -# Test File 85 | +# Test File ! 86 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '21 3 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /pkg/condition_files.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | gh "github.com/google/go-github/v50/github" 13 | "github.com/waigani/diffparser" 14 | ) 15 | 16 | func FilesCondition(l *Labeler) Condition { 17 | prFiles := []string{} 18 | 19 | return Condition{ 20 | GetName: func() string { 21 | return "File matches regex" 22 | }, 23 | CanEvaluate: func(target *Target) bool { 24 | return target.ghPR != nil 25 | }, 26 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 27 | 28 | if len(matcher.Files) <= 0 { 29 | return false, fmt.Errorf("Files are not set in config") 30 | } 31 | 32 | if len(prFiles) == 0 { 33 | var err error 34 | prFiles, err = l.getPrFileNames(target.ghPR) 35 | if err != nil { 36 | return false, err 37 | } 38 | } 39 | 40 | log.Printf("Matching `%s` against: %s", strings.Join(matcher.Files, ", "), strings.Join(prFiles, ", ")) 41 | for _, fileMatcher := range matcher.Files { 42 | for _, prFile := range prFiles { 43 | isMatched, _ := regexp.Match(fileMatcher, []byte(prFile)) 44 | if isMatched { 45 | log.Printf("Matched `%s` against: `%s`", prFile, fileMatcher) 46 | return isMatched, nil 47 | } 48 | } 49 | } 50 | return false, nil 51 | }, 52 | } 53 | } 54 | 55 | // getPrFileNames returns all of the file names (old and new) of files changed in the given PR 56 | func (l *Labeler) getPrFileNames(pr *gh.PullRequest) ([]string, error) { 57 | log.Printf("getPrFileNames for pr - " + pr.GetURL()) 58 | ghToken := os.Getenv("GITHUB_TOKEN") 59 | diffReq, err := http.NewRequest("GET", pr.GetURL(), nil) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if ghToken != "" { 66 | diffReq.Header.Add("Authorization", "Bearer "+ghToken) 67 | } else { 68 | log.Printf("Env var GITHUB_TOKEN is missing, using annonymous request") 69 | } 70 | diffReq.Header.Add("Accept", "application/vnd.github.v3.diff") 71 | diffRes, err := l.Client.Do(diffReq) 72 | 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | defer diffRes.Body.Close() 78 | 79 | var diffRaw []byte 80 | prFiles := make([]string, 0) 81 | if diffRes.StatusCode == http.StatusOK { 82 | diffRaw, err = ioutil.ReadAll(diffRes.Body) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | diff, err := diffparser.Parse(string(diffRaw)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | log.Printf("got diff %s, parsed %+v", string(diffRaw), diff) 93 | prFilesSet := map[string]struct{}{} 94 | // Place in a set to remove duplicates 95 | for _, file := range diff.Files { 96 | prFilesSet[file.OrigName] = struct{}{} 97 | prFilesSet[file.NewName] = struct{}{} 98 | } 99 | // Convert to list to make it easier to consume 100 | for k := range prFilesSet { 101 | prFiles = append(prFiles, k) 102 | } 103 | log.Printf("diff files %s", prFiles) 104 | } else { 105 | log.Printf("failed with status %s", diffRes.Status) 106 | } 107 | 108 | return prFiles, nil 109 | } 110 | -------------------------------------------------------------------------------- /pkg/condition_size.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | gh "github.com/google/go-github/v50/github" 12 | ) 13 | 14 | func SizeCondition(l *Labeler) Condition { 15 | return Condition{ 16 | GetName: func() string { 17 | return "Pull Request contains a number of changes" 18 | }, 19 | CanEvaluate: func(target *Target) bool { 20 | return target.ghPR != nil 21 | }, 22 | Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) { 23 | 24 | if isNewConfig(matcher) && isOldConfig(matcher) { 25 | log.Printf("WARNING: you are using both the old " + 26 | "`size-above`/`size-below` settings together with " + 27 | "the newer `size`. You should use only the latter. " + 28 | "This condition will apply the configurations set in `Size` " + 29 | "and ignore the rest") 30 | } 31 | 32 | realMatcher := matcher.Size 33 | if realMatcher == nil { 34 | if matcher.SizeBelow == "" && matcher.SizeAbove == "" { 35 | return false, fmt.Errorf("no size conditions are set in config") 36 | } 37 | realMatcher = &SizeConfig{ 38 | Above: matcher.SizeAbove, 39 | Below: matcher.SizeBelow, 40 | } 41 | } 42 | 43 | log.Printf("Checking PR size using config: %+v", realMatcher) 44 | 45 | upperBound, err := strconv.ParseInt(realMatcher.Below, 0, 64) 46 | if err != nil { 47 | upperBound = math.MaxInt64 48 | log.Printf("Upper boundary set to %d (config has invalid or empty value)", upperBound) 49 | } 50 | lowerBound, err := strconv.ParseInt(realMatcher.Above, 0, 32) 51 | if err != nil || lowerBound < 0 { 52 | lowerBound = 0 53 | log.Printf("Lower boundary set to 0 (config has invalid or empty value)") 54 | } 55 | 56 | totalChanges, err := l.getModifiedLinesCount(target.ghPR, realMatcher.ExcludeFiles) 57 | log.Printf("Matching %d changes in PR against bounds: (%d, %d)", totalChanges, lowerBound, upperBound) 58 | isWithinBounds := totalChanges > lowerBound && totalChanges < upperBound 59 | return isWithinBounds, nil 60 | }, 61 | } 62 | } 63 | 64 | func isNewConfig(matcher LabelMatcher) bool { 65 | return matcher.Size != nil 66 | } 67 | 68 | func isOldConfig(matcher LabelMatcher) bool { 69 | return matcher.SizeAbove != "" || matcher.SizeBelow != "" 70 | } 71 | 72 | func (l *Labeler) getModifiedLinesCount(pr *gh.PullRequest, exclusions []string) (int64, error) { 73 | 74 | if len(exclusions) == 0 { 75 | // no exclusions so we can just rely on GH's summary which is 76 | // more lightweight 77 | return int64(math.Abs(float64(pr.GetAdditions() + pr.GetDeletions()))), nil 78 | } 79 | 80 | // Get the diff for the pull request 81 | urlParts := strings.Split(pr.GetBase().GetRepo().GetHTMLURL(), "/") 82 | owner := urlParts[len(urlParts)-2] 83 | repo := urlParts[len(urlParts)-1] 84 | diff, err := l.GitHubFacade.GetRawDiff(owner, repo, pr.GetNumber()) 85 | if err != nil { 86 | return 0, err 87 | } 88 | 89 | // Count the number of lines that start with "+" or "-" 90 | var count int64 91 | var countFile = false 92 | for _, line := range strings.Split(diff, "\n") { 93 | if line == "+++ /dev/null" || line == "--- /dev/null" { 94 | // ignore, these are removed or added files 95 | continue 96 | } 97 | if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") { 98 | // We're in a file's block 99 | path := strings.TrimPrefix(line, "---") 100 | path = strings.TrimPrefix(path, "+++") 101 | path = strings.TrimPrefix(path, "a/") 102 | path = strings.TrimPrefix(path, "b/") 103 | path = strings.TrimSpace(path) 104 | // Check if the file path matches any of the excluded files 105 | countFile = !isFileExcluded(path, exclusions) 106 | if countFile { 107 | log.Printf("Counting changes in file %s", path) 108 | } else { 109 | log.Printf("Ignoring file %s", path) 110 | } 111 | continue 112 | } 113 | if countFile && (strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-")) { 114 | log.Printf("Count line %s", line) 115 | count++ 116 | } 117 | } 118 | 119 | log.Printf("Total count %d", count) 120 | 121 | return count, nil 122 | } 123 | 124 | func isFileExcluded(path string, exclusions []string) bool { 125 | for _, exclusion := range exclusions { 126 | exclusionRegex, err := regexp.Compile(exclusion) 127 | if err != nil { 128 | log.Printf("Error compiling file exclusion regex %s: %s", exclusion, err) 129 | } else if exclusionRegex.MatchString(path) { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= 2 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 3 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 4 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 5 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 6 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= 11 | github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 12 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 14 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 15 | github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk= 16 | github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= 17 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 18 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 19 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 28 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 29 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d h1:xQcF7b7cZLWZG/+7A4G7un1qmEDYHIvId9qxRS1mZMs= 30 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d/go.mod h1:BzSc3WEF8R+lCaP5iGFRxd5kIXy4JKOZAwNe1w0cdc0= 31 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 32 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 33 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 34 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 35 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 36 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 41 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 48 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 51 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 52 | -------------------------------------------------------------------------------- /cmd/action_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | l "github.com/srvaroa/labeler/pkg" 11 | labeler "github.com/srvaroa/labeler/pkg" 12 | ) 13 | 14 | func TestGetLabelerConfigV0(t *testing.T) { 15 | 16 | file, err := os.Open("../test_data/config_v0.yml") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | contents, err := ioutil.ReadAll(file) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | var c *l.LabelerConfigV1 27 | c, err = getLabelerConfigV1(&contents) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | if 0 != c.Version { 33 | t.Fatalf("Expect version: %+v Got: %+v", 0, c.Version) 34 | } 35 | 36 | expectMatchers := map[string]l.LabelMatcher{ 37 | "WIP": { 38 | Label: "WIP", 39 | Title: "^WIP:.*", 40 | }, 41 | "WOP": { 42 | Label: "WOP", 43 | Title: "^WOP:.*", 44 | }, 45 | "S": { 46 | Label: "S", 47 | SizeBelow: "10", 48 | }, 49 | "M": { 50 | Label: "M", 51 | SizeAbove: "9", 52 | SizeBelow: "100", 53 | }, 54 | "L": { 55 | Label: "L", 56 | SizeAbove: "100", 57 | }, 58 | } 59 | 60 | if !cmp.Equal(len(expectMatchers), len(c.Labels)) { 61 | t.Fatalf("Expect same number of matchers: %+v Got: %+v", 62 | len(expectMatchers), 63 | len(c.Labels)) 64 | } 65 | 66 | for _, actualMatcher := range c.Labels { 67 | expectMatcher := expectMatchers[actualMatcher.Label] 68 | if !cmp.Equal(expectMatcher, actualMatcher) { 69 | t.Fatalf("Expect matcher: %+v Got: %+v", 70 | expectMatcher, 71 | actualMatcher) 72 | } 73 | } 74 | 75 | } 76 | 77 | func TestGetLabelerConfigV1(t *testing.T) { 78 | 79 | file, err := os.Open("../test_data/config_v1.yml") 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | contents, err := ioutil.ReadAll(file) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | var c *l.LabelerConfigV1 90 | c, err = getLabelerConfigV1(&contents) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | expect := l.LabelerConfigV1{ 96 | Version: 1, 97 | Labels: []l.LabelMatcher{ 98 | { 99 | Label: "WIP", 100 | Branch: "wip", 101 | }, 102 | { 103 | Label: "WIP", 104 | Title: "^WIP:.*", 105 | }, 106 | { 107 | Label: "WOP", 108 | Title: "^WOP:.*", 109 | }, 110 | { 111 | Label: "S", 112 | SizeBelow: "10", 113 | }, 114 | { 115 | Label: "M", 116 | SizeAbove: "9", 117 | SizeBelow: "100", 118 | }, 119 | { 120 | Label: "L", 121 | SizeAbove: "100", 122 | }, 123 | { 124 | Label: "TestFileMatch", 125 | Files: []string{ 126 | "cmd\\/.*.go", 127 | "pkg\\/.*.go", 128 | }, 129 | }, 130 | { 131 | Label: "Test", 132 | Authors: []string{ 133 | "Test1", 134 | "Test2", 135 | }, 136 | }, 137 | { 138 | Label: "TestDraft", 139 | Draft: "True", 140 | }, 141 | { 142 | Label: "TestMergeable", 143 | Mergeable: "True", 144 | }, 145 | { 146 | Label: "TestAuthorCanMerge", 147 | AuthorCanMerge: "True", 148 | }, 149 | { 150 | Label: "TestIsAuthorInTeam", 151 | AuthorInTeam: "team1", 152 | }, 153 | }, 154 | } 155 | 156 | if !cmp.Equal(expect, *c) { 157 | t.Fatalf("Expect: %+v Got: %+v", expect, c) 158 | } 159 | } 160 | 161 | func TestGetLabelerConfigV1WithIssues(t *testing.T) { 162 | 163 | file, err := os.Open("../test_data/config_v1_issues.yml") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | contents, err := ioutil.ReadAll(file) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | var c *l.LabelerConfigV1 174 | c, err = getLabelerConfigV1(&contents) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | expect := l.LabelerConfigV1{ 180 | Version: 1, 181 | Issues: true, 182 | Labels: []l.LabelMatcher{ 183 | { 184 | Label: "Test", 185 | Authors: []string{ 186 | "Test1", 187 | "Test2", 188 | }, 189 | }, 190 | }, 191 | } 192 | 193 | if !cmp.Equal(expect, *c) { 194 | t.Fatalf("Expect: %+v Got: %+v", expect, c) 195 | } 196 | } 197 | 198 | func TestGetLabelerConfigV1WithCompositeSize(t *testing.T) { 199 | 200 | file, err := os.Open("../test_data/config_v1_composite_size.yml") 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | 205 | contents, err := ioutil.ReadAll(file) 206 | if err != nil { 207 | t.Fatal(err) 208 | } 209 | 210 | var c *l.LabelerConfigV1 211 | c, err = getLabelerConfigV1(&contents) 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | 216 | expect := l.LabelerConfigV1{ 217 | Version: 1, 218 | Labels: []l.LabelMatcher{ 219 | { 220 | Label: "S", 221 | SizeAbove: "1", 222 | SizeBelow: "10", 223 | }, 224 | { 225 | Label: "M", 226 | Size: &labeler.SizeConfig{ 227 | ExcludeFiles: []string{"test.yaml"}, 228 | Above: "9", 229 | Below: "100", 230 | }, 231 | }, 232 | { 233 | Label: "L", 234 | Size: &labeler.SizeConfig{ 235 | ExcludeFiles: []string{"test.yaml", "\\/dir\\/test.+.yaml"}, 236 | Above: "100", 237 | }, 238 | }, 239 | }, 240 | } 241 | 242 | if !reflect.DeepEqual(expect, *c) { 243 | t.Fatalf("\nExpect: %#v \nGot: %#v", expect, *c) 244 | } 245 | } 246 | 247 | func TestGetLabelerConfig2V1(t *testing.T) { 248 | 249 | file, err := os.Open("../test_data/config2_v1.yml") 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | contents, err := ioutil.ReadAll(file) 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | 259 | var c *l.LabelerConfigV1 260 | c, err = getLabelerConfigV1(&contents) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | if 1 != c.Version { 266 | t.Fatalf("Expect version: %+v Got: %+v", 1, c.Version) 267 | } 268 | 269 | expectMatchers := map[string]l.LabelMatcher{ 270 | "TestLabel": { 271 | Label: "TestLabel", 272 | Title: ".*", 273 | }, 274 | "TestFileMatch": { 275 | Label: "TestFileMatch", 276 | Files: []string{"cmd\\/.*.go", "pkg\\/.*.go"}, 277 | }, 278 | "TestTypePullRequest": { 279 | Label: "TestTypePullRequest", 280 | Type: "pull_request", 281 | }, 282 | "TestTypeIssue": { 283 | Label: "TestTypeIssue", 284 | Type: "issue", 285 | }, 286 | } 287 | 288 | if !cmp.Equal(len(expectMatchers), len(c.Labels)) { 289 | t.Fatalf("Expect same number of matchers: %+v Got: %+v", 290 | len(expectMatchers), 291 | len(c.Labels)) 292 | } 293 | 294 | for _, actualMatcher := range c.Labels { 295 | expectMatcher := expectMatchers[actualMatcher.Label] 296 | if !cmp.Equal(expectMatcher, actualMatcher) { 297 | t.Fatalf("Expect matcher: %+v Got: %+v", 298 | expectMatcher, 299 | actualMatcher) 300 | } 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /cmd/action.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/go-yaml/yaml" 12 | "github.com/google/go-github/v50/github" 13 | labeler "github.com/srvaroa/labeler/pkg" 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | func main() { 18 | 19 | // Determine if we want the action to fail on error, or be silent to 20 | // prevent blocking CI pipelines 21 | failCode := 0 22 | failOnError, err := strconv.ParseBool(os.Getenv("INPUT_FAIL_ON_ERROR")) 23 | if err != nil { 24 | log.Printf("INPUT_FAIL_ON_ERROR not set, defaulting to silent failures") 25 | } else if failOnError { 26 | log.Printf("INPUT_FAIL_ON_ERROR enabled, the action will exit with an error code on failure") 27 | failCode = 1 28 | } 29 | gh, err := getGithubClient() 30 | if err != nil { 31 | log.Printf("Failed to retrieve a GitHub client: %+v", err) 32 | os.Exit(failCode) 33 | } 34 | eventPayload := getEventPayload() 35 | eventName := os.Getenv("GITHUB_EVENT_NAME") 36 | 37 | // Determine if the user wants to override the upstream config 38 | // in the main branch with the local one in the checkout 39 | useLocalConfig, err := strconv.ParseBool(os.Getenv("INPUT_USE_LOCAL_CONFIG")) 40 | if err != nil { 41 | useLocalConfig = false 42 | } 43 | 44 | configFile := os.Getenv("INPUT_CONFIG_PATH") 45 | 46 | var configRaw *[]byte 47 | if useLocalConfig { 48 | log.Printf("Reading configuration from local file: %s", configFile) 49 | contents, err := ioutil.ReadFile(configFile) 50 | if err != nil { 51 | log.Printf("Error reading configuration from local file: %s", err) 52 | os.Exit(failCode) 53 | } 54 | configRaw = &contents 55 | } else { 56 | log.Printf("Reading configuration file from the repository default branch: %s", configFile) 57 | // TODO: rethink this. Currently we'll take the config from the 58 | // PR's branch, not from master. My intuition is that one wants 59 | // to see the rules that are set in the main branch (as those are 60 | // vetted by the repo's owners). It seems fairly common in GH 61 | // actions to use this approach, and I will need to consider 62 | // whatever branch is set as main in the repo settings, so leaving 63 | // as this for now. 64 | configRaw, err = getRepoFile(gh, 65 | os.Getenv("GITHUB_REPOSITORY"), 66 | configFile, 67 | os.Getenv("GITHUB_SHA")) 68 | 69 | if err != nil { 70 | log.Printf("Error reading configuration from default branch: %s", err) 71 | os.Exit(failCode) 72 | } 73 | 74 | } 75 | 76 | config, err := getLabelerConfigV1(configRaw) 77 | if err != nil { 78 | log.Printf("Unable to parse configuration") 79 | os.Exit(failCode) 80 | } 81 | 82 | log.Printf("Re-evaluating labels on %s@%s", 83 | os.Getenv("GITHUB_REPOSITORY"), 84 | os.Getenv("GITHUB_SHA")) 85 | 86 | log.Printf("Trigger event: %s", os.Getenv("GITHUB_EVENT_NAME")) 87 | 88 | l := newLabeler(gh, config) 89 | 90 | if eventName == "schedule" { 91 | t := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/") 92 | owner, repo := t[0], t[1] 93 | l.ProcessAllPRs(owner, repo) 94 | l.ProcessAllIssues(owner, repo) 95 | } else { 96 | err = l.HandleEvent(eventName, eventPayload) 97 | if err != nil { 98 | log.Printf("Unable to execute action: %+v", err) 99 | } 100 | } 101 | } 102 | 103 | func getRepoFile(gh *github.Client, repo, file, sha string) (*[]byte, error) { 104 | 105 | t := strings.Split(repo, "/") 106 | owner, repoName := t[0], t[1] 107 | 108 | fileContent, _, _, err := gh.Repositories.GetContents( 109 | context.Background(), 110 | owner, 111 | repoName, 112 | file, 113 | &github.RepositoryContentGetOptions{Ref: sha}) 114 | 115 | var content string 116 | if err == nil { 117 | content, err = fileContent.GetContent() 118 | } 119 | 120 | if err != nil { 121 | log.Printf("Unable to load configuration from %s@%s/%s: %s", 122 | repo, sha, file, err) 123 | return nil, err 124 | } 125 | 126 | log.Printf("Loaded config from %s@%s:%s\n--\n%s", repo, sha, file, content) 127 | 128 | raw := []byte(content) 129 | return &raw, err 130 | } 131 | 132 | // getLabelerConfigV1 builds a LabelerConfigV1 from a raw yaml 133 | func getLabelerConfigV1(configRaw *[]byte) (*labeler.LabelerConfigV1, error) { 134 | var c labeler.LabelerConfigV1 135 | err := yaml.Unmarshal(*configRaw, &c) 136 | if err != nil { 137 | log.Printf("Unable to unmarshall config %s: ", err) 138 | } 139 | if c.Version == 0 { 140 | c, err = getLabelerConfigV0(configRaw) 141 | if err != nil { 142 | log.Printf("Unable to unmarshall legacy config %s: ", err) 143 | } 144 | } 145 | return &c, err 146 | } 147 | 148 | func getLabelerConfigV0(configRaw *[]byte) (labeler.LabelerConfigV1, error) { 149 | 150 | // Load v0 151 | var oldCfg map[string]labeler.LabelMatcher 152 | err := yaml.Unmarshal(*configRaw, &oldCfg) 153 | if err != nil { 154 | log.Printf("Unable to unmarshall legacy config: %s", err) 155 | return labeler.LabelerConfigV1{}, err 156 | } 157 | 158 | // Convert 159 | var matchers = []labeler.LabelMatcher{} 160 | for label, matcher := range oldCfg { 161 | matcher.Label = label 162 | matchers = append(matchers, matcher) 163 | } 164 | 165 | return labeler.LabelerConfigV1{ 166 | Version: 0, 167 | Labels: matchers, 168 | }, err 169 | } 170 | 171 | func getGithubClient() (*github.Client, error) { 172 | ghToken := os.Getenv("GITHUB_TOKEN") 173 | ghApiHost := os.Getenv("GITHUB_API_HOST") 174 | ctx := context.Background() 175 | ts := oauth2.StaticTokenSource( 176 | &oauth2.Token{AccessToken: ghToken}, 177 | ) 178 | 179 | if len(ghApiHost) == 0 { 180 | log.Printf("Connecting to GitHub.com") 181 | tc := oauth2.NewClient(ctx, ts) 182 | return github.NewClient(tc), nil 183 | } else { 184 | log.Printf("Connecting to enterprise server at: %s", ghApiHost) 185 | tc := oauth2.NewClient(ctx, ts) 186 | return github.NewEnterpriseClient(ghApiHost, ghApiHost, tc) 187 | } 188 | } 189 | 190 | func getEventPayload() *[]byte { 191 | payloadPath := os.Getenv("GITHUB_EVENT_PATH") 192 | file, err := os.Open(payloadPath) 193 | if err != nil { 194 | log.Fatalf("Failed to open event payload file %s: %s", payloadPath, err) 195 | } 196 | eventPayload, err := ioutil.ReadAll(file) 197 | if err != nil { 198 | log.Fatalf("Failed to load event payload from %s: %s", payloadPath, err) 199 | } 200 | return &eventPayload 201 | } 202 | 203 | func newLabeler(gh *github.Client, config *labeler.LabelerConfigV1) *labeler.Labeler { 204 | ctx := context.Background() 205 | 206 | l := labeler.Labeler{ 207 | 208 | FetchRepoConfig: func() (*labeler.LabelerConfigV1, error) { 209 | return config, nil 210 | }, 211 | 212 | ReplaceLabels: func(target *labeler.Target, labels []string) error { 213 | log.Printf("Setting labels to %s/%s#%d: %s", target.Owner, target.RepoName, target.IssueNo, labels) 214 | _, _, err := gh.Issues.ReplaceLabelsForIssue( 215 | context.Background(), target.Owner, target.RepoName, target.IssueNo, labels) 216 | return err 217 | }, 218 | 219 | GetCurrentLabels: func(target *labeler.Target) ([]string, error) { 220 | opts := github.ListOptions{} // TODO: ignoring pagination here 221 | currLabels, _, err := gh.Issues.ListLabelsByIssue( 222 | context.Background(), target.Owner, target.RepoName, target.IssueNo, &opts) 223 | 224 | labels := []string{} 225 | for _, label := range currLabels { 226 | labels = append(labels, *label.Name) 227 | } 228 | return labels, err 229 | }, 230 | GitHubFacade: &labeler.GitHubFacade{ 231 | GetRawDiff: func(owner, repo string, prNumber int) (string, error) { 232 | diff, _, err := gh.PullRequests.GetRaw(ctx, 233 | owner, repo, prNumber, 234 | github.RawOptions{Type: github.Diff}) 235 | return diff, err 236 | }, 237 | ListIssuesByRepo: func(owner, repo string) ([]*github.Issue, error) { 238 | issues, _, err := gh.Issues.ListByRepo(ctx, 239 | owner, repo, &github.IssueListByRepoOptions{}) 240 | return issues, err 241 | }, 242 | ListPRs: func(owner, repo string) ([]*github.PullRequest, error) { 243 | prs, _, err := gh.PullRequests.List(ctx, 244 | owner, repo, &github.PullRequestListOptions{}) 245 | return prs, err 246 | }, 247 | IsUserMemberOfTeam: func(org, user, team string) (bool, error) { 248 | membership, _, err := gh.Teams.GetTeamMembershipBySlug(ctx, org, team, user) 249 | if err != nil { 250 | return false, err 251 | } 252 | return membership.GetState() == "active", nil 253 | }, 254 | }, 255 | Client: labeler.NewDefaultHttpClient(), 256 | } 257 | return &l 258 | } 259 | -------------------------------------------------------------------------------- /test_data/issue_open_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "issue": { 4 | "url": "https://api.github.com/repos/srvaroa/test-repo/issues/1", 5 | "repository_url": "https://api.github.com/repos/srvaroa/test-repo", 6 | "labels_url": "https://api.github.com/repos/srvaroa/test-repo/issues/1/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/srvaroa/test-repo/issues/1/comments", 8 | "events_url": "https://api.github.com/repos/srvaroa/test-repo/issues/1/events", 9 | "html_url": "https://github.com/srvaroa/test-repo/issues/1", 10 | "id": 1581029562, 11 | "node_id": "I_kwDODfGd5c5ePJi6", 12 | "number": 1, 13 | "title": "Testy test", 14 | "user": { 15 | "login": "srvaroa", 16 | "id": 346110, 17 | "node_id": "MDQ6VXNlcjM0NjExMA==", 18 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/srvaroa", 21 | "html_url": "https://github.com/srvaroa", 22 | "followers_url": "https://api.github.com/users/srvaroa/followers", 23 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 27 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 28 | "repos_url": "https://api.github.com/users/srvaroa/repos", 29 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | 36 | ], 37 | "state": "open", 38 | "locked": false, 39 | "assignee": null, 40 | "assignees": [ 41 | 42 | ], 43 | "milestone": null, 44 | "comments": 0, 45 | "created_at": "2023-02-11T21:59:41Z", 46 | "updated_at": "2023-02-11T21:59:41Z", 47 | "closed_at": null, 48 | "author_association": "OWNER", 49 | "active_lock_reason": null, 50 | "body": "This is the description!", 51 | "reactions": { 52 | "url": "https://api.github.com/repos/srvaroa/test-repo/issues/1/reactions", 53 | "total_count": 0, 54 | "+1": 0, 55 | "-1": 0, 56 | "laugh": 0, 57 | "hooray": 0, 58 | "confused": 0, 59 | "heart": 0, 60 | "rocket": 0, 61 | "eyes": 0 62 | }, 63 | "timeline_url": "https://api.github.com/repos/srvaroa/test-repo/issues/1/timeline", 64 | "performed_via_github_app": null, 65 | "state_reason": null 66 | }, 67 | "repository": { 68 | "id": 233938405, 69 | "node_id": "MDEwOlJlcG9zaXRvcnkyMzM5Mzg0MDU=", 70 | "name": "test-repo", 71 | "full_name": "srvaroa/test-repo", 72 | "private": true, 73 | "owner": { 74 | "login": "srvaroa", 75 | "id": 346110, 76 | "node_id": "MDQ6VXNlcjM0NjExMA==", 77 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 78 | "gravatar_id": "", 79 | "url": "https://api.github.com/users/srvaroa", 80 | "html_url": "https://github.com/srvaroa", 81 | "followers_url": "https://api.github.com/users/srvaroa/followers", 82 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 83 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 84 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 85 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 86 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 87 | "repos_url": "https://api.github.com/users/srvaroa/repos", 88 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 89 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 90 | "type": "User", 91 | "site_admin": false 92 | }, 93 | "html_url": "https://github.com/srvaroa/test-repo", 94 | "description": null, 95 | "fork": false, 96 | "url": "https://api.github.com/repos/srvaroa/test-repo", 97 | "forks_url": "https://api.github.com/repos/srvaroa/test-repo/forks", 98 | "keys_url": "https://api.github.com/repos/srvaroa/test-repo/keys{/key_id}", 99 | "collaborators_url": "https://api.github.com/repos/srvaroa/test-reptest-repo{/collaborator}", 100 | "teams_url": "https://api.github.com/repos/srvaroa/test-repo/teams", 101 | "hooks_url": "https://api.github.com/repos/srvaroa/test-repo/hooks", 102 | "issue_events_url": "https://api.github.com/repos/srvaroa/test-repo/issues/events{/number}", 103 | "events_url": "https://api.github.com/repos/srvaroa/test-repo/events", 104 | "assignees_url": "https://api.github.com/repos/srvaroa/test-repo/assignees{/user}", 105 | "branches_url": "https://api.github.com/repos/srvaroa/test-repo/branches{/branch}", 106 | "tags_url": "https://api.github.com/repos/srvaroa/test-repo/tags", 107 | "blobs_url": "https://api.github.com/repos/srvaroa/test-repo/git/blobs{/sha}", 108 | "git_tags_url": "https://api.github.com/repos/srvaroa/test-repo/git/tags{/sha}", 109 | "git_refs_url": "https://api.github.com/repos/srvaroa/test-repo/git/refs{/sha}", 110 | "trees_url": "https://api.github.com/repos/srvaroa/test-repo/git/trees{/sha}", 111 | "statuses_url": "https://api.github.com/repos/srvaroa/test-repo/statuses/{sha}", 112 | "languages_url": "https://api.github.com/repos/srvaroa/test-repo/languages", 113 | "stargazers_url": "https://api.github.com/repos/srvaroa/test-repo/stargazers", 114 | "contributors_url": "https://api.github.com/repos/srvaroa/test-repo/contributors", 115 | "subscribers_url": "https://api.github.com/repos/srvaroa/test-repo/subscribers", 116 | "subscription_url": "https://api.github.com/repos/srvaroa/test-repo/subscription", 117 | "commits_url": "https://api.github.com/repos/srvaroa/test-repo/commits{/sha}", 118 | "git_commits_url": "https://api.github.com/repos/srvaroa/test-repo/git/commits{/sha}", 119 | "comments_url": "https://api.github.com/repos/srvaroa/test-repo/comments{/number}", 120 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test-repo/issues/comments{/number}", 121 | "contents_url": "https://api.github.com/repos/srvaroa/test-repo/contents/{+path}", 122 | "compare_url": "https://api.github.com/repos/srvaroa/test-repo/compare/{base}...{head}", 123 | "merges_url": "https://api.github.com/repos/srvaroa/test-repo/merges", 124 | "archive_url": "https://api.github.com/repos/srvaroa/test-repo/{archive_format}{/ref}", 125 | "downloads_url": "https://api.github.com/repos/srvaroa/test-repo/downloads", 126 | "issues_url": "https://api.github.com/repos/srvaroa/test-repo/issues{/number}", 127 | "pulls_url": "https://api.github.com/repos/srvaroa/test-repo/pulls{/number}", 128 | "milestones_url": "https://api.github.com/repos/srvaroa/test-repo/milestones{/number}", 129 | "notifications_url": "https://api.github.com/repos/srvaroa/test-repo/notifications{?since,all,participating}", 130 | "labels_url": "https://api.github.com/repos/srvaroa/test-repo/labels{/name}", 131 | "releases_url": "https://api.github.com/repos/srvaroa/test-repo/releases{/id}", 132 | "deployments_url": "https://api.github.com/repos/srvaroa/test-repo/deployments", 133 | "created_at": "2020-01-14T21:22:11Z", 134 | "updated_at": "2020-03-02T22:27:53Z", 135 | "pushed_at": "2020-03-02T22:27:51Z", 136 | "git_url": "git://github.com/srvaroa/test-repo.git", 137 | "ssh_url": "git@github.com:srvaroa/test-repo.git", 138 | "clone_url": "https://github.com/srvaroa/test-repo.git", 139 | "svn_url": "https://github.com/srvaroa/test-repo", 140 | "homepage": null, 141 | "size": 42, 142 | "stargazers_count": 0, 143 | "watchers_count": 0, 144 | "language": "Go", 145 | "has_issues": true, 146 | "has_projects": true, 147 | "has_downloads": true, 148 | "has_wiki": true, 149 | "has_pages": false, 150 | "has_discussions": false, 151 | "forks_count": 0, 152 | "mirror_url": null, 153 | "archived": false, 154 | "disabled": false, 155 | "open_issues_count": 1, 156 | "license": null, 157 | "allow_forking": true, 158 | "is_template": false, 159 | "web_commit_signoff_required": false, 160 | "topics": [ 161 | 162 | ], 163 | "visibility": "private", 164 | "forks": 0, 165 | "open_issues": 1, 166 | "watchers": 0, 167 | "default_branch": "master" 168 | }, 169 | "sender": { 170 | "login": "srvaroa", 171 | "id": 346110, 172 | "node_id": "MDQ6VXNlcjM0NjExMA==", 173 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 174 | "gravatar_id": "", 175 | "url": "https://api.github.com/users/srvaroa", 176 | "html_url": "https://github.com/srvaroa", 177 | "followers_url": "https://api.github.com/users/srvaroa/followers", 178 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 179 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 180 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 181 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 182 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 183 | "repos_url": "https://api.github.com/users/srvaroa/repos", 184 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 185 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 186 | "type": "User", 187 | "site_admin": false 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pkg/labeler.go: -------------------------------------------------------------------------------- 1 | package labeler 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | gh "github.com/google/go-github/v50/github" 8 | ) 9 | 10 | type DurationConfig struct { 11 | AtLeast string `yaml:"at-least"` 12 | AtMost string `yaml:"at-most"` 13 | } 14 | 15 | type SizeConfig struct { 16 | ExcludeFiles []string `yaml:"exclude-files"` 17 | Above string 18 | Below string 19 | } 20 | 21 | type LabelMatcher struct { 22 | Age string `yaml:"age,omitempty"` // Deprecated age config. 23 | AgeRange *DurationConfig `yaml:"age-range,omitempty"` 24 | AuthorCanMerge string `yaml:"author-can-merge"` 25 | Authors []string 26 | AuthorInTeam string `yaml:"author-in-team"` 27 | BaseBranch string `yaml:"base-branch"` 28 | Body string 29 | Branch string 30 | Draft string 31 | Files []string 32 | Label string 33 | LastModified *DurationConfig `yaml:"last-modified"` 34 | Mergeable string 35 | Negate bool 36 | Size *SizeConfig 37 | // size-legacy 38 | // These two are unused in the codebase (they get copied inside 39 | // the Size object), but we keep them to respect backwards 40 | // compatiblity parsing older configs without adding more 41 | // complexity. 42 | SizeAbove string `yaml:"size-above"` 43 | SizeBelow string `yaml:"size-below"` 44 | // size-legacy 45 | Title string 46 | Type string 47 | } 48 | 49 | type LabelerConfigV0 map[string]LabelMatcher 50 | 51 | type LabelerConfigV1 struct { 52 | Version int32 53 | // When set to true, scheduled executions will process both PRs and 54 | // issues. Else, we will only process PRs. Defaults to "False" 55 | Issues bool 56 | // When set to true, we will only add labels when they match a rule 57 | // but it will NOT remove labels that were previously set and stop 58 | // matching a rule 59 | AppendOnly bool `yaml:"appendOnly"` 60 | Labels []LabelMatcher 61 | } 62 | 63 | // LabelUpdates Represents a request to update the set of labels 64 | type LabelUpdates struct { 65 | set map[string]bool 66 | } 67 | 68 | // Just to make this mockable.. 69 | type GitHubFacade struct { 70 | GetRawDiff func(owner, repo string, prNumber int) (string, error) 71 | ListIssuesByRepo func(owner, repo string) ([]*gh.Issue, error) 72 | ListPRs func(owner, repo string) ([]*gh.PullRequest, error) 73 | IsUserMemberOfTeam func(org, user, team string) (bool, error) 74 | } 75 | 76 | type Labeler struct { 77 | FetchRepoConfig func() (*LabelerConfigV1, error) 78 | ReplaceLabels func(target *Target, labels []string) error 79 | GetCurrentLabels func(target *Target) ([]string, error) 80 | GitHubFacade *GitHubFacade 81 | Client HttpClient 82 | } 83 | 84 | type Condition struct { 85 | CanEvaluate func(target *Target) bool 86 | Evaluate func(target *Target, matcher LabelMatcher) (bool, error) 87 | GetName func() string 88 | } 89 | 90 | type Target struct { 91 | Author string 92 | Body string 93 | IssueNo int 94 | Title string 95 | Owner string 96 | RepoName string 97 | ghPR *gh.PullRequest 98 | ghIssue *gh.Issue 99 | } 100 | 101 | // HandleEvent takes a GitHub Event and its raw payload (see link below) 102 | // to trigger an update to the issue / PR's labels. 103 | // 104 | // https://developer.github.com/v3/activity/events/types/ 105 | func (l *Labeler) HandleEvent( 106 | eventName string, 107 | payload *[]byte) error { 108 | 109 | event, err := gh.ParseWebHook(eventName, *payload) 110 | if err != nil { 111 | return err 112 | } 113 | switch event := event.(type) { 114 | case *gh.PullRequestEvent: 115 | err = l.ExecuteOn(wrapPrAsTarget(event.PullRequest)) 116 | case *gh.PullRequestTargetEvent: 117 | err = l.ExecuteOn(wrapPrAsTarget(event.PullRequest)) 118 | case *gh.IssuesEvent: 119 | config, cfgErr := l.FetchRepoConfig() 120 | if cfgErr != nil { 121 | return cfgErr 122 | } 123 | if !config.Issues { 124 | log.Println("Issues must be explicitly enabled in order to process issues in event mode") 125 | return nil 126 | } 127 | err = l.ExecuteOn(wrapIssueAsTarget(event.Issue)) 128 | default: 129 | log.Printf("Event type is not supported, please review your workflow config") 130 | } 131 | return err 132 | } 133 | 134 | func wrapPrAsTarget(pr *gh.PullRequest) *Target { 135 | return &Target{ 136 | Author: *pr.GetUser().Login, 137 | Body: pr.GetBody(), 138 | IssueNo: *pr.Number, 139 | Title: pr.GetTitle(), 140 | Owner: pr.Base.Repo.GetOwner().GetLogin(), 141 | RepoName: *pr.Base.Repo.Name, 142 | ghPR: pr, 143 | ghIssue: nil, 144 | } 145 | } 146 | 147 | func wrapIssueAsTarget(issue *gh.Issue) *Target { 148 | 149 | // TODO: go-github@v50 has a Repository property that 150 | // avoids this. 151 | repoUrlSplit := strings.Split(*issue.RepositoryURL, "/") 152 | repoName := repoUrlSplit[len(repoUrlSplit)-1] 153 | owner := repoUrlSplit[len(repoUrlSplit)-2] 154 | 155 | return &Target{ 156 | Author: *issue.GetUser().Login, 157 | Body: issue.GetBody(), 158 | IssueNo: *issue.Number, 159 | Title: issue.GetTitle(), 160 | Owner: owner, 161 | RepoName: repoName, 162 | ghPR: nil, 163 | ghIssue: issue, 164 | } 165 | } 166 | 167 | func (l *Labeler) ExecuteOn(target *Target) error { 168 | 169 | log.Printf("Matching labels on target %+v", target) 170 | 171 | config, err := l.FetchRepoConfig() 172 | if err != nil { 173 | log.Printf("Unable to load configuration %+v", err) 174 | return err 175 | } 176 | 177 | labelUpdates, err := l.findMatches(target, config) 178 | if err != nil { 179 | log.Printf("Unable to find matches %+v", err) 180 | return err 181 | } 182 | 183 | currLabels, err := l.GetCurrentLabels(target) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | // intentions(label) tells whether `label` should be set in the PR 189 | intentions := map[string]bool{} 190 | 191 | // initialize with current labels 192 | for _, label := range currLabels { 193 | intentions[label] = true 194 | } 195 | 196 | log.Printf("Current labels: `%v`", intentions) 197 | log.Printf("Preliminary label updates: `%v`", labelUpdates) 198 | if config.AppendOnly { 199 | log.Printf("AppendOnly is active, removals are forbidden") 200 | } 201 | // update, adding new ones and unflagging those to remove if 202 | // necessary 203 | for label, isDesired := range labelUpdates.set { 204 | if config.AppendOnly { 205 | // If we DO NOT allow deletions, then we will respect 206 | // labels that were already set in the current set 207 | // but add new ones that matched the repo 208 | intentions[label] = intentions[label] || isDesired 209 | } else { 210 | // If we allow deletions, then we set / unset the label 211 | // based on the result of the rule checks 212 | intentions[label] = isDesired 213 | } 214 | } 215 | log.Printf("Final labels: `%v`", intentions) 216 | 217 | // filter out only labels that must be set 218 | desiredLabels := []string{} 219 | for k, v := range intentions { 220 | if v { 221 | desiredLabels = append(desiredLabels, k) 222 | } 223 | } 224 | log.Printf("Final set of labels: `%q`", desiredLabels) 225 | 226 | return l.ReplaceLabels(target, desiredLabels) 227 | } 228 | 229 | // findMatches returns all updates to be made to labels for the given target 230 | func (l *Labeler) findMatches(target *Target, config *LabelerConfigV1) (LabelUpdates, error) { 231 | 232 | labelUpdates := LabelUpdates{ 233 | set: map[string]bool{}, 234 | } 235 | conditions := []Condition{ 236 | AgeCondition(l), 237 | AuthorCondition(), 238 | AuthorCanMergeCondition(), 239 | AuthorInTeamCondition(l), 240 | BaseBranchCondition(), 241 | BodyCondition(), 242 | BranchCondition(), 243 | FilesCondition(l), 244 | LastModifiedCondition(l), 245 | IsDraftCondition(), 246 | IsMergeableCondition(), 247 | SizeCondition(l), 248 | TitleCondition(), 249 | TypeCondition(), 250 | } 251 | 252 | for _, matcher := range config.Labels { 253 | label := matcher.Label 254 | log.Printf("Evaluating label %s", label) 255 | 256 | if labelUpdates.set[label] { 257 | // This label was already matched in another matcher 258 | // so we already decided to apply it and need to 259 | // evaluate no more matchers. 260 | // 261 | // Note that multiple matchers for the same label 262 | // are combined with an OR. 263 | continue 264 | } 265 | 266 | // Reset the label as we're going to re-evaluate it in a new 267 | // condition 268 | delete(labelUpdates.set, label) 269 | 270 | for _, c := range conditions { 271 | if !c.CanEvaluate(target) { 272 | log.Printf("[%s] skip, event not supported by condition", c.GetName()) 273 | continue 274 | } 275 | isMatched, err := c.Evaluate(target, matcher) 276 | if err != nil { 277 | log.Printf("[%s] skip, %s", c.GetName(), err) 278 | continue 279 | } 280 | log.Printf("[%s] yields %t", c.GetName(), isMatched) 281 | 282 | prev, ok := labelUpdates.set[label] 283 | if ok { // Other conditions were evaluated for the label 284 | labelUpdates.set[label] = prev && isMatched 285 | } else { // First condition evaluated for this label 286 | labelUpdates.set[label] = isMatched 287 | } 288 | } 289 | 290 | if matcher.Negate { 291 | result, _ := labelUpdates.set[label] 292 | labelUpdates.set[label] = !result 293 | log.Printf("[%s] is negated from %t", label, result) 294 | } 295 | } 296 | 297 | return labelUpdates, nil 298 | } 299 | 300 | func (l *Labeler) ProcessAllIssues(owner, repo string) { 301 | 302 | config, err := l.FetchRepoConfig() 303 | if err != nil { 304 | log.Printf("Unable to load configuration %+v", err) 305 | return 306 | } 307 | 308 | if !config.Issues { 309 | log.Println("Issues must be explicitly enabled in order to process issues in the scheduled execution mode") 310 | return 311 | } 312 | 313 | issues, err := l.GitHubFacade.ListIssuesByRepo(owner, repo) 314 | 315 | if err != nil { 316 | log.Printf("Unable to list issues in %s/%s: %+v", owner, repo, err) 317 | return 318 | } 319 | 320 | for _, pr := range issues { 321 | err = l.ExecuteOn(wrapIssueAsTarget(pr)) 322 | log.Printf("Unable to execute action: %+v", err) 323 | } 324 | } 325 | 326 | func (l *Labeler) ProcessAllPRs(owner, repo string) { 327 | 328 | prs, err := l.GitHubFacade.ListPRs(owner, repo) 329 | 330 | if err != nil { 331 | log.Printf("Unable to list pull requests in %s/%s: %+v", owner, repo, err) 332 | return 333 | } 334 | 335 | for _, pr := range prs { 336 | err = l.ExecuteOn(wrapPrAsTarget(pr)) 337 | log.Printf("Unable to execute action: %+v", err) 338 | } 339 | 340 | } 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Label manager for PRs and issues based on configurable conditions 2 | 3 | [![labeler release (latest SemVer)](https://img.shields.io/github/v/release/srvaroa/labeler?sort=semver)](https://github.com/srvaroa/labeler/releases) [![sponsor the project!](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/srvaroa) 4 | 5 | Implements an all-in-one [GitHub 6 | Action](https://help.github.com/en/categories/automating-your-workflow-with-github-actions) 7 | that can manage multiple labels for both Pull Requests and Issues using 8 | configurable matching rules. Available conditions: 9 | 10 | * [Age](#age): label based on the age of a PR or Issue 11 | * [Author can merge](#author-can-merge): label based on whether the author can merge the PR 12 | * [Author is member of team](#author-in-team): label based on whether the author is an active member of the given team 13 | * [Authors](#authors): label based on the PR/Issue authors 14 | * [Base branch](#base-branch): label based on the PR's base branch name 15 | * [Body](#body): label based on the PR/Issue body 16 | * [Branch](#branch): label based on the PR's branch name 17 | * [Draft](#draft): label based on whether the PR is a draft 18 | * [Files](#files): label based on the files modified in the PR 19 | * [Last modified](#last-modified): label based on the last modification to a PR or Issue 20 | * [Mergeable](#mergeable): label based on whether the PR is mergeable 21 | * [Size](#size): label based on the PR size, allowing file exclusions 22 | * [Title](#title): label based on the PR/Issue title 23 | * [Type](#type): label based on record type (PR or Issue) 24 | 25 | ## Sponsors 26 | 27 | Please consider supporting the project if your organization finds it useful, 28 | you can do this through [GitHub Sponsors](https://github.com/sponsors/srvaroa). 29 | Sponsorships also help speed up bug fixes or new features. 30 | 31 | Thanks to [Launchgood](https://github.com/launchgood) and others that 32 | preferred to remain private for supporting this project! 33 | 34 | ## Installing 35 | 36 | The action is configured by adding a file `.github/labeler.yml` (which 37 | you can override). The file contains matching rules expanded in the 38 | `Configuration` section below. 39 | 40 | The action will strive to maintain backwards compatibility with older 41 | configuration versions. It is nevertheless encouraged to update your 42 | configuration files to benefit from newer features. Please follow our 43 | [releases](https://github.com/srvaroa/labeler/releases) page to stay up 44 | to date. 45 | 46 | ### GitHub Enterprise support 47 | 48 | Add `GITHUB_API_HOST` to your env variables, it should be in the form 49 | `http(s)://[hostname]/` 50 | 51 | Please consider [sponsoring the project](https://github.com/sponsors/srvaroa) if you're using Labeler in your organization! 52 | 53 | ### How to trigger action 54 | 55 | To trigger the action on events, add a file `.github/workflows/main.yml` 56 | to your repository: 57 | 58 | ```yaml 59 | name: Label PRs 60 | 61 | on: 62 | - pull_request 63 | - issues 64 | 65 | jobs: 66 | build: 67 | 68 | runs-on: ubuntu-latest 69 | 70 | steps: 71 | - uses: srvaroa/labeler@master 72 | env: 73 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 74 | ``` 75 | 76 | Using `@master` will run the latest available release. Feel free to pin 77 | this to a specific version from the [releases 78 | page](https://github.com/srvaroa/labeler/releases). We also maintain a 79 | floating tag on the major `v1`. This gets updated whenever a new 80 | minor/patch v1.x.y version is released. 81 | 82 | Use the [`on` 83 | clause](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) 84 | to control when to run it. 85 | 86 | * To trigger on PR events, [use 87 | `pull_request`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request). 88 | to trigger on PR events and run on the merge commit of the PR. Use 89 | [`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) 90 | instead if you prefer to run on the base. 91 | * To trigger on issue events, add [`issues`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues). 92 | 93 | You may combine multiple event triggers. 94 | 95 | A final option is to trigger the action periodically using the 96 | [`schedule`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule) 97 | trigger. For backwards compatibility reasons this will examine all 98 | active pull requests and update their labels. If you wish to examine 99 | issues as well, you'll need to explicitly add the `issues` flag in your 100 | config file: 101 | 102 | ```yaml 103 | version: 1 104 | issues: True 105 | labels: 106 | - label: "WIP" 107 | title: "^WIP:.*" 108 | ``` 109 | 110 | ### Advanced action settings 111 | 112 | Please refer to the [action.yml](action.yml) file in the repository 113 | for the available inputs to the action. Below is an example using all of 114 | them: 115 | 116 | ```yaml 117 | name: Label PRs 118 | 119 | on: 120 | - pull_request 121 | - issues 122 | 123 | jobs: 124 | build: 125 | 126 | runs-on: ubuntu-latest 127 | 128 | 129 | steps: 130 | 131 | - name: Checkout your code 132 | uses: actions/checkout@v3 133 | 134 | - uses: srvaroa/labeler@master 135 | with: 136 | config_path: .github/labeler.yml 137 | use_local_config: false 138 | fail_on_error: false 139 | env: 140 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 141 | ``` 142 | 143 | Use `config_path` to provide an alternative path for the configuration 144 | file for the action. The default is `.github/labeler.yml`. 145 | 146 | Use `use_local_config` to chose where to read the config file from. By 147 | default, the action will read the file from the default branch of your 148 | repository. If you set `use_local_config` to `true`, then the action 149 | will read the config file from the local checkout. Note that you may 150 | need to checkout your branch before the action runs! 151 | 152 | Use `fail_on_error` to decide whether an error in the action execution 153 | should trigger a failure of the workflow. By default it's disabled to 154 | prevent the action from disrupting CI pipelines. 155 | 156 | ## Troubleshooting 157 | 158 | To avoid blocking CI pipelines, the action will never return an error 159 | code and just log information about the problem. Typical errors are 160 | related to non-existing configuration file or invalid yaml. 161 | 162 | ## Configuring matching rules 163 | 164 | Configuration can be stored at `.github/labeler.yml` as a plain list of 165 | label matchers, which consist of a label and a set of conditions for 166 | each. When *all* conditions for a label match, then the Action will set 167 | the given label. When *any* condition for a label does not match, then 168 | the Action will unset the given label. 169 | 170 | All matchers follow this configuration pattern: 171 | 172 | ```yaml 173 | 310 | 311 | This condition evaluates the creation date of the PR or Issue. 312 | 313 | If you're looking to evaluate on the modification date of the issue or PR, 314 | check on 315 | 316 | This condition is best used when with a schedule trigger. 317 | 318 | Examples: 319 | 320 | ```yaml 321 | age-range: 322 | at-most: 1d 323 | ``` 324 | 325 | Will label PRs or issues that were created at most one day ago. 326 | 327 | ```yaml 328 | age-range: 329 | at-least: 1w 330 | ``` 331 | 332 | Will label PRs or issues that were created at least one week ago. 333 | 334 | The syntax for values is based on a number, followed by a suffix: 335 | 336 | * s: seconds 337 | * m: minutes 338 | * h: hours 339 | * d: days 340 | * w: weeks 341 | * y: years 342 | 343 | For example, `2d` means 2 days, `4w` means 4 weeks, and so on. 344 | 345 | ### Author can merge (PRs) 346 | 347 | This condition is satisfied when the author of the PR can merge it. 348 | This is implemented by checking if the author is an owner of the repo. 349 | 350 | ```yaml 351 | author-can-merge: True 352 | ``` 353 | 354 | 355 | ### Author is member (PRs and Issues) 356 | 357 | This condition is satisfied when the author of the PR is an active 358 | member of the given team (identified by its url slug). 359 | 360 | ```yaml 361 | author-in-team: core-team 362 | ``` 363 | 364 | ### Authors (PRs and Issues) 365 | 366 | This condition is satisfied when the author of the PR or Issue matches 367 | any of the given usernames. 368 | 369 | ```yaml 370 | authors: ["serubin"] 371 | ``` 372 | 373 | ### Base branch (PRs only) 374 | 375 | This condition is satisfied when the PR base branch matches on the given 376 | regex. 377 | 378 | ```yaml 379 | base-branch: "master" 380 | ``` 381 | 382 | ### Body (PRs and Issues) 383 | 384 | This condition is satisfied when the body (description) matches on the 385 | given regex. 386 | 387 | ``` yaml 388 | body: "^patch.*" 389 | ``` 390 | 391 | ### Branch (PRs only) 392 | 393 | This condition is satisfied when the PR branch matches on the given 394 | regex. 395 | 396 | ```yaml 397 | branch: "^feature/.*" 398 | ``` 399 | 400 | ### Draft status (PRs only) 401 | 402 | This condition is satisfied when the PR [draft 403 | state](https://developer.github.com/v3/pulls/#response-1) matches that of the 404 | PR. 405 | 406 | ```yaml 407 | draft: True 408 | ``` 409 | 410 | Matches if the PR is a draft. 411 | 412 | ```yaml 413 | draft: False 414 | ``` 415 | 416 | Matches if the PR is not a draft. 417 | 418 | ### Files affected (PRs only) 419 | 420 | This condition is satisfied when any of the PR files matches on the 421 | given regexs. 422 | 423 | ```yaml 424 | files: 425 | - "cmd\\/.*_tests.go" 426 | - ".*\\/subfolder\\/.*\\.md" 427 | ``` 428 | 429 | > **NOTICE** the double backslash (`\\`) in the example above. This GitHub 430 | Action is coded in Go (Golang), which means you need to pay special attention to 431 | regular expressions (Regex). Special characters need to be escaped with double 432 | backslashes. This is because the backslash in Go strings is an escape character 433 | and therefore must be escaped itself to appear as a literal in the regex. 434 | 435 | ### Last Modified (PRs and Issues) 436 | 437 | This condition evaluates the modification date of the PR or Issue. 438 | 439 | If you're looking to evaluate on the creation date of the issue or PR, 440 | check on 441 | 442 | This condition is best used when with a schedule trigger. 443 | 444 | Examples: 445 | 446 | ```yaml 447 | last-modified: 448 | at-most: 1d 449 | ``` 450 | Will label PRs or issues that were last modified at most one day ago 451 | 452 | ```yaml 453 | last-modified: 454 | at-least: 1d 455 | ``` 456 | 457 | Will label PRs or issues that were last modified at least one day ago 458 | 459 | The syntax for values is based on a number, followed by a suffix: 460 | 461 | * s: seconds 462 | * m: minutes 463 | * h: hours 464 | * d: days 465 | * w: weeks 466 | * y: years 467 | 468 | For example, `2d` means 2 days, `4w` means 4 weeks, and so on. 469 | 470 | ### Mergeable status (PRs only) 471 | 472 | This condition is satisfied when the [mergeable 473 | state](https://developer.github.com/v3/pulls/#response-1) matches that 474 | of the PR. 475 | 476 | ```yaml 477 | mergeable: True 478 | ``` 479 | 480 | Will match if the label is mergeable. 481 | 482 | ```yaml 483 | mergeable: False 484 | ``` 485 | 486 | Will match if the label is not mergeable. 487 | 488 | ### Size (PRs only) 489 | 490 | This condition is satisfied when the total number of changed lines in 491 | the PR is within given thresholds. 492 | 493 | The number of changed lines is calculated as the sum of all `additions + 494 | deletions` in the PR. 495 | 496 | For example, given this `.github/labeler.yml`: 497 | 498 | ```yaml 499 | - label: "S" 500 | size: 501 | below: 10 502 | - label: "M" 503 | size: 504 | above: 9 505 | below: 100 506 | - label: "L" 507 | size: 508 | above: 100 509 | ``` 510 | 511 | These would be the labels assigned to some PRs, based on their size as 512 | reported by the [GitHub API](https://developer.github.com/v3/pulls). 513 | 514 | |PR|additions|deletions|Resulting labels| 515 | |---|---|---|---| 516 | |First example|1|1|S| 517 | |Second example|5|42|M| 518 | |Third example|68|148|L| 519 | 520 | You can exclude some files so that their changes are not taken into 521 | account for the overall count. This can be useful for `yarn.lock`, 522 | `go.sum` and such. Use `exclude-files`, which supports both an explicit 523 | file or a Regex expression: 524 | 525 | ```yaml 526 | - label: "L" 527 | size: 528 | exclude-files: ["yarn.lock", "\\/root\\/.+\\/test.md"] 529 | above: 100 530 | ``` 531 | 532 | This condition will apply the `L` label if the diff is above 100 lines, 533 | but NOT taking into account changes in `yarn.lock`, or any `test.md` 534 | file that is in a subdirectory of `root`. 535 | 536 | **NOTICE** the double backslash (`\\`) in the example above. This GitHub 537 | Action is coded in Go (Golang), which means you need to pay special attention to 538 | regular expressions (Regex). Special characters need to be escaped with double 539 | backslashes. This is because the backslash in Go strings is an escape character 540 | and therefore must be escaped itself to appear as a literal in the regex. 541 | 542 | **NOTICE** the old format for specifying size properties (`size-above` 543 | and `size-below`) has been deprecated. The action will continue 544 | supporting old configs for now, but users are encouraged to migrate to 545 | the new configuration schema. 546 | 547 | ### Title 548 | 549 | This condition is satisfied when the title matches on the given regex. 550 | 551 | ```yaml 552 | title: "^WIP:.*" 553 | ``` 554 | 555 | ### Type 556 | 557 | By setting the type attribute in your label configuration, you can specify whether a rule applies exclusively to Pull 558 | Requests (PRs) or Issues. This allows for more precise label management based on the type of GitHub record. The 559 | type condition accepts one of two values: 560 | 561 | - `pull_request` 562 | - `issue` 563 | 564 | This functionality increases the adaptability of this GitHub Action, allowing users to create more tailored labeling 565 | strategies that differentiate between PRs and Issues or apply universally to both. 566 | 567 | #### Pull-Request Only: 568 | 569 | ```yaml 570 | - label: "needs review" 571 | type: "pull_request" 572 | name: ".*bug.*" 573 | ``` 574 | This rule applies the label "needs review" to Pull Requests with "bug" in the title. 575 | 576 | #### Issue Only: 577 | 578 | ```yaml 579 | - label: "needs triage" 580 | type: "issue" 581 | name: ".*bug.*" 582 | ``` 583 | 584 | This rule applies the label "needs triage" to Issues with "bug" in the title. 585 | -------------------------------------------------------------------------------- /test_data/create_pr_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "number": 2, 4 | "pull_request": { 5 | "url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2", 6 | "id": 288571928, 7 | "node_id": "MDExOlB1bGxSZXF1ZXN0Mjg4NTcxOTI4", 8 | "html_url": "https://github.com/srvaroa/jsonrouter/pull/2", 9 | "diff_url": "https://github.com/srvaroa/jsonrouter/pull/2.diff", 10 | "patch_url": "https://github.com/srvaroa/jsonrouter/pull/2.patch", 11 | "issue_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2", 12 | "number": 2, 13 | "state": "open", 14 | "locked": false, 15 | "title": "WIP: this is a test", 16 | "user": { 17 | "login": "srvaroa", 18 | "id": 346110, 19 | "node_id": "MDQ6VXNlcjM0NjExMA==", 20 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/srvaroa", 23 | "html_url": "https://github.com/srvaroa", 24 | "followers_url": "https://api.github.com/users/srvaroa/followers", 25 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 29 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 30 | "repos_url": "https://api.github.com/users/srvaroa/repos", 31 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "body": "Signed-off-by: Galo Navarro ", 37 | "created_at": "2019-06-15T17:52:33Z", 38 | "updated_at": "2019-06-15T17:52:33Z", 39 | "closed_at": null, 40 | "merged_at": null, 41 | "merge_commit_sha": null, 42 | "assignee": null, 43 | "assignees": [ 44 | 45 | ], 46 | "requested_reviewers": [ 47 | 48 | ], 49 | "requested_teams": [ 50 | 51 | ], 52 | "labels": [ 53 | 54 | ], 55 | "milestone": null, 56 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/commits", 57 | "review_comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/comments", 58 | "review_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/comments{/number}", 59 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2/comments", 60 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/77b472948e9aa504c1b584c3073318b3aa58bc0b", 61 | "head": { 62 | "label": "srvaroa:m", 63 | "ref": "m", 64 | "sha": "77b472948e9aa504c1b584c3073318b3aa58bc0b", 65 | "user": { 66 | "login": "srvaroa", 67 | "id": 346110, 68 | "node_id": "MDQ6VXNlcjM0NjExMA==", 69 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 70 | "gravatar_id": "", 71 | "url": "https://api.github.com/users/srvaroa", 72 | "html_url": "https://github.com/srvaroa", 73 | "followers_url": "https://api.github.com/users/srvaroa/followers", 74 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 75 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 76 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 77 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 78 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 79 | "repos_url": "https://api.github.com/users/srvaroa/repos", 80 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 81 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 82 | "type": "User", 83 | "site_admin": false 84 | }, 85 | "repo": { 86 | "id": 190467543, 87 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 88 | "name": "jsonrouter", 89 | "full_name": "srvaroa/jsonrouter", 90 | "private": false, 91 | "owner": { 92 | "login": "srvaroa", 93 | "id": 346110, 94 | "node_id": "MDQ6VXNlcjM0NjExMA==", 95 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 96 | "gravatar_id": "", 97 | "url": "https://api.github.com/users/srvaroa", 98 | "html_url": "https://github.com/srvaroa", 99 | "followers_url": "https://api.github.com/users/srvaroa/followers", 100 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 101 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 102 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 103 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 104 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 105 | "repos_url": "https://api.github.com/users/srvaroa/repos", 106 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 107 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 108 | "type": "User", 109 | "site_admin": false 110 | }, 111 | "html_url": "https://github.com/srvaroa/jsonrouter", 112 | "description": null, 113 | "fork": false, 114 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 115 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 116 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 117 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 118 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 119 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 120 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 121 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 122 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 123 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 124 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 125 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 126 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 127 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 128 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 129 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 130 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 131 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 132 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 133 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 134 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 135 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 136 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 137 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 138 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 139 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 140 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 141 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 142 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 143 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 144 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 145 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 146 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 147 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 148 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 149 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 150 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 151 | "created_at": "2019-06-05T20:57:56Z", 152 | "updated_at": "2019-06-15T17:48:32Z", 153 | "pushed_at": "2019-06-15T17:52:28Z", 154 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 155 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 156 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 157 | "svn_url": "https://github.com/srvaroa/jsonrouter", 158 | "homepage": null, 159 | "size": 4, 160 | "stargazers_count": 0, 161 | "watchers_count": 0, 162 | "language": "Go", 163 | "has_issues": false, 164 | "has_projects": false, 165 | "has_downloads": true, 166 | "has_wiki": false, 167 | "has_pages": false, 168 | "forks_count": 0, 169 | "mirror_url": null, 170 | "archived": false, 171 | "disabled": false, 172 | "open_issues_count": 1, 173 | "license": null, 174 | "forks": 0, 175 | "open_issues": 1, 176 | "watchers": 0, 177 | "default_branch": "master" 178 | } 179 | }, 180 | "base": { 181 | "label": "srvaroa:master", 182 | "ref": "master", 183 | "sha": "54806dc574efef6487d8ac7dd26e96712f1781e5", 184 | "user": { 185 | "login": "srvaroa", 186 | "id": 346110, 187 | "node_id": "MDQ6VXNlcjM0NjExMA==", 188 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 189 | "gravatar_id": "", 190 | "url": "https://api.github.com/users/srvaroa", 191 | "html_url": "https://github.com/srvaroa", 192 | "followers_url": "https://api.github.com/users/srvaroa/followers", 193 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 194 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 195 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 196 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 197 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 198 | "repos_url": "https://api.github.com/users/srvaroa/repos", 199 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 200 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 201 | "type": "User", 202 | "site_admin": false 203 | }, 204 | "repo": { 205 | "id": 190467543, 206 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 207 | "name": "jsonrouter", 208 | "full_name": "srvaroa/jsonrouter", 209 | "private": false, 210 | "owner": { 211 | "login": "srvaroa", 212 | "id": 346110, 213 | "node_id": "MDQ6VXNlcjM0NjExMA==", 214 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 215 | "gravatar_id": "", 216 | "url": "https://api.github.com/users/srvaroa", 217 | "html_url": "https://github.com/srvaroa", 218 | "followers_url": "https://api.github.com/users/srvaroa/followers", 219 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 220 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 221 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 222 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 223 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 224 | "repos_url": "https://api.github.com/users/srvaroa/repos", 225 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 226 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 227 | "type": "User", 228 | "site_admin": false 229 | }, 230 | "html_url": "https://github.com/srvaroa/jsonrouter", 231 | "description": null, 232 | "fork": false, 233 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 234 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 235 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 236 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 237 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 238 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 239 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 240 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 241 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 242 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 243 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 244 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 245 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 246 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 247 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 248 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 249 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 250 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 251 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 252 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 253 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 254 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 255 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 256 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 257 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 258 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 259 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 260 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 261 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 262 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 263 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 264 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 265 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 266 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 267 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 268 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 269 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 270 | "created_at": "2019-06-05T20:57:56Z", 271 | "updated_at": "2019-06-15T17:48:32Z", 272 | "pushed_at": "2019-06-15T17:52:28Z", 273 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 274 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 275 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 276 | "svn_url": "https://github.com/srvaroa/jsonrouter", 277 | "homepage": null, 278 | "size": 4, 279 | "stargazers_count": 0, 280 | "watchers_count": 0, 281 | "language": "Go", 282 | "has_issues": false, 283 | "has_projects": false, 284 | "has_downloads": true, 285 | "has_wiki": false, 286 | "has_pages": false, 287 | "forks_count": 0, 288 | "mirror_url": null, 289 | "archived": false, 290 | "disabled": false, 291 | "open_issues_count": 1, 292 | "license": null, 293 | "forks": 0, 294 | "open_issues": 1, 295 | "watchers": 0, 296 | "default_branch": "master" 297 | } 298 | }, 299 | "_links": { 300 | "self": { 301 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2" 302 | }, 303 | "html": { 304 | "href": "https://github.com/srvaroa/jsonrouter/pull/2" 305 | }, 306 | "issue": { 307 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2" 308 | }, 309 | "comments": { 310 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2/comments" 311 | }, 312 | "review_comments": { 313 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/comments" 314 | }, 315 | "review_comment": { 316 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/comments{/number}" 317 | }, 318 | "commits": { 319 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/commits" 320 | }, 321 | "statuses": { 322 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/77b472948e9aa504c1b584c3073318b3aa58bc0b" 323 | } 324 | }, 325 | "author_association": "OWNER", 326 | "draft": false, 327 | "merged": false, 328 | "mergeable": null, 329 | "rebaseable": null, 330 | "mergeable_state": "unknown", 331 | "merged_by": null, 332 | "comments": 0, 333 | "review_comments": 0, 334 | "maintainer_can_modify": false, 335 | "commits": 1, 336 | "additions": 0, 337 | "deletions": 0, 338 | "changed_files": 0 339 | }, 340 | "repository": { 341 | "id": 190467543, 342 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 343 | "name": "jsonrouter", 344 | "full_name": "srvaroa/jsonrouter", 345 | "private": false, 346 | "owner": { 347 | "login": "srvaroa", 348 | "id": 346110, 349 | "node_id": "MDQ6VXNlcjM0NjExMA==", 350 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 351 | "gravatar_id": "", 352 | "url": "https://api.github.com/users/srvaroa", 353 | "html_url": "https://github.com/srvaroa", 354 | "followers_url": "https://api.github.com/users/srvaroa/followers", 355 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 356 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 357 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 358 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 359 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 360 | "repos_url": "https://api.github.com/users/srvaroa/repos", 361 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 362 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 363 | "type": "User", 364 | "site_admin": false 365 | }, 366 | "html_url": "https://github.com/srvaroa/jsonrouter", 367 | "description": null, 368 | "fork": false, 369 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 370 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 371 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 372 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 373 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 374 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 375 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 376 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 377 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 378 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 379 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 380 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 381 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 382 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 383 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 384 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 385 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 386 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 387 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 388 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 389 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 390 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 391 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 392 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 393 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 394 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 395 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 396 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 397 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 398 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 399 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 400 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 401 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 402 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 403 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 404 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 405 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 406 | "created_at": "2019-06-05T20:57:56Z", 407 | "updated_at": "2019-06-15T17:48:32Z", 408 | "pushed_at": "2019-06-15T17:52:28Z", 409 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 410 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 411 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 412 | "svn_url": "https://github.com/srvaroa/jsonrouter", 413 | "homepage": null, 414 | "size": 4, 415 | "stargazers_count": 0, 416 | "watchers_count": 0, 417 | "language": "Go", 418 | "has_issues": false, 419 | "has_projects": false, 420 | "has_downloads": true, 421 | "has_wiki": false, 422 | "has_pages": false, 423 | "forks_count": 0, 424 | "mirror_url": null, 425 | "archived": false, 426 | "disabled": false, 427 | "open_issues_count": 1, 428 | "license": null, 429 | "forks": 0, 430 | "open_issues": 1, 431 | "watchers": 0, 432 | "default_branch": "master" 433 | }, 434 | "sender": { 435 | "login": "srvaroa", 436 | "id": 346110, 437 | "node_id": "MDQ6VXNlcjM0NjExMA==", 438 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 439 | "gravatar_id": "", 440 | "url": "https://api.github.com/users/srvaroa", 441 | "html_url": "https://github.com/srvaroa", 442 | "followers_url": "https://api.github.com/users/srvaroa/followers", 443 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 444 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 445 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 446 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 447 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 448 | "repos_url": "https://api.github.com/users/srvaroa/repos", 449 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 450 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 451 | "type": "User", 452 | "site_admin": false 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /test_data/create_pr_non_owner_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "number": 2, 4 | "pull_request": { 5 | "url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2", 6 | "id": 288571928, 7 | "node_id": "MDExOlB1bGxSZXF1ZXN0Mjg4NTcxOTI4", 8 | "html_url": "https://github.com/srvaroa/jsonrouter/pull/2", 9 | "diff_url": "https://github.com/srvaroa/jsonrouter/pull/2.diff", 10 | "patch_url": "https://github.com/srvaroa/jsonrouter/pull/2.patch", 11 | "issue_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2", 12 | "number": 2, 13 | "state": "open", 14 | "locked": false, 15 | "title": "WIP: this is a test", 16 | "user": { 17 | "login": "srvaroa", 18 | "id": 346110, 19 | "node_id": "MDQ6VXNlcjM0NjExMA==", 20 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/srvaroa", 23 | "html_url": "https://github.com/srvaroa", 24 | "followers_url": "https://api.github.com/users/srvaroa/followers", 25 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 29 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 30 | "repos_url": "https://api.github.com/users/srvaroa/repos", 31 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "body": "Signed-off-by: Galo Navarro ", 37 | "created_at": "2019-06-15T17:52:33Z", 38 | "updated_at": "2019-06-15T17:52:33Z", 39 | "closed_at": null, 40 | "merged_at": null, 41 | "merge_commit_sha": null, 42 | "assignee": null, 43 | "assignees": [ 44 | 45 | ], 46 | "requested_reviewers": [ 47 | 48 | ], 49 | "requested_teams": [ 50 | 51 | ], 52 | "labels": [ 53 | 54 | ], 55 | "milestone": null, 56 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/commits", 57 | "review_comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/comments", 58 | "review_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/comments{/number}", 59 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2/comments", 60 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/77b472948e9aa504c1b584c3073318b3aa58bc0b", 61 | "head": { 62 | "label": "srvaroa:m", 63 | "ref": "m", 64 | "sha": "77b472948e9aa504c1b584c3073318b3aa58bc0b", 65 | "user": { 66 | "login": "srvaroa", 67 | "id": 346110, 68 | "node_id": "MDQ6VXNlcjM0NjExMA==", 69 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 70 | "gravatar_id": "", 71 | "url": "https://api.github.com/users/srvaroa", 72 | "html_url": "https://github.com/srvaroa", 73 | "followers_url": "https://api.github.com/users/srvaroa/followers", 74 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 75 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 76 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 77 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 78 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 79 | "repos_url": "https://api.github.com/users/srvaroa/repos", 80 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 81 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 82 | "type": "User", 83 | "site_admin": false 84 | }, 85 | "repo": { 86 | "id": 190467543, 87 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 88 | "name": "jsonrouter", 89 | "full_name": "srvaroa/jsonrouter", 90 | "private": false, 91 | "owner": { 92 | "login": "srvaroa", 93 | "id": 346110, 94 | "node_id": "MDQ6VXNlcjM0NjExMA==", 95 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 96 | "gravatar_id": "", 97 | "url": "https://api.github.com/users/srvaroa", 98 | "html_url": "https://github.com/srvaroa", 99 | "followers_url": "https://api.github.com/users/srvaroa/followers", 100 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 101 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 102 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 103 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 104 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 105 | "repos_url": "https://api.github.com/users/srvaroa/repos", 106 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 107 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 108 | "type": "User", 109 | "site_admin": false 110 | }, 111 | "html_url": "https://github.com/srvaroa/jsonrouter", 112 | "description": null, 113 | "fork": false, 114 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 115 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 116 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 117 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 118 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 119 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 120 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 121 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 122 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 123 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 124 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 125 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 126 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 127 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 128 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 129 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 130 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 131 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 132 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 133 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 134 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 135 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 136 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 137 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 138 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 139 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 140 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 141 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 142 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 143 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 144 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 145 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 146 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 147 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 148 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 149 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 150 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 151 | "created_at": "2019-06-05T20:57:56Z", 152 | "updated_at": "2019-06-15T17:48:32Z", 153 | "pushed_at": "2019-06-15T17:52:28Z", 154 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 155 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 156 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 157 | "svn_url": "https://github.com/srvaroa/jsonrouter", 158 | "homepage": null, 159 | "size": 4, 160 | "stargazers_count": 0, 161 | "watchers_count": 0, 162 | "language": "Go", 163 | "has_issues": false, 164 | "has_projects": false, 165 | "has_downloads": true, 166 | "has_wiki": false, 167 | "has_pages": false, 168 | "forks_count": 0, 169 | "mirror_url": null, 170 | "archived": false, 171 | "disabled": false, 172 | "open_issues_count": 1, 173 | "license": null, 174 | "forks": 0, 175 | "open_issues": 1, 176 | "watchers": 0, 177 | "default_branch": "master" 178 | } 179 | }, 180 | "base": { 181 | "label": "srvaroa:master", 182 | "ref": "master", 183 | "sha": "54806dc574efef6487d8ac7dd26e96712f1781e5", 184 | "user": { 185 | "login": "srvaroa", 186 | "id": 346110, 187 | "node_id": "MDQ6VXNlcjM0NjExMA==", 188 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 189 | "gravatar_id": "", 190 | "url": "https://api.github.com/users/srvaroa", 191 | "html_url": "https://github.com/srvaroa", 192 | "followers_url": "https://api.github.com/users/srvaroa/followers", 193 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 194 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 195 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 196 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 197 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 198 | "repos_url": "https://api.github.com/users/srvaroa/repos", 199 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 200 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 201 | "type": "User", 202 | "site_admin": false 203 | }, 204 | "repo": { 205 | "id": 190467543, 206 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 207 | "name": "jsonrouter", 208 | "full_name": "srvaroa/jsonrouter", 209 | "private": false, 210 | "owner": { 211 | "login": "srvaroa", 212 | "id": 346110, 213 | "node_id": "MDQ6VXNlcjM0NjExMA==", 214 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 215 | "gravatar_id": "", 216 | "url": "https://api.github.com/users/srvaroa", 217 | "html_url": "https://github.com/srvaroa", 218 | "followers_url": "https://api.github.com/users/srvaroa/followers", 219 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 220 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 221 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 222 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 223 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 224 | "repos_url": "https://api.github.com/users/srvaroa/repos", 225 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 226 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 227 | "type": "User", 228 | "site_admin": false 229 | }, 230 | "html_url": "https://github.com/srvaroa/jsonrouter", 231 | "description": null, 232 | "fork": false, 233 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 234 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 235 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 236 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 237 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 238 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 239 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 240 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 241 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 242 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 243 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 244 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 245 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 246 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 247 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 248 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 249 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 250 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 251 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 252 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 253 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 254 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 255 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 256 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 257 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 258 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 259 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 260 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 261 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 262 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 263 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 264 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 265 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 266 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 267 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 268 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 269 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 270 | "created_at": "2019-06-05T20:57:56Z", 271 | "updated_at": "2019-06-15T17:48:32Z", 272 | "pushed_at": "2019-06-15T17:52:28Z", 273 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 274 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 275 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 276 | "svn_url": "https://github.com/srvaroa/jsonrouter", 277 | "homepage": null, 278 | "size": 4, 279 | "stargazers_count": 0, 280 | "watchers_count": 0, 281 | "language": "Go", 282 | "has_issues": false, 283 | "has_projects": false, 284 | "has_downloads": true, 285 | "has_wiki": false, 286 | "has_pages": false, 287 | "forks_count": 0, 288 | "mirror_url": null, 289 | "archived": false, 290 | "disabled": false, 291 | "open_issues_count": 1, 292 | "license": null, 293 | "forks": 0, 294 | "open_issues": 1, 295 | "watchers": 0, 296 | "default_branch": "master" 297 | } 298 | }, 299 | "_links": { 300 | "self": { 301 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2" 302 | }, 303 | "html": { 304 | "href": "https://github.com/srvaroa/jsonrouter/pull/2" 305 | }, 306 | "issue": { 307 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2" 308 | }, 309 | "comments": { 310 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/issues/2/comments" 311 | }, 312 | "review_comments": { 313 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/comments" 314 | }, 315 | "review_comment": { 316 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/comments{/number}" 317 | }, 318 | "commits": { 319 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/pulls/2/commits" 320 | }, 321 | "statuses": { 322 | "href": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/77b472948e9aa504c1b584c3073318b3aa58bc0b" 323 | } 324 | }, 325 | "author_association": "CONTRIBUTOR", 326 | "draft": false, 327 | "merged": false, 328 | "mergeable": null, 329 | "rebaseable": null, 330 | "mergeable_state": "unknown", 331 | "merged_by": null, 332 | "comments": 0, 333 | "review_comments": 0, 334 | "maintainer_can_modify": false, 335 | "commits": 1, 336 | "additions": 0, 337 | "deletions": 0, 338 | "changed_files": 0 339 | }, 340 | "repository": { 341 | "id": 190467543, 342 | "node_id": "MDEwOlJlcG9zaXRvcnkxOTA0Njc1NDM=", 343 | "name": "jsonrouter", 344 | "full_name": "srvaroa/jsonrouter", 345 | "private": false, 346 | "owner": { 347 | "login": "srvaroa", 348 | "id": 346110, 349 | "node_id": "MDQ6VXNlcjM0NjExMA==", 350 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 351 | "gravatar_id": "", 352 | "url": "https://api.github.com/users/srvaroa", 353 | "html_url": "https://github.com/srvaroa", 354 | "followers_url": "https://api.github.com/users/srvaroa/followers", 355 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 356 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 357 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 358 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 359 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 360 | "repos_url": "https://api.github.com/users/srvaroa/repos", 361 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 362 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 363 | "type": "User", 364 | "site_admin": false 365 | }, 366 | "html_url": "https://github.com/srvaroa/jsonrouter", 367 | "description": null, 368 | "fork": false, 369 | "url": "https://api.github.com/repos/srvaroa/jsonrouter", 370 | "forks_url": "https://api.github.com/repos/srvaroa/jsonrouter/forks", 371 | "keys_url": "https://api.github.com/repos/srvaroa/jsonrouter/keys{/key_id}", 372 | "collaborators_url": "https://api.github.com/repos/srvaroa/jsonrouter/collaborators{/collaborator}", 373 | "teams_url": "https://api.github.com/repos/srvaroa/jsonrouter/teams", 374 | "hooks_url": "https://api.github.com/repos/srvaroa/jsonrouter/hooks", 375 | "issue_events_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/events{/number}", 376 | "events_url": "https://api.github.com/repos/srvaroa/jsonrouter/events", 377 | "assignees_url": "https://api.github.com/repos/srvaroa/jsonrouter/assignees{/user}", 378 | "branches_url": "https://api.github.com/repos/srvaroa/jsonrouter/branches{/branch}", 379 | "tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/tags", 380 | "blobs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/blobs{/sha}", 381 | "git_tags_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/tags{/sha}", 382 | "git_refs_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/refs{/sha}", 383 | "trees_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/trees{/sha}", 384 | "statuses_url": "https://api.github.com/repos/srvaroa/jsonrouter/statuses/{sha}", 385 | "languages_url": "https://api.github.com/repos/srvaroa/jsonrouter/languages", 386 | "stargazers_url": "https://api.github.com/repos/srvaroa/jsonrouter/stargazers", 387 | "contributors_url": "https://api.github.com/repos/srvaroa/jsonrouter/contributors", 388 | "subscribers_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscribers", 389 | "subscription_url": "https://api.github.com/repos/srvaroa/jsonrouter/subscription", 390 | "commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/commits{/sha}", 391 | "git_commits_url": "https://api.github.com/repos/srvaroa/jsonrouter/git/commits{/sha}", 392 | "comments_url": "https://api.github.com/repos/srvaroa/jsonrouter/comments{/number}", 393 | "issue_comment_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues/comments{/number}", 394 | "contents_url": "https://api.github.com/repos/srvaroa/jsonrouter/contents/{+path}", 395 | "compare_url": "https://api.github.com/repos/srvaroa/jsonrouter/compare/{base}...{head}", 396 | "merges_url": "https://api.github.com/repos/srvaroa/jsonrouter/merges", 397 | "archive_url": "https://api.github.com/repos/srvaroa/jsonrouter/{archive_format}{/ref}", 398 | "downloads_url": "https://api.github.com/repos/srvaroa/jsonrouter/downloads", 399 | "issues_url": "https://api.github.com/repos/srvaroa/jsonrouter/issues{/number}", 400 | "pulls_url": "https://api.github.com/repos/srvaroa/jsonrouter/pulls{/number}", 401 | "milestones_url": "https://api.github.com/repos/srvaroa/jsonrouter/milestones{/number}", 402 | "notifications_url": "https://api.github.com/repos/srvaroa/jsonrouter/notifications{?since,all,participating}", 403 | "labels_url": "https://api.github.com/repos/srvaroa/jsonrouter/labels{/name}", 404 | "releases_url": "https://api.github.com/repos/srvaroa/jsonrouter/releases{/id}", 405 | "deployments_url": "https://api.github.com/repos/srvaroa/jsonrouter/deployments", 406 | "created_at": "2019-06-05T20:57:56Z", 407 | "updated_at": "2019-06-15T17:48:32Z", 408 | "pushed_at": "2019-06-15T17:52:28Z", 409 | "git_url": "git://github.com/srvaroa/jsonrouter.git", 410 | "ssh_url": "git@github.com:srvaroa/jsonrouter.git", 411 | "clone_url": "https://github.com/srvaroa/jsonrouter.git", 412 | "svn_url": "https://github.com/srvaroa/jsonrouter", 413 | "homepage": null, 414 | "size": 4, 415 | "stargazers_count": 0, 416 | "watchers_count": 0, 417 | "language": "Go", 418 | "has_issues": false, 419 | "has_projects": false, 420 | "has_downloads": true, 421 | "has_wiki": false, 422 | "has_pages": false, 423 | "forks_count": 0, 424 | "mirror_url": null, 425 | "archived": false, 426 | "disabled": false, 427 | "open_issues_count": 1, 428 | "license": null, 429 | "forks": 0, 430 | "open_issues": 1, 431 | "watchers": 0, 432 | "default_branch": "master" 433 | }, 434 | "sender": { 435 | "login": "srvaroa", 436 | "id": 346110, 437 | "node_id": "MDQ6VXNlcjM0NjExMA==", 438 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 439 | "gravatar_id": "", 440 | "url": "https://api.github.com/users/srvaroa", 441 | "html_url": "https://github.com/srvaroa", 442 | "followers_url": "https://api.github.com/users/srvaroa/followers", 443 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 444 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 445 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 446 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 447 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 448 | "repos_url": "https://api.github.com/users/srvaroa/repos", 449 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 450 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 451 | "type": "User", 452 | "site_admin": false 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /test_data/create_draft_pr_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "number": 2, 4 | "pull_request": { 5 | "url": "https://api.github.com/repos/srvaroa/test/pulls/2", 6 | "id": 1238110216, 7 | "node_id": "PR_kwDODfGd5c5JzBAI", 8 | "html_url": "https://github.com/srvaroa/test/pull/2", 9 | "diff_url": "https://github.com/srvaroa/test/pull/2.diff", 10 | "patch_url": "https://github.com/srvaroa/test/pull/2.patch", 11 | "issue_url": "https://api.github.com/repos/srvaroa/test/issues/2", 12 | "number": 2, 13 | "state": "open", 14 | "locked": false, 15 | "title": "WIP: customer model", 16 | "user": { 17 | "login": "srvaroa", 18 | "id": 346110, 19 | "node_id": "MDQ6VXNlcjM0NjExMA==", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/srvaroa", 23 | "html_url": "https://github.com/srvaroa", 24 | "followers_url": "https://api.github.com/users/srvaroa/followers", 25 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 29 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 30 | "repos_url": "https://api.github.com/users/srvaroa/repos", 31 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "body": "Signed-off-by: Galo Navarro ", 37 | "created_at": "2023-02-12T17:11:11Z", 38 | "updated_at": "2023-02-12T17:11:11Z", 39 | "closed_at": null, 40 | "merged_at": null, 41 | "merge_commit_sha": null, 42 | "assignee": null, 43 | "assignees": [ 44 | 45 | ], 46 | "requested_reviewers": [ 47 | 48 | ], 49 | "requested_teams": [ 50 | 51 | ], 52 | "labels": [ 53 | 54 | ], 55 | "milestone": null, 56 | "draft": true, 57 | "commits_url": "https://api.github.com/repos/srvaroa/test/pulls/2/commits", 58 | "review_comments_url": "https://api.github.com/repos/srvaroa/test/pulls/2/comments", 59 | "review_comment_url": "https://api.github.com/repos/srvaroa/test/pulls/comments{/number}", 60 | "comments_url": "https://api.github.com/repos/srvaroa/test/issues/2/comments", 61 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/6f1da8f1a57f7054b221f023784bca73dfc85b00", 62 | "head": { 63 | "label": "srvaroa:master-old", 64 | "ref": "master-old", 65 | "sha": "6f1da8f1a57f7054b221f023784bca73dfc85b00", 66 | "user": { 67 | "login": "srvaroa", 68 | "id": 346110, 69 | "node_id": "MDQ6VXNlcjM0NjExMA==", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 71 | "gravatar_id": "", 72 | "url": "https://api.github.com/users/srvaroa", 73 | "html_url": "https://github.com/srvaroa", 74 | "followers_url": "https://api.github.com/users/srvaroa/followers", 75 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 76 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 77 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 78 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 79 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 80 | "repos_url": "https://api.github.com/users/srvaroa/repos", 81 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 82 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 83 | "type": "User", 84 | "site_admin": false 85 | }, 86 | "repo": { 87 | "id": 233938405, 88 | "node_id": "MDEwOlJlcG9zaXRvcnkyMzM5Mzg0MDU=", 89 | "name": "test", 90 | "full_name": "srvaroa/test", 91 | "private": true, 92 | "owner": { 93 | "login": "srvaroa", 94 | "id": 346110, 95 | "node_id": "MDQ6VXNlcjM0NjExMA==", 96 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 97 | "gravatar_id": "", 98 | "url": "https://api.github.com/users/srvaroa", 99 | "html_url": "https://github.com/srvaroa", 100 | "followers_url": "https://api.github.com/users/srvaroa/followers", 101 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 102 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 103 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 104 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 105 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 106 | "repos_url": "https://api.github.com/users/srvaroa/repos", 107 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 108 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 109 | "type": "User", 110 | "site_admin": false 111 | }, 112 | "html_url": "https://github.com/srvaroa/test", 113 | "description": null, 114 | "fork": false, 115 | "url": "https://api.github.com/repos/srvaroa/test", 116 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 117 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 118 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 119 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 120 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 121 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 122 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 123 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 124 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 125 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 126 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 127 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 128 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 129 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 130 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 131 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 132 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 133 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 134 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 135 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 136 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 137 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 138 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 139 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 140 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 141 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 142 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 143 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 144 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 145 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 146 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 147 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 148 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 149 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 150 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 151 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 152 | "created_at": "2020-01-14T21:22:11Z", 153 | "updated_at": "2020-03-02T22:27:53Z", 154 | "pushed_at": "2023-02-12T17:11:11Z", 155 | "git_url": "git://github.com/srvaroa/test.git", 156 | "ssh_url": "git@github.com:srvaroa/test.git", 157 | "clone_url": "https://github.com/srvaroa/test.git", 158 | "svn_url": "https://github.com/srvaroa/test", 159 | "homepage": null, 160 | "size": 42, 161 | "stargazers_count": 0, 162 | "watchers_count": 0, 163 | "language": "Go", 164 | "has_issues": true, 165 | "has_projects": true, 166 | "has_downloads": true, 167 | "has_wiki": true, 168 | "has_pages": false, 169 | "has_discussions": false, 170 | "forks_count": 0, 171 | "mirror_url": null, 172 | "archived": false, 173 | "disabled": false, 174 | "open_issues_count": 2, 175 | "license": null, 176 | "allow_forking": true, 177 | "is_template": false, 178 | "web_commit_signoff_required": false, 179 | "topics": [ 180 | 181 | ], 182 | "visibility": "private", 183 | "forks": 0, 184 | "open_issues": 2, 185 | "watchers": 0, 186 | "default_branch": "master", 187 | "allow_squash_merge": true, 188 | "allow_merge_commit": true, 189 | "allow_rebase_merge": true, 190 | "allow_auto_merge": false, 191 | "delete_branch_on_merge": false, 192 | "allow_update_branch": false, 193 | "use_squash_pr_title_as_default": false, 194 | "squash_merge_commit_message": "COMMIT_MESSAGES", 195 | "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", 196 | "merge_commit_message": "PR_TITLE", 197 | "merge_commit_title": "MERGE_MESSAGE" 198 | } 199 | }, 200 | "base": { 201 | "label": "srvaroa:master", 202 | "ref": "master", 203 | "sha": "8486f9a4796ed6e5b9c8206bf6f12e12a6de623e", 204 | "user": { 205 | "login": "srvaroa", 206 | "id": 346110, 207 | "node_id": "MDQ6VXNlcjM0NjExMA==", 208 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 209 | "gravatar_id": "", 210 | "url": "https://api.github.com/users/srvaroa", 211 | "html_url": "https://github.com/srvaroa", 212 | "followers_url": "https://api.github.com/users/srvaroa/followers", 213 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 214 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 215 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 216 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 217 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 218 | "repos_url": "https://api.github.com/users/srvaroa/repos", 219 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 220 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 221 | "type": "User", 222 | "site_admin": false 223 | }, 224 | "repo": { 225 | "id": 233938405, 226 | "node_id": "MDEwOlJlcG9zaXRvcnkyMzM5Mzg0MDU=", 227 | "name": "test", 228 | "full_name": "srvaroa/test", 229 | "private": true, 230 | "owner": { 231 | "login": "srvaroa", 232 | "id": 346110, 233 | "node_id": "MDQ6VXNlcjM0NjExMA==", 234 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 235 | "gravatar_id": "", 236 | "url": "https://api.github.com/users/srvaroa", 237 | "html_url": "https://github.com/srvaroa", 238 | "followers_url": "https://api.github.com/users/srvaroa/followers", 239 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 240 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 241 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 242 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 243 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 244 | "repos_url": "https://api.github.com/users/srvaroa/repos", 245 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 246 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 247 | "type": "User", 248 | "site_admin": false 249 | }, 250 | "html_url": "https://github.com/srvaroa/test", 251 | "description": null, 252 | "fork": false, 253 | "url": "https://api.github.com/repos/srvaroa/test", 254 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 255 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 256 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 257 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 258 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 259 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 260 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 261 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 262 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 263 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 264 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 265 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 266 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 267 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 268 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 269 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 270 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 271 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 272 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 273 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 274 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 275 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 276 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 277 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 278 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 279 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 280 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 281 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 282 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 283 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 284 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 285 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 286 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 287 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 288 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 289 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 290 | "created_at": "2020-01-14T21:22:11Z", 291 | "updated_at": "2020-03-02T22:27:53Z", 292 | "pushed_at": "2023-02-12T17:11:11Z", 293 | "git_url": "git://github.com/srvaroa/test.git", 294 | "ssh_url": "git@github.com:srvaroa/test.git", 295 | "clone_url": "https://github.com/srvaroa/test.git", 296 | "svn_url": "https://github.com/srvaroa/test", 297 | "homepage": null, 298 | "size": 42, 299 | "stargazers_count": 0, 300 | "watchers_count": 0, 301 | "language": "Go", 302 | "has_issues": true, 303 | "has_projects": true, 304 | "has_downloads": true, 305 | "has_wiki": true, 306 | "has_pages": false, 307 | "has_discussions": false, 308 | "forks_count": 0, 309 | "mirror_url": null, 310 | "archived": false, 311 | "disabled": false, 312 | "open_issues_count": 2, 313 | "license": null, 314 | "allow_forking": true, 315 | "is_template": false, 316 | "web_commit_signoff_required": false, 317 | "topics": [ 318 | 319 | ], 320 | "visibility": "private", 321 | "forks": 0, 322 | "open_issues": 2, 323 | "watchers": 0, 324 | "default_branch": "master", 325 | "allow_squash_merge": true, 326 | "allow_merge_commit": true, 327 | "allow_rebase_merge": true, 328 | "allow_auto_merge": false, 329 | "delete_branch_on_merge": false, 330 | "allow_update_branch": false, 331 | "use_squash_pr_title_as_default": false, 332 | "squash_merge_commit_message": "COMMIT_MESSAGES", 333 | "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", 334 | "merge_commit_message": "PR_TITLE", 335 | "merge_commit_title": "MERGE_MESSAGE" 336 | } 337 | }, 338 | "_links": { 339 | "self": { 340 | "href": "https://api.github.com/repos/srvaroa/test/pulls/2" 341 | }, 342 | "html": { 343 | "href": "https://github.com/srvaroa/test/pull/2" 344 | }, 345 | "issue": { 346 | "href": "https://api.github.com/repos/srvaroa/test/issues/2" 347 | }, 348 | "comments": { 349 | "href": "https://api.github.com/repos/srvaroa/test/issues/2/comments" 350 | }, 351 | "review_comments": { 352 | "href": "https://api.github.com/repos/srvaroa/test/pulls/2/comments" 353 | }, 354 | "review_comment": { 355 | "href": "https://api.github.com/repos/srvaroa/test/pulls/comments{/number}" 356 | }, 357 | "commits": { 358 | "href": "https://api.github.com/repos/srvaroa/test/pulls/2/commits" 359 | }, 360 | "statuses": { 361 | "href": "https://api.github.com/repos/srvaroa/test/statuses/6f1da8f1a57f7054b221f023784bca73dfc85b00" 362 | } 363 | }, 364 | "author_association": "OWNER", 365 | "auto_merge": null, 366 | "active_lock_reason": null, 367 | "merged": false, 368 | "mergeable": null, 369 | "rebaseable": null, 370 | "mergeable_state": "unknown", 371 | "merged_by": null, 372 | "comments": 0, 373 | "review_comments": 0, 374 | "maintainer_can_modify": false, 375 | "commits": 1, 376 | "additions": 76, 377 | "deletions": 14, 378 | "changed_files": 2 379 | }, 380 | "repository": { 381 | "id": 233938405, 382 | "node_id": "MDEwOlJlcG9zaXRvcnkyMzM5Mzg0MDU=", 383 | "name": "test", 384 | "full_name": "srvaroa/test", 385 | "private": true, 386 | "owner": { 387 | "login": "srvaroa", 388 | "id": 346110, 389 | "node_id": "MDQ6VXNlcjM0NjExMA==", 390 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 391 | "gravatar_id": "", 392 | "url": "https://api.github.com/users/srvaroa", 393 | "html_url": "https://github.com/srvaroa", 394 | "followers_url": "https://api.github.com/users/srvaroa/followers", 395 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 396 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 397 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 398 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 399 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 400 | "repos_url": "https://api.github.com/users/srvaroa/repos", 401 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 402 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 403 | "type": "User", 404 | "site_admin": false 405 | }, 406 | "html_url": "https://github.com/srvaroa/test", 407 | "description": null, 408 | "fork": false, 409 | "url": "https://api.github.com/repos/srvaroa/test", 410 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 411 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 412 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 413 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 414 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 415 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 416 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 417 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 418 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 419 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 420 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 421 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 422 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 423 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 424 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 425 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 426 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 427 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 428 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 429 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 430 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 431 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 432 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 433 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 434 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 435 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 436 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 437 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 438 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 439 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 440 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 441 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 442 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 443 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 444 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 445 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 446 | "created_at": "2020-01-14T21:22:11Z", 447 | "updated_at": "2020-03-02T22:27:53Z", 448 | "pushed_at": "2023-02-12T17:11:11Z", 449 | "git_url": "git://github.com/srvaroa/test.git", 450 | "ssh_url": "git@github.com:srvaroa/test.git", 451 | "clone_url": "https://github.com/srvaroa/test.git", 452 | "svn_url": "https://github.com/srvaroa/test", 453 | "homepage": null, 454 | "size": 42, 455 | "stargazers_count": 0, 456 | "watchers_count": 0, 457 | "language": "Go", 458 | "has_issues": true, 459 | "has_projects": true, 460 | "has_downloads": true, 461 | "has_wiki": true, 462 | "has_pages": false, 463 | "has_discussions": false, 464 | "forks_count": 0, 465 | "mirror_url": null, 466 | "archived": false, 467 | "disabled": false, 468 | "open_issues_count": 2, 469 | "license": null, 470 | "allow_forking": true, 471 | "is_template": false, 472 | "web_commit_signoff_required": false, 473 | "topics": [ 474 | 475 | ], 476 | "visibility": "private", 477 | "forks": 0, 478 | "open_issues": 2, 479 | "watchers": 0, 480 | "default_branch": "master" 481 | }, 482 | "sender": { 483 | "login": "srvaroa", 484 | "id": 346110, 485 | "node_id": "MDQ6VXNlcjM0NjExMA==", 486 | "avatar_url": "https://avatars.githubusercontent.com/u/346110?v=4", 487 | "gravatar_id": "", 488 | "url": "https://api.github.com/users/srvaroa", 489 | "html_url": "https://github.com/srvaroa", 490 | "followers_url": "https://api.github.com/users/srvaroa/followers", 491 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 492 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 493 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 494 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 495 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 496 | "repos_url": "https://api.github.com/users/srvaroa/repos", 497 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 498 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 499 | "type": "User", 500 | "site_admin": false 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /test_data/big_pr_payload: -------------------------------------------------------------------------------- 1 | { 2 | "action": "reopened", 3 | "number": 1, 4 | "pull_request": { 5 | "_links": { 6 | "comments": { 7 | "href": "https://api.github.com/repos/srvaroa/test/issues/1/comments" 8 | }, 9 | "commits": { 10 | "href": "https://api.github.com/repos/srvaroa/test/pulls/1/commits" 11 | }, 12 | "html": { 13 | "href": "https://github.com/srvaroa/test/pull/1" 14 | }, 15 | "issue": { 16 | "href": "https://api.github.com/repos/srvaroa/test/issues/1" 17 | }, 18 | "review_comment": { 19 | "href": "https://api.github.com/repos/srvaroa/test/pulls/comments{/number}" 20 | }, 21 | "review_comments": { 22 | "href": "https://api.github.com/repos/srvaroa/test/pulls/1/comments" 23 | }, 24 | "self": { 25 | "href": "https://api.github.com/repos/srvaroa/test/pulls/1" 26 | }, 27 | "statuses": { 28 | "href": "https://api.github.com/repos/srvaroa/test/statuses/9e70f2ce79dad397cd51cf68df06bb8e81f1dcae" 29 | } 30 | }, 31 | "additions": 3, 32 | "assignee": { 33 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 34 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 35 | "followers_url": "https://api.github.com/users/srvaroa/followers", 36 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 37 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 38 | "gravatar_id": "", 39 | "html_url": "https://github.com/srvaroa", 40 | "id": 346110, 41 | "login": "srvaroa", 42 | "node_id": "MDQ6VXNlcjM0NjExMA==", 43 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 44 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 45 | "repos_url": "https://api.github.com/users/srvaroa/repos", 46 | "site_admin": false, 47 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 48 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 49 | "type": "User", 50 | "url": "https://api.github.com/users/srvaroa" 51 | }, 52 | "assignees": [ 53 | { 54 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 55 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 56 | "followers_url": "https://api.github.com/users/srvaroa/followers", 57 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 58 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 59 | "gravatar_id": "", 60 | "html_url": "https://github.com/srvaroa", 61 | "id": 346110, 62 | "login": "srvaroa", 63 | "node_id": "MDQ6VXNlcjM0NjExMA==", 64 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 65 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 66 | "repos_url": "https://api.github.com/users/srvaroa/repos", 67 | "site_admin": false, 68 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 69 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 70 | "type": "User", 71 | "url": "https://api.github.com/users/srvaroa" 72 | } 73 | ], 74 | "author_association": "OWNER", 75 | "base": { 76 | "label": "srvaroa:master", 77 | "ref": "master", 78 | "repo": { 79 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 80 | "archived": false, 81 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 82 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 83 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 84 | "clone_url": "https://github.com/srvaroa/test.git", 85 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 86 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 87 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 88 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 89 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 90 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 91 | "created_at": "2019-08-21T22:50:07Z", 92 | "default_branch": "master", 93 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 94 | "description": null, 95 | "disabled": false, 96 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 97 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 98 | "fork": false, 99 | "forks": 0, 100 | "forks_count": 0, 101 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 102 | "full_name": "srvaroa/test", 103 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 104 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 105 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 106 | "git_url": "git://github.com/srvaroa/test.git", 107 | "has_downloads": true, 108 | "has_issues": true, 109 | "has_pages": false, 110 | "has_projects": true, 111 | "has_wiki": true, 112 | "homepage": null, 113 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 114 | "html_url": "https://github.com/srvaroa/test", 115 | "id": 203675431, 116 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 117 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 118 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 119 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 120 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 121 | "language": null, 122 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 123 | "license": null, 124 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 125 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 126 | "mirror_url": null, 127 | "name": "test", 128 | "node_id": "MDEwOlJlcG9zaXRvcnkyMDM2NzU0MzE=", 129 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 130 | "open_issues": 1, 131 | "open_issues_count": 1, 132 | "owner": { 133 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 134 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 135 | "followers_url": "https://api.github.com/users/srvaroa/followers", 136 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 137 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 138 | "gravatar_id": "", 139 | "html_url": "https://github.com/srvaroa", 140 | "id": 346110, 141 | "login": "srvaroa", 142 | "node_id": "MDQ6VXNlcjM0NjExMA==", 143 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 144 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 145 | "repos_url": "https://api.github.com/users/srvaroa/repos", 146 | "site_admin": false, 147 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 148 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 149 | "type": "User", 150 | "url": "https://api.github.com/users/srvaroa" 151 | }, 152 | "private": true, 153 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 154 | "pushed_at": "2019-08-22T22:08:28Z", 155 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 156 | "size": 3, 157 | "ssh_url": "git@github.com:srvaroa/test.git", 158 | "stargazers_count": 0, 159 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 160 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 161 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 162 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 163 | "svn_url": "https://github.com/srvaroa/test", 164 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 165 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 166 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 167 | "updated_at": "2019-08-22T22:05:35Z", 168 | "url": "https://api.github.com/repos/srvaroa/test", 169 | "watchers": 0, 170 | "watchers_count": 0 171 | }, 172 | "sha": "e3a8471a3505efc9fbe75a81759812f7e1daf449", 173 | "user": { 174 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 175 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 176 | "followers_url": "https://api.github.com/users/srvaroa/followers", 177 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 178 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 179 | "gravatar_id": "", 180 | "html_url": "https://github.com/srvaroa", 181 | "id": 346110, 182 | "login": "srvaroa", 183 | "node_id": "MDQ6VXNlcjM0NjExMA==", 184 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 185 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 186 | "repos_url": "https://api.github.com/users/srvaroa/repos", 187 | "site_admin": false, 188 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 189 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 190 | "type": "User", 191 | "url": "https://api.github.com/users/srvaroa" 192 | } 193 | }, 194 | "body": "", 195 | "changed_files": 2, 196 | "closed_at": null, 197 | "comments": 0, 198 | "comments_url": "https://api.github.com/repos/srvaroa/test/issues/1/comments", 199 | "commits": 3, 200 | "commits_url": "https://api.github.com/repos/srvaroa/test/pulls/1/commits", 201 | "created_at": "2019-08-21T22:56:32Z", 202 | "deletions": 100, 203 | "diff_url": "https://github.com/srvaroa/test/pull/1.diff", 204 | "draft": false, 205 | "head": { 206 | "label": "srvaroa:srvaroa-patch-1", 207 | "ref": "srvaroa-patch-1", 208 | "repo": { 209 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 210 | "archived": false, 211 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 212 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 213 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 214 | "clone_url": "https://github.com/srvaroa/test.git", 215 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 216 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 217 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 218 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 219 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 220 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 221 | "created_at": "2019-08-21T22:50:07Z", 222 | "default_branch": "master", 223 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 224 | "description": null, 225 | "disabled": false, 226 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 227 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 228 | "fork": false, 229 | "forks": 0, 230 | "forks_count": 0, 231 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 232 | "full_name": "srvaroa/test", 233 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 234 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 235 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 236 | "git_url": "git://github.com/srvaroa/test.git", 237 | "has_downloads": true, 238 | "has_issues": true, 239 | "has_pages": false, 240 | "has_projects": true, 241 | "has_wiki": true, 242 | "homepage": null, 243 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 244 | "html_url": "https://github.com/srvaroa/test", 245 | "id": 203675431, 246 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 247 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 248 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 249 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 250 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 251 | "language": null, 252 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 253 | "license": null, 254 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 255 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 256 | "mirror_url": null, 257 | "name": "test", 258 | "node_id": "MDEwOlJlcG9zaXRvcnkyMDM2NzU0MzE=", 259 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 260 | "open_issues": 1, 261 | "open_issues_count": 1, 262 | "owner": { 263 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 264 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 265 | "followers_url": "https://api.github.com/users/srvaroa/followers", 266 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 267 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 268 | "gravatar_id": "", 269 | "html_url": "https://github.com/srvaroa", 270 | "id": 346110, 271 | "login": "srvaroa", 272 | "node_id": "MDQ6VXNlcjM0NjExMA==", 273 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 274 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 275 | "repos_url": "https://api.github.com/users/srvaroa/repos", 276 | "site_admin": false, 277 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 278 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 279 | "type": "User", 280 | "url": "https://api.github.com/users/srvaroa" 281 | }, 282 | "private": true, 283 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 284 | "pushed_at": "2019-08-22T22:08:28Z", 285 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 286 | "size": 3, 287 | "ssh_url": "git@github.com:srvaroa/test.git", 288 | "stargazers_count": 0, 289 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 290 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 291 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 292 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 293 | "svn_url": "https://github.com/srvaroa/test", 294 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 295 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 296 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 297 | "updated_at": "2019-08-22T22:05:35Z", 298 | "url": "https://api.github.com/repos/srvaroa/test", 299 | "watchers": 0, 300 | "watchers_count": 0 301 | }, 302 | "sha": "9e70f2ce79dad397cd51cf68df06bb8e81f1dcae", 303 | "user": { 304 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 305 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 306 | "followers_url": "https://api.github.com/users/srvaroa/followers", 307 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 308 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 309 | "gravatar_id": "", 310 | "html_url": "https://github.com/srvaroa", 311 | "id": 346110, 312 | "login": "srvaroa", 313 | "node_id": "MDQ6VXNlcjM0NjExMA==", 314 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 315 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 316 | "repos_url": "https://api.github.com/users/srvaroa/repos", 317 | "site_admin": false, 318 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 319 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 320 | "type": "User", 321 | "url": "https://api.github.com/users/srvaroa" 322 | } 323 | }, 324 | "html_url": "https://github.com/srvaroa/test/pull/1", 325 | "id": 309718413, 326 | "issue_url": "https://api.github.com/repos/srvaroa/test/issues/1", 327 | "labels": [ 328 | { 329 | "color": "d73a4a", 330 | "default": true, 331 | "id": 1512553797, 332 | "name": "bug", 333 | "node_id": "MDU6TGFiZWwxNTEyNTUzNzk3", 334 | "url": "https://api.github.com/repos/srvaroa/test/labels/bug" 335 | } 336 | ], 337 | "locked": false, 338 | "maintainer_can_modify": false, 339 | "merge_commit_sha": "21f10b93703d510906e2f36c73a93dad3d63c7cf", 340 | "mergeable": null, 341 | "mergeable_state": "unknown", 342 | "merged": false, 343 | "merged_at": null, 344 | "merged_by": null, 345 | "milestone": null, 346 | "node_id": "MDExOlB1bGxSZXF1ZXN0MzA5NzE4NDEz", 347 | "number": 1, 348 | "patch_url": "https://github.com/srvaroa/test/pull/1.patch", 349 | "rebaseable": null, 350 | "requested_reviewers": [], 351 | "requested_teams": [], 352 | "review_comment_url": "https://api.github.com/repos/srvaroa/test/pulls/comments{/number}", 353 | "review_comments": 0, 354 | "review_comments_url": "https://api.github.com/repos/srvaroa/test/pulls/1/comments", 355 | "state": "open", 356 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/9e70f2ce79dad397cd51cf68df06bb8e81f1dcae", 357 | "title": "WIP: Update README", 358 | "updated_at": "2019-08-23T18:55:36Z", 359 | "url": "https://api.github.com/repos/srvaroa/test/pulls/1", 360 | "user": { 361 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 362 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 363 | "followers_url": "https://api.github.com/users/srvaroa/followers", 364 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 365 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 366 | "gravatar_id": "", 367 | "html_url": "https://github.com/srvaroa", 368 | "id": 346110, 369 | "login": "srvaroa", 370 | "node_id": "MDQ6VXNlcjM0NjExMA==", 371 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 372 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 373 | "repos_url": "https://api.github.com/users/srvaroa/repos", 374 | "site_admin": false, 375 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 376 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 377 | "type": "User", 378 | "url": "https://api.github.com/users/srvaroa" 379 | } 380 | }, 381 | "repository": { 382 | "archive_url": "https://api.github.com/repos/srvaroa/test/{archive_format}{/ref}", 383 | "archived": false, 384 | "assignees_url": "https://api.github.com/repos/srvaroa/test/assignees{/user}", 385 | "blobs_url": "https://api.github.com/repos/srvaroa/test/git/blobs{/sha}", 386 | "branches_url": "https://api.github.com/repos/srvaroa/test/branches{/branch}", 387 | "clone_url": "https://github.com/srvaroa/test.git", 388 | "collaborators_url": "https://api.github.com/repos/srvaroa/test/collaborators{/collaborator}", 389 | "comments_url": "https://api.github.com/repos/srvaroa/test/comments{/number}", 390 | "commits_url": "https://api.github.com/repos/srvaroa/test/commits{/sha}", 391 | "compare_url": "https://api.github.com/repos/srvaroa/test/compare/{base}...{head}", 392 | "contents_url": "https://api.github.com/repos/srvaroa/test/contents/{+path}", 393 | "contributors_url": "https://api.github.com/repos/srvaroa/test/contributors", 394 | "created_at": "2019-08-21T22:50:07Z", 395 | "default_branch": "master", 396 | "deployments_url": "https://api.github.com/repos/srvaroa/test/deployments", 397 | "description": null, 398 | "disabled": false, 399 | "downloads_url": "https://api.github.com/repos/srvaroa/test/downloads", 400 | "events_url": "https://api.github.com/repos/srvaroa/test/events", 401 | "fork": false, 402 | "forks": 0, 403 | "forks_count": 0, 404 | "forks_url": "https://api.github.com/repos/srvaroa/test/forks", 405 | "full_name": "srvaroa/test", 406 | "git_commits_url": "https://api.github.com/repos/srvaroa/test/git/commits{/sha}", 407 | "git_refs_url": "https://api.github.com/repos/srvaroa/test/git/refs{/sha}", 408 | "git_tags_url": "https://api.github.com/repos/srvaroa/test/git/tags{/sha}", 409 | "git_url": "git://github.com/srvaroa/test.git", 410 | "has_downloads": true, 411 | "has_issues": true, 412 | "has_pages": false, 413 | "has_projects": true, 414 | "has_wiki": true, 415 | "homepage": null, 416 | "hooks_url": "https://api.github.com/repos/srvaroa/test/hooks", 417 | "html_url": "https://github.com/srvaroa/test", 418 | "id": 203675431, 419 | "issue_comment_url": "https://api.github.com/repos/srvaroa/test/issues/comments{/number}", 420 | "issue_events_url": "https://api.github.com/repos/srvaroa/test/issues/events{/number}", 421 | "issues_url": "https://api.github.com/repos/srvaroa/test/issues{/number}", 422 | "keys_url": "https://api.github.com/repos/srvaroa/test/keys{/key_id}", 423 | "labels_url": "https://api.github.com/repos/srvaroa/test/labels{/name}", 424 | "language": null, 425 | "languages_url": "https://api.github.com/repos/srvaroa/test/languages", 426 | "license": null, 427 | "merges_url": "https://api.github.com/repos/srvaroa/test/merges", 428 | "milestones_url": "https://api.github.com/repos/srvaroa/test/milestones{/number}", 429 | "mirror_url": null, 430 | "name": "test", 431 | "node_id": "MDEwOlJlcG9zaXRvcnkyMDM2NzU0MzE=", 432 | "notifications_url": "https://api.github.com/repos/srvaroa/test/notifications{?since,all,participating}", 433 | "open_issues": 1, 434 | "open_issues_count": 1, 435 | "owner": { 436 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 437 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 438 | "followers_url": "https://api.github.com/users/srvaroa/followers", 439 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 440 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 441 | "gravatar_id": "", 442 | "html_url": "https://github.com/srvaroa", 443 | "id": 346110, 444 | "login": "srvaroa", 445 | "node_id": "MDQ6VXNlcjM0NjExMA==", 446 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 447 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 448 | "repos_url": "https://api.github.com/users/srvaroa/repos", 449 | "site_admin": false, 450 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 451 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 452 | "type": "User", 453 | "url": "https://api.github.com/users/srvaroa" 454 | }, 455 | "private": true, 456 | "pulls_url": "https://api.github.com/repos/srvaroa/test/pulls{/number}", 457 | "pushed_at": "2019-08-22T22:08:28Z", 458 | "releases_url": "https://api.github.com/repos/srvaroa/test/releases{/id}", 459 | "size": 3, 460 | "ssh_url": "git@github.com:srvaroa/test.git", 461 | "stargazers_count": 0, 462 | "stargazers_url": "https://api.github.com/repos/srvaroa/test/stargazers", 463 | "statuses_url": "https://api.github.com/repos/srvaroa/test/statuses/{sha}", 464 | "subscribers_url": "https://api.github.com/repos/srvaroa/test/subscribers", 465 | "subscription_url": "https://api.github.com/repos/srvaroa/test/subscription", 466 | "svn_url": "https://github.com/srvaroa/test", 467 | "tags_url": "https://api.github.com/repos/srvaroa/test/tags", 468 | "teams_url": "https://api.github.com/repos/srvaroa/test/teams", 469 | "trees_url": "https://api.github.com/repos/srvaroa/test/git/trees{/sha}", 470 | "updated_at": "2019-08-22T22:05:35Z", 471 | "url": "https://api.github.com/repos/srvaroa/test", 472 | "watchers": 0, 473 | "watchers_count": 0 474 | }, 475 | "sender": { 476 | "avatar_url": "https://avatars2.githubusercontent.com/u/346110?v=4", 477 | "events_url": "https://api.github.com/users/srvaroa/events{/privacy}", 478 | "followers_url": "https://api.github.com/users/srvaroa/followers", 479 | "following_url": "https://api.github.com/users/srvaroa/following{/other_user}", 480 | "gists_url": "https://api.github.com/users/srvaroa/gists{/gist_id}", 481 | "gravatar_id": "", 482 | "html_url": "https://github.com/srvaroa", 483 | "id": 346110, 484 | "login": "srvaroa", 485 | "node_id": "MDQ6VXNlcjM0NjExMA==", 486 | "organizations_url": "https://api.github.com/users/srvaroa/orgs", 487 | "received_events_url": "https://api.github.com/users/srvaroa/received_events", 488 | "repos_url": "https://api.github.com/users/srvaroa/repos", 489 | "site_admin": false, 490 | "starred_url": "https://api.github.com/users/srvaroa/starred{/owner}{/repo}", 491 | "subscriptions_url": "https://api.github.com/users/srvaroa/subscriptions", 492 | "type": "User", 493 | "url": "https://api.github.com/users/srvaroa" 494 | } 495 | } 496 | --------------------------------------------------------------------------------