// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package lint import ( "context" "fmt" "os" "path" "path/filepath" "strings" "github.com/urfave/cli/v3" "go.woodpecker-ci.org/woodpecker/v3/cli/common" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter" "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) // Command exports the info command. var Command = &cli.Command{ Name: "lint", Usage: "lint a pipeline configuration file", ArgsUsage: "[path/to/.woodpecker.yaml]", Action: lint, Flags: []cli.Flag{ &cli.StringSliceFlag{ Sources: cli.EnvVars("WOODPECKER_PLUGINS_PRIVILEGED"), Name: "plugins-privileged", Usage: "allow plugins to run in privileged mode, if set empty, there is no", }, &cli.StringSliceFlag{ Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"), Name: "plugins-trusted-clone", Usage: "plugins that are trusted to handle Git credentials in cloning steps", Value: constant.TrustedClonePlugins, }, &cli.BoolFlag{ Sources: cli.EnvVars("WOODPECKER_LINT_STRICT"), Name: "strict", Usage: "treat warnings as errors", }, }, } func lint(ctx context.Context, c *cli.Command) error { return common.RunPipelineFunc(ctx, c, lintFile, lintDir) } func lintDir(ctx context.Context, c *cli.Command, dir string) error { var errorStrings []string if err := filepath.Walk(dir, func(path string, info os.FileInfo, e error) error { if e != nil { return e } // check if it is a regular file (not dir) if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) { fmt.Println("#", info.Name()) if err := lintFile(ctx, c, path); err != nil { errorStrings = append(errorStrings, err.Error()) } fmt.Println("") return nil } return nil }); err != nil { return err } if len(errorStrings) != 0 { return fmt.Errorf("ERRORS: %s", strings.Join(errorStrings, "; ")) } return nil } func lintFile(_ context.Context, c *cli.Command, file string) error { fi, err := os.Open(file) if err != nil { return err } defer fi.Close() buf, err := os.ReadFile(file) if err != nil { return err } rawConfig := string(buf) parsedConfig, err := yaml.ParseString(rawConfig) if err != nil { return err } config := &linter.WorkflowConfig{ File: path.Base(file), RawConfig: rawConfig, Workflow: parsedConfig, } // TODO: lint multiple files at once to allow checks for sth like "depends_on" to work err = linter.New( linter.WithTrusted(linter.TrustedConfiguration{ Network: true, Volumes: true, Security: true, }), linter.PrivilegedPlugins(c.StringSlice("plugins-privileged")), linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")), ).Lint([]*linter.WorkflowConfig{config}) if err != nil { str, err := FormatLintError(config.File, err, c.Bool("strict")) if str != "" { fmt.Print(str) } return err } fmt.Println("✅ Config is valid") return nil }