Skip to content

Commit

Permalink
POR-1874 cancel pending workflows when pr is closed or merged (#3785)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianedwards authored Oct 10, 2023
1 parent c562784 commit 60fb770
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 1 deletion.
104 changes: 103 additions & 1 deletion api/server/handlers/webhook/app_v2_github.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package webhook

import (
"context"
"fmt"
"net/http"
"strings"

"connectrpc.com/connect"
"github.com/google/go-github/v41/github"
"github.com/google/go-github/v39/github"
"github.com/google/uuid"
porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
"github.com/porter-dev/porter/api/server/authz"
Expand All @@ -15,6 +18,7 @@ import (
"github.com/porter-dev/porter/api/server/shared/requestutils"
"github.com/porter-dev/porter/api/types"
"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/porter_app"
"github.com/porter-dev/porter/internal/telemetry"
)

Expand Down Expand Up @@ -96,6 +100,23 @@ func (c *GithubWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
telemetry.AttributeKV{Key: "project-id", Value: webhook.ProjectID},
)

porterApp, err := c.Repo().PorterApp().ReadPorterAppByID(ctx, uint(webhook.PorterAppID))
if err != nil {
err := telemetry.Error(ctx, span, err, "error getting porter app")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}
if porterApp.ID == 0 {
err := telemetry.Error(ctx, span, err, "porter app not found")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}
if porterApp.ProjectID != uint(webhook.ProjectID) {
err := telemetry.Error(ctx, span, err, "porter app project id does not match")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

switch event := event.(type) {
case *github.PullRequestEvent:
if event.GetAction() != GithubPRStatus_Closed {
Expand All @@ -107,6 +128,20 @@ func (c *GithubWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
branch := event.GetPullRequest().GetHead().GetRef()
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "event-branch", Value: branch})

err = cancelPendingWorkflows(ctx, cancelPendingWorkflowsInput{
appName: porterApp.Name,
repoID: porterApp.GitRepoID,
repoName: porterApp.RepoName,
githubAppSecret: c.Config().ServerConf.GithubAppSecret,
githubAppID: c.Config().ServerConf.GithubAppID,
event: event,
})
if err != nil {
err := telemetry.Error(ctx, span, err, "error cancelling pending workflows")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

deploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType(
uint(webhook.ProjectID),
uint(webhook.ClusterID),
Expand Down Expand Up @@ -149,3 +184,70 @@ func (c *GithubWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

c.WriteResult(w, r, nil)
}

type cancelPendingWorkflowsInput struct {
appName string
repoID uint
repoName string
githubAppSecret []byte
githubAppID string
event *github.PullRequestEvent
}

func cancelPendingWorkflows(ctx context.Context, inp cancelPendingWorkflowsInput) error {
ctx, span := telemetry.NewSpan(ctx, "cancel-pending-workflows")
defer span.End()

if inp.repoID == 0 {
return telemetry.Error(ctx, span, nil, "repo id is 0")
}
if inp.repoName == "" {
return telemetry.Error(ctx, span, nil, "repo name is empty")
}
if inp.appName == "" {
return telemetry.Error(ctx, span, nil, "app name is empty")
}
if inp.githubAppSecret == nil {
return telemetry.Error(ctx, span, nil, "github app secret is nil")
}
if inp.githubAppID == "" {
return telemetry.Error(ctx, span, nil, "github app id is empty")
}

client, err := porter_app.GetGithubClientByRepoID(ctx, inp.repoID, inp.githubAppSecret, inp.githubAppID)
if err != nil {
return telemetry.Error(ctx, span, err, "error getting github client")
}

repoDetails := strings.Split(inp.repoName, "/")
if len(repoDetails) != 2 {
return telemetry.Error(ctx, span, nil, "repo name is invalid")
}
owner := repoDetails[0]
repo := repoDetails[1]

pendingStatusOptions := []string{"in_progress", "queued", "requested", "waiting"}
for _, status := range pendingStatusOptions {
runs, _, err := client.Actions.ListWorkflowRunsByFileName(
ctx, owner, repo, fmt.Sprintf("porter_preview_%s.yml", inp.appName),
&github.ListWorkflowRunsOptions{
Branch: inp.event.GetPullRequest().GetHead().GetRef(),
Status: status,
},
)
if err != nil {
return telemetry.Error(ctx, span, err, "error listing workflow runs")
}

for _, run := range runs.WorkflowRuns {
_, err := client.Actions.CancelWorkflowRunByID(
ctx, owner, repo, run.GetID(),
)
if err != nil {
return telemetry.Error(ctx, span, err, "error cancelling workflow run")
}
}
}

return nil
}
13 changes: 13 additions & 0 deletions internal/repository/gorm/porter_app.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package gorm

import (
"context"

"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/repository"
"gorm.io/gorm"
Expand Down Expand Up @@ -34,6 +36,17 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
return apps, nil
}

// ReadPorterAppByID returns a PorterApp by its ID
func (repo *PorterAppRepository) ReadPorterAppByID(ctx context.Context, id uint) (*models.PorterApp, error) {
app := &models.PorterApp{}

if err := repo.db.Where("id = ?", id).Limit(1).Find(&app).Error; err != nil {
return nil, err
}

return app, nil
}

func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
app := &models.PorterApp{}

Expand Down
3 changes: 3 additions & 0 deletions internal/repository/porter_app.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package repository

import (
"context"

"github.com/porter-dev/porter/internal/models"
)

// PorterAppRepository represents the set of queries on the PorterApp model
type PorterAppRepository interface {
ReadPorterAppByID(ctx context.Context, id uint) (*models.PorterApp, error)
ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error)
ReadPorterAppsByProjectIDAndName(projectID uint, name string) ([]*models.PorterApp, error)
CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
Expand Down
6 changes: 6 additions & 0 deletions internal/repository/test/porter_app.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package test

import (
"context"
"errors"
"strings"

Expand Down Expand Up @@ -42,3 +43,8 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
func (repo *PorterAppRepository) DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
return nil, errors.New("cannot write database")
}

// ReadPorterAppByID is a test method that is not implemented
func (repo *PorterAppRepository) ReadPorterAppByID(ctx context.Context, id uint) (*models.PorterApp, error) {
return nil, errors.New("cannot read database")
}

0 comments on commit 60fb770

Please sign in to comment.