Fix panic on webhook for user-owned repository (#344)
* Fix panic on webhook for user-owned repository Follow-up for #282 and #334
This commit is contained in:
		
							parent
							
								
									2d7fbbfb68
								
							
						
					
					
						commit
						991535e567
					
				|  | @ -24,6 +24,7 @@ import ( | |||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"net/http" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/reconcile" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/go-logr/logr" | ||||
|  | @ -133,22 +134,25 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons | |||
| 	case *gogithub.PushEvent: | ||||
| 		target, err = autoscaler.getScaleUpTarget( | ||||
| 			context.TODO(), | ||||
| 			*e.Repo.Name, | ||||
| 			*e.Repo.Organization, | ||||
| 			e.Repo.GetName(), | ||||
| 			e.Repo.Owner.GetLogin(), | ||||
| 			e.Repo.Owner.GetType(), | ||||
| 			autoscaler.MatchPushEvent(e), | ||||
| 		) | ||||
| 	case *gogithub.PullRequestEvent: | ||||
| 		target, err = autoscaler.getScaleUpTarget( | ||||
| 			context.TODO(), | ||||
| 			*e.Repo.Name, | ||||
| 			*e.Repo.Organization.Name, | ||||
| 			e.Repo.GetName(), | ||||
| 			e.Repo.Owner.GetLogin(), | ||||
| 			e.Repo.Owner.GetType(), | ||||
| 			autoscaler.MatchPullRequestEvent(e), | ||||
| 		) | ||||
| 	case *gogithub.CheckRunEvent: | ||||
| 		target, err = autoscaler.getScaleUpTarget( | ||||
| 			context.TODO(), | ||||
| 			e.GetRepo().GetName(), | ||||
| 			e.GetOrg().GetLogin(), // empty string if the repo is not in an organization
 | ||||
| 			e.Repo.GetName(), | ||||
| 			e.Repo.Owner.GetLogin(), | ||||
| 			e.Repo.Owner.GetType(), | ||||
| 			autoscaler.MatchCheckRunEvent(e), | ||||
| 		) | ||||
| 	case *gogithub.PingEvent: | ||||
|  | @ -227,6 +231,10 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) findHRAsByKey(ctx con | |||
| 		opts := append([]client.ListOption{}, defaultListOpts...) | ||||
| 		opts = append(opts, client.MatchingFields{scaleTargetKey: value}) | ||||
| 
 | ||||
| 		if autoscaler.WatchNamespace != "" { | ||||
| 			opts = append(opts, client.InNamespace(autoscaler.WatchNamespace)) | ||||
| 		} | ||||
| 
 | ||||
| 		var hraList v1alpha1.HorizontalRunnerAutoscalerList | ||||
| 
 | ||||
| 		if err := autoscaler.List(ctx, &hraList, opts...); err != nil { | ||||
|  | @ -296,27 +304,45 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleTarget(ctx co | |||
| 	targets := autoscaler.searchScaleTargets(hras, f) | ||||
| 
 | ||||
| 	if len(targets) != 1 { | ||||
| 		var scaleTargetIDs []string | ||||
| 
 | ||||
| 		for _, t := range targets { | ||||
| 			scaleTargetIDs = append(scaleTargetIDs, t.HorizontalRunnerAutoscaler.Name) | ||||
| 		} | ||||
| 
 | ||||
| 		autoscaler.Log.Info( | ||||
| 			"Found too many scale targets: "+ | ||||
| 				"It must be exactly one to avoid ambiguity. "+ | ||||
| 				"Either set WatchNamespace for the webhook-based autoscaler to let it only find HRAs in the namespace, "+ | ||||
| 				"or update Repository or Organization fields in your RunnerDeployment resources to fix the ambiguity.", | ||||
| 			"scaleTargets", strings.Join(scaleTargetIDs, ",")) | ||||
| 
 | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return &targets[0], nil | ||||
| } | ||||
| 
 | ||||
| func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, repoNameFromWebhook, orgNameFromWebhook string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) { | ||||
| 	repositoryRunnerKey := orgNameFromWebhook + "/" + repoNameFromWebhook | ||||
| func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, repo, owner, ownerType string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) { | ||||
| 	repositoryRunnerKey := owner + "/" + repo | ||||
| 
 | ||||
| 	autoscaler.Log.Info("finding repository-wide runner", "repository", repositoryRunnerKey) | ||||
| 	if target, err := autoscaler.getScaleTarget(ctx, repositoryRunnerKey, f); err != nil { | ||||
| 		return nil, err | ||||
| 	} else if target != nil { | ||||
| 		autoscaler.Log.Info("scale up target is repository-wide runners", "repository", repoNameFromWebhook) | ||||
| 		autoscaler.Log.Info("scale up target is repository-wide runners", "repository", repo) | ||||
| 		return target, nil | ||||
| 	} | ||||
| 
 | ||||
| 	autoscaler.Log.Info("finding organizational runner", "organization", orgNameFromWebhook) | ||||
| 	if target, err := autoscaler.getScaleTarget(ctx, orgNameFromWebhook, f); err != nil { | ||||
| 	if ownerType == "User" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	autoscaler.Log.Info("finding organizational runner", "organization", owner) | ||||
| 	if target, err := autoscaler.getScaleTarget(ctx, owner, f); err != nil { | ||||
| 		return nil, err | ||||
| 	} else if target != nil { | ||||
| 		autoscaler.Log.Info("scale up target is organizational runners", "organization", orgNameFromWebhook) | ||||
| 		autoscaler.Log.Info("scale up target is organizational runners", "organization", owner) | ||||
| 		return target, nil | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,8 +28,26 @@ func init() { | |||
| 	_ = actionsv1alpha1.AddToScheme(sc) | ||||
| } | ||||
| 
 | ||||
| func TestWebhookCheckRun(t *testing.T) { | ||||
| 	f, err := os.Open("testdata/webhook_check_run_payload.json") | ||||
| func TestOrgWebhookCheckRun(t *testing.T) { | ||||
| 	f, err := os.Open("testdata/org_webhook_check_run_payload.json") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not open the fixture: %s", err) | ||||
| 	} | ||||
| 	defer f.Close() | ||||
| 	var e github.CheckRunEvent | ||||
| 	if err := json.NewDecoder(f).Decode(&e); err != nil { | ||||
| 		t.Fatalf("invalid json: %s", err) | ||||
| 	} | ||||
| 	testServer(t, | ||||
| 		"check_run", | ||||
| 		&e, | ||||
| 		200, | ||||
| 		"no horizontalrunnerautoscaler to scale for this github event", | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func TestRepoWebhookCheckRun(t *testing.T) { | ||||
| 	f, err := os.Open("testdata/repo_webhook_check_run_payload.json") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not open the fixture: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -136,6 +136,7 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment { | |||
| 			Log:            logf.Log, | ||||
| 			Recorder:       mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"), | ||||
| 			Name:           controllerName("horizontalrunnerautoscalergithubwebhook"), | ||||
| 			WatchNamespace: ns.Name, | ||||
| 		} | ||||
| 		err = autoscalerWebhook.SetupWithManager(mgr) | ||||
| 		Expect(err).NotTo(HaveOccurred(), "failed to setup autoscaler webhook") | ||||
|  | @ -173,7 +174,7 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() { | |||
| 
 | ||||
| 	Describe("when no existing resources exist", func() { | ||||
| 
 | ||||
| 		It("should create and scale runners on pull_request event", func() { | ||||
| 		It("should create and scale organization's repository runners on pull_request event", func() { | ||||
| 			name := "example-runnerdeploy" | ||||
| 
 | ||||
| 			{ | ||||
|  | @ -296,19 +297,19 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() { | |||
| 
 | ||||
| 			// Scale-up to 2 replicas on first pull_request create webhook event
 | ||||
| 			{ | ||||
| 				env.SendPullRequestEvent("test", "valid", "main", "created") | ||||
| 				env.SendOrgPullRequestEvent("test", "valid", "main", "created") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2, "runners after first webhook event") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 3 replicas on second pull_request create webhook event
 | ||||
| 			{ | ||||
| 				env.SendPullRequestEvent("test", "valid", "main", "created") | ||||
| 				env.SendOrgPullRequestEvent("test", "valid", "main", "created") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3, "runners after second webhook event") | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		It("should create and scale runners on check_run event", func() { | ||||
| 		It("should create and scale organization's repository runners on check_run event", func() { | ||||
| 			name := "example-runnerdeploy" | ||||
| 
 | ||||
| 			{ | ||||
|  | @ -385,7 +386,7 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() { | |||
| 
 | ||||
| 			// Scale-up to 4 replicas on first check_run create webhook event
 | ||||
| 			{ | ||||
| 				env.SendCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				env.SendOrgCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 4, "runners after first webhook event") | ||||
| 			} | ||||
|  | @ -396,12 +397,243 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() { | |||
| 
 | ||||
| 			// Scale-up to 5 replicas on second check_run create webhook event
 | ||||
| 			{ | ||||
| 				env.SendCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				env.SendOrgCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 5, "runners after second webhook event") | ||||
| 			} | ||||
| 
 | ||||
| 			env.ExpectRegisteredNumberCountEventuallyEquals(5, "count of fake list runners") | ||||
| 		}) | ||||
| 
 | ||||
| 		It("should create and scale user's repository runners on pull_request event", func() { | ||||
| 			name := "example-runnerdeploy" | ||||
| 
 | ||||
| 			{ | ||||
| 				rd := &actionsv1alpha1.RunnerDeployment{ | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      name, | ||||
| 						Namespace: ns.Name, | ||||
| 					}, | ||||
| 					Spec: actionsv1alpha1.RunnerDeploymentSpec{ | ||||
| 						Replicas: intPtr(1), | ||||
| 						Template: actionsv1alpha1.RunnerTemplate{ | ||||
| 							Spec: actionsv1alpha1.RunnerSpec{ | ||||
| 								Repository: "test/valid", | ||||
| 								Image:      "bar", | ||||
| 								Group:      "baz", | ||||
| 								Env: []corev1.EnvVar{ | ||||
| 									{Name: "FOO", Value: "FOOVALUE"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				} | ||||
| 
 | ||||
| 				ExpectCreate(ctx, rd, "test RunnerDeployment") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 			} | ||||
| 
 | ||||
| 			{ | ||||
| 				ExpectRunnerDeploymentEventuallyUpdates(ctx, ns.Name, name, func(rd *actionsv1alpha1.RunnerDeployment) { | ||||
| 					rd.Spec.Replicas = intPtr(2) | ||||
| 				}) | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2) | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 3 replicas
 | ||||
| 			{ | ||||
| 				hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{ | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      name, | ||||
| 						Namespace: ns.Name, | ||||
| 					}, | ||||
| 					Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{ | ||||
| 						ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{ | ||||
| 							Name: name, | ||||
| 						}, | ||||
| 						MinReplicas:                       intPtr(1), | ||||
| 						MaxReplicas:                       intPtr(3), | ||||
| 						ScaleDownDelaySecondsAfterScaleUp: intPtr(1), | ||||
| 						Metrics:                           nil, | ||||
| 						ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{ | ||||
| 							{ | ||||
| 								GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{ | ||||
| 									PullRequest: &actionsv1alpha1.PullRequestSpec{ | ||||
| 										Types:    []string{"created"}, | ||||
| 										Branches: []string{"main"}, | ||||
| 									}, | ||||
| 								}, | ||||
| 								Amount:   1, | ||||
| 								Duration: metav1.Duration{Duration: time.Minute}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				} | ||||
| 
 | ||||
| 				ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler") | ||||
| 
 | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3) | ||||
| 			} | ||||
| 
 | ||||
| 			{ | ||||
| 				var runnerList actionsv1alpha1.RunnerList | ||||
| 
 | ||||
| 				err := k8sClient.List(ctx, &runnerList, client.InNamespace(ns.Name)) | ||||
| 				if err != nil { | ||||
| 					logf.Log.Error(err, "list runners") | ||||
| 				} | ||||
| 
 | ||||
| 				for i, r := range runnerList.Items { | ||||
| 					env.fakeRunnerList.Add(&github3.Runner{ | ||||
| 						ID:     github.Int64(int64(i)), | ||||
| 						Name:   github.String(r.Name), | ||||
| 						OS:     github.String("linux"), | ||||
| 						Status: github.String("online"), | ||||
| 						Busy:   github.Bool(false), | ||||
| 					}) | ||||
| 				} | ||||
| 
 | ||||
| 				rs, err := env.ghClient.ListRunners(context.Background(), "", "", "test/valid") | ||||
| 				Expect(err).NotTo(HaveOccurred(), "verifying list fake runners response") | ||||
| 				Expect(len(rs)).To(Equal(3), "count of fake list runners") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-down to 1 replica
 | ||||
| 			{ | ||||
| 				time.Sleep(time.Second) | ||||
| 
 | ||||
| 				env.Responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas | ||||
| 				env.Responses.ListRepositoryWorkflowRuns.Statuses["queued"] = workflowRunsFor1Replicas_queued | ||||
| 				env.Responses.ListRepositoryWorkflowRuns.Statuses["in_progress"] = workflowRunsFor1Replicas_in_progress | ||||
| 
 | ||||
| 				var hra actionsv1alpha1.HorizontalRunnerAutoscaler | ||||
| 
 | ||||
| 				err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: name}, &hra) | ||||
| 
 | ||||
| 				Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource") | ||||
| 
 | ||||
| 				hra.Annotations = map[string]string{ | ||||
| 					"force-update": "1", | ||||
| 				} | ||||
| 
 | ||||
| 				err = k8sClient.Update(ctx, &hra) | ||||
| 
 | ||||
| 				Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource") | ||||
| 
 | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1, "runners after HRA force update for scale-down") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 2 replicas on first pull_request create webhook event
 | ||||
| 			{ | ||||
| 				env.SendUserPullRequestEvent("test", "valid", "main", "created") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2, "runners after first webhook event") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 3 replicas on second pull_request create webhook event
 | ||||
| 			{ | ||||
| 				env.SendUserPullRequestEvent("test", "valid", "main", "created") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3, "runners after second webhook event") | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		It("should create and scale user's repository runners on check_run event", func() { | ||||
| 			name := "example-runnerdeploy" | ||||
| 
 | ||||
| 			{ | ||||
| 				rd := &actionsv1alpha1.RunnerDeployment{ | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      name, | ||||
| 						Namespace: ns.Name, | ||||
| 					}, | ||||
| 					Spec: actionsv1alpha1.RunnerDeploymentSpec{ | ||||
| 						Replicas: intPtr(1), | ||||
| 						Template: actionsv1alpha1.RunnerTemplate{ | ||||
| 							Spec: actionsv1alpha1.RunnerSpec{ | ||||
| 								Repository: "test/valid", | ||||
| 								Image:      "bar", | ||||
| 								Group:      "baz", | ||||
| 								Env: []corev1.EnvVar{ | ||||
| 									{Name: "FOO", Value: "FOOVALUE"}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				} | ||||
| 
 | ||||
| 				ExpectCreate(ctx, rd, "test RunnerDeployment") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 			} | ||||
| 
 | ||||
| 			{ | ||||
| 				env.ExpectRegisteredNumberCountEventuallyEquals(1, "count of fake list runners") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 3 replicas by the default TotalNumberOfQueuedAndInProgressWorkflowRuns-based scaling
 | ||||
| 			// See workflowRunsFor3Replicas_queued and workflowRunsFor3Replicas_in_progress for GitHub List-Runners API responses
 | ||||
| 			// used while testing.
 | ||||
| 			{ | ||||
| 				hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{ | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      name, | ||||
| 						Namespace: ns.Name, | ||||
| 					}, | ||||
| 					Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{ | ||||
| 						ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{ | ||||
| 							Name: name, | ||||
| 						}, | ||||
| 						MinReplicas:                       intPtr(1), | ||||
| 						MaxReplicas:                       intPtr(5), | ||||
| 						ScaleDownDelaySecondsAfterScaleUp: intPtr(1), | ||||
| 						Metrics:                           nil, | ||||
| 						ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{ | ||||
| 							{ | ||||
| 								GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{ | ||||
| 									CheckRun: &actionsv1alpha1.CheckRunSpec{ | ||||
| 										Types:  []string{"created"}, | ||||
| 										Status: "pending", | ||||
| 									}, | ||||
| 								}, | ||||
| 								Amount:   1, | ||||
| 								Duration: metav1.Duration{Duration: time.Minute}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				} | ||||
| 
 | ||||
| 				ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler") | ||||
| 
 | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1) | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3) | ||||
| 			} | ||||
| 
 | ||||
| 			{ | ||||
| 				env.ExpectRegisteredNumberCountEventuallyEquals(3, "count of fake list runners") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 4 replicas on first check_run create webhook event
 | ||||
| 			{ | ||||
| 				env.SendUserCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 4, "runners after first webhook event") | ||||
| 			} | ||||
| 
 | ||||
| 			{ | ||||
| 				env.ExpectRegisteredNumberCountEventuallyEquals(4, "count of fake list runners") | ||||
| 			} | ||||
| 
 | ||||
| 			// Scale-up to 5 replicas on second check_run create webhook event
 | ||||
| 			{ | ||||
| 				env.SendUserCheckRunEvent("test", "valid", "pending", "created") | ||||
| 				ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 5, "runners after second webhook event") | ||||
| 			} | ||||
| 
 | ||||
| 			env.ExpectRegisteredNumberCountEventuallyEquals(5, "count of fake list runners") | ||||
| 		}) | ||||
| 
 | ||||
| 	}) | ||||
| }) | ||||
| 
 | ||||
|  | @ -416,10 +648,10 @@ func (env *testEnvironment) ExpectRegisteredNumberCountEventuallyEquals(want int | |||
| 
 | ||||
| 			return len(rs) | ||||
| 		}, | ||||
| 		time.Second*1, time.Millisecond*500).Should(Equal(want), optionalDescriptions...) | ||||
| 		time.Second*5, time.Millisecond*500).Should(Equal(want), optionalDescriptions...) | ||||
| } | ||||
| 
 | ||||
| func (env *testEnvironment) SendPullRequestEvent(org, repo, branch, action string) { | ||||
| func (env *testEnvironment) SendOrgPullRequestEvent(org, repo, branch, action string) { | ||||
| 	resp, err := sendWebhook(env.webhookServer, "pull_request", &github.PullRequestEvent{ | ||||
| 		PullRequest: &github.PullRequest{ | ||||
| 			Base: &github.PullRequestBranch{ | ||||
|  | @ -428,8 +660,9 @@ func (env *testEnvironment) SendPullRequestEvent(org, repo, branch, action strin | |||
| 		}, | ||||
| 		Repo: &github.Repository{ | ||||
| 			Name: github.String(repo), | ||||
| 			Organization: &github.Organization{ | ||||
| 				Name: github.String(org), | ||||
| 			Owner: &github.User{ | ||||
| 				Login: github.String(org), | ||||
| 				Type:  github.String("Organization"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: github.String(action), | ||||
|  | @ -440,7 +673,7 @@ func (env *testEnvironment) SendPullRequestEvent(org, repo, branch, action strin | |||
| 	ExpectWithOffset(1, resp.StatusCode).To(Equal(200)) | ||||
| } | ||||
| 
 | ||||
| func (env *testEnvironment) SendCheckRunEvent(org, repo, status, action string) { | ||||
| func (env *testEnvironment) SendOrgCheckRunEvent(org, repo, status, action string) { | ||||
| 	resp, err := sendWebhook(env.webhookServer, "check_run", &github.CheckRunEvent{ | ||||
| 		CheckRun: &github.CheckRun{ | ||||
| 			Status: github.String(status), | ||||
|  | @ -450,6 +683,52 @@ func (env *testEnvironment) SendCheckRunEvent(org, repo, status, action string) | |||
| 		}, | ||||
| 		Repo: &github.Repository{ | ||||
| 			Name: github.String(repo), | ||||
| 			Owner: &github.User{ | ||||
| 				Login: github.String(org), | ||||
| 				Type:  github.String("Organization"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: github.String(action), | ||||
| 	}) | ||||
| 
 | ||||
| 	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send check_run event") | ||||
| 
 | ||||
| 	ExpectWithOffset(1, resp.StatusCode).To(Equal(200)) | ||||
| } | ||||
| 
 | ||||
| func (env *testEnvironment) SendUserPullRequestEvent(owner, repo, branch, action string) { | ||||
| 	resp, err := sendWebhook(env.webhookServer, "pull_request", &github.PullRequestEvent{ | ||||
| 		PullRequest: &github.PullRequest{ | ||||
| 			Base: &github.PullRequestBranch{ | ||||
| 				Ref: github.String(branch), | ||||
| 			}, | ||||
| 		}, | ||||
| 		Repo: &github.Repository{ | ||||
| 			Name: github.String(repo), | ||||
| 			Owner: &github.User{ | ||||
| 				Login: github.String(owner), | ||||
| 				Type:  github.String("User"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: github.String(action), | ||||
| 	}) | ||||
| 
 | ||||
| 	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send pull_request event") | ||||
| 
 | ||||
| 	ExpectWithOffset(1, resp.StatusCode).To(Equal(200)) | ||||
| } | ||||
| 
 | ||||
| func (env *testEnvironment) SendUserCheckRunEvent(owner, repo, status, action string) { | ||||
| 	resp, err := sendWebhook(env.webhookServer, "check_run", &github.CheckRunEvent{ | ||||
| 		CheckRun: &github.CheckRun{ | ||||
| 			Status: github.String(status), | ||||
| 		}, | ||||
| 		Repo: &github.Repository{ | ||||
| 			Name: github.String(repo), | ||||
| 			Owner: &github.User{ | ||||
| 				Login: github.String(owner), | ||||
| 				Type:  github.String("User"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: github.String(action), | ||||
| 	}) | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ func SetupDeploymentTest(ctx context.Context) *corev1.Namespace { | |||
| 			Scheme:   scheme.Scheme, | ||||
| 			Log:      logf.Log, | ||||
| 			Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"), | ||||
| 			Name:     "runnerdeployment-" + ns.Name, | ||||
| 		} | ||||
| 		err = controller.SetupWithManager(mgr) | ||||
| 		Expect(err).NotTo(HaveOccurred(), "failed to setup controller") | ||||
|  |  | |||
|  | @ -60,6 +60,7 @@ func SetupTest(ctx context.Context) *corev1.Namespace { | |||
| 			Log:          logf.Log, | ||||
| 			Recorder:     mgr.GetEventRecorderFor("runnerreplicaset-controller"), | ||||
| 			GitHubClient: ghClient, | ||||
| 			Name:         "runnerreplicaset-" + ns.Name, | ||||
| 		} | ||||
| 		err = controller.SetupWithManager(mgr) | ||||
| 		Expect(err).NotTo(HaveOccurred(), "failed to setup controller") | ||||
|  |  | |||
|  | @ -241,8 +241,8 @@ | |||
|       } | ||||
|     } | ||||
|   ] | ||||
|   }, | ||||
|   "repository": { | ||||
| }, | ||||
| "repository": { | ||||
|     "id": 1234567890, | ||||
|     "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|     "name": "MYREPO", | ||||
|  | @ -0,0 +1,360 @@ | |||
| { | ||||
|   "action": "completed", | ||||
|   "check_run": { | ||||
|     "id": 1949438388, | ||||
|     "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|     "head_sha": "1234567890123456789012345678901234567890", | ||||
|     "external_id": "ca395085-040a-526b-2ce8-bdc85f692774", | ||||
|     "url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890", | ||||
|     "html_url": "https://github.com/MYORG/MYREPO/runs/123467890", | ||||
|     "details_url": "https://github.com/MYORG/MYREPO/runs/123467890", | ||||
|     "status": "queued", | ||||
|     "conclusion": null, | ||||
|     "started_at": "2021-02-18T06:16:31Z", | ||||
|     "completed_at": null, | ||||
|     "output": { | ||||
|       "title": null, | ||||
|       "summary": null, | ||||
|       "text": null, | ||||
|       "annotations_count": 0, | ||||
|       "annotations_url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890/annotations" | ||||
|     }, | ||||
|     "name": "build", | ||||
|     "name": "validate", | ||||
|     "check_suite": { | ||||
|       "id": 1234567890, | ||||
|       "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|       "head_branch": "MYNAME/actions-runner-controller-webhook", | ||||
|       "head_sha": "1234567890123456789012345678901234567890", | ||||
|       "status": "queued", | ||||
|       "conclusion": null, | ||||
|       "url": "https://api.github.com/repos/MYORG/MYREPO/check-suites/1234567890", | ||||
|       "before": "1234567890123456789012345678901234567890", | ||||
|       "after": "1234567890123456789012345678901234567890", | ||||
|       "pull_requests": [ | ||||
|         { | ||||
|           "url": "https://api.github.com/repos/MYORG/MYREPO/pulls/2033", | ||||
|           "id": 1234567890, | ||||
|           "number": 1234567890, | ||||
|           "head": { | ||||
|             "ref": "feature", | ||||
|             "sha": "1234567890123456789012345678901234567890", | ||||
|             "repo": { | ||||
|               "id": 1234567890, | ||||
|               "url": "https://api.github.com/repos/MYORG/MYREPO", | ||||
|               "name": "MYREPO" | ||||
|             } | ||||
|           }, | ||||
|           "base": { | ||||
|             "ref": "master", | ||||
|             "sha": "1234567890123456789012345678901234567890", | ||||
|             "repo": { | ||||
|               "id": 1234567890, | ||||
|               "url": "https://api.github.com/repos/MYORG/MYREPO", | ||||
|               "name": "MYREPO" | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "app": { | ||||
|         "id": 1234567890, | ||||
|         "slug": "github-actions", | ||||
|         "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|         "owner": { | ||||
|           "login": "github", | ||||
|           "id": 1234567890, | ||||
|           "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|           "avatar_url": "https://avatars.githubusercontent.com/u/123467890?v=4", | ||||
|           "gravatar_id": "", | ||||
|           "url": "https://api.github.com/users/github", | ||||
|           "html_url": "https://github.com/github", | ||||
|           "followers_url": "https://api.github.com/users/github/followers", | ||||
|           "following_url": "https://api.github.com/users/github/following{/other_user}", | ||||
|           "gists_url": "https://api.github.com/users/github/gists{/gist_id}", | ||||
|           "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", | ||||
|           "subscriptions_url": "https://api.github.com/users/github/subscriptions", | ||||
|           "organizations_url": "https://api.github.com/users/github/orgs", | ||||
|           "repos_url": "https://api.github.com/users/github/repos", | ||||
|           "events_url": "https://api.github.com/users/github/events{/privacy}", | ||||
|           "received_events_url": "https://api.github.com/users/github/received_events", | ||||
|           "type": "Organization", | ||||
|           "site_admin": false | ||||
|         }, | ||||
|         "name": "GitHub Actions", | ||||
|         "description": "Automate your workflow from idea to production", | ||||
|         "external_url": "https://help.github.com/en/actions", | ||||
|         "html_url": "https://github.com/apps/github-actions", | ||||
|         "created_at": "2018-07-30T09:30:17Z", | ||||
|         "updated_at": "2019-12-10T19:04:12Z", | ||||
|         "permissions": { | ||||
|           "actions": "write", | ||||
|           "checks": "write", | ||||
|           "contents": "write", | ||||
|           "deployments": "write", | ||||
|           "issues": "write", | ||||
|           "metadata": "read", | ||||
|           "organization_packages": "write", | ||||
|           "packages": "write", | ||||
|           "pages": "write", | ||||
|           "pull_requests": "write", | ||||
|           "repository_hooks": "write", | ||||
|           "repository_projects": "write", | ||||
|           "security_events": "write", | ||||
|           "statuses": "write", | ||||
|           "vulnerability_alerts": "read" | ||||
|         }, | ||||
|         "events": [ | ||||
|           "check_run", | ||||
|           "check_suite", | ||||
|           "create", | ||||
|           "delete", | ||||
|           "deployment", | ||||
|           "deployment_status", | ||||
|           "fork", | ||||
|           "gollum", | ||||
|           "issues", | ||||
|           "issue_comment", | ||||
|           "label", | ||||
|           "milestone", | ||||
|           "page_build", | ||||
|           "project", | ||||
|           "project_card", | ||||
|           "project_column", | ||||
|           "public", | ||||
|           "pull_request", | ||||
|           "pull_request_review", | ||||
|           "pull_request_review_comment", | ||||
|           "push", | ||||
|           "registry_package", | ||||
|           "release", | ||||
|           "repository", | ||||
|           "repository_dispatch", | ||||
|           "status", | ||||
|           "watch", | ||||
|           "workflow_dispatch", | ||||
|           "workflow_run" | ||||
|         ] | ||||
|       }, | ||||
|       "created_at": "2021-02-18T06:15:32Z", | ||||
|       "updated_at": "2021-02-18T06:16:31Z" | ||||
|     }, | ||||
|     "app": { | ||||
|       "id": 1234567890, | ||||
|       "slug": "github-actions", | ||||
|       "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|       "owner": { | ||||
|         "login": "github", | ||||
|         "id": 1234567890, | ||||
|         "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|         "avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4", | ||||
|         "gravatar_id": "", | ||||
|         "url": "https://api.github.com/users/github", | ||||
|         "html_url": "https://github.com/github", | ||||
|         "followers_url": "https://api.github.com/users/github/followers", | ||||
|         "following_url": "https://api.github.com/users/github/following{/other_user}", | ||||
|         "gists_url": "https://api.github.com/users/github/gists{/gist_id}", | ||||
|         "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", | ||||
|         "subscriptions_url": "https://api.github.com/users/github/subscriptions", | ||||
|         "organizations_url": "https://api.github.com/users/github/orgs", | ||||
|         "repos_url": "https://api.github.com/users/github/repos", | ||||
|         "events_url": "https://api.github.com/users/github/events{/privacy}", | ||||
|         "received_events_url": "https://api.github.com/users/github/received_events", | ||||
|         "type": "Organization", | ||||
|         "site_admin": false | ||||
|       }, | ||||
|       "name": "GitHub Actions", | ||||
|       "description": "Automate your workflow from idea to production", | ||||
|       "external_url": "https://help.github.com/en/actions", | ||||
|       "html_url": "https://github.com/apps/github-actions", | ||||
|       "created_at": "2018-07-30T09:30:17Z", | ||||
|       "updated_at": "2019-12-10T19:04:12Z", | ||||
|       "permissions": { | ||||
|         "actions": "write", | ||||
|         "checks": "write", | ||||
|         "contents": "write", | ||||
|         "deployments": "write", | ||||
|         "issues": "write", | ||||
|         "metadata": "read", | ||||
|         "organization_packages": "write", | ||||
|         "packages": "write", | ||||
|         "pages": "write", | ||||
|         "pull_requests": "write", | ||||
|         "repository_hooks": "write", | ||||
|         "repository_projects": "write", | ||||
|         "security_events": "write", | ||||
|         "statuses": "write", | ||||
|         "vulnerability_alerts": "read" | ||||
|       }, | ||||
|       "events": [ | ||||
|         "check_run", | ||||
|         "check_suite", | ||||
|         "create", | ||||
|         "delete", | ||||
|         "deployment", | ||||
|         "deployment_status", | ||||
|         "fork", | ||||
|         "gollum", | ||||
|         "issues", | ||||
|         "issue_comment", | ||||
|         "label", | ||||
|         "milestone", | ||||
|         "page_build", | ||||
|         "project", | ||||
|         "project_card", | ||||
|         "project_column", | ||||
|         "public", | ||||
|         "pull_request", | ||||
|         "pull_request_review", | ||||
|         "pull_request_review_comment", | ||||
|         "push", | ||||
|         "registry_package", | ||||
|         "release", | ||||
|         "repository", | ||||
|         "repository_dispatch", | ||||
|         "status", | ||||
|         "watch", | ||||
|         "workflow_dispatch", | ||||
|         "workflow_run" | ||||
|       ] | ||||
|     }, | ||||
|     "pull_requests": [ | ||||
|       { | ||||
|         "url": "https://api.github.com/repos/MYORG/MYREPO/pulls/1234567890", | ||||
|         "id": 1234567890, | ||||
|         "number": 1234567890, | ||||
|         "head": { | ||||
|           "ref": "feature", | ||||
|           "sha": "1234567890123456789012345678901234567890", | ||||
|           "repo": { | ||||
|             "id": 1234567890, | ||||
|             "url": "https://api.github.com/repos/MYORG/MYREPO", | ||||
|             "name": "MYREPO" | ||||
|           } | ||||
|         }, | ||||
|         "base": { | ||||
|           "ref": "master", | ||||
|           "sha": "1234567890123456789012345678901234567890", | ||||
|           "repo": { | ||||
|             "id": 1234567890, | ||||
|             "url": "https://api.github.com/repos/MYORG/MYREPO", | ||||
|             "name": "MYREPO" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "repository": { | ||||
|     "id": 1234567890, | ||||
|     "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|     "name": "MYREPO", | ||||
|     "full_name": "MYORG/MYREPO", | ||||
|     "private": true, | ||||
|     "owner": { | ||||
|       "login": "MYUSER", | ||||
|       "id": 1234567890, | ||||
|       "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|       "avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4", | ||||
|       "gravatar_id": "", | ||||
|       "url": "https://api.github.com/users/MYUSER", | ||||
|       "html_url": "https://github.com/MYUSER", | ||||
|       "followers_url": "https://api.github.com/users/MYUSER/followers", | ||||
|       "following_url": "https://api.github.com/users/MYUSER/following{/other_user}", | ||||
|       "gists_url": "https://api.github.com/users/MYUSER/gists{/gist_id}", | ||||
|       "starred_url": "https://api.github.com/users/MYUSER/starred{/owner}{/repo}", | ||||
|       "subscriptions_url": "https://api.github.com/users/MYUSER/subscriptions", | ||||
|       "organizations_url": "https://api.github.com/users/MYUSER/orgs", | ||||
|       "repos_url": "https://api.github.com/users/MYUSER/repos", | ||||
|       "events_url": "https://api.github.com/users/MYUSER/events{/privacy}", | ||||
|       "received_events_url": "https://api.github.com/users/MYUSER/received_events", | ||||
|       "type": "User", | ||||
|       "site_admin": false | ||||
|     }, | ||||
|     "html_url": "https://github.com/MYUSER/MYREPO", | ||||
|     "description": null, | ||||
|     "fork": false, | ||||
|     "url": "https://api.github.com/repos/MYUSER/MYREPO", | ||||
|     "forks_url": "https://api.github.com/repos/MYUSER/MYREPO/forks", | ||||
|     "keys_url": "https://api.github.com/repos/MYUSER/MYREPO/keys{/key_id}", | ||||
|     "collaborators_url": "https://api.github.com/repos/MYUSER/MYREPO/collaborators{/collaborator}", | ||||
|     "teams_url": "https://api.github.com/repos/MYUSER/MYREPO/teams", | ||||
|     "hooks_url": "https://api.github.com/repos/MYUSER/MYREPO/hooks", | ||||
|     "issue_events_url": "https://api.github.com/repos/MYUSER/MYREPO/issues/events{/number}", | ||||
|     "events_url": "https://api.github.com/repos/MYUSER/MYREPO/events", | ||||
|     "assignees_url": "https://api.github.com/repos/MYUSER/MYREPO/assignees{/user}", | ||||
|     "branches_url": "https://api.github.com/repos/MYUSER/MYREPO/branches{/branch}", | ||||
|     "tags_url": "https://api.github.com/repos/MYUSER/MYREPO/tags", | ||||
|     "blobs_url": "https://api.github.com/repos/MYUSER/MYREPO/git/blobs{/sha}", | ||||
|     "git_tags_url": "https://api.github.com/repos/MYUSER/MYREPO/git/tags{/sha}", | ||||
|     "git_refs_url": "https://api.github.com/repos/MYUSER/MYREPO/git/refs{/sha}", | ||||
|     "trees_url": "https://api.github.com/repos/MYUSER/MYREPO/git/trees{/sha}", | ||||
|     "statuses_url": "https://api.github.com/repos/MYUSER/MYREPO/statuses/{sha}", | ||||
|     "languages_url": "https://api.github.com/repos/MYUSER/MYREPO/languages", | ||||
|     "stargazers_url": "https://api.github.com/repos/MYUSER/MYREPO/stargazers", | ||||
|     "contributors_url": "https://api.github.com/repos/MYUSER/MYREPO/contributors", | ||||
|     "subscribers_url": "https://api.github.com/repos/MYUSER/MYREPO/subscribers", | ||||
|     "subscription_url": "https://api.github.com/repos/MYUSER/MYREPO/subscription", | ||||
|     "commits_url": "https://api.github.com/repos/MYUSER/MYREPO/commits{/sha}", | ||||
|     "git_commits_url": "https://api.github.com/repos/MYUSER/MYREPO/git/commits{/sha}", | ||||
|     "comments_url": "https://api.github.com/repos/MYUSER/MYREPO/comments{/number}", | ||||
|     "issue_comment_url": "https://api.github.com/repos/MYUSER/MYREPO/issues/comments{/number}", | ||||
|     "contents_url": "https://api.github.com/repos/MYUSER/MYREPO/contents/{+path}", | ||||
|     "compare_url": "https://api.github.com/repos/MYUSER/MYREPO/compare/{base}...{head}", | ||||
|     "merges_url": "https://api.github.com/repos/MYUSER/MYREPO/merges", | ||||
|     "archive_url": "https://api.github.com/repos/MYUSER/MYREPO/{archive_format}{/ref}", | ||||
|     "downloads_url": "https://api.github.com/repos/MYUSER/MYREPO/downloads", | ||||
|     "issues_url": "https://api.github.com/repos/MYUSER/MYREPO/issues{/number}", | ||||
|     "pulls_url": "https://api.github.com/repos/MYUSER/MYREPO/pulls{/number}", | ||||
|     "milestones_url": "https://api.github.com/repos/MYUSER/MYREPO/milestones{/number}", | ||||
|     "notifications_url": "https://api.github.com/repos/MYUSER/MYREPO/notifications{?since,all,participating}", | ||||
|     "labels_url": "https://api.github.com/repos/MYUSER/MYREPO/labels{/name}", | ||||
|     "releases_url": "https://api.github.com/repos/MYUSER/MYREPO/releases{/id}", | ||||
|     "deployments_url": "https://api.github.com/repos/MYUSER/MYREPO/deployments", | ||||
|     "created_at": "2021-02-18T06:16:31Z", | ||||
|     "updated_at": "2021-02-18T06:16:31Z", | ||||
|     "pushed_at": "2021-02-18T06:16:31Z", | ||||
|     "git_url": "git://github.com/MYUSER/MYREPO.git", | ||||
|     "ssh_url": "git@github.com:MYUSER/MYREPO.git", | ||||
|     "clone_url": "https://github.com/MYUSER/MYREPO.git", | ||||
|     "svn_url": "https://github.com/MYUSER/MYREPO", | ||||
|     "homepage": null, | ||||
|     "size": 4, | ||||
|     "stargazers_count": 0, | ||||
|     "watchers_count": 0, | ||||
|     "language": null, | ||||
|     "has_issues": true, | ||||
|     "has_projects": true, | ||||
|     "has_downloads": true, | ||||
|     "has_wiki": true, | ||||
|     "has_pages": false, | ||||
|     "forks_count": 0, | ||||
|     "mirror_url": null, | ||||
|     "archived": false, | ||||
|     "disabled": false, | ||||
|     "open_issues_count": 0, | ||||
|     "license": null, | ||||
|     "forks": 0, | ||||
|     "open_issues": 0, | ||||
|     "watchers": 0, | ||||
|     "default_branch": "main" | ||||
|   }, | ||||
|   "sender": { | ||||
|     "login": "MYUSER", | ||||
|     "id": 1234567890, | ||||
|     "node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
|     "avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4", | ||||
|     "gravatar_id": "", | ||||
|     "url": "https://api.github.com/users/MYUSER", | ||||
|     "html_url": "https://github.com/MYUSER", | ||||
|     "followers_url": "https://api.github.com/users/MYUSER/followers", | ||||
|     "following_url": "https://api.github.com/users/MYUSER/following{/other_user}", | ||||
|     "gists_url": "https://api.github.com/users/MYUSER/gists{/gist_id}", | ||||
|     "starred_url": "https://api.github.com/users/MYUSER/starred{/owner}{/repo}", | ||||
|     "subscriptions_url": "https://api.github.com/users/MYUSER/subscriptions", | ||||
|     "organizations_url": "https://api.github.com/users/MYUSER/orgs", | ||||
|     "repos_url": "https://api.github.com/users/MYUSER/repos", | ||||
|     "events_url": "https://api.github.com/users/MYUSER/events{/privacy}", | ||||
|     "received_events_url": "https://api.github.com/users/MYUSER/received_events", | ||||
|     "type": "User", | ||||
|     "site_admin": false | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue