Skip to content

Commit

Permalink
Add ability to pass environment variables and run from vscode
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Dec 20, 2023
1 parent 28aebca commit 2ac27ec
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 91 deletions.
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
LOCAL_AZURE_ACCESS_TOKEN=
LOCAL_GITHUB_ACCESS_TOKEN=
DEPENDABOT_PROVIDER=
DEPENDABOT_PACKAGE_MANAGER=
DEPENDABOT_REPO=
DEPENDABOT_PROVIDER=github
DEPENDABOT_PACKAGE_MANAGER=go_modules
DEPENDABOT_REPO=rsc/quote
DEPENDABOT_BRANCH=master
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ testdata/caches
cache
out.yaml
./dependabot
dependabot.exe
.env
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "dependabot",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/dependabot",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"args": [
"update"
]
}
]
}
201 changes: 141 additions & 60 deletions cmd/dependabot/internal/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ import (
"net"
"net/url"
"os"
"strings"

"github.com/spf13/pflag"

"github.com/MakeNowJust/heredoc"
"github.com/dependabot/cli/internal/infra"
"github.com/dependabot/cli/internal/model"
"github.com/dependabot/cli/internal/server"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

const (
EnvPrefix = "DEPENDABOT_"
)

var updateCmd = NewUpdateCommand()

func init() {
Expand All @@ -27,8 +35,11 @@ func init() {

type UpdateFlags struct {
SharedFlags
packageManager string
provider string
repo string
directory string
branch string
local string
commit string
dependencies []string
Expand All @@ -46,6 +57,7 @@ func NewUpdateCommand() *cobra.Command {
$ dependabot update go_modules rsc/quote
$ dependabot update -f input.yml
`),
PreRunE: bindViper,
RunE: func(cmd *cobra.Command, args []string) error {
var outFile *os.File
if flags.output != "" {
Expand All @@ -57,12 +69,7 @@ func NewUpdateCommand() *cobra.Command {
defer outFile.Close()
}

input, err := extractInput(cmd, &flags)
if err != nil {
return err
}

err = processInput(input, &flags)
input, err := readUpdateInput(cmd, &flags)
if err != nil {
return err
}
Expand Down Expand Up @@ -100,10 +107,15 @@ func NewUpdateCommand() *cobra.Command {
},
}

viper.AutomaticEnv()

cmd.Flags().StringVarP(&flags.file, "file", "f", "", "path to input file")

cmd.Flags().StringVarP(&flags.packageManager, "package-manager", "m", "", "package manager for the given directory")
cmd.Flags().StringVarP(&flags.provider, "provider", "p", "github", "provider of the repository")
cmd.Flags().StringVarP(&flags.directory, "directory", "d", "/", "directory to update")
cmd.Flags().StringVarP(&flags.repo, "repo", "r", "", "the repo to run dependabot on")
cmd.Flags().StringVarP(&flags.branch, "branch", "b", "", "target branch to update")
cmd.Flags().StringVarP(&flags.commit, "commit", "", "", "commit to update")
cmd.Flags().StringArrayVarP(&flags.dependencies, "dep", "", nil, "dependencies to update")

Expand All @@ -123,7 +135,7 @@ func NewUpdateCommand() *cobra.Command {
return cmd
}

func extractInput(cmd *cobra.Command, flags *UpdateFlags) (*model.Input, error) {
func readUpdateInput(cmd *cobra.Command, flags *UpdateFlags) (*model.Input, error) {
hasFile := flags.file != ""
hasArguments := len(cmd.Flags().Args()) > 0
hasServer := flags.inputServerPort != 0
Expand All @@ -139,14 +151,6 @@ func extractInput(cmd *cobra.Command, flags *UpdateFlags) (*model.Input, error)
return nil, errors.New("can only use one of: input file, arguments, server, or stdin")
}

if hasFile {
return readInputFile(flags.file)
}

if hasArguments {
return readArguments(cmd, flags)
}

if hasServer {
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", flags.inputServerPort))
if err != nil {
Expand All @@ -159,7 +163,44 @@ func extractInput(cmd *cobra.Command, flags *UpdateFlags) (*model.Input, error)
return readStdin()
}

return nil, fmt.Errorf("requires input as arguments, input file, or stdin")
// TODO: better defaults?
input := &model.Input{
Job: model.Job{
PackageManager: flags.packageManager,
AllowedUpdates: []model.Allowed{{
UpdateType: "all",
}},
DependencyGroups: nil,
Dependencies: nil,
ExistingPullRequests: [][]model.ExistingPR{},
IgnoreConditions: []model.Condition{},
LockfileOnly: false,
RequirementsUpdateStrategy: nil,
SecurityAdvisories: []model.Advisory{},
SecurityUpdatesOnly: false,
UpdateSubdependencies: false,
UpdatingAPullRequest: false,
},
}

if hasFile {
if err := applyFileToInput(flags.file, input); err != nil {
return nil, err
}
}

if hasArguments {
if err := applyPositionalArgumentsToInput(cmd, input); err != nil {
return nil, err
}
}

err := processInput(input, flags)
if err != nil {
return nil, err
}

return input, nil
}

func readStdin() (*model.Input, error) {
Expand All @@ -178,77 +219,47 @@ func readStdin() (*model.Input, error) {
return input, nil
}

func readArguments(cmd *cobra.Command, flags *UpdateFlags) (*model.Input, error) {
func applyPositionalArgumentsToInput(cmd *cobra.Command, input *model.Input) error {
if len(cmd.Flags().Args()) != 2 {
return nil, errors.New("requires a package manager and repo argument")
return errors.New("requires a package manager and repo argument")
}

packageManager := cmd.Flags().Args()[0]
if packageManager == "" {
return nil, errors.New("requires a package manager argument")
return errors.New("requires a package manager argument")
}

repo := cmd.Flags().Args()[1]
if repo == "" {
return nil, errors.New("requires a repo argument")
return errors.New("requires a repo argument")
}

allowed := []model.Allowed{{UpdateType: "all"}}
if len(flags.dependencies) > 0 {
allowed = allowed[:0]
for _, dep := range flags.dependencies {
allowed = append(allowed, model.Allowed{DependencyName: dep})
}
}

input := &model.Input{
Job: model.Job{
PackageManager: packageManager,
AllowedUpdates: allowed,
DependencyGroups: nil,
Dependencies: nil,
ExistingPullRequests: [][]model.ExistingPR{},
IgnoreConditions: []model.Condition{},
LockfileOnly: false,
RequirementsUpdateStrategy: nil,
SecurityAdvisories: []model.Advisory{},
SecurityUpdatesOnly: false,
Source: model.Source{
Provider: flags.provider,
Repo: repo,
Directory: flags.directory,
Commit: flags.commit,
Branch: nil,
Hostname: nil,
APIEndpoint: nil,
},
UpdateSubdependencies: false,
UpdatingAPullRequest: false,
},
}
return input, nil
return nil
}

func readInputFile(file string) (*model.Input, error) {
var input model.Input

func applyFileToInput(file string, input *model.Input) error {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("failed to open input file: %w", err)
return fmt.Errorf("failed to open input file: %w", err)
}
if err = json.Unmarshal(data, &input); err != nil {
if err = yaml.Unmarshal(data, &input); err != nil {
return nil, fmt.Errorf("failed to decode input file: %w", err)
return fmt.Errorf("failed to decode input file: %w", err)
}
}

return &input, nil
return nil
}

func processInput(input *model.Input, flags *UpdateFlags) error {
job := &input.Job
// a few of the fields need to be initialized instead of null,
// it would be nice if the updater didn't care
if job.AllowedUpdates == nil {
job.AllowedUpdates = []model.Allowed{{
UpdateType: "all",
}}
}
if job.ExistingPullRequests == nil {
job.ExistingPullRequests = [][]model.ExistingPR{}
}
Expand All @@ -265,6 +276,31 @@ func processInput(input *model.Input, flags *UpdateFlags) error {
job.DependencyGroups = []model.Group{}
}

input.Job.PackageManager = firstNonEmpty(input.Job.PackageManager, flags.packageManager, os.Getenv("DEPENDABOT_PACKAGE_MANAGER"))
input.Job.Source.Provider = firstNonEmpty(input.Job.Source.Provider, flags.provider, os.Getenv("DEPENDABOT_PROVIDER"), "github")
input.Job.Source.Directory = firstNonEmpty(input.Job.Source.Directory, flags.directory, os.Getenv("DEPENDABOT_DIRECTORY"), "/")
input.Job.Source.Repo = firstNonEmpty(input.Job.Source.Repo, flags.repo, os.Getenv("DEPENDABOT_REPO"))

// because the branch is a pointer, we need to get creative to avoid a null-ref
br := ""
if input.Job.Source.Branch != nil {
br = *input.Job.Source.Branch
}
br = firstNonEmpty(br, flags.branch, os.Getenv("DEPENDABOT_BRANCH"))
input.Job.Source.Branch = &br

if input.Job.PackageManager == "" {
return errors.New("package manager is required")
}

if input.Job.Source.Repo == "" {
return errors.New("repo is required")
}

if *input.Job.Source.Branch == "" && input.Job.Source.Commit == "" {
return errors.New("either branch or commit is required")
}

azureRepo := model.NewAzureRepo(input.Job.PackageManager, input.Job.Source.Repo, input.Job.Source.Directory)

// As a convenience, fill in a git_source if credentials are in the environment and a git_source
Expand Down Expand Up @@ -378,10 +414,55 @@ func processInput(input *model.Input, flags *UpdateFlags) error {
return nil
}

func firstNonEmpty(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}

return ""
}

func doesStdinHaveData() bool {
fi, err := os.Stdin.Stat()
if err != nil {
log.Println("file.Stat()", err)
}
return fi.Size() > 0
}

// Viper binding is sourced from: https://github.com/kubereboot/kured/pull/464
// bindViper initializes viper and binds command flags with environment variables
func bindViper(cmd *cobra.Command, args []string) error {
v := viper.New()

v.SetEnvPrefix(EnvPrefix)
v.AutomaticEnv()
bindFlags(cmd, v)

return nil
}

// bindFlags binds each cobra flag to its associated viper configuration (environment variable)
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// Environment variables can't have dashes in them, so bind them to their equivalent keys with underscores
if strings.Contains(f.Name, "-") {
v.BindEnv(f.Name, flagToEnvVar(f.Name))
}

// Apply the viper config value to the flag when the flag is not set and viper has a value
if !f.Changed && v.IsSet(f.Name) {
val := v.Get(f.Name)
log.Printf("Binding %s command flag to environment variable: %s=%s", f.Name, flagToEnvVar(f.Name), fmt.Sprintf("%v", val))
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
}
})
}

// flagToEnvVar converts command flag name to equivalent environment variable name
func flagToEnvVar(flag string) string {
envVarSuffix := strings.ToUpper(strings.ReplaceAll(flag, "-", "_"))
return fmt.Sprintf("%s_%s", EnvPrefix, envVarSuffix)
}
Loading

0 comments on commit 2ac27ec

Please sign in to comment.