From 96d5360415ddfc992bd7345ff28b36f23700148d Mon Sep 17 00:00:00 2001 From: ianedwards Date: Mon, 9 Oct 2023 13:14:06 -0400 Subject: [PATCH] POR-1852 comment deploy details with URL on first preview deploy (#3759) --- api/client/porter_app.go | 34 +++ api/server/handlers/porter_app/apply.go | 2 +- .../handlers/porter_app/report_status.go | 261 ++++++++++++++++++ api/server/router/porter_app.go | 29 ++ cli/cmd/v2/apply.go | 82 +++++- .../home/app-dashboard/new-app-flow/utils.ts | 5 +- internal/deployment_target/get.go | 2 + internal/integrations/ci/actions/steps.go | 1 + internal/porter_app/github.go | 5 +- internal/porter_app/revisions.go | 37 ++- 10 files changed, 438 insertions(+), 20 deletions(-) create mode 100644 api/server/handlers/porter_app/report_status.go diff --git a/api/client/porter_app.go b/api/client/porter_app.go index 4a66966af2..e87884b28f 100644 --- a/api/client/porter_app.go +++ b/api/client/porter_app.go @@ -439,6 +439,40 @@ func (c *Client) GetBuildEnv( return resp, err } +// ReportRevisionStatusInput is the input struct to ReportRevisionStatus +type ReportRevisionStatusInput struct { + ProjectID uint + ClusterID uint + AppName string + AppRevisionID string + PRNumber int + CommitSHA string +} + +// ReportRevisionStatus reports the status of an app revision to external services +func (c *Client) ReportRevisionStatus( + ctx context.Context, + inp ReportRevisionStatusInput, +) (*porter_app.ReportRevisionStatusResponse, error) { + resp := &porter_app.ReportRevisionStatusResponse{} + + req := &porter_app.ReportRevisionStatusRequest{ + PRNumber: inp.PRNumber, + CommitSHA: inp.CommitSHA, + } + + err := c.postRequest( + fmt.Sprintf( + "/projects/%d/clusters/%d/apps/%s/revisions/%s/status", + inp.ProjectID, inp.ClusterID, inp.AppName, inp.AppRevisionID, + ), + req, + resp, + ) + + return resp, err +} + // CreateOrUpdateAppEnvironment updates the app environment group and creates it if it doesn't exist func (c *Client) CreateOrUpdateAppEnvironment( ctx context.Context, diff --git a/api/server/handlers/porter_app/apply.go b/api/server/handlers/porter_app/apply.go index b2f22d9685..6c72de312b 100644 --- a/api/server/handlers/porter_app/apply.go +++ b/api/server/handlers/porter_app/apply.go @@ -221,7 +221,7 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app *porterv1.PorterApp if !webConfig.GetPrivate() && len(webConfig.Domains) == 0 { if deploymentTarget.Namespace != DeploymentTargetSelector_Default { - createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.Namespace) + createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.ID[:6]) } subdomain, err := porter_app.CreatePorterSubdomain(ctx, createSubdomainInput) diff --git a/api/server/handlers/porter_app/report_status.go b/api/server/handlers/porter_app/report_status.go new file mode 100644 index 0000000000..8e7d2a6d07 --- /dev/null +++ b/api/server/handlers/porter_app/report_status.go @@ -0,0 +1,261 @@ +package porter_app + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/google/go-github/v39/github" + "github.com/google/uuid" + "github.com/porter-dev/api-contracts/generated/go/helpers" + porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1" + "github.com/porter-dev/porter/api/server/handlers" + "github.com/porter-dev/porter/api/server/shared" + "github.com/porter-dev/porter/api/server/shared/apierrors" + "github.com/porter-dev/porter/api/server/shared/config" + "github.com/porter-dev/porter/api/server/shared/requestutils" + "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/deployment_target" + "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/porter_app" + "github.com/porter-dev/porter/internal/telemetry" + "k8s.io/utils/pointer" +) + +// ReportRevisionStatusHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint +type ReportRevisionStatusHandler struct { + handlers.PorterHandlerReadWriter +} + +// NewReportRevisionStatusHandler handles POST requests to the endpoint /apps/{porter_app_name}/revisions/{app_revision_id}/status +func NewReportRevisionStatusHandler( + config *config.Config, + decoderValidator shared.RequestDecoderValidator, + writer shared.ResultWriter, +) *ReportRevisionStatusHandler { + return &ReportRevisionStatusHandler{ + PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + } +} + +// ReportRevisionStatusRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint +type ReportRevisionStatusRequest struct { + PRNumber int `json:"pr_number"` + CommitSHA string `json:"commit_sha"` +} + +// ReportRevisionStatusResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint +type ReportRevisionStatusResponse struct{} + +// ServeHTTP reports the status of a revision to Github and other integrations, depending on the status and the deployment target +func (c *ReportRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, span := telemetry.NewSpan(r.Context(), "serve-report-revision-status") + defer span.End() + + project, _ := ctx.Value(types.ProjectScope).(*models.Project) + cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) + + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { + err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden)) + return + } + + appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName) + if reqErr != nil { + err := telemetry.Error(ctx, span, nil, "error parsing porter app name") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-name", Value: appName}) + + revisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID) + if reqErr != nil { + err := telemetry.Error(ctx, span, nil, "error parsing app revision id") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: revisionID}) + + appRevisionUuid, err := uuid.Parse(revisionID) + if err != nil { + err := telemetry.Error(ctx, span, err, "error parsing app revision id") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + if appRevisionUuid == uuid.Nil { + err := telemetry.Error(ctx, span, nil, "app revision id is nil") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionUuid.String()}) + + porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName) + if err != nil { + err := telemetry.Error(ctx, span, err, "error reading porter app by name") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + if porterApp.ID == 0 { + err := telemetry.Error(ctx, span, nil, "porter app not found") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound)) + return + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: porterApp.ID}) + + request := &ReportRevisionStatusRequest{} + if ok := c.DecodeAndValidate(w, r, request); !ok { + err := telemetry.Error(ctx, span, nil, "error decoding request") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{ + AppRevisionID: appRevisionUuid, + ProjectID: project.ID, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting app revision") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{ + ProjectID: int64(project.ID), + ClusterID: int64(cluster.ID), + DeploymentTargetID: revision.DeploymentTargetID, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting deployment target details") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID}) + + resp := &ReportRevisionStatusResponse{} + + if !deploymentTarget.Preview || request.PRNumber == 0 || revision.RevisionNumber > 1 { + c.WriteResult(w, r, resp) + return + } + + err = writePRComment(ctx, writePRCommentInput{ + revision: revision, + porterApp: porterApp, + prNumber: request.PRNumber, + commitSha: request.CommitSHA, + githubAppSecret: c.Config().ServerConf.GithubAppSecret, + githubAppID: c.Config().ServerConf.GithubAppID, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error writing pr comment") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + c.WriteResult(w, r, resp) +} + +type writePRCommentInput struct { + revision porter_app.Revision + porterApp *models.PorterApp + prNumber int + commitSha string + serverURL string + + githubAppSecret []byte + githubAppID string +} + +func writePRComment(ctx context.Context, inp writePRCommentInput) error { + ctx, span := telemetry.NewSpan(ctx, "write-pr-comment") + defer span.End() + + if inp.porterApp == nil { + return telemetry.Error(ctx, span, nil, "porter app is nil") + } + if inp.prNumber == 0 { + return telemetry.Error(ctx, span, nil, "pr number is empty") + } + if inp.commitSha == "" { + return telemetry.Error(ctx, span, nil, "commit sha is empty") + } + if inp.githubAppSecret == nil { + return telemetry.Error(ctx, span, nil, "github app secret is empty") + } + if inp.githubAppID == "" { + return telemetry.Error(ctx, span, nil, "github app id is empty") + } + + client, err := porter_app.GetGithubClientByRepoID(ctx, inp.porterApp.GitRepoID, inp.githubAppSecret, inp.githubAppID) + if err != nil { + return telemetry.Error(ctx, span, err, "error getting github client") + } + + repoDetails := strings.Split(inp.porterApp.RepoName, "/") + if len(repoDetails) != 2 { + return telemetry.Error(ctx, span, nil, "repo name is not in the format /") + } + + telemetry.WithAttributes(span, + telemetry.AttributeKV{Key: "repo-owner", Value: repoDetails[0]}, + telemetry.AttributeKV{Key: "repo-name", Value: repoDetails[1]}, + telemetry.AttributeKV{Key: "pr-number", Value: inp.prNumber}, + telemetry.AttributeKV{Key: "commit-sha", Value: inp.commitSha}, + ) + + decoded, err := base64.StdEncoding.DecodeString(inp.revision.B64AppProto) + if err != nil { + return telemetry.Error(ctx, span, err, "error decoding base proto") + } + + appProto := &porterv1.PorterApp{} + err = helpers.UnmarshalContractObject(decoded, appProto) + if err != nil { + return telemetry.Error(ctx, span, err, "error unmarshalling app proto") + } + + body := "## Porter Preview Environments\n" + porterURL := fmt.Sprintf("%s/preview-environments/apps/%s?target=%s", inp.serverURL, inp.porterApp.Name, inp.revision.DeploymentTargetID) + + switch inp.revision.Status { + case models.AppRevisionStatus_BuildFailed: + body = fmt.Sprintf("%s❌ The latest deploy failed to build. Check the [Porter Dashboard](%s) or [action logs](https://github.com/%s/actions/runs/) for more information.", body, porterURL, inp.porterApp.RepoName) + case models.AppRevisionStatus_DeployFailed: + body = fmt.Sprintf("%s❌ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) failed to deploy.\nCheck the [Porter Dashboard](%s) or [action logs](https://github.com/%s/actions/runs/) for more information.\nContact Porter Support if the errors persists", body, inp.commitSha, repoDetails[0], repoDetails[1], inp.commitSha, porterURL, inp.porterApp.RepoName) + case models.AppRevisionStatus_Deployed: + body = fmt.Sprintf("%s✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed.\nApp details available in the [Porter Dashboard](%s)", body, inp.commitSha, repoDetails[0], repoDetails[1], inp.commitSha, porterURL) + default: + return nil + } + + for _, service := range appProto.Services { + webConfig := service.GetWebConfig() + if webConfig != nil { + domains := webConfig.GetDomains() + + if len(domains) > 0 { + body = fmt.Sprintf("%s\n\n**Preview URL**: https://%s", body, domains[0].Name) + } + } + } + + _, _, err = client.Issues.CreateComment( + ctx, + repoDetails[0], + repoDetails[1], + inp.prNumber, + &github.IssueComment{ + Body: pointer.String(body), + }, + ) + if err != nil { + return telemetry.Error(ctx, span, err, "error creating github comment") + } + + return nil +} diff --git a/api/server/router/porter_app.go b/api/server/router/porter_app.go index 33758bd60a..6b87353500 100644 --- a/api/server/router/porter_app.go +++ b/api/server/router/porter_app.go @@ -1154,6 +1154,35 @@ func getPorterAppRoutes( Router: r, }) + // POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/status -> porter_app.NewReportRevisionStatusHandler + reportRevisionStatusEndpoint := factory.NewAPIEndpoint( + &types.APIRequestMetadata{ + Verb: types.APIVerbUpdate, + Method: types.HTTPVerbPost, + Path: &types.Path{ + Parent: basePath, + RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}/status", types.URLParamPorterAppName, types.URLParamAppRevisionID), + }, + Scopes: []types.PermissionScope{ + types.UserScope, + types.ProjectScope, + types.ClusterScope, + }, + }, + ) + + reportRevisionStatusHandler := porter_app.NewReportRevisionStatusHandler( + config, + factory.GetDecoderValidator(), + factory.GetResultWriter(), + ) + + routes = append(routes, &router.Route{ + Endpoint: reportRevisionStatusEndpoint, + Handler: reportRevisionStatusHandler, + Router: r, + }) + // POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/update-environment -> porter_app.NewUpdateAppEnvironmentHandler updateAppEnvironmentGroupEndpoint := factory.NewAPIEndpoint( &types.APIRequestMetadata{ diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 4d15fa4dbe..bfa744eabe 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -51,6 +51,15 @@ func Apply(ctx context.Context, inp ApplyInput) error { return fmt.Errorf("error getting deployment target from config: %w", err) } + var prNumber int + prNumberEnv := os.Getenv("PORTER_PR_NUMBER") + if prNumberEnv != "" { + prNumber, err = strconv.Atoi(prNumberEnv) + if err != nil { + return fmt.Errorf("error parsing PORTER_PR_NUMBER to int: %w", err) + } + } + porterYamlExists := len(inp.PorterYamlPath) != 0 if porterYamlExists { @@ -174,43 +183,55 @@ func Apply(ctx context.Context, inp ApplyInput) error { eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID) + reportBuildFailureInput := reportBuildFailureInput{ + client: client, + appName: appName, + cliConf: cliConf, + deploymentTargetID: deploymentTargetID, + appRevisionID: applyResp.AppRevisionId, + eventID: eventID, + buildError: err, + commitSHA: commitSHA, + prNumber: prNumber, + } + if commitSHA == "" { err := errors.New("Build is required but commit SHA cannot be identified. Please set the PORTER_COMMIT_SHA environment variable or run apply in git repository with access to the git CLI.") - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } buildSettings, err := buildSettingsFromBase64AppProto(base64AppProto) if err != nil { err := fmt.Errorf("error getting build settings from base64 app proto: %w", err) - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } currentAppRevisionResp, err := client.CurrentAppRevision(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID) if err != nil { err := fmt.Errorf("error getting current app revision: %w", err) - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } if currentAppRevisionResp == nil { err := errors.New("current app revision is nil") - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } appRevision := currentAppRevisionResp.AppRevision if appRevision.B64AppProto == "" { err := errors.New("current app revision b64 app proto is empty") - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } currentImageTag, err := imageTagFromBase64AppProto(appRevision.B64AppProto) if err != nil { err := fmt.Errorf("error getting image tag from current app revision: %w", err) - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } @@ -220,7 +241,7 @@ func Apply(ctx context.Context, inp ApplyInput) error { buildEnv, err := client.GetBuildEnv(ctx, cliConf.Project, cliConf.Cluster, appName, appRevision.ID) if err != nil { err := fmt.Errorf("error getting build env: %w", err) - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, "") + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } buildSettings.Env = buildEnv.BuildEnvVariables @@ -228,7 +249,8 @@ func Apply(ctx context.Context, inp ApplyInput) error { buildOutput := build(ctx, client, buildSettings) if buildOutput.Error != nil { err := fmt.Errorf("error building app: %w", buildOutput.Error) - _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID, err, buildOutput.Logs) + reportBuildFailureInput.buildLogs = buildOutput.Logs + _ = reportBuildFailure(ctx, reportBuildFailureInput) return err } @@ -295,6 +317,15 @@ func Apply(ctx context.Context, inp ApplyInput) error { return fmt.Errorf("unexpected CLI action: %s", applyResp.CLIAction) } + _, _ = client.ReportRevisionStatus(ctx, api.ReportRevisionStatusInput{ + ProjectID: cliConf.Project, + ClusterID: cliConf.Cluster, + AppName: appName, + AppRevisionID: applyResp.AppRevisionId, + PRNumber: prNumber, + CommitSHA: commitSHA, + }) + color.New(color.FgGreen).Printf("Successfully applied new revision %s for app %s\n", applyResp.AppRevisionId, appName) // nolint:errcheck,gosec return nil } @@ -516,8 +547,21 @@ func updateEnvGroupsInProto(ctx context.Context, base64AppProto string, envGroup return editedB64AppProto, nil } -func reportBuildFailure(ctx context.Context, client api.Client, appName string, cliConf config.CLIConfig, deploymentTargetID string, appRevisionID string, eventID string, buildError error, buildLogs string) error { - _, err := client.UpdateRevisionStatus(ctx, cliConf.Project, cliConf.Cluster, appName, appRevisionID, models.AppRevisionStatus_BuildFailed) +type reportBuildFailureInput struct { + client api.Client + appName string + cliConf config.CLIConfig + deploymentTargetID string + appRevisionID string + eventID string + buildError error + buildLogs string + commitSHA string + prNumber int +} + +func reportBuildFailure(ctx context.Context, inp reportBuildFailureInput) error { + _, err := inp.client.UpdateRevisionStatus(ctx, inp.cliConf.Project, inp.cliConf.Cluster, inp.appName, inp.appRevisionID, models.AppRevisionStatus_BuildFailed) if err != nil { return err } @@ -527,16 +571,28 @@ func reportBuildFailure(ctx context.Context, client api.Client, appName string, // the below is a temporary solution until we can report build errors via telemetry from the CLI errorStringMap := make(map[string]string) - errorStringMap["build-error"] = fmt.Sprintf("%+v", buildError) - b64BuildLogs := base64.StdEncoding.EncodeToString([]byte(buildLogs)) + errorStringMap["build-error"] = fmt.Sprintf("%+v", inp.buildError) + b64BuildLogs := base64.StdEncoding.EncodeToString([]byte(inp.buildLogs)) // the key name below must be kept the same so that reportBuildStatus in the CreateOrUpdatePorterAppEvent handler reports logs correctly errorStringMap["b64-build-logs"] = b64BuildLogs buildMetadata["errors"] = errorStringMap - err = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, types.PorterAppEventType_Build, eventID, types.PorterAppEventStatus_Failed, buildMetadata) + err = updateExistingEvent(ctx, inp.client, inp.appName, inp.cliConf.Project, inp.cliConf.Cluster, inp.deploymentTargetID, types.PorterAppEventType_Build, inp.eventID, types.PorterAppEventStatus_Failed, buildMetadata) if err != nil { return err } + _, err = inp.client.ReportRevisionStatus(ctx, api.ReportRevisionStatusInput{ + ProjectID: inp.cliConf.Project, + ClusterID: inp.cliConf.Cluster, + AppName: inp.appName, + AppRevisionID: inp.appRevisionID, + PRNumber: inp.prNumber, + CommitSHA: inp.commitSHA, + }) + if err != nil { + return err + } + return nil } diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts b/dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts index 0dfa8ea44d..159b7d929f 100644 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts +++ b/dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts @@ -71,7 +71,7 @@ export const getPreviewGithubAction = ( - opened - synchronize -name: Deploy preview environment +name: Deploy to Preview Environment jobs: porter-deploy: runs-on: ubuntu-latest @@ -92,5 +92,6 @@ jobs: PORTER_PROJECT: ${projectID} PORTER_STACK_NAME: ${stackName} PORTER_TAG: \${{ steps.vars.outputs.sha_short }} - PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectID}_${clusterId} }}`; + PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectID}_${clusterId} }} + PORTER_PR_NUMBER: \${{ github.event.inputs.pr_number }}`; }; diff --git a/internal/deployment_target/get.go b/internal/deployment_target/get.go index 628b9d9a89..109b6ec845 100644 --- a/internal/deployment_target/get.go +++ b/internal/deployment_target/get.go @@ -19,6 +19,7 @@ type DeploymentTargetDetailsInput struct { // DeploymentTarget is a struct representing the unique cluster, namespace pair for a deployment target type DeploymentTarget struct { + ID string `json:"id"` ClusterID int64 `json:"cluster_id"` Namespace string `json:"namespace"` Preview bool `json:"preview"` @@ -63,6 +64,7 @@ func DeploymentTargetDetails(ctx context.Context, inp DeploymentTargetDetailsInp } deploymentTarget = DeploymentTarget{ + ID: inp.DeploymentTargetID, Namespace: deploymentTargetDetailsResp.Msg.Namespace, ClusterID: deploymentTargetDetailsResp.Msg.ClusterId, Preview: deploymentTargetDetailsResp.Msg.IsPreview, diff --git a/internal/integrations/ci/actions/steps.go b/internal/integrations/ci/actions/steps.go index 30ce762e5d..f0c5098c50 100644 --- a/internal/integrations/ci/actions/steps.go +++ b/internal/integrations/ci/actions/steps.go @@ -106,6 +106,7 @@ func getDeployStackStep( "PORTER_TOKEN": fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName), "PORTER_TAG": "${{ steps.vars.outputs.sha_short }}", "PORTER_STACK_NAME": stackName, + "PORTER_PR_NUMBER": "${{ github.event.inputs.pr_number }}", }, Timeout: 30, } diff --git a/internal/porter_app/github.go b/internal/porter_app/github.go index 62225aa3e7..fd7918d8e6 100644 --- a/internal/porter_app/github.go +++ b/internal/porter_app/github.go @@ -71,7 +71,7 @@ func CreateAppWebhook(ctx context.Context, inp CreateAppWebhookInput) error { return telemetry.Error(ctx, span, nil, "porter app git repo id is empty") } - githubClient, err := getGithubClientByRepoID(ctx, porterApp.GitRepoID, inp.GithubAppSecret, inp.GithubAppID) + githubClient, err := GetGithubClientByRepoID(ctx, porterApp.GitRepoID, inp.GithubAppSecret, inp.GithubAppID) if err != nil { return telemetry.Error(ctx, span, err, "error creating github client") } @@ -133,7 +133,8 @@ func CreateAppWebhook(ctx context.Context, inp CreateAppWebhookInput) error { return nil } -func getGithubClientByRepoID(ctx context.Context, repoID uint, githubAppSecret []byte, githubAppID string) (*github.Client, error) { +// GetGithubClientByRepoID creates a github client for a given repo id +func GetGithubClientByRepoID(ctx context.Context, repoID uint, githubAppSecret []byte, githubAppID string) (*github.Client, error) { ctx, span := telemetry.NewSpan(ctx, "get-github-client-by-repo-id") defer span.End() diff --git a/internal/porter_app/revisions.go b/internal/porter_app/revisions.go index a0e715ea94..b3dc2a7e9d 100644 --- a/internal/porter_app/revisions.go +++ b/internal/porter_app/revisions.go @@ -3,6 +3,7 @@ package porter_app import ( "context" "encoding/base64" + "fmt" "time" "connectrpc.com/connect" @@ -13,6 +14,7 @@ import ( "github.com/porter-dev/porter/internal/deployment_target" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/kubernetes/environment_groups" + "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" "github.com/porter-dev/porter/internal/telemetry" ) @@ -24,7 +26,7 @@ type Revision struct { // B64AppProto is the base64 encoded app proto definition B64AppProto string `json:"b64_app_proto"` // Status is the status of the revision - Status string `json:"status"` + Status models.AppRevisionStatus `json:"status"` // RevisionNumber is the revision number with respect to the app and deployment target RevisionNumber uint64 `json:"revision_number"` // CreatedAt is the time the revision was created @@ -105,9 +107,14 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi b64 := base64.StdEncoding.EncodeToString(encoded) + status, err := appRevisionStatusFromProto(appRevision.Status) + if err != nil { + return revision, telemetry.Error(ctx, span, err, "error getting app revision status from proto") + } + revision = Revision{ B64AppProto: b64, - Status: appRevision.Status, + Status: status, ID: appRevision.Id, RevisionNumber: appRevision.RevisionNumber, CreatedAt: appRevision.CreatedAt.AsTime(), @@ -184,3 +191,29 @@ func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Rev return revision, nil } + +func appRevisionStatusFromProto(status string) (models.AppRevisionStatus, error) { + var appRevisionStatus models.AppRevisionStatus + switch status { + case string(models.AppRevisionStatus_AwaitingBuild): + appRevisionStatus = models.AppRevisionStatus_AwaitingBuild + case string(models.AppRevisionStatus_AwaitingPredeploy): + appRevisionStatus = models.AppRevisionStatus_AwaitingPredeploy + case string(models.AppRevisionStatus_Deployed): + appRevisionStatus = models.AppRevisionStatus_Deployed + case string(models.AppRevisionStatus_BuildCanceled): + appRevisionStatus = models.AppRevisionStatus_BuildCanceled + case string(models.AppRevisionStatus_BuildFailed): + appRevisionStatus = models.AppRevisionStatus_BuildFailed + case string(models.AppRevisionStatus_PredeployFailed): + appRevisionStatus = models.AppRevisionStatus_PredeployFailed + case string(models.AppRevisionStatus_DeployFailed): + appRevisionStatus = models.AppRevisionStatus_DeployFailed + case string(models.AppRevisionStatus_Created): + appRevisionStatus = models.AppRevisionStatus_Created + default: + return appRevisionStatus, fmt.Errorf("unknown app revision status") + } + + return appRevisionStatus, nil +}