Skip to content

Commit

Permalink
Use buildplanner ImpactReport endpoint to show change summary.
Browse files Browse the repository at this point in the history
The ImpactReport sometimes has issues resolving one or both buildplans, so fall back on the old comparison if necessary.
  • Loading branch information
mitchell-as committed Jul 29, 2024
1 parent 16b5648 commit 319ad24
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 18 deletions.
103 changes: 90 additions & 13 deletions internal/runbits/dependencies/changesummary.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
package dependencies

import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"

"github.com/go-openapi/strfmt"

"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/multilog"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/internal/sliceutils"
"github.com/ActiveState/cli/pkg/buildplan"
"github.com/ActiveState/cli/pkg/platform/api/buildplanner/response"
"github.com/ActiveState/cli/pkg/platform/model/buildplanner"
)

type primeable interface {
primer.Outputer
primer.Auther
primer.Projecter
}

// showUpdatedPackages specifies whether or not to include updated dependencies in the direct
// dependencies list, and whether or not to include updated dependencies when calculating indirect
// dependency numbers.
const showUpdatedPackages = true

// OutputChangeSummary looks over the given build plans, and computes and lists the additional
// dependencies being installed for the requested packages, if any.
func OutputChangeSummary(out output.Outputer, newBuildPlan *buildplan.BuildPlan, oldBuildPlan *buildplan.BuildPlan) {
func OutputChangeSummary(prime primeable, rtCommit *buildplanner.Commit, oldBuildPlan *buildplan.BuildPlan) {
if expr, err := json.Marshal(rtCommit.BuildScript()); err == nil {
bpm := buildplanner.NewBuildPlannerModel(prime.Auth())
params := &buildplanner.ImpactReportParams{
Owner: prime.Project().Owner(),
Project: prime.Project().Name(),
BeforeCommitId: rtCommit.ParentID,
AfterExpr: expr,
}
if impactReport, err := bpm.ImpactReport(params); err == nil {
outputChangeSummaryFromImpactReport(prime.Output(), rtCommit.BuildPlan(), impactReport)
return
} else {
multilog.Error("Failed to fetch impact report: %v", err)
}
multilog.Error("Failed to marshal buildexpression: %v", err)
}
outputChangeSummaryFromBuildPlans(prime.Output(), rtCommit.BuildPlan(), oldBuildPlan)
}

// outputChangeSummaryFromBuildPlans looks over the given build plans, and computes and lists the
// additional dependencies being installed for the requested packages, if any.
func outputChangeSummaryFromBuildPlans(out output.Outputer, newBuildPlan *buildplan.BuildPlan, oldBuildPlan *buildplan.BuildPlan) {
requested := newBuildPlan.RequestedArtifacts().ToIDMap()

addedString := []string{}
Expand All @@ -41,6 +74,57 @@ func OutputChangeSummary(out output.Outputer, newBuildPlan *buildplan.BuildPlan,
}
}

alreadyInstalledVersions := map[strfmt.UUID]string{}
if oldBuildPlan != nil {
for _, a := range oldBuildPlan.Artifacts() {
alreadyInstalledVersions[a.ArtifactID] = a.Version()
}
}

outputChangeSummary(out, addedString, addedLocale, dependencies, directDependencies, alreadyInstalledVersions)
}

func outputChangeSummaryFromImpactReport(out output.Outputer, buildPlan *buildplan.BuildPlan, report *response.ImpactReportResult) {
alreadyInstalledVersions := map[strfmt.UUID]string{}
addedString := []string{}
addedLocale := []string{}
dependencies := buildplan.Ingredients{}
directDependencies := buildplan.Ingredients{}
for _, i := range report.Ingredients {
if i.Before != nil {
alreadyInstalledVersions[strfmt.UUID(i.Before.IngredientID)] = i.Before.Version
}

if i.After == nil || !i.After.IsRequirement {
continue
}

if i.Before == nil {
v := fmt.Sprintf("%s@%s", i.Name, i.After.Version)
addedString = append(addedLocale, v)
addedLocale = append(addedLocale, fmt.Sprintf("[ACTIONABLE]%s[/RESET]", v))
}

for _, bpi := range buildPlan.Ingredients() {
if bpi.IngredientID != strfmt.UUID(i.After.IngredientID) {
continue
}
dependencies = append(dependencies, bpi.RuntimeDependencies(true)...)
directDependencies = append(directDependencies, bpi.RuntimeDependencies(false)...)
}
}

outputChangeSummary(out, addedString, addedLocale, dependencies, directDependencies, alreadyInstalledVersions)
}

func outputChangeSummary(
out output.Outputer,
addedString []string,
addedLocale []string,
dependencies buildplan.Ingredients,
directDependencies buildplan.Ingredients,
alreadyInstalledVersions map[strfmt.UUID]string,
) {
dependencies = sliceutils.UniqueByProperty(dependencies, func(i *buildplan.Ingredient) any { return i.IngredientID })
directDependencies = sliceutils.UniqueByProperty(directDependencies, func(i *buildplan.Ingredient) any { return i.IngredientID })
commonDependencies := directDependencies.CommonRuntimeDependencies().ToIDMap()
Expand All @@ -56,13 +140,6 @@ func OutputChangeSummary(out output.Outputer, newBuildPlan *buildplan.BuildPlan,
return
}

// Process the existing runtime requirements into something we can easily compare against.
alreadyInstalled := buildplan.Artifacts{}
if oldBuildPlan != nil {
alreadyInstalled = oldBuildPlan.Artifacts()
}
oldRequirements := alreadyInstalled.Ingredients().ToIDMap()

localeKey := "additional_dependencies"
if numIndirect > 0 {
localeKey = "additional_total_dependencies"
Expand Down Expand Up @@ -93,9 +170,9 @@ func OutputChangeSummary(out output.Outputer, newBuildPlan *buildplan.BuildPlan,

item := fmt.Sprintf("[ACTIONABLE]%s@%s[/RESET]%s", // intentional omission of space before last %s
ingredient.Name, ingredient.Version, subdependencies)
oldVersion, exists := oldRequirements[ingredient.IngredientID]
if exists && ingredient.Version != "" && oldVersion.Version != ingredient.Version {
item = fmt.Sprintf("[ACTIONABLE]%s@%s[/RESET] → %s (%s)", oldVersion.Name, oldVersion.Version, item, locale.Tl("updated", "updated"))
oldVersion, exists := alreadyInstalledVersions[ingredient.IngredientID]
if exists && ingredient.Version != "" && oldVersion != ingredient.Version {
item = fmt.Sprintf("[ACTIONABLE]%s@%s[/RESET] → %s (%s)", ingredient.Name, oldVersion, item, locale.Tl("updated", "updated"))
}

out.Notice(fmt.Sprintf(" [DISABLED]%s[/RESET] %s", prefix, item))
Expand Down
2 changes: 1 addition & 1 deletion internal/runbits/runtime/requirements/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
}

r.Output.Notice("") // blank line
dependencies.OutputChangeSummary(r.Output, rtCommit.BuildPlan(), oldBuildPlan)
dependencies.OutputChangeSummary(r.prime, rtCommit, oldBuildPlan)

// Report CVEs
names := requirementNames(requirements...)
Expand Down
2 changes: 1 addition & 1 deletion internal/runners/commit/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (c *Commit) Run() (rerr error) {
pgSolve = nil

// Output dependency list.
dependencies.OutputChangeSummary(out, rtCommit.BuildPlan(), oldBuildPlan)
dependencies.OutputChangeSummary(c.prime, rtCommit, oldBuildPlan)

// Report CVEs.
if err := cves.NewCveReport(c.prime).Report(rtCommit.BuildPlan(), oldBuildPlan); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/runners/packages/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (i *Import) Run(params *ImportRunParams) (rerr error) {
}
oldBuildPlan := previousCommit.BuildPlan()
out.Notice("") // blank line
dependencies.OutputChangeSummary(out, rtCommit.BuildPlan(), oldBuildPlan)
dependencies.OutputChangeSummary(i.prime, rtCommit, oldBuildPlan)

// Report CVEs.
if err := cves.NewCveReport(i.prime).Report(rtCommit.BuildPlan(), oldBuildPlan); err != nil {
Expand Down
56 changes: 56 additions & 0 deletions pkg/platform/api/buildplanner/request/impactreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package request

import (
"github.com/go-openapi/strfmt"
)

func ImpactReport(organization, project string, beforeCommitId strfmt.UUID, afterExpr []byte) *impactReport {
bp := &impactReport{map[string]interface{}{
"organization": organization,
"project": project,
"beforeCommitId": beforeCommitId.String(),
"afterExpr": string(afterExpr),
}}

return bp
}

type impactReport struct {
vars map[string]interface{}
}

func (b *impactReport) Query() string {
return `
query ($organization: String!, $project: String!, $beforeCommitId: ID!, $afterExpr: BuildExpr!) {
impactReport(
before: {organization: $organization, project: $project, buildExprOrCommit: {commitId: $beforeCommitId}}
after: {organization: $organization, project: $project, buildExprOrCommit: {buildExpr: $afterExpr}}
) {
__typename
... on ImpactReport {
ingredients {
namespace
name
before {
ingredientID
version
isRequirement
}
after {
ingredientID
version
isRequirement
}
}
}
... on ImpactReportError {
message
}
}
}
`
}

func (b *impactReport) Vars() (map[string]interface{}, error) {
return b.vars, nil
}
43 changes: 43 additions & 0 deletions pkg/platform/api/buildplanner/response/impactreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package response

import (
"github.com/ActiveState/cli/internal/errs"
)

type ImpactReportIngredientState struct {
IngredientID string `json:"ingredientID"`
Version string `json:"version"`
IsRequirement bool `json:"isRequirement"`
}

type ImpactReportIngredient struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Before *ImpactReportIngredientState `json:"before"`
After *ImpactReportIngredientState `json:"after"`
}

type ImpactReportResult struct {
Type string `json:"__typename"`
Ingredients []ImpactReportIngredient `json:"ingredients"`
*Error
}

type ImpactReportResponse struct {
*ImpactReportResult `json:"impactReport"`
}

type ImpactReportError struct {
Type string
Message string
}

func (e ImpactReportError) Error() string { return e.Message }

func ProcessImpactReportError(err *ImpactReportResult, fallbackMessage string) error {
if err.Error == nil {
return errs.New(fallbackMessage)
}

return &ImpactReportError{err.Type, err.Message}
}
3 changes: 2 additions & 1 deletion pkg/platform/api/buildplanner/response/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ func IsErrorResponse(errorType string) bool {
errorType == types.MergeConflictErrorType ||
errorType == types.RevertConflictErrorType ||
errorType == types.CommitNotInTargetHistoryErrorType ||
errorType == types.ComitHasNoParentErrorType
errorType == types.CommitHasNoParentErrorType ||
errorType == types.ImpactReportErrorType
}

// NotFoundError represents an error that occurred because a resource was not found.
Expand Down
3 changes: 2 additions & 1 deletion pkg/platform/api/buildplanner/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
MergeConflictErrorType = "MergeConflict"
RevertConflictErrorType = "RevertConflict"
CommitNotInTargetHistoryErrorType = "CommitNotInTargetHistory"
ComitHasNoParentErrorType = "CommitHasNoParent"
CommitHasNoParentErrorType = "CommitHasNoParent"
TargetNotFoundErrorType = "TargetNotFound"
ImpactReportErrorType = "ImpactReportError"
)
35 changes: 35 additions & 0 deletions pkg/platform/model/buildplanner/impactreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package buildplanner

import (
"github.com/go-openapi/strfmt"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/pkg/platform/api/buildplanner/request"
"github.com/ActiveState/cli/pkg/platform/api/buildplanner/response"
)

type ImpactReportParams struct {
Owner string
Project string
BeforeCommitId strfmt.UUID
AfterExpr []byte
}

func (b *BuildPlanner) ImpactReport(params *ImpactReportParams) (*response.ImpactReportResult, error) {
request := request.ImpactReport(params.Owner, params.Project, params.BeforeCommitId, params.AfterExpr)
resp := &response.ImpactReportResponse{}
err := b.client.Run(request, resp)
if err != nil {
return nil, processBuildPlannerError(err, "failed to get impact report")
}

if resp.ImpactReportResult == nil {
return nil, errs.New("ImpactReport is nil")
}

if response.IsErrorResponse(resp.ImpactReportResult.Type) {
return nil, response.ProcessImpactReportError(resp.ImpactReportResult, "Could not get impact report")
}

return resp.ImpactReportResult, nil
}

0 comments on commit 319ad24

Please sign in to comment.