Skip to content

Commit

Permalink
Adding validator for packages in package.json. (#116)
Browse files Browse the repository at this point in the history
* Adding validator for packages in `package.json`.

* Renaming function argument to be more verbose.

Co-authored-by: Igor Petkovic <igor@tenderly.co>

* Using the more verbose argument.

* Removing unused variable.

* Changing the check to avoid unexpected behaviour.

Co-authored-by: Igor Petkovic <igor@tenderly.co>

* Limiting `@tenderly/actions` to `<0.1.0` for `V1` runtime.

Co-authored-by: Igor Petkovic <igor@tenderly.co>
  • Loading branch information
g4ndr4 and IgorPetkovic authored Nov 30, 2022
1 parent 9555f08 commit 0a8df71
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 39 deletions.
19 changes: 19 additions & 0 deletions commands/actions/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import (
"github.com/spf13/cobra"
"github.com/tenderly/tenderly-cli/commands"
"github.com/tenderly/tenderly-cli/commands/util"
"github.com/tenderly/tenderly-cli/commands/util/packagejson"
"github.com/tenderly/tenderly-cli/config"
"github.com/tenderly/tenderly-cli/model"
actionsModel "github.com/tenderly/tenderly-cli/model/actions"
"github.com/tenderly/tenderly-cli/rest"
"github.com/tenderly/tenderly-cli/typescript"
"github.com/tenderly/tenderly-cli/userError"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -170,6 +172,23 @@ func mustGetProjectActions(actions map[string]actionsModel.ProjectActions, proje
return &ret
}

func mustValidateDependencies(packageJSON *typescript.PackageJson, validator *packagejson.Validator) (*packagejson.ValidationResult, error) {
depResult, err := validator.Validate(packageJSON.Dependencies)
if err != nil {
return nil, err
}

devDepResult, err := validator.Validate(packageJSON.DevDependencies)
if err != nil {
return nil, err
}

return &packagejson.ValidationResult{
Success: depResult.Success && devDepResult.Success,
Errors: append(depResult.Errors, devDepResult.Errors...),
}, nil
}

func mustInstallDependencies(sourcesDir string) {
exists := util.PackageJSONExists(sourcesDir)
if !exists {
Expand Down
70 changes: 31 additions & 39 deletions commands/actions/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import (
"time"

"github.com/briandowns/spinner"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/tenderly/tenderly-cli/commands"
"github.com/tenderly/tenderly-cli/commands/util"
"github.com/tenderly/tenderly-cli/commands/util/packagejson"
"github.com/tenderly/tenderly-cli/config"
actionsModel "github.com/tenderly/tenderly-cli/model/actions"
"github.com/tenderly/tenderly-cli/rest"
Expand Down Expand Up @@ -123,6 +123,29 @@ func buildFunc(cmd *cobra.Command, args []string) {
}

outDir = actions.Sources

exists := util.PackageJSONExists(actions.Sources)
if exists {
packageJSON := util.MustLoadPackageJSON(actions.Sources)
if util.HasDependencies(packageJSON) {
logrus.Info("\nValidating package.json dependencies...")

validator := packagejson.NewValidator(actions.Runtime)
result, err := mustValidateDependencies(packageJSON, validator)
if err != nil {
userError.LogErrorf("failed to validate package.json packages: %s", err)
os.Exit(1)
}

if !result.Success {
printPackageValidationErrors(result.Errors)
os.Exit(1)
}

logrus.Info("\nFinished validating package.json dependencies...")
}
}

if tsconfig != nil {
outDir = filepath.Join(outDir, *tsconfig.CompilerOptions.OutDir)
mustInstallDependencies(actions.Sources)
Expand Down Expand Up @@ -350,8 +373,6 @@ func mustValidate(
DependenciesVersion: nil,
}

mustValidatePackageJson(actions.Sources)

_, logicHash := util.MustZipAndHashDir(outDir, srcPathInZip, zipLimitBytes)

request.LogicVersion = &logicHash
Expand Down Expand Up @@ -458,43 +479,14 @@ func mustExistCompiledFiles(outDir string, actions *actionsModel.ProjectActions)
}
}

func mustValidatePackageJson(directory string) {
if !util.PackageJSONExists(directory) {
return
}

packageJson := util.MustLoadPackageJSON(directory)

axios := packageJson.Dependencies["axios"]
if axios == "" {
return
}

axiosMaxVersion, err := version.NewVersion("0.27.2")
if err != nil {
func printPackageValidationErrors(validationErrors []*packagejson.ValidationError) {
logrus.Error("The following packages have invalid versions:")
for _, e := range validationErrors {
logrus.Error(commands.Colorizer.Sprintf(
"Cannot parse axios version!",
" %s\n\tFound: %s\n\tRequired: %s",
commands.Colorizer.Bold(commands.Colorizer.Bold(e.Name)),
commands.Colorizer.Bold(commands.Colorizer.Red(e.PackageJsonVersion)),
commands.Colorizer.Bold(commands.Colorizer.Red(e.Constraint)),
))
os.Exit(1)
}

axiosPackageVersion, err := version.NewVersion(strings.TrimLeft(axios, "^~"))
if err != nil {
logrus.Error(commands.Colorizer.Sprintf(
"Cannot parse axios version! Version in `package.json`: %s",
commands.Colorizer.Bold(commands.Colorizer.Red(axios)),
))
os.Exit(1)
}

if axiosPackageVersion.GreaterThan(axiosMaxVersion) {
logrus.Error(commands.Colorizer.Sprintf(
"Invalid axios version - Version %s or lower required. Version in `package.json`: %s",
commands.Colorizer.Bold(commands.Colorizer.Red(axiosMaxVersion.String())),
commands.Colorizer.Bold(commands.Colorizer.Red(axios)),
))
os.Exit(1)
}

return
}
26 changes: 26 additions & 0 deletions commands/util/packagejson/constraints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package packagejson

import "github.com/hashicorp/go-version"

type Constraints map[string]string

func (dc Constraints) findConstraints(dependencyName string) (version.Constraints, error) {
constraintString, ok := dc[dependencyName]
if !ok {
return nil, nil
}

constraints, err := version.NewConstraint(constraintString)
if err != nil {
return nil, err
}

return constraints, nil
}

var runtimesToConstraints = map[string]*Constraints{
"V1": {
"axios": "<1.0.0",
"@tenderly/actions": "<0.1.0",
},
}
140 changes: 140 additions & 0 deletions commands/util/packagejson/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package packagejson

import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/tenderly/tenderly-cli/userError"
)

type Validator struct {
constraints *Constraints
}

func NewValidator(runtimeName string) *Validator {
constraints := runtimesToConstraints[strings.ToUpper(runtimeName)]
if constraints == nil {
return &Validator{
constraints: &Constraints{},
}
}

return &Validator{
constraints: constraints,
}
}

type ValidationError struct {
Name string
PackageJsonVersion string
Constraint string
VersionToBeInstalled string
}

type ValidationResult struct {
Success bool
Errors []*ValidationError
}

func (dv *Validator) Validate(dependencies map[string]string) (*ValidationResult, error) {
var validationErrors []*ValidationError

for packageName, packageVersion := range dependencies {
constraint, err := dv.constraints.findConstraints(packageName)
if err != nil {
return nil, err
}

if constraint == nil {
continue
}

versionToBeInstalled, err := FindPackageVersion(packageName, packageVersion)
if err != nil {
return nil, err
}

parsedVersion, err := version.NewVersion(versionToBeInstalled)
if err != nil {
return nil, errors.New("error parsing version")
}

if !constraint.Check(parsedVersion) {
validationErrors = append(validationErrors, &ValidationError{
Name: packageName,
PackageJsonVersion: packageVersion,
Constraint: constraint.String(),
VersionToBeInstalled: versionToBeInstalled,
})
}
}

return &ValidationResult{
Success: len(validationErrors) == 0,
Errors: validationErrors,
}, nil
}

func FindPackageVersion(name string, version string) (string, error) {
lookup := name + "@" + version

cmd := exec.Command("npm", "view", lookup, "version", "--json")
var out bytes.Buffer
cmd.Stderr = os.Stderr
cmd.Stdout = &out

err := cmd.Start()
if err != nil {
return "", errors.New(fmt.Sprintf("Failed to run: npm view %s version --json", lookup))
}

err = cmd.Wait()
if err != nil {
return "", errors.New(fmt.Sprintf("Failed to run: npm view %s version --json", lookup))
}

ver, err := unmarshalVersion(out)
if err != nil {
return "", err
}

return ver, nil
}

func unmarshalVersion(message bytes.Buffer) (string, error) {
var rawMessage json.RawMessage

err := json.Unmarshal(message.Bytes(), &rawMessage)
if err != nil {
return "", userError.NewUserError(err, "error unmarshalling response from npm")
}

switch rawMessage[0] {
case '"':
var v string
err = json.Unmarshal(rawMessage, &v)
if err != nil {
return "", userError.NewUserError(err, "error unmarshalling response from npm")
}

return v, nil
case '[':
var versions []string
err = json.Unmarshal(rawMessage, &versions)
if err != nil {
return "", userError.NewUserError(err, "error unmarshalling response from npm")
}

highestVersion := versions[len(versions)-1]
return highestVersion, nil
default:
err := errors.New(fmt.Sprintf("Unexpected response from npm: %s", rawMessage))
return "", userError.NewUserError(err, "error unmarshalling response from npm")
}
}
4 changes: 4 additions & 0 deletions commands/util/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ func MustLoadPackageJSON(directory string) *typescript.PackageJson {
func PackageJSONExists(directory string) bool {
return ExistFile(filepath.Join(directory, typescript.PackageJsonFile))
}

func HasDependencies(packageJSON *typescript.PackageJson) bool {
return len(packageJSON.Dependencies)+len(packageJSON.DevDependencies) > 0
}

0 comments on commit 0a8df71

Please sign in to comment.