Skip to content

Commit

Permalink
WIP using buildplanner ImpactReport endpoint to show change summary.
Browse files Browse the repository at this point in the history
It currently has issues because the afterCommitId is not yet published as part of the project history. A buildexpression is needed.
  • Loading branch information
mitchell-as committed Jul 26, 2024
1 parent 16b5648 commit c7d90d6
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 3 deletions.
89 changes: 89 additions & 0 deletions internal/runbits/dependencies/changesummary.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"strconv"
"strings"

"github.com/go-openapi/strfmt"

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

// showUpdatedPackages specifies whether or not to include updated dependencies in the direct
Expand Down Expand Up @@ -103,3 +106,89 @@ func OutputChangeSummary(out output.Outputer, newBuildPlan *buildplan.BuildPlan,

out.Notice("") // blank line
}

func OutputChangeSummaryFromImpactReport(out output.Outputer, report *response.ImpactReportResult, buildPlan *buildplan.BuildPlan) {
addedString := []string{}
addedLocale := []string{}
dependencies := buildplan.Ingredients{}
directDependencies := buildplan.Ingredients{}
for _, i := range report.Ingredients {
if i.After == nil || !i.After.IsRequirement {
continue
}

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)...)
}
}

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()
numIndirect := len(dependencies) - len(directDependencies) - len(commonDependencies)

sort.SliceStable(directDependencies, func(i, j int) bool {
return directDependencies[i].Name < directDependencies[j].Name
})

logging.Debug("packages %s have %d direct dependencies and %d indirect dependencies",
strings.Join(addedString, ", "), len(directDependencies), numIndirect)
if len(directDependencies) == 0 {
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"
}
out.Notice(" " + locale.Tr(localeKey, strings.Join(addedLocale, ", "), strconv.Itoa(len(directDependencies)), strconv.Itoa(numIndirect)))

// A direct dependency list item is of the form:
// ├─ name@version (X dependencies)
// or
// └─ name@oldVersion → name@newVersion (Updated)
// depending on whether or not it has subdependencies, and whether or not showUpdatedPackages is
// `true`.
for i, ingredient := range directDependencies {
prefix := " ├─"
if i == len(directDependencies)-1 {
prefix = " └─"
}

// Retrieve runtime dependencies, and then filter out any dependencies that are common between all added ingredients.
runtimeDeps := ingredient.RuntimeDependencies(true)
runtimeDeps = runtimeDeps.Filter(func(i *buildplan.Ingredient) bool { _, ok := commonDependencies[i.IngredientID]; return !ok })

subdependencies := ""
if numSubs := len(runtimeDeps); numSubs > 0 {
subdependencies = fmt.Sprintf(" ([ACTIONABLE]%s[/RESET] dependencies)", // intentional leading space
strconv.Itoa(numSubs))
}

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"))
}

out.Notice(fmt.Sprintf(" [DISABLED]%s[/RESET] %s", prefix, item))
}

out.Notice("") // blank line
}
11 changes: 10 additions & 1 deletion internal/runbits/runtime/requirements/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,16 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
}

r.Output.Notice("") // blank line
dependencies.OutputChangeSummary(r.Output, rtCommit.BuildPlan(), oldBuildPlan)
//dependencies.OutputChangeSummary(r.Output, rtCommit.BuildPlan(), oldBuildPlan)
impactReport, err := bpm.ImpactReport(&bpModel.ImpactReportParams{
Owner: r.Project.Owner(),
Project: r.Project.Name(),
BeforeCommitId: parentCommitID,
AfterCommitId: rtCommit.CommitID})
if err != nil {
return errs.Wrap(err, "Failed to fetch impact report")
}
dependencies.OutputChangeSummaryFromImpactReport(r.Output, impactReport, rtCommit.BuildPlan())

// Report CVEs
names := requirementNames(requirements...)
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, afterCommitId strfmt.UUID) *impactReport {
bp := &impactReport{map[string]interface{}{
"organization": organization,
"project": project,
"beforeCommitId": beforeCommitId.String(),
"afterCommitId": afterCommitId.String(),
}}

return bp
}

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

func (b *impactReport) Query() string {
return `
query ($organization: String!, $project: String!, $beforeCommitId: ID!, $afterCommitId: ID!) {
impactReport(
before: {organization: $organization, project: $project, buildExprOrCommit: {commitId: $beforeCommitId}}
after: {organization: $organization, project: $project, buildExprOrCommit: {commitId: $afterCommitId}}
) {
__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
AfterCommitId strfmt.UUID
}

func (b *BuildPlanner) ImpactReport(params *ImpactReportParams) (*response.ImpactReportResult, error) {
request := request.ImpactReport(params.Owner, params.Project, params.BeforeCommitId, params.AfterCommitId)
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 c7d90d6

Please sign in to comment.