├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── deps │ ├── ci.go │ ├── dev.go │ ├── init.go │ ├── install.go │ ├── lag.go │ ├── main.go │ ├── root.go │ ├── shellhook.go │ ├── test.go │ └── upgrade.go ├── docs ├── .gitignore ├── combine.yml ├── content │ ├── 404.keep.html │ ├── _sidebar.html │ ├── assets │ │ ├── _tailwind.css │ │ └── img │ │ │ ├── .DS_Store │ │ │ ├── dependencies-logo.svg │ │ │ ├── open-graph.png │ │ │ ├── screenshots │ │ │ ├── bitbucket-app-password.png │ │ │ ├── bitbucket-pipeline-manual.png │ │ │ ├── bitbucket-pipeline-schedule.png │ │ │ ├── bitbucket-pipeline-variables.png │ │ │ ├── circleci-context.png │ │ │ ├── deps-lockfile-pr.png │ │ │ ├── deps-manifest-pr.png │ │ │ ├── github-actions-secrets.png │ │ │ ├── gitlab-ci-pipeline-schedule.png │ │ │ └── gitlab-personal-access-token.png │ │ │ └── supporting │ │ │ ├── bitbucket-icon.svg │ │ │ ├── bitbucket.svg │ │ │ ├── circleci-icon.png │ │ │ ├── circleci.png │ │ │ ├── composer.png │ │ │ ├── docker.png │ │ │ ├── git.png │ │ │ ├── github.png │ │ │ ├── gitlab.svg │ │ │ ├── golang.png │ │ │ ├── javascript.png │ │ │ ├── lerna.svg │ │ │ ├── npm.svg │ │ │ ├── php.svg │ │ │ ├── python.png │ │ │ ├── ruby.svg │ │ │ ├── travisci-icon.png │ │ │ ├── travisci.png │ │ │ └── yarn.png │ ├── base.template.html │ ├── bitbucket.md │ ├── ci.md │ ├── config.md │ ├── docker.md │ ├── dotnet.md │ ├── favicon.ico │ ├── git.md │ ├── github.md │ ├── gitlab.md │ ├── go.md │ ├── index.html │ ├── install.sh │ ├── java.md │ ├── javascript.md │ ├── local.md │ ├── other-ci.md │ ├── permanent │ │ ├── github-readme-logotype.png │ │ └── logotype.png │ ├── php.md │ ├── pricing.html │ ├── python.md │ ├── quickstart.md │ ├── ruby.md │ ├── rust.md │ ├── support.html │ ├── swift.md │ └── terraform.md ├── netlify.toml ├── requirements.txt ├── scripts │ ├── install │ ├── update-components │ └── work ├── tailwind.config.js └── wip │ ├── building.md │ ├── schema.md │ ├── settings.md │ └── testing.md ├── go.mod ├── go.sum ├── internal ├── billing │ └── main.go ├── cache │ └── main.go ├── changelogs │ └── main.go ├── ci │ ├── bitbucketpipelines │ │ └── main.go │ ├── circleci │ │ └── main.go │ ├── generic │ │ └── main.go │ ├── githubactions │ │ └── main.go │ ├── gitlabci │ │ └── main.go │ ├── main.go │ └── travisci │ │ └── main.go ├── component │ ├── act.go │ ├── collect.go │ ├── config.go │ ├── install.go │ └── runner.go ├── config │ ├── config.go │ ├── config_dependency.go │ ├── config_dependency_test.go │ ├── config_lockfile_updates.go │ ├── config_manifest_updates.go │ ├── config_test.go │ ├── find.go │ ├── inference.go │ ├── inference_test.go │ ├── settings.go │ └── testdata │ │ ├── repo │ │ ├── Dockerfile-dev │ │ ├── Gemfile │ │ ├── Pipfile │ │ ├── app │ │ │ ├── requirements.txt │ │ │ ├── requirements_test.txt │ │ │ └── third │ │ │ │ └── requirements.txt │ │ ├── composer.json │ │ ├── package.json │ │ └── vendor │ │ │ └── requirements_test.txt │ │ ├── v2_full.yml │ │ └── v2_minimal.yml ├── env │ ├── main.go │ └── main_test.go ├── filefinder │ └── main.go ├── git │ ├── main.go │ └── main_test.go ├── hooks │ └── main.go ├── install │ └── lockfiles.go ├── lag │ └── main.go ├── output │ └── main.go ├── pullrequest │ ├── bitbucket │ │ ├── main.go │ │ ├── repo.go │ │ └── utils.go │ ├── github │ │ ├── main.go │ │ ├── main_test.go │ │ ├── repo.go │ │ ├── testdata │ │ │ └── action_dependencies.json │ │ ├── utils.go │ │ └── utils_test.go │ ├── gitlab │ │ ├── main.go │ │ ├── main_test.go │ │ ├── repo.go │ │ ├── testdata │ │ │ └── action_dependencies.json │ │ └── utils.go │ ├── main.go │ └── main_test.go ├── runner │ ├── ci.go │ ├── ci_test.go │ ├── local.go │ ├── main.go │ ├── testdata │ │ ├── single_dependency.json │ │ ├── single_lockfile.json │ │ └── two_dependencies.json │ ├── update.go │ ├── updates.go │ └── updates_test.go ├── schemaext │ ├── dependencies_test.go │ ├── id.go │ ├── lockfile.go │ ├── main.go │ ├── manifest.go │ ├── testdata │ │ ├── single_body.txt │ │ ├── single_body_empty_manifest.txt │ │ ├── single_dependency.json │ │ ├── single_dependency_empty_manifest.json │ │ ├── single_lockfile.json │ │ ├── single_lockfile.txt │ │ ├── two_body.txt │ │ └── two_dependencies.json │ └── utils.go ├── test │ ├── compare.go │ ├── config.go │ └── main.go └── version │ └── main.go ├── pkg └── schema │ ├── dependencies.go │ ├── dependency.go │ ├── lockfile.go │ ├── manifest.go │ └── version.go └── scripts ├── generate-dist ├── generate-install-script ├── go-install ├── install ├── pre-commit ├── run └── test /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: ["*"] 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.16 21 | - 22 | name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v2 24 | with: 25 | version: latest 26 | args: release --rm-dist 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/setup-go@v2 9 | with: 10 | go-version: 1.16 11 | - uses: actions/checkout@v2 12 | - run: ./scripts/install 13 | - run: ./scripts/test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .env* 3 | /pullrequest 4 | /deps 5 | /dist 6 | .docker 7 | 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, build with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/theme"] 2 | path = docs/theme 3 | url = https://github.com/dropseed/dropseed-docs-theme 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: deps 2 | release: 3 | github: 4 | owner: dropseed 5 | name: deps 6 | builds: 7 | - goos: 8 | - linux 9 | - darwin 10 | goarch: 11 | - amd64 12 | - "386" 13 | - arm64 14 | goarm: 15 | - "6" 16 | main: ./cmd/deps 17 | ldflags: -s -w -X github.com/dropseed/deps/internal/version.version={{.Version}} -X github.com/dropseed/deps/internal/version.commit={{.Commit}} -X github.com/dropseed/deps/internal/version.date={{.Date}} 18 | binary: deps 19 | env: 20 | - CGO_ENABLED=0 21 | archives: 22 | - format: tar.gz 23 | name_template: '{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ 24 | .Arm }}{{ end }}' 25 | files: 26 | - LICENSE* 27 | - README* 28 | snapshot: 29 | name_template: SNAPSHOT-{{ .Commit }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TBD - *not open source* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Deps 4 | 5 |

6 | 7 | # deps [![GitHub release](https://img.shields.io/github/release/dropseed/deps.svg)](https://github.com/dropseed/deps/releases) 8 | 9 | **Deps is a command line tool for staying on top of dependencies. It runs updates, automates pull requests, and keeps your local installations in check.** 10 | 11 | This repo contains the code for the `deps` command line tool itself (written in Go), 12 | but each language/ecosystem has it's own repo which often uses the native language and acts as a light wrapper around the native dependency management tools. 13 | This way deps can automate updates using the same tools that you would use in your terminal. 14 | 15 | [Read the docs →](https://www.dependencies.io) 16 | 17 | 18 | ![deps overview flowchart](https://user-images.githubusercontent.com/649496/111809843-675b1000-88a3-11eb-8b5c-d85d71cb5a25.png) 19 | 20 | The key features of deps are: 21 | 22 | - **Native languages and tools**: The goal is to wrap the native package managers when possible (npm, yarn, pipenv, composer, etc.), so the updates delivered by deps are the same as updates you would make yourself on the command line. 23 | - **Manifests vs Lockfiles**: If you use `"react": "^17.0.0"` in your package.json, we'll send you a pull request when 18.0.0 comes out. This is an out-of-range update to a direct dependency. But when react 17.1.2 is released, all you need to do is update your lockfile (package-lock.json or yarn.lock). In JavaScript, for example, your lockfile can be outdated daily between all of your direct and indirect (transitive) dependencies. These are in-range updates to direct and indirect (transitive) dependencies, and deps will send you a single rolling pull request to keep your lockfile up-to-date. 24 | ![Lockfile and manifest pull requests](https://user-images.githubusercontent.com/649496/119998663-87d7d280-bf96-11eb-8e73-4c686cc08c34.png) 25 | - **Runs in an environment you control**: Deps runs in the same CI environment that you use for testing. You have full control over the container/host and system requirements. 26 | - **Pluggable ecosystem**: We maintain a set of "official" components, but new or bespoke dependency types can be supported by pointing to a different component repo. 27 | 28 | ## Quick install 29 | 30 | ```console 31 | $ curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 32 | ``` 33 | 34 | ## Official components 35 | 36 | - [dropseed/deps-python](https://github.com/dropseed/deps-python) 37 | - [dropseed/deps-js](https://github.com/dropseed/deps-js) 38 | - [dropseed/deps-php](https://github.com/dropseed/deps-php) 39 | - [dropseed/deps-git](https://github.com/dropseed/deps-git) 40 | - [dropseed/deps-manual](https://github.com/dropseed/deps-manual) 41 | - [dropseed/deps-wordpress-core](https://github.com/dropseed/deps-wordpress-core) 42 | - [dropseed/deps-wordpress-plugins](https://github.com/dropseed/deps-wordpress-plugins) 43 | - [dropseed/deps-wordpress-themes](https://github.com/dropseed/deps-wordpress-themes) 44 | 45 | ## License 46 | 47 | The license for the command line tool itself (this repo) is TBD. The individual components are all open-source, usually MIT licensed. 48 | -------------------------------------------------------------------------------- /cmd/deps/ci.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dropseed/deps/internal/output" 5 | "github.com/dropseed/deps/internal/runner" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ciManual bool 10 | var ciTypes []string 11 | var ciPaths []string 12 | var ciQuiet bool 13 | 14 | var ciCMD = &cobra.Command{ 15 | Use: "ci", 16 | Short: "Update all dependencies of the current branch, as pull requests", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | // CI will run verbose by default 19 | if !ciQuiet { 20 | output.Verbosity = 1 21 | } 22 | 23 | auto := !ciManual 24 | if err := runner.CI(auto, ciTypes, ciPaths); err != nil { 25 | printErrAndExitFailure(err) 26 | } 27 | }, 28 | } 29 | 30 | func init() { 31 | ciCMD.Flags().BoolVarP(&ciManual, "manual", "m", false, "do not automatically configure repo") 32 | ciCMD.Flags().BoolVarP(&ciQuiet, "quiet", "q", false, "disable verbose output") 33 | ciCMD.Flags().StringArrayVarP(&ciTypes, "type", "t", []string{}, "only run on specified dependency types (use ! to negate)") 34 | ciCMD.Flags().StringArrayVarP(&ciPaths, "path", "p", []string{}, "only run on specified dependency paths (use ! to negate)") 35 | rootCmd.AddCommand(ciCMD) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/deps/dev.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var devCmd = &cobra.Command{ 8 | Use: "dev", 9 | Short: "Component development commands", 10 | } 11 | 12 | func init() { 13 | rootCmd.AddCommand(devCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/deps/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/dropseed/deps/internal/output" 9 | 10 | "github.com/dropseed/deps/internal/config" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var initCmd = &cobra.Command{ 15 | Use: "init", 16 | Short: "Create a deps config in the current", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | cfg, err := config.InferredConfigFromDir(".") 19 | if err != nil { 20 | printErrAndExitFailure(err) 21 | } 22 | inferred, err := cfg.DumpYAML() 23 | if err != nil { 24 | printErrAndExitFailure(err) 25 | } 26 | 27 | filename := config.DefaultFilenames[0] 28 | 29 | if _, err := os.Stat(filename); !os.IsNotExist(err) { 30 | printErrAndExitFailure(fmt.Errorf("%s already exists!", filename)) 31 | } 32 | 33 | fmt.Printf("Generating config...\n\n%s\n", inferred) 34 | 35 | err = ioutil.WriteFile(filename, []byte(inferred), 0644) 36 | if err != nil { 37 | printErrAndExitFailure(err) 38 | } 39 | output.Success("✔ saved as %s", filename) 40 | }, 41 | } 42 | 43 | func init() { 44 | rootCmd.AddCommand(initCmd) 45 | } 46 | -------------------------------------------------------------------------------- /cmd/deps/install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dropseed/deps/internal/install" 7 | "github.com/dropseed/deps/internal/lag" 8 | "github.com/dropseed/deps/internal/output" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var installCmd = &cobra.Command{ 13 | Use: "install", 14 | Short: "Install dependencies in this directory", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | cwd, err := os.Getwd() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | lockfiles := install.FindLockfiles(cwd) 22 | 23 | if len(lockfiles) < 1 { 24 | return 25 | } 26 | 27 | output.Event("Running install for:") 28 | for _, lockfile := range lockfiles { 29 | output.Event("- %s", lockfile.RelPath()) 30 | } 31 | 32 | lagManager, err := lag.NewLagManager() 33 | if err != nil { 34 | printErrAndExitFailure(err) 35 | } 36 | 37 | for _, lockfile := range lockfiles { 38 | if err := lockfile.Install(); err == nil { 39 | id := lag.IdentifierForFile(lockfile.Path) 40 | lagManager.SaveLockfileIdentifier(lockfile.Path, id) 41 | } else { 42 | printErrAndExitFailure(err) 43 | } 44 | } 45 | }, 46 | } 47 | 48 | func init() { 49 | rootCmd.AddCommand(installCmd) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/deps/lag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dropseed/deps/internal/lag" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var lagCmd = &cobra.Command{ 11 | Use: "lag", 12 | Short: "See if installed dependencies are lagging", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | cwd, err := os.Getwd() 15 | if err != nil { 16 | panic(err) 17 | } 18 | if err := lag.Run(cwd); err != nil { 19 | printErrAndExitFailure(err) 20 | } 21 | }, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand(lagCmd) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/deps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dropseed/deps/internal/output" 7 | ) 8 | 9 | func printErrAndExitFailure(err error) { 10 | output.Error(err.Error()) 11 | os.Exit(1) 12 | } 13 | 14 | func main() { 15 | if err := rootCmd.Execute(); err != nil { 16 | printErrAndExitFailure(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/deps/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dropseed/deps/internal/output" 5 | "github.com/dropseed/deps/internal/version" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var verbose bool 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "deps", 13 | Version: version.WithMeta, 14 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 15 | if verbose { 16 | output.Verbosity = 1 17 | } 18 | }, 19 | } 20 | 21 | func init() { 22 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 23 | } 24 | -------------------------------------------------------------------------------- /cmd/deps/shellhook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const hookSlug = "deps" 13 | const hookCmd = "lag" 14 | 15 | type hookContext struct { 16 | // SelfPath is the unescaped absolute path to direnv 17 | SelfPath string 18 | HookCmd string 19 | HookSlug string 20 | } 21 | 22 | const bashHook = ` 23 | _{{.HookSlug}}_hook() { 24 | local previous_exit_status=$?; 25 | trap -- '' SIGINT; 26 | "{{.SelfPath}}" {{.HookCmd}}; 27 | trap - SIGINT; 28 | return $previous_exit_status; 29 | }; 30 | if ! [[ "${PROMPT_COMMAND:-}" =~ _{{.HookSlug}}_hook ]]; then 31 | PROMPT_COMMAND="_{{.HookSlug}}_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" 32 | fi 33 | ` 34 | 35 | const zshHook = ` 36 | _{{.HookSlug}}_hook() { 37 | trap -- '' SIGINT; 38 | "{{.SelfPath}}" {{.HookCmd}}; 39 | trap - SIGINT; 40 | } 41 | typeset -ag precmd_functions; 42 | if [[ -z ${precmd_functions[(r)_{{.HookSlug}}_hook]} ]]; then 43 | precmd_functions=( _{{.HookSlug}}_hook ${precmd_functions[@]} ) 44 | fi 45 | ` 46 | 47 | const usage = `## BASH 48 | 49 | Add the following line at the end of the ~/.bashrc file: 50 | 51 | eval "$(deps shellhook bash)" 52 | 53 | Make sure it appears even after rvm, git-prompt and other shell extensions that manipulate the prompt. 54 | 55 | ## ZSH 56 | 57 | Add the following line at the end of the ~/.zshrc file: 58 | 59 | eval "$(deps shellhook zsh)" 60 | ` 61 | 62 | var shellhookCmd = &cobra.Command{ 63 | Use: "shellhook", 64 | Args: cobra.MinimumNArgs(1), 65 | Hidden: true, 66 | Long: usage, 67 | Run: func(cmd *cobra.Command, args []string) { 68 | if os.Getenv("DEPS_DISABLED") == "1" || strings.ToLower(os.Getenv("DEPS_DISABLED")) == "true" { 69 | return 70 | } 71 | 72 | shellType := args[0] 73 | 74 | selfPath, err := os.Executable() 75 | if err != nil { 76 | printErrAndExitFailure(err) 77 | } 78 | 79 | // Convert Windows path if needed 80 | selfPath = strings.Replace(selfPath, "\\", "/", -1) 81 | ctx := hookContext{ 82 | SelfPath: selfPath, 83 | HookCmd: hookCmd, 84 | HookSlug: hookSlug, 85 | } 86 | 87 | hookStr := "" 88 | 89 | if shellType == "bash" { 90 | hookStr = bashHook 91 | } else if shellType == "zsh" { 92 | hookStr = zshHook 93 | } else { 94 | printErrAndExitFailure(fmt.Errorf("unknown target shell '%s'", shellType)) 95 | } 96 | 97 | hookTemplate, err := template.New("hook").Parse(hookStr) 98 | if err != nil { 99 | printErrAndExitFailure(err) 100 | } 101 | 102 | err = hookTemplate.Execute(os.Stdout, ctx) 103 | if err != nil { 104 | printErrAndExitFailure(err) 105 | } 106 | }, 107 | } 108 | 109 | func init() { 110 | rootCmd.AddCommand(shellhookCmd) 111 | } 112 | -------------------------------------------------------------------------------- /cmd/deps/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dropseed/deps/internal/test" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var testCmd = &cobra.Command{ 9 | Use: "test", 10 | Short: "Run component tests", 11 | Run: func(cmd *cobra.Command, args []string) { 12 | if err := test.Run(); err != nil { 13 | printErrAndExitFailure(err) 14 | } 15 | }, 16 | } 17 | 18 | func init() { 19 | devCmd.AddCommand(testCmd) 20 | // Set these variables directly in the test module 21 | testCmd.Flags().BoolVarP(&test.UpdateOutputData, "update", "u", false, "Update output data") 22 | testCmd.Flags().BoolVarP(&test.LooseOutputDataComparison, "loose", "l", false, "Loose output data comparison") 23 | testCmd.Flags().BoolVarP(&test.ExitEarly, "exit-early", "x", false, "Exit on first failure or error") 24 | testCmd.Flags().StringVar(&test.FilterName, "filter", "", "Filter test cases by name substring") 25 | } 26 | -------------------------------------------------------------------------------- /cmd/deps/upgrade.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dropseed/deps/internal/runner" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var upgradeCmd = &cobra.Command{ 9 | Use: "upgrade", 10 | Aliases: []string{"update"}, 11 | Short: "Locally upgrade deps in the current directory", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | if err := runner.Local(); err != nil { 14 | printErrAndExitFailure(err) 15 | } 16 | }, 17 | } 18 | 19 | func init() { 20 | rootCmd.AddCommand(upgradeCmd) 21 | } 22 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /output 2 | /.venv 3 | /node_modules 4 | /.cache 5 | -------------------------------------------------------------------------------- /docs/combine.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - run: "./theme/node_modules/.bin/parcel build theme/content/assets/_app.js --out-dir output/assets --out-file app.js" 3 | watch: "./theme/node_modules/.bin/parcel watch theme/content/assets/_app.js --out-dir output/assets --out-file app.js" 4 | - run: "./theme/node_modules/.bin/tailwind -i ./content/assets/_tailwind.css -o ./output/assets/tailwind.css" 5 | watch: "./theme/node_modules/.bin/tailwind -i ./content/assets/_tailwind.css -o ./output/assets/tailwind.css --watch" 6 | - run: "./theme/node_modules/.bin/pitchfork index output -c .content" 7 | 8 | variables: 9 | name: Deps 10 | support_url: "/support/" 11 | login_url: "https://3.dependencies.io/login/" 12 | base_url: 13 | default: "https://docs.dependencies.io" 14 | from_env: URL # netlify 15 | version: 16 | default: "\"latest\"" 17 | from_env: COMMIT_REF 18 | google_tag_manager_id: 19 | from_env: GOOGLE_TAG_MANAGER_ID 20 | -------------------------------------------------------------------------------- /docs/content/404.keep.html: -------------------------------------------------------------------------------- 1 | {% extends "base.template.html" %} 2 | 3 | {% set title = "Page not found" %} 4 | {% set description = "Couldn't find this page. It may have moved to a new location." %} 5 | 6 | {% block content %} 7 |
8 |

404!

9 |

We can't find the page you're looking for.

10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /docs/content/assets/_tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | h1 { @apply mb-6 text-3xl font-extrabold tracking-wide; } 5 | h2 { @apply pt-4 mb-4 text-2xl font-medium border-b border-gray-300; } 6 | h3 { @apply pt-2 mb-2 text-xl font-medium; } 7 | h4 { @apply pt-2 mb-2 text-lg font-medium; } 8 | h5 { @apply pt-1 mb-1 font-medium; } 9 | 10 | .markdown ul { 11 | @apply pl-8 mb-4 list-disc; 12 | } 13 | .markdown ol { 14 | @apply pl-8 mb-4 list-decimal; 15 | } 16 | .markdown li { 17 | @apply mb-1; 18 | } 19 | 20 | p a, ul a { 21 | @apply text-blue-600; 22 | } 23 | p a:hover, ul a:hover { 24 | @apply underline; 25 | } 26 | a code { 27 | @apply text-gray-600 bg-blue-50; 28 | font-size: .95em; 29 | padding: 0 2px; 30 | } 31 | 32 | p { 33 | margin-bottom: 1rem; 34 | } 35 | 36 | p code, li code { 37 | @apply text-gray-600 bg-blue-50; 38 | font-size: .95em; 39 | padding: 0 2px; 40 | } 41 | 42 | h3 > code { 43 | @apply font-bold; 44 | } 45 | 46 | table { 47 | @apply w-full mb-8; 48 | } 49 | th { 50 | @apply p-2 bg-gray-200 border border-gray-400; 51 | } 52 | td { 53 | @apply p-2 border; 54 | } 55 | 56 | blockquote { 57 | @apply p-4 mb-4 italic text-gray-700 bg-gray-200 rounded; 58 | } 59 | blockquote p:last-child { @apply m-0; } 60 | 61 | details { @apply mb-4; } 62 | 63 | .highlight { 64 | background-color: #282c34; 65 | @apply p-4 mb-8 text-sm text-blue-200 rounded shadow-md; 66 | } 67 | .highlight pre { white-space: pre-wrap; word-break: break-word; } 68 | .highlight .hll { background-color: #4f424c } 69 | .highlight .c { color: #776e71 } /* Comment */ 70 | .highlight .err { color: #ef6155 } /* Error */ 71 | .highlight .k { color: #815ba4 } /* Keyword */ 72 | .highlight .l { color: #a5cadc } /* Literal */ 73 | .highlight .n { color: #e7e9db } /* Name */ 74 | .highlight .o { color: #5bc4bf } /* Operator */ 75 | .highlight .p { color: #e7e9db } /* Punctuation */ 76 | .highlight .ch { color: #776e71 } /* Comment.Hashbang */ 77 | .highlight .cm { color: #776e71 } /* Comment.Multiline */ 78 | .highlight .cp { color: #776e71 } /* Comment.Preproc */ 79 | .highlight .cpf { color: #776e71 } /* Comment.PreprocFile */ 80 | .highlight .c1 { color: #776e71 } /* Comment.Single */ 81 | .highlight .cs { color: #776e71 } /* Comment.Special */ 82 | .highlight .gd { color: #ef6155 } /* Generic.Deleted */ 83 | .highlight .ge { font-style: italic } /* Generic.Emph */ 84 | .highlight .gh { color: #e7e9db; font-weight: bold } /* Generic.Heading */ 85 | .highlight .gi { color: #48b685 } /* Generic.Inserted */ 86 | .highlight .gp { color: #776e71; font-weight: bold } /* Generic.Prompt */ 87 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 88 | .highlight .gu { color: #5bc4bf; font-weight: bold } /* Generic.Subheading */ 89 | .highlight .kc { color: #815ba4 } /* Keyword.Constant */ 90 | .highlight .kd { color: #815ba4 } /* Keyword.Declaration */ 91 | .highlight .kn { color: #5bc4bf } /* Keyword.Namespace */ 92 | .highlight .kp { color: #815ba4 } /* Keyword.Pseudo */ 93 | .highlight .kr { color: #815ba4 } /* Keyword.Reserved */ 94 | .highlight .kt { color: #fec418 } /* Keyword.Type */ 95 | .highlight .ld { color: #48b685 } /* Literal.Date */ 96 | .highlight .m { color: #a5cadc } /* Literal.Number */ 97 | .highlight .s { color: #48b685 } /* Literal.String */ 98 | .highlight .na { color: #06b6ef } /* Name.Attribute */ 99 | .highlight .nb { color: #e7e9db } /* Name.Builtin */ 100 | .highlight .nc { color: #fec418 } /* Name.Class */ 101 | .highlight .no { color: #48b685 } /* Name.Constant */ 102 | .highlight .nd { color: #5bc4bf } /* Name.Decorator */ 103 | .highlight .ni { color: #e7e9db } /* Name.Entity */ 104 | .highlight .ne { color: #ef6155 } /* Name.Exception */ 105 | .highlight .nf { color: #06b6ef } /* Name.Function */ 106 | .highlight .nl { color: #e7e9db } /* Name.Label */ 107 | .highlight .nn { color: #fec418 } /* Name.Namespace */ 108 | .highlight .nx { color: #06b6ef } /* Name.Other */ 109 | .highlight .py { color: #e7e9db } /* Name.Property */ 110 | .highlight .nt { color: #a5cadc } /* Name.Tag */ 111 | .highlight .nv { color: #ef6155 } /* Name.Variable */ 112 | .highlight .ow { color: #5bc4bf } /* Operator.Word */ 113 | .highlight .w { color: #e7e9db } /* Text.Whitespace */ 114 | .highlight .mb { color: #a5cadc } /* Literal.Number.Bin */ 115 | .highlight .mf { color: #a5cadc } /* Literal.Number.Float */ 116 | .highlight .mh { color: #a5cadc } /* Literal.Number.Hex */ 117 | .highlight .mi { color: #a5cadc } /* Literal.Number.Integer */ 118 | .highlight .mo { color: #a5cadc } /* Literal.Number.Oct */ 119 | .highlight .sa { color: #48b685 } /* Literal.String.Affix */ 120 | .highlight .sb { color: #48b685 } /* Literal.String.Backtick */ 121 | .highlight .sc { color: #e7e9db } /* Literal.String.Char */ 122 | .highlight .dl { color: #48b685 } /* Literal.String.Delimiter */ 123 | .highlight .sd { color: #776e71 } /* Literal.String.Doc */ 124 | .highlight .s2 { color: #48b685 } /* Literal.String.Double */ 125 | .highlight .se { color: #a5cadc } /* Literal.String.Escape */ 126 | .highlight .sh { color: #48b685 } /* Literal.String.Heredoc */ 127 | .highlight .si { color: #a5cadc } /* Literal.String.Interpol */ 128 | .highlight .sx { color: #48b685 } /* Literal.String.Other */ 129 | .highlight .sr { color: #48b685 } /* Literal.String.Regex */ 130 | .highlight .s1 { color: #48b685 } /* Literal.String.Single */ 131 | .highlight .ss { color: #48b685 } /* Literal.String.Symbol */ 132 | .highlight .bp { color: #e7e9db } /* Name.Builtin.Pseudo */ 133 | .highlight .fm { color: #06b6ef } /* Name.Function.Magic */ 134 | .highlight .vc { color: #ef6155 } /* Name.Variable.Class */ 135 | .highlight .vg { color: #ef6155 } /* Name.Variable.Global */ 136 | .highlight .vi { color: #ef6155 } /* Name.Variable.Instance */ 137 | .highlight .vm { color: #ef6155 } /* Name.Variable.Magic */ 138 | .highlight .il { color: #a5cadc } /* Literal.Number.Integer.Long */ 139 | 140 | @tailwind utilities; 141 | -------------------------------------------------------------------------------- /docs/content/assets/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/.DS_Store -------------------------------------------------------------------------------- /docs/content/assets/img/open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/open-graph.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/bitbucket-app-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/bitbucket-app-password.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/bitbucket-pipeline-manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/bitbucket-pipeline-manual.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/bitbucket-pipeline-schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/bitbucket-pipeline-schedule.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/bitbucket-pipeline-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/bitbucket-pipeline-variables.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/circleci-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/circleci-context.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/deps-lockfile-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/deps-lockfile-pr.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/deps-manifest-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/deps-manifest-pr.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/github-actions-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/github-actions-secrets.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/gitlab-ci-pipeline-schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/gitlab-ci-pipeline-schedule.png -------------------------------------------------------------------------------- /docs/content/assets/img/screenshots/gitlab-personal-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/screenshots/gitlab-personal-access-token.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/bitbucket-icon.svg: -------------------------------------------------------------------------------- 1 | Bitbucket-icon-blue -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/bitbucket.svg: -------------------------------------------------------------------------------- 1 | Bitbucket-blue -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/circleci-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/circleci-icon.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/circleci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/circleci.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/composer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/composer.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/docker.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/git.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/github.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/gitlab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/golang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/golang.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/javascript.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/npm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/python.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/travisci-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/travisci-icon.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/travisci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/travisci.png -------------------------------------------------------------------------------- /docs/content/assets/img/supporting/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/assets/img/supporting/yarn.png -------------------------------------------------------------------------------- /docs/content/base.template.html: -------------------------------------------------------------------------------- 1 | {% extends "theme.template.html" %} 2 | 3 | {% block head_top %} 4 | 23 | {% endblock %} 24 | 25 | {% block body_class %}text-gray-700{% endblock %} 26 | {% block sidebar_class %}bg-white md:bg-transparent{% endblock %} 27 | 28 | {% block sidebar_home_link %} 29 | 30 | Dependencies.io logo 32 | Dependencies.io 33 | 34 | {% endblock %} 35 | 36 | {% block navbar_content_right %} 37 | Get a $DEPS_TOKEN 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /docs/content/bitbucket.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bitbucket" 3 | description: "Examples and instructions for setting up deps in Bitbucket" 4 | --- 5 | 6 | # Bitbucket 7 | 8 | Run deps in Bitbucket via a custom pipeline that only runs from a schedule. 9 | 10 | If languages are split across containers, 11 | use the `--type` option to tell deps which updates to run. 12 | 13 | ```yaml 14 | # bitbucket-pipelines.yml 15 | clone: 16 | depth: full 17 | 18 | pipelines: 19 | custom: 20 | deps: 21 | - parallel: 22 | - step: 23 | image: "python:3.7" 24 | script: 25 | - curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 26 | - python3 -m venv .venv 27 | - .venv/bin/pip install -r requirements.txt 28 | - $HOME/bin/deps ci --type python 29 | 30 | - step: 31 | image: "node:latest" 32 | script: 33 | - curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 34 | - yarn install 35 | - $HOME/bin/deps ci --type js 36 | ``` 37 | 38 | ## Pipeline repository variables 39 | 40 | Deps will need API access through an app password. 41 | You can use your personal account to do this, or a "bot" account that your team has. 42 | 43 | 1. Log in with the account you want deps to use (this will be the author of deps pull requests) 44 | 1. Give it access to the repo you're setting up 45 | 1. Generate a new app password with the repositories and pull requests `write` scopes 46 | [![Bitbucket app password settings for deps](/assets/img/screenshots/bitbucket-app-password.png)](/assets/img/screenshots/bitbucket-app-password.png) 47 | 1. Set the required environment variables in your CI 48 | - `DEPS_BITBUCKET_USERNAME` to the user who owns the app password 49 | - `DEPS_BITBUCKET_PASSWORD` to the app password from above 50 | - `DEPS_TOKEN` to the token from [3.dependencies.io](https://3.dependencies.io/) 51 | 52 | [![Bitbucket pipeline variables for deps](/assets/img/screenshots/bitbucket-pipeline-variables.png)](/assets/img/screenshots/bitbucket-pipeline-variables.png) 53 | 54 | ## Pipeline schedule 55 | 56 | Create a daily or weekly schedule to run your new deps pipeline. 57 | 58 | [![Bitbucket pipeline schedule for deps](/assets/img/screenshots/bitbucket-pipeline-schedule.png)](/assets/img/screenshots/bitbucket-pipeline-schedule.png) 59 | 60 | ## Test or run pipeline manually 61 | 62 | If you want to test your new pipeline without waiting for the schedule, 63 | just navigate to the branches view and click "run pipeline". 64 | 65 | [![Bitbucket pipeline manual run for deps](/assets/img/screenshots/bitbucket-pipeline-manual.png)](/assets/img/screenshots/bitbucket-pipeline-manual.png) 66 | 67 | ## Pull request settings 68 | 69 | When working with a Bitbucket repo, 70 | there are a few settings you can use to determine what your pull requests look like. 71 | 72 | ```yaml 73 | # deps.yml 74 | version: 3 75 | dependencies: 76 | - type: python 77 | settings: 78 | bitbucket_destination: "dev" # branch name 79 | bitbucket_close_source_branch: true 80 | bitbucket_reviewers: 81 | - uuid: "{638373c3b62-8120-4f0c-a7bc-87800b9d6f70}" 82 | ``` 83 | 84 | If you don't need a `deps.yml` then you can also configure these settings via environment variables. 85 | This is an easy way to put settings directly in your CI config. 86 | 87 | Note that they'll need to be in the format of a JSON-encoded string, 88 | with an uppercase name prefixed by `DEPS_SETTING_`. 89 | 90 | ```console 91 | $ DEPS_SETTING_BITBUCKET_CLOSE_SOURCE_BRANCH='true' deps ci 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/content/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Automate dependency updates in CI 3 | description: Use deps to automatically create and update pull requests on GitHub, GitLab, and Bitbucket. 4 | --- 5 | # Using deps in CI 6 | 7 | Most CI systems have a cron or scheduled job function, 8 | which is how we recommend using `deps ci`. 9 | This way you can decide how often you want updates (daily, weekly, monthly, etc.) and keep slow dependency updates out of your normal commit pipeline. 10 | This way you'll also get dependency updates even if you haven't committed to the repo recently. 11 | 12 | The basic steps for setting up `deps ci` are: 13 | 14 | - Copy your `DEPS_TOKEN` from [3.dependencies.io](https://3.dependencies.io) 15 | - Add a scheduled job that installs and runs `deps ci` 16 | - Set any other required environment variables and secrets 17 | 18 | Supported platforms: 19 | 20 | - [GitHub](/github/) 21 | - [GitLab](/gitlab/) 22 | - [Bitbucket](/bitbucket/) 23 | - [Other CI providers](/other-ci/) 24 | 25 | ## Autoconfigure 26 | 27 | By default, `deps ci` automatically detects and configures various settings for common CI providers. 28 | This includes setting `git config` user name and email, 29 | and changing the git remote from ssh to https. 30 | 31 | To disable the automatic configuration (if you have other requirements or want to do it yourself) use `deps ci --manual`. 32 | 33 | For specifics on what is configured and how, you can [read the code here](https://github.com/dropseed/deps/search?l=Go&q=autoconfigure). 34 | 35 | ## Filtering by type or path 36 | 37 | In some container-based CI systems, 38 | you'll only have certain languages and requirements installed in certain containers. 39 | 40 | You can use the `--type` and `--path` options to run the appropriate updates based on the container you're in. For example, use `deps ci --type js` in your container with your JavaScript environment and `deps ci --type python` in your Python container. Likewise you can use `deps ci --path docs`. Both `--type` and `--path` also support negation, allowing you to do `--type js` in one container and `--type !js` in another. 41 | -------------------------------------------------------------------------------- /docs/content/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docker 3 | description: Automated updates and pull requests for Docker dependencies. 4 | --- 5 | 6 | # Docker 7 | 8 | > Docs coming soon! 9 | -------------------------------------------------------------------------------- /docs/content/dotnet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: .NET 3 | description: Automated updates and pull requests for .NET dependencies. 4 | --- 5 | 6 | # .NET 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/favicon.ico -------------------------------------------------------------------------------- /docs/content/git.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Git 3 | description: Automated updates and pull requests for Git dependencies. 4 | --- 5 | 6 | # Git 7 | 8 | This component allows you to track remote repositories (on GitHub or elsewhere) and do a find-and-replace in your repo when new tags are pushed. 9 | 10 | This is especially useful for dependencies that don't use a package manager. 11 | 12 | ## Example `deps.yml` 13 | 14 | ```yaml 15 | version: 3 16 | dependencies: 17 | - type: git 18 | settings: 19 | remotes: 20 | https://github.com/kubernetes/minikube.git: 21 | replace_in_files: 22 | - filename: dev/install.go 23 | # pattern is a regex that must have 1 capture group 24 | pattern: minikube version (\S+) 25 | # you can optionally disable semver parsing on the tags 26 | # (which means latest version will be the last tag) 27 | semver: false 28 | 29 | https://github.com/dropseed/deps-git.git: 30 | replace_in_files: 31 | - filename: file.txt 32 | pattern: deps-git (\S+) 33 | # use a semver range to limit updates 34 | # https://github.com/blang/semver#ranges 35 | range: "< 1.0.0" 36 | 37 | https://github.com/getsentry/sentry-javascript.git: 38 | replace_in_files: 39 | - filename: file.txt 40 | pattern: raven==(\S+) 41 | # only use tags with this prefix (and remove the prefix so we just get the version number) 42 | tag_prefix: raven-js@ 43 | # include semver pre-releases 44 | prereleases: true 45 | 46 | https://github.com/libevent/libevent.git: 47 | replace_in_files: 48 | - filename: file.txt 49 | pattern: libevent (\S+) 50 | # filter tags to those that match a specific pattern, and use the captured 51 | # group as the version name (i.e. you'll get "2.1.10" instead of "release-2.1.10") 52 | tag_filter: 53 | matching: 'release-(\S+)-stable' 54 | output_as: '$1' 55 | 56 | https://github.com/libevent/libevent.git: 57 | replace_in_files: 58 | - filename: file.txt 59 | pattern: libevent (\S+) 60 | # filter tags to those that match a specific pattern, and use the 61 | # full tag name as the version 62 | tag_filter: 63 | matching: 'release-\S+-stable' 64 | 65 | https://github.com/curl/curl.git: 66 | replace_in_files: 67 | - filename: file.txt 68 | pattern: curl==(\S+) 69 | 70 | tag_filter: 71 | matching: 'curl-(\d+)_(\d+)_(\d+)' 72 | sort_as: '$1.$2.$3' # sort as a semver-compatible version, without affecting output 73 | ``` 74 | 75 | ## Support 76 | 77 | Any questions or issues with this specific component should be discussed in [GitHub issues](https://github.com/dropseed/deps-git/issues). 78 | 79 | If there is private information which needs to be shared then please use the private support channels in [dependencies.io](https://www.dependencies.io/contact/). 80 | -------------------------------------------------------------------------------- /docs/content/github.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "GitHub" 3 | description: "Examples and instructions for setting up deps in GitHub" 4 | --- 5 | 6 | # GitHub 7 | 8 | Deps is designed to run directly inside of GitHub Actions, using a workflow very similar to what you would use for tests. 9 | 10 | You'll need two things: 11 | 12 | 1. A `DEPS_TOKEN` from [3.dependencies.io](https://3.dependencies.io) (set this as a [repo](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) or [org](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-an-organization) "secret"). 13 | 1. A `DEPS_GITHUB_TOKEN` that is either a [personal access token](#personal-access-token) or a [GitHub App token](#github-app-token). 14 | 15 | ```yaml 16 | # .github/workflows/deps.yml 17 | name: deps 18 | 19 | on: 20 | schedule: 21 | - cron: 0 0 * * Mon # Weekly 22 | # cron: 0 0 * * * # Daily 23 | # cron: 0 0 1 * * # Monthly 24 | workflow_dispatch: {} 25 | 26 | jobs: 27 | deps: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | 32 | # Configure your languages and package managers 33 | - uses: actions/setup-python@v2 34 | with: 35 | python-version: 3.8 36 | - run: | 37 | pip install -U pip pipenv 38 | 39 | # Install and run deps 40 | - run: curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 41 | - run: $HOME/bin/deps ci 42 | env: 43 | DEPS_TOKEN: ${{ secrets.DEPS_TOKEN }} 44 | DEPS_GITHUB_TOKEN: ${{ secrets.DEPS_GITHUB_TOKEN }} 45 | ``` 46 | 47 | ## GitHub API access 48 | 49 | Deps will need access to the GitHub API to manage pull requests. 50 | The easiest way to do this is with a [personal access token](#personal-access-token), 51 | but a [GitHub App](#github-app-token) is recommended for larger organizations. 52 | 53 | We generally recommend *not* using the default `${{ secrets.GITHUB_TOKEN }}`, 54 | as it won't trigger your other workflows to run on the deps commits. 55 | 56 | ### Personal access token 57 | 58 | 1. Log in with the account you want deps to use (this will be the author of deps pull requests) 59 | 1. Give it access to the repo you're setting up 60 | 1. [Generate a new token](https://github.com/settings/tokens) with the `repo` scope 61 | 1. Add the new token as a repo or organization `DEPS_GITHUB_TOKEN` "secret" 62 | 1. Set the `DEPS_GITHUB_TOKEN` environment variable in your CI as `${{ secrets.DEPS_GITHUB_TOKEN }}` 63 | 64 | ```yaml 65 | # .github/workflows/deps.yml 66 | - run: $HOME/bin/deps ci 67 | env: 68 | DEPS_GITHUB_TOKEN: ${{ secrets.DEPS_GITHUB_TOKEN }} 69 | ... 70 | ``` 71 | 72 | ### GitHub App token 73 | 74 | > We may publish more detailed instructions for this in the future. 75 | [Contact us if you have questions or need help.](https://www.dependencies.io/contact/) 76 | 77 | 1. [Create an internal GitHub app in your organization](https://developer.github.com/apps/building-github-apps/creating-a-github-app/) 78 | 1. Give it access to the repo you're setting up 79 | 1. Set the required environment variables in your "secrets" and then in your workflow 80 | - `DEPS_GITHUB_APP_KEY` - base64 encoded private key 81 | - `DEPS_GITHUB_APP_ID` - your app ID 82 | - `DEPS_GITHUB_APP_INSTALLATION_ID` - the ID for the installation in your org 83 | 84 | Here's an example workflow that uses another GitHub action for generating the app token: 85 | 86 | ```yaml 87 | # .github/workflows/deps.yml 88 | steps: 89 | - id: generate_token 90 | uses: tibdex/github-app-token@v1 91 | with: 92 | app_id: ${{ secrets.DEPS_GITHUB_APP_ID }} 93 | private_key: ${{ secrets.DEPS_GITHUB_APP_KEY }} 94 | 95 | - uses: actions/checkout@v2 96 | with: 97 | token: ${{ steps.generate_token.outputs.token }} 98 | 99 | - run: curl https://deps.app/install.sh | bash -s -- -b $HOME/bin 100 | - run: $HOME/bin/deps ci 101 | env: 102 | DEPS_TOKEN: ${{ secrets.DEPS_TOKEN }} 103 | DEPS_GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 104 | ``` 105 | 106 | ## Pull request settings 107 | 108 | When working with a GitHub repo, 109 | there are a few settings you can use to determine what your PRs look like. 110 | 111 | ```yaml 112 | # deps.yml 113 | version: 3 114 | dependencies: 115 | - type: python 116 | settings: 117 | github_labels: ["dependencies"] 118 | github_base_branch: test 119 | github_assignees: ["user1"] 120 | github_milestone: 1 121 | ``` 122 | 123 | If you don't need a `deps.yml` then you can also configure these settings via environment variables. 124 | This is an easy way to put settings directly in your CI config. 125 | 126 | Note that they'll need to be in the format of a JSON-encoded string, 127 | with an uppercase name prefixed by `DEPS_SETTING_`. 128 | 129 | ```console 130 | $ DEPS_SETTING_GITHUB_LABELS='["dependencies"]' deps ci 131 | ``` 132 | 133 | ### Automerge 134 | 135 | You can use `github_automerge` to enable the [GitHub auto-merge setting](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request) 136 | (make sure this is enabled on the repo). 137 | If you use `github_automerge` and the pull request does not have any branch requirements (or has already met them), 138 | then deps will immediately merge the PR manually. 139 | 140 | The `github_automerge` setting should be set to one of the available merge methods (`merge`, `squash`, or `rebase`). 141 | 142 | ```yaml 143 | # deps.yml 144 | version: 3 145 | dependencies: 146 | - type: python 147 | settings: 148 | github_automerge: squash 149 | ``` 150 | -------------------------------------------------------------------------------- /docs/content/gitlab.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "GitLab" 3 | description: "Examples and instructions for setting up deps in GitLab" 4 | --- 5 | 6 | # GitLab 7 | 8 | Deps can run directly in GitLab CI using a [pipeline schedule](#pipeline-schedule). 9 | 10 | If your CI is separated into language-specific containers, 11 | you can use the `--type` option to tell deps which updates to run. 12 | 13 | A minimal example of `.gitlab-ci.yml` would look like this: 14 | 15 | ```yaml 16 | # .gitlab-ci.yml 17 | deps-js: 18 | image: "node:latest" 19 | only: [schedules] 20 | script: 21 | - curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 22 | - yarn install 23 | - $HOME/bin/deps ci --type js 24 | 25 | deps-python: 26 | image: "python:3" 27 | only: [schedules] 28 | script: 29 | - curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 30 | - pipenv sync --dev 31 | - $HOME/bin/deps ci --type python 32 | ``` 33 | 34 | ## Pipeline schedule 35 | 36 | Configure a pipeline schedule for running deps. 37 | It will need two variables: 38 | 39 | 1. A `DEPS_TOKEN` from [3.dependencies.io](https://3.dependencies.io). 40 | 1. A `DEPS_GITLAB_TOKEN` that is a [personal access token](#personal-access-token). 41 | 42 | ![GitLab pipeline schedule for deps](/assets/img/screenshots/gitlab-ci-pipeline-schedule.png) 43 | 44 | 45 | ## Personal access token 46 | 47 | The standard way to give deps write-access to your repo is with a *personal access token*. 48 | You can use your personal account to do this, or a "bot" account that your team has. 49 | 50 | 1. Log in with the account you want deps to use (this will be the author of deps pull requests) 51 | 1. Give it access to the repo you're setting up 52 | 1. [Generate a new token](https://gitlab.com/profile/personal_access_tokens) with the `write_repository` and `api` scopes 53 | [![GitLab personal access token settings for deps](/assets/img/screenshots/gitlab-personal-access-token.png)](/assets/img/screenshots/gitlab-personal-access-token.png) 54 | 1. Set the `DEPS_GITLAB_TOKEN` environment variable in your CI 55 | 56 | 57 | ## Merge request settings 58 | 59 | When working with a GitLab repo, 60 | there are a few settings you can use to determine what your MRs look like. 61 | 62 | ```yaml 63 | # deps.yml 64 | version: 3 65 | dependencies: 66 | - type: python 67 | settings: 68 | gitlab_target_branch: "dev" 69 | gitlab_labels: ["dependencies"] 70 | gitlab_assignee_id: 1 71 | gitlab_assignee_ids: [1, 2] 72 | gitlab_target_project_id: 1 73 | gitlab_milestone_id: 1 74 | gitlab_remove_source_branch: true 75 | gitlab_allow_collaboration: true 76 | gitlab_allow_maintainer_to_push: true 77 | gitlab_squash: true 78 | ``` 79 | 80 | If you don't need a `deps.yml` then you can also configure these settings via environment variables. 81 | This is an easy way to put settings directly in your CI config. 82 | 83 | Note that they'll need to be in the format of a JSON-encoded string, 84 | with an uppercase name prefixed by `DEPS_SETTING_`. 85 | 86 | ```console 87 | $ DEPS_SETTING_GITLAB_LABELS='["dependencies"]' deps ci 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/content/go.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Go 3 | description: Automated updates and pull requests for Go dependencies. 4 | --- 5 | 6 | # Go 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/java.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Java 3 | description: Automated updates and pull requests for Java dependencies. 4 | --- 5 | 6 | # Java 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/javascript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JavaScript 3 | description: Automated updates and pull requests for JavaScript dependencies. 4 | --- 5 | 6 | # JavaScript 7 | 8 | Currently supports: 9 | 10 | - `package.json` 11 | - `package-lock.json` 12 | - `yarn.lock` 13 | 14 | ## Example `deps.yml` 15 | 16 | ```yaml 17 | version: 3 18 | dependencies: 19 | - type: js 20 | path: app # a directory 21 | settings: 22 | # Enable updates for specific kinds of dependencies 23 | # in package.json. 24 | # 25 | # Default: [dependencies, devDependencies] 26 | manifest_package_types: 27 | - dependencies 28 | ``` 29 | 30 | ## Support 31 | 32 | Any questions or issues with this specific component should be discussed in [GitHub issues](https://github.com/dropseed/deps-js/issues). 33 | 34 | If there is private information which needs to be shared then please use the private support channels in [dependencies.io](https://www.dependencies.io/contact/). 35 | -------------------------------------------------------------------------------- /docs/content/local.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using deps locally 3 | description: Interactively update dependencies with the deps command line tool. 4 | --- 5 | 6 | # Using deps locally 7 | 8 | Locally `deps` can: 9 | 10 | - Install dependencies with one command 11 | - Interactively upgrade dependencies (including out-of-range updates) 12 | - Tell you when to re-install (new yarn.lock after `git pull`, etc.) 13 | 14 | [Setting up `deps` in CI](/ci/) will automatically create pull requests for new updates. 15 | 16 | ## Installing 17 | 18 | To install `deps`, 19 | [manually download a release](https://github.com/dropseed/deps/releases) or use the auto-install script: 20 | ```console 21 | $ curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 22 | ``` 23 | 24 | ## Checking for dependency updates 25 | 26 | With `deps` in your `$PATH`, you can check a repo for dependency updates. 27 | You'll get an interactive prompt where you can choose and install updates one-by-one: 28 | 29 | ```console 30 | $ cd project 31 | $ deps upgrade 32 | 33 | No local config found, detecting your dependencies automatically 34 | --- 35 | version: 3 36 | dependencies: 37 | - type: python 38 | path: Pipfile 39 | - type: js 40 | path: . 41 | 42 | --- 43 | Collecting with deps-python... 44 | Collecting with deps-js... 45 | 46 | 2 new updates to be made 47 | [a711128] Update package-lock.json 48 | [2231fa2] Update tailwindcss in package.json from 1.0.5 to 1.1.2 49 | 50 | Use the arrow keys to navigate: ↓ ↑ → ← 51 | ? Choose an update to make: 52 | ▸ Update package-lock.json 53 | Update tailwindcss in package.json from 1.0.5 to 1.1.2 54 | Skip 55 | ``` 56 | 57 | Using `deps upgrade` will perform the same steps as `deps ci`, 58 | but it will *not* commit changes or create pull requests. 59 | 60 | If your dependencies were not found automatically, 61 | or you need a more advanced configuration, 62 | [take a look at `deps.yml`](/config/). 63 | 64 | ## Shell hook 65 | 66 |
67 | 68 |
69 | 70 | The optional deps shell hook will help ensure your local installations are actually accurate. 71 | After you change git branches or run git pull, it's easy to miss dependency changes that you actually need to install. 72 | The deps shell hook will run (quickly!) before every bash/zsh prompt and let you know if you forget to install dependency updates. 73 | 74 | ```bash 75 | # For ZSH, add this to the end of .zshrc 76 | eval "$(deps shellhook zsh)" 77 | 78 | # For BASH, add this to the end of .bashrc (or .bash_profile) 79 | eval "$(deps shellhook bash)" 80 | ``` 81 | 82 | Now when you switch branches or pull dependency commits from team members and bots, 83 | deps will automatically remind you to re-install dependencies so that your installation matches the lockfile. 84 | 85 | ```console 86 | $ cd project 87 | $ git pull 88 | Updating 436b311..49c2cc3 89 | Fast-forward 90 | package-lock.json | 47 ++--- 91 | 92 | [Run `deps install` to sync with poetry.lock] 93 | 94 | $ deps install 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/content/other-ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Other CI providers 3 | description: Examples and instructions for setting up deps in other CI providers. 4 | --- 5 | 6 | # Other CI providers 7 | 8 | Don't see what you need? 9 | [Let us know so we can get it added!](https://www.dependencies.io/contact/) 10 | 11 | The basic steps for using `deps ci` are generally the same, regardless of provider. 12 | 13 | ## 1. Set the environment variables 14 | 15 | - `DEPS_TOKEN` from [3.dependencies.io](https://3.dependencies.io) 16 | - Credentials for your git host (ex. `DEPS_GITHUB_TOKEN`) 17 | 18 | ## 2. Add a scheduled job to run deps 19 | 20 | - Use the install script to get the latest version (`curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin`) 21 | - Run the `deps ci` command 22 | 23 | ## 3. Add a `deps.yml` (optional) 24 | 25 | Most dependencies and languages we support will be detected automatically, 26 | so you may not even need a `deps.yml`. 27 | 28 | But if you need to tweak the settings or point deps to custom dependency locations, 29 | you'll want to [add `deps.yml` to your repo](/config/). 30 | -------------------------------------------------------------------------------- /docs/content/permanent/github-readme-logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/permanent/github-readme-logotype.png -------------------------------------------------------------------------------- /docs/content/permanent/logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/docs/content/permanent/logotype.png -------------------------------------------------------------------------------- /docs/content/php.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PHP 3 | description: Automated updates and pull requests for PHP dependencies. 4 | --- 5 | 6 | # PHP 7 | 8 | Supports `composer.json` and `composer.lock`. 9 | 10 | ## Example `deps.yml` 11 | 12 | ```yaml 13 | version: 3 14 | dependencies: 15 | - type: php 16 | path: app 17 | settings: 18 | # Set the options for composer install/update/require 19 | # 20 | # Default: "--no-progress --no-suggest" 21 | composer_options: "--no-scripts" 22 | ``` 23 | 24 | ## Support 25 | 26 | Any questions or issues with this specific component should be discussed in [GitHub issues](https://github.com/dropseed/deps-php/issues). 27 | 28 | If there is private information which needs to be shared then please use the private support channels in [dependencies.io](https://www.dependencies.io/contact/). 29 | -------------------------------------------------------------------------------- /docs/content/python.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Python 3 | description: Automated updates and pull requests for Python dependencies. 4 | --- 5 | 6 | # Python 7 | 8 | Currently supports: 9 | 10 | - `Pipfile` 11 | - `poetry.lock` 12 | - `requirements.txt` (filename doesn't matter) 13 | 14 | ## Example `deps.yml` 15 | 16 | ```yaml 17 | version: 3 18 | dependencies: 19 | - type: python 20 | path: app/requirements.txt 21 | settings: 22 | # Enable updates for specific sections in Pipfile 23 | # 24 | # Default: ["packages", "dev-packages"] 25 | pipfile_sections: 26 | - packages 27 | 28 | # Enable updates for specific sections in Pipfile.lock 29 | # 30 | # Default: ["default", "develop"] 31 | pipfilelock_sections: 32 | - default 33 | 34 | # If your virtualenv is in a specific location that 35 | # isn't found automatically 36 | # 37 | # Default: "" 38 | pip_path: myenv/bin/pip 39 | ``` 40 | 41 | ## Support 42 | 43 | Any questions or issues with this specific component should be discussed in [GitHub issues](https://github.com/dropseed/deps-python/issues). 44 | 45 | If there is private information which needs to be shared then please use the private support channels in [dependencies.io](https://www.dependencies.io/contact/). 46 | -------------------------------------------------------------------------------- /docs/content/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | description: See how the deps command line tools works, and automate your first pull request. 4 | --- 5 | 6 | # Quickstart 7 | 8 | Follow these steps to see how deps works and get your first automated pull request. 9 | 10 | ## 1. Install deps locally 11 | 12 | [Download a release manually](https://github.com/dropseed/deps/releases), or use the auto-install script: 13 | 14 | ```console 15 | $ curl -sSL https://deps.app/install.sh | bash -s -- -b $HOME/bin 16 | ``` 17 | 18 | ## 2. Try deps upgrade 19 | 20 | Open a repo and run `deps upgrade` to see what is outdated and interactively upgrade: 21 | 22 | ```console 23 | $ cd project 24 | $ deps upgrade 25 | 26 | No local config found, detecting your dependencies automatically 27 | --- 28 | version: 3 29 | dependencies: 30 | - type: python 31 | path: Pipfile 32 | - type: js 33 | path: . 34 | 35 | --- 36 | Collecting with deps-python... 37 | Collecting with deps-js... 38 | 39 | 2 new updates to be made 40 | [a711128] Update package-lock.json 41 | [2231fa2] Update tailwindcss in package.json from 1.0.5 to 1.1.2 42 | 43 | Use the arrow keys to navigate: ↓ ↑ → ← 44 | ? Choose an update to make: 45 | ▸ Update package-lock.json 46 | Update tailwindcss in package.json from 1.0.5 to 1.1.2 47 | Skip 48 | ``` 49 | 50 | ## 3. Install deps in CI 51 | 52 | To automate pull requests, use the `deps ci` command in a cron or scheduled job on your CI provider. 53 | How often the job runs will determine how often pull requests are created and updated. 54 | 55 | You will need a `DEPS_TOKEN` in order to run `deps ci` ([see pricing](/pricing/)). 56 | 57 | 58 | 59 | Supported platforms: 60 | 61 | - [GitHub](/github/) 62 | - [GitLab](/gitlab/) 63 | - [Bitbucket](/bitbucket/) 64 | - [Other CI providers](/other-ci/) 65 | -------------------------------------------------------------------------------- /docs/content/ruby.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ruby 3 | description: Automated updates and pull requests for Ruby dependencies. 4 | --- 5 | 6 | # Ruby 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/rust.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rust 3 | description: Automated updates and pull requests for Rust dependencies. 4 | --- 5 | 6 | # Rust 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/support.html: -------------------------------------------------------------------------------- 1 | {% extends "base.template.html" %} 2 | 3 | {% set title = "Support" %} 4 | {% set description = "Get help with the deps command line tool, through email or public GitHub issues." %} 5 | 6 | {% block content %} 7 | 8 |

Contact Support

9 |

10 | Use the form below to submit a private support request and we'll follow up via email. 11 | For public issues, see the dropseed/deps repo on GitHub. 12 |

13 | 14 |
15 |
16 |
17 | 19 |
20 |
21 | 23 |
24 |
25 | 28 |
29 |
30 | 32 |
33 |
34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /docs/content/swift.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Swift 3 | description: Automated updates and pull requests for Swift dependencies. 4 | --- 5 | 6 | # Swift 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/content/terraform.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Terraform 3 | description: Automated updates and pull requests for Terraform dependencies. 4 | --- 5 | 6 | # Terraform 7 | 8 | > This component is a work-in-progress while we port things over from dependencies.io v2. 9 | If you're interested in this one specifically, 10 | [please let us know!](https://www.dependencies.io/contact/) 11 | -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "output" 3 | command = "cd theme && ./scripts/install && cd .. && combine build" 4 | 5 | 6 | [[redirects]] 7 | from = "/contact/" 8 | to = "/support/" 9 | 10 | [[redirects]] 11 | from = "https://docs.dependencies.io/*" 12 | to = "https://www.dependencies.io/:splat" 13 | status = 301 14 | force = true 15 | 16 | [[redirects]] 17 | from = "/github-redirect/*" 18 | to = "https://github.com/:splat" 19 | status = 302 20 | 21 | [[redirects]] 22 | from = "/apply/" 23 | to = "https://www.dropseed.dev/free/" 24 | 25 | [[redirects]] 26 | from = "/travisci/" 27 | to = "/other-ci/" 28 | 29 | [[redirects]] 30 | from = "/circleci/" 31 | to = "/other-ci/" 32 | 33 | [[redirects]] 34 | from = "/github-actions/" 35 | to = "/github/" 36 | 37 | [[redirects]] 38 | from = "/gitlab-ci/" 39 | to = "/gitlab/" 40 | 41 | [[redirects]] 42 | from = "/bitbucket-pipelines/" 43 | to = "/bitbucket/" 44 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is managed automatically by combine 2 | combine==2.7.0 3 | -------------------------------------------------------------------------------- /docs/scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | curl -sSL https://barrel.dev/install.py | python3 - combine --reinstall 3 | cd theme && yarn install && cd .. 4 | -------------------------------------------------------------------------------- /docs/scripts/update-components: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | echo "---\ntitle: JavaScript\ndescription: Automated updates and pull requests for JavaScript dependencies.\n---\n\n# JavaScript" > content/javascript.md 4 | curl -s https://raw.githubusercontent.com/dropseed/deps-js/master/README.md?d=$(date +%s) | tail -n +2 >> content/javascript.md 5 | 6 | echo "---\ntitle: Python\ndescription: Automated updates and pull requests for Python dependencies.\n---\n\n# Python" > content/python.md 7 | curl -s https://raw.githubusercontent.com/dropseed/deps-python/master/README.md?d=$(date +%s) | tail -n +2 >> content/python.md 8 | 9 | echo "---\ntitle: PHP\ndescription: Automated updates and pull requests for PHP dependencies.\n---\n\n# PHP" > content/php.md 10 | curl -s https://raw.githubusercontent.com/dropseed/deps-php/master/README.md?d=$(date +%s) | tail -n +2 >> content/php.md 11 | 12 | echo "---\ntitle: Git\ndescription: Automated updates and pull requests for Git dependencies.\n---\n\n# Git" > content/git.md 13 | curl -s https://raw.githubusercontent.com/dropseed/deps-git/master/README.md?d=$(date +%s) | tail -n +2 >> content/git.md 14 | -------------------------------------------------------------------------------- /docs/scripts/work: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | ./.venv/bin/combine work --port 8070 3 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const docsTheme = require("./theme/tailwind.config.js") 2 | 3 | module.exports = docsTheme 4 | -------------------------------------------------------------------------------- /docs/wip/building.md: -------------------------------------------------------------------------------- 1 | # Building a dependencies.io component 2 | 3 | - local or git repo 4 | - dependencies_component.yml 5 | - install 6 | - collect 7 | - act 8 | - manifests 9 | - dependencies updated individually by default 10 | - json example 11 | - lockfiles 12 | - any updates to a lockfile will be grouped together (the lockfile is either up to date or not) 13 | - json example 14 | - testing 15 | -------------------------------------------------------------------------------- /docs/wip/schema.md: -------------------------------------------------------------------------------- 1 | # Dependencies.io JSON schema 2 | 3 | The data that a component collects needs to follow a specific, universal format. 4 | That format is defined by a [JSON schema](https://json-schema.org/) and the data 5 | is automatically validated using the schema when you run `deps component 6 | collect`. 7 | 8 | ```json 9 | { 10 | "lockfiles": { 11 | "example_lockfile.json": { 12 | "current": { 13 | "dependencies": { 14 | "package1": { 15 | "installed": { 16 | "name": "1.1.0" 17 | }, 18 | "source": "example-package-manager" 19 | } 20 | }, 21 | "fingerprint": "d8db5538e62deadd2174b03d7b4ef7e2" 22 | }, 23 | "updated": { 24 | "dependencies": { 25 | "package1": { 26 | "installed": { 27 | "name": "1.2.0" 28 | }, 29 | "source": "example-package-manager" 30 | } 31 | }, 32 | "fingerprint": "42c294a77caca9723baf339634a6b9ec" 33 | } 34 | } 35 | }, 36 | "manifests": { 37 | "example_manifest.json": { 38 | "current": { 39 | "dependencies": { 40 | "package1": { 41 | "available": [ 42 | { 43 | "name": "2.0.0" 44 | } 45 | ], 46 | "constraint": "> 1.0.0", 47 | "source": "example-package-manager" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/wip/settings.md: -------------------------------------------------------------------------------- 1 | # Dependencies.io component settings 2 | 3 | When someone uses your component, they can pass in settings by adding them to 4 | their `dependencies.yml` config. Those settings are given to the container as 5 | JSON-encoded strings, with the name of `SETTING_{all-caps name from config}`. 6 | 7 | For example: 8 | ```yaml 9 | version: 2 10 | dependencies: 11 | - type: python 12 | path: requirements.txt 13 | settings: 14 | python_version: "3.5.6" 15 | ``` 16 | 17 | The container will be given `SETTING_PYTHON_VERSION="3.5.6"` which can be used 18 | at runtime to further customize the behavior of the component. 19 | 20 | A number of other settings are added to your component automatically by deps, like 21 | `commit_message_prefix: "(chore) "` and other PR-specific options. 22 | -------------------------------------------------------------------------------- /docs/wip/testing.md: -------------------------------------------------------------------------------- 1 | # Testing a dependencies.io component 2 | 3 | The deps binary comes with a testing framework for components. You can install 4 | it on your machine and in CI to ensure that your component gives the proper 5 | output based on specified input. 6 | 7 | ## Installing deps on your machine 8 | 9 | ```console 10 | $ curl -sSL https://deps.app/install.sh | bash -s 2.5.0-beta.1 11 | ``` 12 | 13 | Or, you can download binaries manually from the [releases on 14 | GitHub](https://github.com/dropseed/deps/releases). 15 | 16 | ## Writing tests 17 | 18 | Test cases are written in a YAML file named `dependencies_test.yml`. These will 19 | be discovered automatically. The YAML format is as follows... 20 | 21 | ### Collector tests 22 | 23 | ```yaml 24 | cases: 25 | - name: basic 26 | type: collector 27 | dockerfile: Dockerfile 28 | # path from the repo root to a directory that will be mounted as the test repo 29 | # (it will be turned into a git repo automatically) 30 | repo_contents: tests/collector/basic/repo 31 | output_data_path: tests/collector/basic/expected_output_data.json 32 | # user_config is only required if you need a path other than ".", 33 | # or need to test settings 34 | user_config: 35 | path: / 36 | settings: 37 | example_string: foo 38 | example_list: 39 | - foo 40 | - bar 41 | ``` 42 | 43 | ### Actor tests 44 | 45 | ```yaml 46 | cases: 47 | - name: basic 48 | type: actor 49 | dockerfile: Dockerfile 50 | repo_contents: tests/actor/basic/repo 51 | # input_data_path is required when using "type: actor" 52 | input_data_path: tests/actor/basic/input_data.json 53 | output_data_path: tests/actor/basic/expected_output_data.json 54 | user_config: 55 | path: / 56 | settings: 57 | example_string: foo 58 | example_list: 59 | - foo 60 | - bar 61 | ``` 62 | 63 | Your file structure might look something like this: 64 | ```console 65 | example-component/tests/ 66 | ├── actor 67 | │   ├── basic 68 | │   │   ├── expected_output_data.json 69 | │   │   ├── input_data.json 70 | │   │   └── repo 71 | │   │   ├── example_lockfile.json 72 | │   │   └── example_manifest.json 73 | │   └── dependencies_tests.yml 74 | └── collector 75 | ├── basic 76 | │   ├── expected_output_data.json 77 | │   └── repo 78 | │   ├── example_lockfile.json 79 | │   └── example_manifest.json 80 | └── dependencies_tests.yml 81 | ``` 82 | 83 | ## Running tests 84 | 85 | From the root of your repo: 86 | ```console 87 | $ deps dev test 88 | ``` 89 | 90 | You can read about all of the test options with `deps dev test --help`, but one 91 | of the more commonly used options is to automatically update your 92 | `output_data_path` files. This makes it easy to save the output that your 93 | component generates (especially when dealing with lockfiles). Be sure to 94 | actually review the changes though and ensure it is still generating the output 95 | you expect. 96 | 97 | If your component deals with lockfiles that tend to update frequently (like 98 | `yarn.lock`), you may want to use `deps dev test --loose-output-data-comparison` 99 | while running in CI. This does a less accurate comparison of the output, but 100 | makes it a little easier to keep your CI pipeline running and will still catch 101 | major issues. 102 | 103 | A `.travis.yml` might look like this: 104 | ```yaml 105 | 106 | ``` 107 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropseed/deps 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/bradleyfalzon/ghinstallation v1.1.1 7 | github.com/chzyer/logex v1.1.10 // indirect 8 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 10 | github.com/fatih/color v1.7.0 11 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect 12 | github.com/lunixbochs/vtclean v1.0.0 // indirect 13 | github.com/manifoldco/promptui v0.3.1 14 | github.com/mattn/go-colorable v0.0.9 // indirect 15 | github.com/mattn/go-isatty v0.0.9 // indirect 16 | github.com/mitchellh/mapstructure v1.1.2 17 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 18 | github.com/sergi/go-diff v1.0.0 19 | github.com/spf13/cobra v0.0.5 20 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 21 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 22 | gopkg.in/yaml.v2 v2.2.2 23 | ) 24 | 25 | replace gopkg.in/yaml.v2 => github.com/algobardo/yaml v0.0.0-20180709211108-fd13018f8a5a 26 | -------------------------------------------------------------------------------- /internal/billing/main.go: -------------------------------------------------------------------------------- 1 | package billing 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/dropseed/deps/internal/output" 12 | ) 13 | 14 | const appBaseURL = "https://3.dependencies.io" 15 | 16 | type API struct { 17 | token string 18 | } 19 | 20 | func NewAPI() (*API, error) { 21 | // TODO will later be able to also check a DEPS_KEY for 22 | // users not connected to hosted service 23 | 24 | token := os.Getenv("DEPS_TOKEN") 25 | if token == "" { 26 | return nil, fmt.Errorf("DEPS_TOKEN must be set. Ask your team admin or log in to %s to get your token.", appBaseURL) 27 | } 28 | return &API{ 29 | token: token, 30 | }, nil 31 | } 32 | 33 | func (api *API) Validate() error { 34 | client := &http.Client{} 35 | 36 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/usage/", appBaseURL), nil) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | req.Header.Add("Authorization", "token "+api.token) 42 | req.Header.Add("User-Agent", "deps") 43 | req.Header.Set("Content-Type", "application/json") 44 | 45 | resp, err := client.Do(req) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | body, err := ioutil.ReadAll(resp.Body) 51 | resp.Body.Close() 52 | 53 | if resp.StatusCode >= 400 { 54 | return fmt.Errorf("API call failed with response %d\n\n%s", resp.StatusCode, string(body)) 55 | } 56 | 57 | output.Debug("Authorized with %s", appBaseURL) 58 | 59 | return nil 60 | } 61 | 62 | func (api *API) IncrementUsage(quantity int) error { 63 | inputJSON, err := json.Marshal(map[string]int{ 64 | "quantity": quantity, 65 | }) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | client := &http.Client{} 71 | 72 | req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/usage/", appBaseURL), bytes.NewBuffer(inputJSON)) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | req.Header.Add("Authorization", "token "+api.token) 78 | req.Header.Add("User-Agent", "deps") 79 | req.Header.Set("Content-Type", "application/json") 80 | 81 | resp, err := client.Do(req) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | body, err := ioutil.ReadAll(resp.Body) 87 | resp.Body.Close() 88 | 89 | if resp.StatusCode >= 400 { 90 | return fmt.Errorf("API call failed with response %d\n\n%s", resp.StatusCode, string(body)) 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/cache/main.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | const DefaultCacheDirName = "deps" 9 | 10 | func GetCachePath() string { 11 | userCache, err := os.UserCacheDir() 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | cachePath := path.Join(userCache, DefaultCacheDirName) 17 | if err := os.MkdirAll(cachePath, os.ModePerm); err != nil { 18 | panic(err) 19 | } 20 | return cachePath 21 | } 22 | -------------------------------------------------------------------------------- /internal/changelogs/main.go: -------------------------------------------------------------------------------- 1 | package changelogs 2 | 3 | func GetURL(source, dependency, version string) string { 4 | return "" 5 | } 6 | -------------------------------------------------------------------------------- /internal/ci/bitbucketpipelines/main.go: -------------------------------------------------------------------------------- 1 | package bitbucketpipelines 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type BitbucketPipelines struct { 9 | } 10 | 11 | func Is() bool { 12 | return os.Getenv("BITBUCKET_BUILD_NUMBER") != "" 13 | } 14 | 15 | func (gitlab *BitbucketPipelines) Autoconfigure() error { 16 | return nil 17 | } 18 | 19 | func (gitlab *BitbucketPipelines) Branch() string { 20 | if s := os.Getenv("BITBUCKET_BRANCH"); s != "" { 21 | return s 22 | } 23 | return "" 24 | } 25 | 26 | func GetProjectAPIURL() string { 27 | if slug := os.Getenv("BITBUCKET_REPO_FULL_NAME"); slug != "" { 28 | return fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s", slug) 29 | } 30 | return "" 31 | } 32 | -------------------------------------------------------------------------------- /internal/ci/circleci/main.go: -------------------------------------------------------------------------------- 1 | package circleci 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/git" 9 | "github.com/dropseed/deps/internal/output" 10 | ) 11 | 12 | type CircleCI struct { 13 | } 14 | 15 | func Is() bool { 16 | return os.Getenv("CIRCLECI") != "" 17 | } 18 | 19 | func (circle *CircleCI) Autoconfigure() error { 20 | // CircleCI uses ssh clones by default, 21 | // so try to switch to https 22 | if cmd := exec.Command("git", "config", "--global", "--remove-section", "url.ssh://git@github.com"); cmd != nil { 23 | output.Event("Autoconfigure: %s", strings.Join(cmd.Args, " ")) 24 | cmd.Stdout = os.Stdout 25 | cmd.Stderr = os.Stderr 26 | cmd.Run() // Don't worry about an error 27 | } 28 | 29 | originalOrigin := git.GitRemote() 30 | if updatedOrigin := git.GitRemoteToHTTPS(originalOrigin); originalOrigin != updatedOrigin { 31 | if cmd := exec.Command("git", "remote", "set-url", "origin", updatedOrigin); cmd != nil { 32 | output.Event("Autoconfigure: %s", strings.Join(cmd.Args, " ")) 33 | cmd.Stdout = os.Stdout 34 | cmd.Stderr = os.Stderr 35 | if err := cmd.Run(); err != nil { 36 | return err 37 | } 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (circle *CircleCI) Branch() string { 45 | return os.Getenv("CIRCLE_BRANCH") 46 | } 47 | -------------------------------------------------------------------------------- /internal/ci/generic/main.go: -------------------------------------------------------------------------------- 1 | package generic 2 | 3 | type GenericCI struct { 4 | } 5 | 6 | func (generic *GenericCI) Autoconfigure() error { 7 | return nil 8 | } 9 | 10 | func (generic *GenericCI) Branch() string { 11 | return "" 12 | } 13 | -------------------------------------------------------------------------------- /internal/ci/githubactions/main.go: -------------------------------------------------------------------------------- 1 | package githubactions 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | type GitHubActions struct { 9 | } 10 | 11 | func Is() bool { 12 | return os.Getenv("GITHUB_ACTION") != "" 13 | } 14 | 15 | func (actions *GitHubActions) Autoconfigure() error { 16 | return nil 17 | } 18 | 19 | func (actions *GitHubActions) Branch() string { 20 | if b := os.Getenv("GITHUB_REF"); strings.HasPrefix(b, "refs/heads/") { 21 | return b[11:] 22 | } 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /internal/ci/gitlabci/main.go: -------------------------------------------------------------------------------- 1 | package gitlabci 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | ) 8 | 9 | type GitLabCI struct { 10 | } 11 | 12 | func Is() bool { 13 | return os.Getenv("GITLAB_CI") != "" 14 | } 15 | 16 | func (gitlab *GitLabCI) Autoconfigure() error { 17 | return nil 18 | } 19 | 20 | func (gitlab *GitLabCI) Branch() string { 21 | if b := os.Getenv("CI_COMMIT_REF_NAME"); b != "" { 22 | return b 23 | } 24 | return "" 25 | } 26 | 27 | func GetProjectAPIURL() string { 28 | if base := os.Getenv("CI_API_V4_URL"); base != "" { 29 | path := url.PathEscape(os.Getenv("CI_PROJECT_PATH")) 30 | return fmt.Sprintf("%s/projects/%s", base, path) 31 | } 32 | return "" 33 | } 34 | -------------------------------------------------------------------------------- /internal/ci/main.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/ci/bitbucketpipelines" 9 | "github.com/dropseed/deps/internal/ci/circleci" 10 | "github.com/dropseed/deps/internal/ci/generic" 11 | "github.com/dropseed/deps/internal/ci/githubactions" 12 | "github.com/dropseed/deps/internal/ci/gitlabci" 13 | "github.com/dropseed/deps/internal/ci/travisci" 14 | "github.com/dropseed/deps/internal/output" 15 | ) 16 | 17 | type CIProvider interface { 18 | Autoconfigure() error 19 | Branch() string 20 | } 21 | 22 | func NewCIProvider() CIProvider { 23 | if circleci.Is() { 24 | return &circleci.CircleCI{} 25 | } 26 | if travisci.Is() { 27 | return &travisci.TravisCI{} 28 | } 29 | if githubactions.Is() { 30 | return &githubactions.GitHubActions{} 31 | } 32 | if gitlabci.Is() { 33 | return &gitlabci.GitLabCI{} 34 | } 35 | if bitbucketpipelines.Is() { 36 | return &bitbucketpipelines.BitbucketPipelines{} 37 | } 38 | return &generic.GenericCI{} 39 | } 40 | 41 | func BaseAutoconfigure() { 42 | 43 | gitName := "deps" 44 | gitEmail := "bot@dependencies.io" 45 | 46 | if s := os.Getenv("DEPS_GIT_NAME"); s != "" { 47 | gitName = s 48 | } 49 | if s := os.Getenv("DEPS_GIT_EMAIL"); s != "" { 50 | gitEmail = s 51 | } 52 | 53 | if cmd := exec.Command("git", "config", "user.name", gitName); cmd != nil { 54 | output.Event("Autoconfigure: %s", strings.Join(cmd.Args, " ")) 55 | cmd.Stdout = os.Stdout 56 | cmd.Stderr = os.Stderr 57 | if err := cmd.Run(); err != nil { 58 | panic(err) 59 | } 60 | } 61 | 62 | if cmd := exec.Command("git", "config", "user.email", gitEmail); cmd != nil { 63 | output.Event("Autoconfigure: %s", strings.Join(cmd.Args, " ")) 64 | cmd.Stdout = os.Stdout 65 | cmd.Stderr = os.Stderr 66 | if err := cmd.Run(); err != nil { 67 | panic(err) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/ci/travisci/main.go: -------------------------------------------------------------------------------- 1 | package travisci 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type TravisCI struct { 8 | } 9 | 10 | func Is() bool { 11 | return os.Getenv("TRAVIS") != "" 12 | } 13 | 14 | func (travis *TravisCI) Autoconfigure() error { 15 | return nil 16 | } 17 | 18 | func (travis *TravisCI) Branch() string { 19 | if b := os.Getenv("TRAVIS_PULL_REQUEST_BRANCH"); b != "" { 20 | return b 21 | } 22 | if b := os.Getenv("TRAVIS_BRANCH"); b != "" { 23 | return b 24 | } 25 | return "" 26 | } 27 | -------------------------------------------------------------------------------- /internal/component/act.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/dropseed/deps/internal/output" 9 | "github.com/dropseed/deps/pkg/schema" 10 | ) 11 | 12 | func (r *Runner) Act(inputDependencies *schema.Dependencies) (*schema.Dependencies, error) { 13 | output.Event("Updating with %s", r.Given) 14 | 15 | inputFilename, err := inputTempFile(inputDependencies) 16 | if err != nil { 17 | return nil, err 18 | } 19 | if !output.IsDebug() { 20 | defer os.Remove(inputFilename) 21 | } 22 | 23 | // TODO hooks?? 24 | 25 | outputPath, err := r.run(r.getCommand(r.Config.Act, "act"), inputFilename) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if !output.IsDebug() { 30 | defer os.Remove(outputPath) 31 | } 32 | 33 | outputDependencies, err := schema.NewDependenciesFromJSONPath(outputPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return outputDependencies, nil 39 | } 40 | 41 | func inputTempFile(inputDependencies *schema.Dependencies) (string, error) { 42 | inputJSON, err := json.MarshalIndent(inputDependencies, "", " ") 43 | if err != nil { 44 | return "", err 45 | } 46 | inputFile, err := ioutil.TempFile("", "deps-") 47 | if err != nil { 48 | return "", err 49 | } 50 | if _, err := inputFile.Write(inputJSON); err != nil { 51 | panic(err) 52 | } 53 | return inputFile.Name(), nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/component/collect.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dropseed/deps/internal/output" 7 | "github.com/dropseed/deps/pkg/schema" 8 | ) 9 | 10 | func (r *Runner) Collect(inputPath string) (*schema.Dependencies, error) { 11 | if output.Verbosity > 0 { 12 | output.Event("Collecting with %s...", r.Given) 13 | } else { 14 | output.Event("Collecting with %s...", r.GetName()) 15 | } 16 | output.Debug("Input path: %s", inputPath) 17 | 18 | outputPath, err := r.run(r.getCommand(r.Config.Collect, "collect"), inputPath) 19 | if err != nil { 20 | return nil, err 21 | } 22 | if !output.IsDebug() { 23 | defer os.Remove(outputPath) 24 | } 25 | 26 | output.Debug("Finished") 27 | 28 | dependencies, err := schema.NewDependenciesFromJSONPath(outputPath) 29 | if err != nil { 30 | output.Error("Unable to load output JSON from collector") 31 | return nil, err 32 | } 33 | 34 | return dependencies, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/component/config.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "reflect" 7 | 8 | "github.com/mitchellh/mapstructure" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var DefaultFilenames = []string{ 13 | "deps_component.yml", 14 | ".deps_component.yml", 15 | "dependencies_component.yml", 16 | ".dependencies_component.yml", 17 | } 18 | 19 | type Config struct { 20 | Install string `mapstructure:"install" yaml:"install" json:"install"` 21 | Collect string `mapstructure:"collect" yaml:"collect" json:"collect"` 22 | Act string `mapstructure:"act" yaml:"act" json:"act"` 23 | } 24 | 25 | func NewConfigFromPath(path string) (*Config, error) { 26 | f, err := os.Open(path) 27 | defer f.Close() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return NewConfigFromReader(f) 33 | } 34 | 35 | func NewConfigFromReader(reader io.Reader) (*Config, error) { 36 | temp := map[string]interface{}{} 37 | decoder := yaml.NewDecoder(reader) 38 | decoder.SetDefaultMapType(reflect.TypeOf(map[string]interface{}{})) 39 | if err := decoder.Decode(&temp); err != nil { 40 | return nil, err 41 | } 42 | 43 | return newConfigFromMap(temp) 44 | } 45 | 46 | func newConfigFromMap(m map[string]interface{}) (*Config, error) { 47 | config := &Config{} 48 | 49 | mapDecoderConfig := mapstructure.DecoderConfig{ 50 | Result: config, 51 | ErrorUnused: true, 52 | } 53 | mapDecoder, err := mapstructure.NewDecoder(&mapDecoderConfig) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if err = mapDecoder.Decode(m); err != nil { 59 | return nil, err 60 | } 61 | 62 | return config, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/component/install.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/dropseed/deps/internal/output" 8 | ) 9 | 10 | func (r *Runner) Install() error { 11 | if !r.shouldInstall { 12 | output.Debug("Skipping install of %s", r.Given) 13 | return nil 14 | } 15 | 16 | output.Event("Installing %s", r.Given) 17 | 18 | command := r.getCommand(r.Config.Install, "install") 19 | output.Debug(command) 20 | 21 | cmd := exec.Command("sh", "-c", command) 22 | cmd.Dir = r.Path 23 | cmd.Env = append(os.Environ(), r.Env...) 24 | if output.IsDebug() { 25 | cmd.Stdout = os.Stdout 26 | cmd.Stderr = os.Stderr 27 | } 28 | err := cmd.Run() 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/component/runner.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "strings" 10 | 11 | "github.com/dropseed/deps/internal/cache" 12 | "github.com/dropseed/deps/internal/config" 13 | "github.com/dropseed/deps/internal/git" 14 | 15 | "github.com/dropseed/deps/internal/output" 16 | ) 17 | 18 | type Runner struct { 19 | Index int 20 | Given string 21 | Config *Config 22 | Path string 23 | Env []string 24 | shouldInstall bool 25 | } 26 | 27 | const DefaultRemotePrefix = "dropseed/deps-" 28 | 29 | func NewRunnerFromString(s string) (*Runner, error) { 30 | runner, err := NewRunnerFromPath(s) 31 | 32 | if os.IsNotExist(err) { 33 | runner, err = newRunnerFromRemote(s) 34 | } 35 | 36 | return runner, err 37 | } 38 | 39 | func NewRunnerFromPath(s string) (*Runner, error) { 40 | componentPath := s 41 | 42 | configPath := config.FindFilename(componentPath, DefaultFilenames...) 43 | if configPath == "" { 44 | return nil, os.ErrNotExist 45 | } 46 | 47 | cfg, err := NewConfigFromPath(configPath) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &Runner{ 53 | Given: s, 54 | Config: cfg, 55 | Path: componentPath, 56 | shouldInstall: true, 57 | }, nil 58 | } 59 | 60 | func newRunnerFromRemote(s string) (*Runner, error) { 61 | url := s 62 | 63 | if !strings.Contains(url, "/") { 64 | // shorthand for dropseed/deps-{} 65 | url = DefaultRemotePrefix + url 66 | } 67 | 68 | if !strings.HasPrefix(url, "http") { 69 | // automatically prefix owner/repo with github 70 | url = "https://github.com/" + url 71 | } 72 | 73 | output.Debug("Using component from %s", url) 74 | 75 | depsCache := cache.GetCachePath() 76 | 77 | cloneDirName := path.Base(url) 78 | cloneDirName = strings.Replace(cloneDirName, ".git", "", -1) 79 | clonePath := path.Join(depsCache, "components", cloneDirName) 80 | 81 | output.Debug("Storing component in %s", clonePath) 82 | 83 | cloned := false 84 | cwd, err := os.Getwd() 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | if _, err := os.Stat(clonePath); os.IsNotExist(err) { 90 | git.Clone(url, clonePath) 91 | cloned = true 92 | } else if err != nil { 93 | return nil, err 94 | } 95 | 96 | // run git commands from the new repo 97 | if err := os.Chdir(clonePath); err != nil { 98 | panic(err) 99 | } 100 | 101 | refBefore := "" 102 | 103 | if !cloned { 104 | refBefore = git.CurrentRef() 105 | if err := git.Pull(); err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | // TODO checkout user specified Version 111 | // split @ from string? 112 | // git.Checkout 113 | 114 | refAfter := git.CurrentRef() 115 | 116 | if err := os.Chdir(cwd); err != nil { 117 | panic(err) 118 | } 119 | 120 | runner, err := NewRunnerFromPath(clonePath) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | runner.shouldInstall = refBefore != refAfter 126 | 127 | return runner, nil 128 | } 129 | 130 | func (r *Runner) GetName() string { 131 | return path.Base(r.Path) 132 | } 133 | 134 | func (r *Runner) getCommand(defaultCmd, cmdName string) string { 135 | command := defaultCmd 136 | if override := r.getOverrideFromEnv(cmdName); override != "" { 137 | command = override 138 | } 139 | return command 140 | } 141 | 142 | func (r *Runner) getOverrideFromEnv(name string) string { 143 | override := os.Getenv(fmt.Sprintf("DEPS_%d_%s", r.Index, strings.ToUpper(name))) 144 | if override != "" { 145 | output.Event("Overriding %s command from env", name) 146 | } 147 | return override 148 | } 149 | 150 | func (r *Runner) run(command string, inputPath string) (string, error) { 151 | tmpfile, err := ioutil.TempFile("", "deps-") 152 | if err != nil { 153 | return "", err 154 | } 155 | outputPath := tmpfile.Name() 156 | 157 | commandString := fmt.Sprintf("%s %s %s", command, inputPath, outputPath) 158 | 159 | output.Debug(commandString) 160 | 161 | cmd := exec.Command( 162 | "sh", 163 | "-c", 164 | commandString, 165 | ) 166 | 167 | if output.IsDebug() { 168 | cmd.Stdin = os.Stdin 169 | cmd.Stdout = os.Stdout 170 | cmd.Stderr = os.Stderr 171 | } 172 | 173 | cmd.Env = r.Env 174 | cmd.Env = append(cmd.Env, fmt.Sprintf("DEPS_COMPONENT_PATH=%s", r.Path)) 175 | if err != nil { 176 | return "", err 177 | } 178 | 179 | err = cmd.Run() 180 | if err != nil { 181 | return "", err 182 | } 183 | 184 | output.Debug(outputPath) 185 | 186 | return outputPath, nil 187 | } 188 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | 10 | "github.com/dropseed/deps/internal/output" 11 | "github.com/mitchellh/mapstructure" 12 | yaml "gopkg.in/yaml.v2" 13 | ) 14 | 15 | var DefaultFilenames = []string{ 16 | "deps.yml", 17 | ".deps.yml", 18 | "dependencies.yml", 19 | ".dependencies.yml", 20 | } 21 | 22 | const Version = 3 23 | 24 | // Config stores a dependencies.yml config 25 | type Config struct { 26 | Version int `mapstructure:"version" yaml:"version" json:"version"` 27 | Dependencies []*Dependency `mapstructure:"dependencies" yaml:"dependencies" json:"dependencies"` 28 | } 29 | 30 | func (config *Config) Compile() { 31 | for _, dependency := range config.Dependencies { 32 | dependency.Compile() 33 | } 34 | } 35 | 36 | func LoadOrInferConfigFromPath(configpath string) (*Config, error) { 37 | config, err := NewConfigFromPath(configpath) 38 | if os.IsNotExist(err) { 39 | output.Event("No local config found, inferring one from your files") 40 | config, err = InferredConfigFromDir(filepath.Dir(configpath)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | } else if err != nil { 45 | return nil, err 46 | } 47 | 48 | return config, nil 49 | } 50 | 51 | // NewConfigFromPath loads a Config from a file 52 | func NewConfigFromPath(path string) (*Config, error) { 53 | f, err := os.Open(path) 54 | defer f.Close() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return NewConfigFromReader(f) 60 | } 61 | 62 | func NewConfigFromReader(reader io.Reader) (*Config, error) { 63 | temp := map[string]interface{}{} 64 | decoder := yaml.NewDecoder(reader) 65 | decoder.SetDefaultMapType(reflect.TypeOf(map[string]interface{}{})) 66 | if err := decoder.Decode(&temp); err != nil { 67 | return nil, err 68 | } 69 | 70 | return newConfigFromMap(temp) 71 | } 72 | 73 | func newConfigFromMap(m map[string]interface{}) (*Config, error) { 74 | config := &Config{} 75 | 76 | mapDecoderConfig := mapstructure.DecoderConfig{ 77 | Result: config, 78 | ErrorUnused: true, 79 | } 80 | mapDecoder, err := mapstructure.NewDecoder(&mapDecoderConfig) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if err = mapDecoder.Decode(m); err != nil { 86 | return nil, err 87 | } 88 | 89 | if config.Version != Version { 90 | return nil, fmt.Errorf("Config must be version %d", Version) 91 | } 92 | 93 | return config, nil 94 | } 95 | 96 | func (config *Config) DumpYAML() (string, error) { 97 | out, err := yaml.Marshal(config) 98 | if err != nil { 99 | return "", err 100 | } 101 | return string(out), nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/config/config_dependency.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/env" 9 | "github.com/dropseed/deps/pkg/schema" 10 | ) 11 | 12 | // Dependency is a path + type in dependencies.yml 13 | type Dependency struct { 14 | Type string `mapstructure:"type" yaml:"type" json:"type"` 15 | // Version should be a git ref that is checked out/pulled 16 | // Version string `mapstructure:"version,omitempty" yaml:"version,omitempty" json:"version,omitempty"` 17 | Path string `mapstructure:"path,omitempty" yaml:"path,omitempty" json:"path,omitempty"` 18 | Env map[string]string `mapstructure:"env,omitempty" yaml:"env,omitempty" json:"env"` 19 | Settings Settings `mapstructure:"settings,omitempty" yaml:"settings,omitempty" json:"settings"` 20 | LockfileUpdates LockfileUpdates `mapstructure:"lockfile_updates,omitempty" yaml:"lockfile_updates,omitempty" json:"lockfile_updates,omitempty"` 21 | ManifestUpdates ManifestUpdates `mapstructure:"manifest_updates,omitempty" yaml:"manifest_updates,omitempty" json:"manifest_updates,omitempty"` 22 | } 23 | 24 | func (dependency *Dependency) Compile() { 25 | dependency.Path = strings.Trim(dependency.Path, "/") 26 | if dependency.Path == "" { 27 | dependency.Path = "." 28 | } 29 | 30 | // set defaults 31 | if dependency.Settings == nil { 32 | dependency.Settings = Settings{} 33 | } 34 | if dependency.LockfileUpdates.Enabled == nil { 35 | t := true 36 | dependency.LockfileUpdates.Enabled = &t 37 | } 38 | if dependency.LockfileUpdates.Settings == nil { 39 | dependency.LockfileUpdates.Settings = Settings{} 40 | } 41 | if dependency.ManifestUpdates.Enabled == nil { 42 | t := true 43 | dependency.ManifestUpdates.Enabled = &t 44 | } 45 | if dependency.ManifestUpdates.Settings == nil { 46 | dependency.ManifestUpdates.Settings = Settings{} 47 | } 48 | 49 | // if no filters then set the default 1 50 | if len(dependency.ManifestUpdates.Filters) == 0 { 51 | defaultFilter := &Filter{ 52 | Name: ".*", 53 | } 54 | dependency.ManifestUpdates.Filters = append(dependency.ManifestUpdates.Filters, defaultFilter) 55 | } 56 | for _, filter := range dependency.ManifestUpdates.Filters { 57 | if filter.Enabled == nil { 58 | t := true 59 | filter.Enabled = &t 60 | } 61 | if filter.Group == nil { 62 | t := false 63 | filter.Group = &t 64 | } 65 | if filter.Settings == nil { 66 | filter.Settings = Settings{} 67 | } 68 | } 69 | } 70 | 71 | func (dependency *Dependency) Environ() ([]string, error) { 72 | environ := os.Environ() 73 | 74 | environ = append(environ, dependency.Settings.AsEnviron()...) 75 | 76 | for k, v := range dependency.Env { 77 | environ = append(environ, fmt.Sprintf("%s=%s", k, v)) 78 | } 79 | 80 | return environ, nil 81 | } 82 | 83 | func (dependency *Dependency) GetSettingForSchema(name string, deps *schema.Dependencies) interface{} { 84 | // In order of precedence: 85 | // 1. Env 86 | // 2. Settings 87 | // 3. Lockfile settings (if lockfiles) 88 | // 4. Manifest settings (if manifests) 89 | // 5. Manifest filter settings 90 | 91 | value := env.SettingFromEnviron(name) 92 | 93 | if v := dependency.Settings.Get(name); v != nil { 94 | value = v 95 | } 96 | 97 | // Lockfile- and Manifest-specific settings take priority over general settings 98 | 99 | if v := dependency.LockfileUpdates.Settings.Get(name); v != nil && deps.HasLockfiles() { 100 | value = v 101 | } 102 | 103 | if v := dependency.ManifestUpdates.Settings.Get(name); v != nil && deps.HasManifests() { 104 | value = v 105 | } 106 | 107 | // Filters take top priority (and we take the first match) 108 | 109 | for _, filter := range dependency.ManifestUpdates.Filters { 110 | if filter.MatchesEntireSchema(deps) { 111 | if v := filter.Settings.Get(name); v != nil { 112 | return v 113 | } 114 | } 115 | } 116 | 117 | return value 118 | } 119 | -------------------------------------------------------------------------------- /internal/config/config_dependency_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "testing" 4 | 5 | func TestEmptyCompile(t *testing.T) { 6 | dep := Dependency{} 7 | dep.Compile() 8 | if dep.Type != "" { 9 | t.Error("wrong type") 10 | } 11 | if dep.Path != "." { 12 | t.Error("wrong path") 13 | } 14 | if dep.Settings == nil { 15 | t.Error("settings nil") 16 | } 17 | if !*dep.LockfileUpdates.Enabled { 18 | t.Error("lockfile updates disabled") 19 | } 20 | if dep.LockfileUpdates.Settings == nil { 21 | t.Error("lockfile settings nil") 22 | } 23 | if !*dep.ManifestUpdates.Enabled { 24 | t.Error("manifest updates disabled") 25 | } 26 | if dep.ManifestUpdates.Settings == nil { 27 | t.Error("manifest settings nil") 28 | } 29 | if len(dep.ManifestUpdates.Filters) != 1 { 30 | t.Error("manifest filters wrong") 31 | } 32 | filter := dep.ManifestUpdates.Filters[0] 33 | if filter.Name != ".*" { 34 | t.Error("filter name wrong") 35 | } 36 | if !*filter.Enabled { 37 | t.Error("filter disabled") 38 | } 39 | if *filter.Group { 40 | t.Error("filter group wrong") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/config/config_lockfile_updates.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type LockfileUpdates struct { 4 | Enabled *bool `mapstructure:"enabled,omitempty" yaml:"enabled,omitempty" json:"enabled,omitempty"` 5 | Settings Settings `mapstructure:"settings,omitempty" yaml:"settings,omitempty" json:"settings"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/config/config_manifest_updates.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/dropseed/deps/pkg/schema" 7 | ) 8 | 9 | type ManifestUpdates struct { 10 | Enabled *bool `mapstructure:"enabled,omitempty" yaml:"enabled,omitempty" json:"enabled,omitempty"` 11 | Settings Settings `mapstructure:"settings,omitempty" yaml:"settings,omitempty" json:"settings"` 12 | Filters []*Filter `mapstructure:"filters,omitempty" yaml:"filters,omitempty" json:"filters,omitempty"` 13 | } 14 | 15 | type Filter struct { 16 | Name string `mapstructure:"name" yaml:"name" json:"name"` 17 | Enabled *bool `mapstructure:"enabled,omitempty" yaml:"enabled,omitempty" json:"enabled,omitempty"` 18 | Group *bool `mapstructure:"group,omitempty" yaml:"group,omitempty" json:"group,omitempty"` 19 | Settings Settings `mapstructure:"settings,omitempty" yaml:"settings,omitempty" json:"settings"` 20 | } 21 | 22 | func (manifestUpdates *ManifestUpdates) FilteredDependencyGroups(dependencies map[string]*schema.ManifestDependency) (map[string]map[string]*schema.ManifestDependency, error) { 23 | groups := map[string]map[string]*schema.ManifestDependency{} 24 | 25 | dependenciesSeen := map[string]bool{} 26 | 27 | for _, filter := range manifestUpdates.Filters { 28 | for name, dep := range dependencies { 29 | if filter.MatchesName(name) { 30 | 31 | if _, seen := dependenciesSeen[name]; seen { 32 | // dependency will only be grouped in 33 | // the first filter that matched it 34 | continue 35 | } 36 | 37 | dependenciesSeen[name] = true 38 | 39 | if *filter.Enabled { 40 | 41 | groupName := filter.Name 42 | 43 | if !*filter.Group { 44 | // make a new group just for this dependency 45 | // works because it is only seen once anyway 46 | // (won't work if multiple manifests in 1 collector though?) 47 | groupName = name 48 | } 49 | 50 | groupDeps := groups[groupName] 51 | if groupDeps == nil { 52 | groupDeps = map[string]*schema.ManifestDependency{} 53 | } 54 | groupDeps[name] = dep 55 | groups[groupName] = groupDeps 56 | 57 | } 58 | } 59 | } 60 | } 61 | 62 | return groups, nil 63 | } 64 | 65 | func (filter *Filter) MatchesName(name string) bool { 66 | nameRegex := regexp.MustCompile(filter.Name) 67 | return nameRegex.MatchString(name) 68 | } 69 | 70 | func (filter *Filter) MatchesEntireSchema(deps *schema.Dependencies) bool { 71 | // Manifest filters can't match if there are any lockfiles involved (unusual) 72 | if deps.HasLockfiles() { 73 | return false 74 | } 75 | 76 | // The filter has to match *all* deps in the schema to be applied 77 | if deps.HasManifests() { 78 | for _, manifest := range deps.Manifests { 79 | if manifest.HasUpdates() { 80 | for depName := range manifest.Updated.Dependencies { 81 | if !filter.MatchesName(depName) { 82 | return false 83 | } 84 | } 85 | } 86 | } 87 | 88 | return true 89 | } 90 | 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dropseed/deps/pkg/schema" 8 | ) 9 | 10 | func compareToYAML(t *testing.T, config *Config, expected string) { 11 | dumped, err := config.DumpYAML() 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | if expected != dumped { 16 | fmt.Print(dumped) 17 | t.FailNow() 18 | } 19 | } 20 | 21 | func TestFull(t *testing.T) { 22 | config, err := NewConfigFromPath("./testdata/v2_full.yml") 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | if len(config.Dependencies) != 2 { 27 | t.FailNow() 28 | } 29 | expected := `version: 3 30 | dependencies: 31 | - type: js 32 | - type: python 33 | path: requirements.txt 34 | settings: 35 | ok: true 36 | lockfile_updates: 37 | enabled: false 38 | manifest_updates: 39 | enabled: true 40 | filters: 41 | - name: django-braces 42 | enabled: false 43 | - name: django-.* 44 | settings: 45 | github_labels: 46 | - django 47 | - name: django 48 | - name: .* 49 | ` 50 | compareToYAML(t, config, expected) 51 | } 52 | 53 | func TestMinimal(t *testing.T) { 54 | config, err := NewConfigFromPath("./testdata/v2_minimal.yml") 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | expected := `version: 3 60 | dependencies: 61 | - type: python 62 | path: requirements.txt 63 | ` 64 | compareToYAML(t, config, expected) 65 | } 66 | 67 | func TestConfigFromMap(t *testing.T) { 68 | m := map[string]interface{}{ 69 | "version": Version, 70 | } 71 | config, err := newConfigFromMap(m) 72 | 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | if config.Version != Version { 78 | t.FailNow() 79 | } 80 | } 81 | 82 | func TestFilterSettings(t *testing.T) { 83 | // create a schema with a manifest update 84 | deps, err := schema.NewDependenciesFromJSONPath("../runner/testdata/single_dependency.json") 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | 89 | depConfig := &Dependency{ 90 | Settings: Settings{ 91 | "github_labels": []string{"test"}, 92 | }, 93 | ManifestUpdates: ManifestUpdates{ 94 | Filters: []*Filter{ 95 | { 96 | Name: "pullrequest", 97 | Settings: Settings{ 98 | "github_labels": []string{"pullrequest"}, 99 | }, 100 | }, 101 | { 102 | Name: ".*", 103 | Settings: Settings{ 104 | "github_labels": []string{"all"}, 105 | }, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | value := depConfig.GetSettingForSchema("github_labels", deps) 112 | if len(value.([]string)) != 1 { 113 | t.FailNow() 114 | } 115 | if value.([]string)[0] != "pullrequest" { 116 | t.Error("expected pullrequest, got ", value.([]string)[0]) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/config/find.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path" 7 | 8 | "github.com/dropseed/deps/internal/output" 9 | ) 10 | 11 | func FindFilename(dir string, filenames ...string) string { 12 | for _, f := range filenames { 13 | p := path.Join(dir, f) 14 | if fileExists(p) { 15 | return p 16 | } 17 | } 18 | return "" 19 | } 20 | 21 | func fileExists(filename string) bool { 22 | info, err := os.Stat(filename) 23 | if os.IsNotExist(err) { 24 | return false 25 | } 26 | return !info.IsDir() 27 | } 28 | 29 | func FindOrInfer() (*Config, error) { 30 | configPath := FindFilename("", DefaultFilenames...) 31 | 32 | if configPath != "" { 33 | cfg, err := NewConfigFromPath(configPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | cfg.Compile() 39 | 40 | return cfg, nil 41 | } 42 | 43 | output.Event("No local config found, detecting your dependencies automatically") 44 | // should we always check for inferred? and could let them know what they 45 | // don't have in theirs? 46 | // dump both to yaml, use regular diff tool and highlighting? 47 | cfg, err := InferredConfigFromDir(".") 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | inferred, err := cfg.DumpYAML() 53 | if err != nil { 54 | return nil, err 55 | } 56 | output.Unstyled("---") 57 | output.Unstyled(inferred) 58 | output.Unstyled("---") 59 | 60 | cfg.Compile() 61 | 62 | if len(cfg.Dependencies) < 1 { 63 | return nil, errors.New("no dependencies found") 64 | } 65 | 66 | return cfg, nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/config/inference.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | "regexp" 6 | "sort" 7 | 8 | "github.com/dropseed/deps/internal/filefinder" 9 | ) 10 | 11 | // InferredConfigFromPath loads a Config object based on the dependency files present 12 | func InferredConfigFromDir(dir string) (*Config, error) { 13 | patterns := map[string]*regexp.Regexp{ 14 | "requirements.txt": regexp.MustCompile("^.*requirements.*\\.txt$"), 15 | "Pipfile": regexp.MustCompile("^Pipfile$"), 16 | "poetry.lock": regexp.MustCompile("^poetry\\.lock$"), 17 | "package.json": regexp.MustCompile("^package\\.json$"), 18 | "composer.json": regexp.MustCompile("^composer\\.json$"), 19 | } 20 | 21 | types := map[string]struct { 22 | Name string 23 | UseDir bool 24 | }{ 25 | "requirements.txt": { 26 | Name: "python", 27 | UseDir: false, 28 | }, 29 | "Pipfile": { 30 | Name: "python", 31 | UseDir: false, 32 | }, 33 | "poetry.lock": { 34 | Name: "python", 35 | UseDir: false, 36 | }, 37 | "package.json": { 38 | Name: "js", 39 | UseDir: true, 40 | }, 41 | "composer.json": { 42 | Name: "php", 43 | UseDir: true, 44 | }, 45 | // Pattern: regexp.MustCompile("^Dockerfile.*$"), 46 | // Type: "dockerfile", 47 | } 48 | 49 | dependencies := []*Dependency{} 50 | 51 | // Have to sort these for YAML consistency 52 | found := filefinder.FindInDir(dir, patterns) 53 | foundPathsSorted := []string{} 54 | for p := range found { 55 | foundPathsSorted = append(foundPathsSorted, p) 56 | } 57 | sort.Strings(foundPathsSorted) 58 | 59 | for _, path := range foundPathsSorted { 60 | patternName := found[path] 61 | depType := types[patternName] 62 | dep := &Dependency{ 63 | Path: path, 64 | Type: depType.Name, 65 | } 66 | if depType.UseDir { 67 | dep.Path = filepath.Dir(dep.Path) 68 | } 69 | dependencies = append(dependencies, dep) 70 | } 71 | 72 | config := &Config{ 73 | Version: Version, 74 | Dependencies: dependencies, 75 | } 76 | // make the dependency paths relative to the dir we were asked to look in 77 | for _, dep := range config.Dependencies { 78 | p, err := filepath.Rel(dir, dep.Path) 79 | if err != nil { 80 | panic(err) 81 | } 82 | dep.Path = p 83 | } 84 | return config, nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/config/inference_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestInference(t *testing.T) { 9 | config, err := InferredConfigFromDir("./testdata/repo") 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | if len(config.Dependencies) != 5 { 14 | t.FailNow() 15 | } 16 | expected := `version: 3 17 | dependencies: 18 | - type: python 19 | path: Pipfile 20 | - type: python 21 | path: app/requirements.txt 22 | - type: python 23 | path: app/requirements_test.txt 24 | - type: php 25 | path: . 26 | - type: js 27 | path: . 28 | ` 29 | dumped, err := config.DumpYAML() 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | if expected != dumped { 34 | fmt.Println(dumped) 35 | t.FailNow() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/config/settings.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dropseed/deps/internal/env" 7 | ) 8 | 9 | type Settings map[string]interface{} 10 | 11 | func (s Settings) Get(name string) interface{} { 12 | for k, v := range s { 13 | if strings.ToLower(k) == strings.ToLower(name) { 14 | return v 15 | } 16 | } 17 | 18 | return nil 19 | } 20 | 21 | func (s Settings) AsEnviron() []string { 22 | environ := []string{} 23 | 24 | for k, v := range s { 25 | environString, err := env.SettingToEnviron(k, v) 26 | if err != nil { 27 | panic(err) 28 | } 29 | environ = append(environ, environString) 30 | } 31 | 32 | return environ 33 | } 34 | -------------------------------------------------------------------------------- /internal/config/testdata/repo/Dockerfile-dev: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/Dockerfile-dev -------------------------------------------------------------------------------- /internal/config/testdata/repo/Gemfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/Gemfile -------------------------------------------------------------------------------- /internal/config/testdata/repo/Pipfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/Pipfile -------------------------------------------------------------------------------- /internal/config/testdata/repo/app/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/app/requirements.txt -------------------------------------------------------------------------------- /internal/config/testdata/repo/app/requirements_test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/app/requirements_test.txt -------------------------------------------------------------------------------- /internal/config/testdata/repo/app/third/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/app/third/requirements.txt -------------------------------------------------------------------------------- /internal/config/testdata/repo/composer.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/composer.json -------------------------------------------------------------------------------- /internal/config/testdata/repo/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/package.json -------------------------------------------------------------------------------- /internal/config/testdata/repo/vendor/requirements_test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropseed/deps/55dd50d91aa2bf4e4301ccb0b098920e0cfc4f6b/internal/config/testdata/repo/vendor/requirements_test.txt -------------------------------------------------------------------------------- /internal/config/testdata/v2_full.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | dependencies: 3 | - type: js 4 | - path: requirements.txt 5 | type: python 6 | 7 | settings: 8 | # collector and actor will both use these settings 9 | ok: true 10 | 11 | lockfile_updates: 12 | enabled: false 13 | 14 | manifest_updates: 15 | enabled: true 16 | 17 | # default 18 | # filters: 19 | # - name: ".*" 20 | # versions: Y.Y.Y 21 | 22 | filters: 23 | # these are evaluated in order, versions for a dependency are filtered 24 | # by the first match (or updates are skipped entirely if no matches) 25 | - name: "django-braces" 26 | enabled: false 27 | - name: "django-.*" 28 | settings: 29 | github_labels: ["django"] 30 | - name: "django" 31 | - name: ".*" 32 | -------------------------------------------------------------------------------- /internal/config/testdata/v2_minimal.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | dependencies: 3 | - path: requirements.txt 4 | type: python 5 | -------------------------------------------------------------------------------- /internal/env/main.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/dropseed/deps/internal/output" 10 | ) 11 | 12 | const SETTING_PREFIX = "DEPS_SETTING_" 13 | 14 | func SettingToEnviron(name string, value interface{}) (string, error) { 15 | envKey := settingNameToKey(name) 16 | envVal, err := settingValToEnv(value) 17 | if err != nil { 18 | return "", err 19 | } 20 | return fmt.Sprintf("%s=%s", envKey, envVal), nil 21 | } 22 | 23 | func SettingFromEnviron(name string) interface{} { 24 | v := os.Getenv(settingNameToKey(name)) 25 | if v != "" { 26 | var data interface{} 27 | if err := json.Unmarshal([]byte(v), &data); err != nil { 28 | output.Error("Setting \"%s\" from env was not valid JSON", name) 29 | panic(err) 30 | } 31 | return data 32 | } 33 | return nil 34 | } 35 | 36 | func settingNameToKey(k string) string { 37 | return fmt.Sprintf("%s%s", SETTING_PREFIX, strings.ToUpper(k)) 38 | } 39 | 40 | func settingValToEnv(v interface{}) (string, error) { 41 | envVarVal := v 42 | 43 | // if it's not already a string, json encode it 44 | // encoding a string seems to double quote 45 | if _, ok := v.(string); !ok { 46 | tmp, jsonErr := json.Marshal(v) 47 | if jsonErr != nil { 48 | return "", jsonErr 49 | } 50 | envVarVal = string(tmp) 51 | } 52 | 53 | return envVarVal.(string), nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/env/main_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import "testing" 4 | 5 | func TestSettingVal(t *testing.T) { 6 | if s, _ := settingValToEnv(2); s != "2" { 7 | t.FailNow() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/filefinder/main.go: -------------------------------------------------------------------------------- 1 | package filefinder 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "regexp" 8 | ) 9 | 10 | const maxDepth = 2 11 | 12 | var directoryNamesToSkip = map[string]bool{ 13 | ".git": true, 14 | "node_modules": true, 15 | "env": true, 16 | "vendor": true, 17 | ".venv": true, 18 | } 19 | 20 | // FindInDir takes a set of named patterns, 21 | // and returns a map of matched paths and the pattern name they matched 22 | func FindInDir(dir string, patterns map[string]*regexp.Regexp) map[string]string { 23 | return findInDir(dir, patterns, 1) 24 | } 25 | 26 | // Same as FindInDir with a specific depth 27 | func DeepFindInDir(dir string, patterns map[string]*regexp.Regexp, depth int) map[string]string { 28 | return findInDir(dir, patterns, maxDepth-depth+1) 29 | } 30 | 31 | func findInDir(dir string, patterns map[string]*regexp.Regexp, depth int) map[string]string { 32 | if depth > maxDepth { 33 | return nil 34 | } 35 | 36 | files, err := ioutil.ReadDir(dir) 37 | if err != nil { 38 | return nil 39 | } 40 | 41 | matches := map[string]string{} 42 | 43 | for _, f := range files { 44 | name := f.Name() 45 | p := path.Join(dir, name) 46 | 47 | fileInfo, err := os.Stat(p) 48 | if err != nil { 49 | continue 50 | } 51 | 52 | if fileInfo.IsDir() { 53 | if directoryNamesToSkip[name] { 54 | continue 55 | } 56 | for k, v := range findInDir(p, patterns, depth+1) { 57 | matches[k] = v 58 | } 59 | } else if match := patternMatchingPath(p, patterns); match != "" { 60 | matches[p] = match 61 | } 62 | } 63 | 64 | return matches 65 | } 66 | 67 | func patternMatchingPath(p string, patterns map[string]*regexp.Regexp) string { 68 | basename := path.Base(p) 69 | for patternName, pattern := range patterns { 70 | if pattern.MatchString(basename) { 71 | return patternName 72 | } 73 | } 74 | return "" 75 | } 76 | -------------------------------------------------------------------------------- /internal/git/main_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // func TestGetJobBranchName(t *testing.T) { 8 | // os.Setenv("JOB_ID", "1111-2222-3333") 9 | // if s, err := GetJobBranchName(); s != "deps/update-1111" || err != nil { 10 | // t.Error(s) 11 | // } 12 | 13 | // os.Setenv("JOB_ID", "") 14 | // if s, err := GetJobBranchName(); s != "" || err == nil { 15 | // t.Error(err) 16 | // } 17 | 18 | // os.Setenv("JOB_ID", "abc") 19 | // if s, err := GetJobBranchName(); s != "deps/update-abc" || err != nil { 20 | // t.Error(s) 21 | // } 22 | 23 | // os.Setenv("JOB_ID", "1111-2222-3333") 24 | // os.Setenv("SETTING_BRANCH_PREFIX", "foo/") 25 | // if s, err := GetJobBranchName(); s != "foo/deps/update-1111" || err != nil { 26 | // t.Error(s) 27 | // } 28 | // } 29 | 30 | // func TestBranchExists(t *testing.T) { 31 | // output.Verbosity = 1 32 | // if !BranchExists("master") { 33 | // t.FailNow() 34 | // } 35 | 36 | // if BranchExists("foo") { 37 | // t.FailNow() 38 | // } 39 | // } 40 | 41 | func TestGitRemoteToHTTPS(t *testing.T) { 42 | tests := []struct { 43 | input string 44 | output string 45 | }{ 46 | { 47 | input: "git@github.com:dropseed/test.git", 48 | output: "https://github.com/dropseed/test.git", 49 | }, 50 | { 51 | input: "git@github.com:dropseed/test", 52 | output: "https://github.com/dropseed/test", 53 | }, 54 | { 55 | input: "git@gitlab.com:dropseed/test.git", 56 | output: "https://gitlab.com/dropseed/test.git", 57 | }, 58 | { 59 | input: "git@gitlab.com:dropseed/test/two.git", 60 | output: "https://gitlab.com/dropseed/test/two.git", 61 | }, 62 | } 63 | 64 | for _, test := range tests { 65 | actual := GitRemoteToHTTPS(test.input) 66 | if actual != test.output { 67 | t.Errorf("%s\n%s != %s", test.input, test.output, actual) 68 | } 69 | } 70 | } 71 | 72 | func TestGitRemoteHostname(t *testing.T) { 73 | if GitRemoteHostname() != "github.com" { 74 | t.Fail() 75 | } 76 | } 77 | 78 | // func TestGetDepsBranches(t *testing.T) { 79 | // branches := GetDepsBranches() 80 | // for _, b := range branches { 81 | // fmt.Println(b) 82 | // } 83 | // t.Fail() 84 | // } 85 | -------------------------------------------------------------------------------- /internal/hooks/main.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/output" 9 | "github.com/dropseed/deps/internal/pullrequest" 10 | ) 11 | 12 | func RunPullrequestHook(pr pullrequest.PullrequestAdapter, hookName string) error { 13 | hookCmd := pr.GetSetting(hookName) 14 | 15 | if hookCmd == nil { 16 | return nil 17 | } 18 | 19 | output.Event("Executing %s hook", hookName) 20 | 21 | hookCmdLines := strings.Split(hookCmd.(string), "\n") 22 | 23 | for _, line := range hookCmdLines { 24 | cmd := exec.Command("sh", "-c", line) 25 | // specific env too? would be pr.Config.Env? 26 | cmd.Env = os.Environ() 27 | cmd.Stdout = os.Stdout 28 | cmd.Stderr = os.Stderr 29 | err := cmd.Run() 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/install/lockfiles.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path" 7 | "path/filepath" 8 | "regexp" 9 | 10 | "github.com/dropseed/deps/internal/filefinder" 11 | "github.com/dropseed/deps/internal/output" 12 | ) 13 | 14 | type lockfile struct { 15 | Path string 16 | installCmd string 17 | } 18 | 19 | func (lf *lockfile) Install() error { 20 | output.Event("Installing %s", lf.RelPath()) 21 | cmd := exec.Command("sh", "-c", lf.installCmd) 22 | cmd.Dir = path.Dir(lf.Path) // Could be down a directory, so make sure we run the command as if we're there 23 | cmd.Env = os.Environ() 24 | cmd.Stdout = os.Stdout 25 | cmd.Stderr = os.Stderr 26 | if err := cmd.Run(); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | func (lf *lockfile) RelPath() string { 33 | cwd, err := os.Getwd() 34 | if err != nil { 35 | panic(err) 36 | } 37 | relPath, err := filepath.Rel(cwd, lf.Path) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return relPath 42 | } 43 | 44 | func FindLockfiles(dir string) []*lockfile { 45 | patterns := map[string]*regexp.Regexp{ 46 | "yarn.lock": regexp.MustCompile("^yarn\\.lock$"), 47 | "package-lock.json": regexp.MustCompile("^package-lock\\.json$"), 48 | "Pipfile.lock": regexp.MustCompile("^Pipfile\\.lock$"), 49 | "poetry.lock": regexp.MustCompile("^poetry\\.lock$"), 50 | } 51 | commands := map[string]string{ 52 | "yarn.lock": "yarn install", 53 | "package-lock.json": "npm ci", 54 | "Pipfile.lock": "pipenv sync --dev", 55 | "poetry.lock": "poetry install", 56 | } 57 | lockfiles := []*lockfile{} 58 | for path, patternName := range filefinder.FindInDir(dir, patterns) { 59 | lockfiles = append(lockfiles, &lockfile{ 60 | Path: path, 61 | installCmd: commands[patternName], 62 | }) 63 | } 64 | return lockfiles 65 | } 66 | -------------------------------------------------------------------------------- /internal/lag/main.go: -------------------------------------------------------------------------------- 1 | package lag 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "strings" 10 | 11 | "github.com/dropseed/deps/internal/cache" 12 | "github.com/dropseed/deps/internal/git" 13 | "github.com/dropseed/deps/internal/install" 14 | "github.com/dropseed/deps/internal/output" 15 | ) 16 | 17 | const lagFilename = "lag.json" 18 | 19 | type lagData struct { 20 | Lockfiles map[string]string `json:"lockfiles"` 21 | } 22 | 23 | type lagManager struct { 24 | dataPath string 25 | data *lagData 26 | } 27 | 28 | func NewLagManager() (*lagManager, error) { 29 | depsCache := cache.GetCachePath() 30 | lagDataPath := path.Join(depsCache, lagFilename) 31 | manager := lagManager{ 32 | dataPath: lagDataPath, 33 | } 34 | 35 | if err := manager.loadData(); err != nil { 36 | return nil, err 37 | } 38 | 39 | return &manager, nil 40 | } 41 | 42 | func (manager *lagManager) loadData() error { 43 | jsonFile, err := os.Open(manager.dataPath) 44 | if os.IsNotExist(err) { 45 | return nil 46 | } 47 | if err != nil { 48 | return err 49 | } 50 | defer jsonFile.Close() 51 | 52 | byteValue, _ := ioutil.ReadAll(jsonFile) 53 | 54 | var data lagData 55 | if err := json.Unmarshal(byteValue, &data); err != nil { 56 | return err 57 | } 58 | 59 | manager.data = &data 60 | 61 | return nil 62 | } 63 | 64 | func (manager *lagManager) saveData() error { 65 | out, err := json.MarshalIndent(manager.data, "", " ") 66 | if err != nil { 67 | return err 68 | } 69 | out = append(out, "\n"...) 70 | if err := ioutil.WriteFile(manager.dataPath, out, 0644); err != nil { 71 | panic(err) 72 | } 73 | return nil 74 | } 75 | 76 | func (manager *lagManager) getSavedLockfileIdentifier(path, id string) string { 77 | if manager.data == nil { 78 | return "" 79 | } 80 | existingID, ok := manager.data.Lockfiles[path] 81 | if !ok { 82 | return "" 83 | } 84 | return existingID 85 | } 86 | 87 | func (manager *lagManager) SaveLockfileIdentifier(path, id string) error { 88 | // Reload before saving to prevent overwrites in another process 89 | if err := manager.loadData(); err != nil { 90 | return err 91 | } 92 | 93 | if manager.data == nil { 94 | manager.data = &lagData{ 95 | Lockfiles: map[string]string{}, 96 | } 97 | } else if manager.data.Lockfiles == nil { 98 | manager.data.Lockfiles = map[string]string{} 99 | } 100 | 101 | manager.data.Lockfiles[path] = id 102 | 103 | return manager.saveData() 104 | } 105 | 106 | func IdentifierForFile(p string) string { 107 | // Can't use mtime because git changes it on checkout etc. 108 | cmd := exec.Command("git", "hash-object", p) 109 | out, err := cmd.Output() 110 | if err != nil { 111 | panic(err) 112 | } 113 | return strings.TrimSpace(string(out)) 114 | } 115 | 116 | func Run(dir string) error { 117 | if git.RepoRoot() == "" { 118 | return nil 119 | } 120 | 121 | // TODO if components are responsible for this, then we do it this way 122 | // Disable all output (FindOrInfer prints some stuff right now...) 123 | // tempVerbosity := output.Verbosity 124 | // output.Verbosity = -1 125 | // cfg, err := config.FindOrInfer() 126 | // if err != nil { 127 | // return err 128 | // } 129 | // output.Verbosity = tempVerbosity 130 | 131 | manager, err := NewLagManager() 132 | if err != nil { 133 | return err 134 | } 135 | 136 | laggingLockfiles := map[string]string{} 137 | 138 | for _, lockfile := range install.FindLockfiles(dir) { 139 | lockfileID := IdentifierForFile(lockfile.Path) 140 | 141 | existingID := manager.getSavedLockfileIdentifier(lockfile.Path, lockfileID) 142 | 143 | if lockfileID == existingID { //} || existingID == "ignore" { 144 | continue 145 | } 146 | 147 | // Use existing ID so we can tell what to do 148 | // "" would be not installed 149 | // "ignore" would be skip (not shown) 150 | // "anything else" would be re-install 151 | laggingLockfiles[lockfile.RelPath()] = existingID 152 | } 153 | 154 | if len(laggingLockfiles) > 0 { 155 | paths := []string{} 156 | 157 | for lockfilePath, lockfileID := range laggingLockfiles { 158 | if lockfileID == "" { 159 | paths = append(paths, "*"+lockfilePath) 160 | } else { 161 | paths = append(paths, lockfilePath) 162 | } 163 | } 164 | 165 | output.Subtle("\n[Run `deps install` to sync with %s]", strings.Join(paths, ", ")) 166 | } 167 | 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /internal/output/main.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "golang.org/x/crypto/ssh/terminal" 9 | ) 10 | 11 | var Verbosity = 0 12 | 13 | func isGitHubActions() bool { 14 | return os.Getenv("GITHUB_ACTIONS") == "true" 15 | } 16 | 17 | func shouldColorize() bool { 18 | isTerm := terminal.IsTerminal(int(os.Stdout.Fd())) 19 | if isGitHubActions() { 20 | // https://github.com/fatih/color#github-actions 21 | color.NoColor = false 22 | } 23 | return isTerm || isGitHubActions() 24 | } 25 | 26 | func IsDebug() bool { 27 | return Verbosity > 0 28 | } 29 | 30 | func Event(f string, args ...interface{}) { 31 | if shouldColorize() && IsDebug() { 32 | color.Set(color.FgMagenta) 33 | fmt.Print("> ") 34 | color.Unset() 35 | color.Set(color.Bold) 36 | } 37 | fmt.Printf(f+"\n", args...) 38 | if shouldColorize() && IsDebug() { 39 | color.Unset() 40 | } 41 | } 42 | 43 | func Debug(f string, args ...interface{}) { 44 | if !IsDebug() { 45 | return 46 | } 47 | if shouldColorize() { 48 | color.Set(color.FgCyan) 49 | fmt.Print("> ") 50 | color.Unset() 51 | } 52 | fmt.Printf(f+"\n", args...) 53 | } 54 | 55 | func Warning(f string, args ...interface{}) { 56 | color.Set(color.FgYellow) 57 | fmt.Printf(f+"\n", args...) 58 | color.Unset() 59 | } 60 | 61 | func Error(f string, args ...interface{}) { 62 | color.Set(color.FgRed) 63 | fmt.Printf(f+"\n", args...) 64 | color.Unset() 65 | } 66 | 67 | func Success(f string, args ...interface{}) { 68 | color.Set(color.FgGreen) 69 | fmt.Printf(f+"\n", args...) 70 | color.Unset() 71 | } 72 | 73 | func Subtle(f string, args ...interface{}) { 74 | color.Set(color.Faint) 75 | fmt.Printf(f+"\n", args...) 76 | color.Unset() 77 | } 78 | 79 | func Unstyled(f string, args ...interface{}) { 80 | color.Unset() 81 | fmt.Printf(f+"\n", args...) 82 | } 83 | 84 | func StartSection(f string, args ...interface{}) { 85 | if isGitHubActions() { 86 | // https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines 87 | formatted := fmt.Sprintf(f, args...) 88 | fmt.Printf("::group::%s\n", formatted) 89 | } else { 90 | Event(f, args...) 91 | } 92 | } 93 | 94 | func EndSection() { 95 | if isGitHubActions() { 96 | fmt.Println("::endgroup::") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/pullrequest/bitbucket/main.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/dropseed/deps/internal/config" 11 | "github.com/dropseed/deps/internal/output" 12 | "github.com/dropseed/deps/internal/schemaext" 13 | "github.com/dropseed/deps/pkg/schema" 14 | ) 15 | 16 | type PullRequest struct { 17 | Base string 18 | Head string 19 | Title string 20 | Body string 21 | Dependencies *schema.Dependencies 22 | Config *config.Dependency 23 | 24 | ProjectAPIURL string 25 | APIUsername string 26 | APIPassword string 27 | } 28 | 29 | func NewPullRequest(base string, head string, deps *schema.Dependencies, cfg *config.Dependency) (*PullRequest, error) { 30 | apiURL, err := getProjectAPIURL() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &PullRequest{ 36 | Base: base, 37 | Head: head, 38 | Title: schemaext.TitleForDeps(deps), 39 | Body: schemaext.DescriptionForDeps(deps), 40 | Dependencies: deps, 41 | Config: cfg, 42 | ProjectAPIURL: apiURL, 43 | APIUsername: getAPIUsername(), 44 | APIPassword: getAPIPassword(), 45 | }, nil 46 | } 47 | 48 | func (pr *PullRequest) GetSetting(name string) interface{} { 49 | return pr.Config.GetSettingForSchema(name, pr.Dependencies) 50 | } 51 | 52 | func (pr *PullRequest) request(verb string, url string, input []byte) (*http.Response, string, error) { 53 | client := &http.Client{} 54 | 55 | req, err := http.NewRequest(verb, url, bytes.NewBuffer(input)) 56 | if err != nil { 57 | return nil, "", err 58 | } 59 | 60 | req.SetBasicAuth(pr.APIUsername, pr.APIPassword) 61 | req.Header.Add("User-Agent", "deps") 62 | req.Header.Set("Content-Type", "application/json") 63 | 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | return nil, "", err 67 | } 68 | 69 | body, err := ioutil.ReadAll(resp.Body) 70 | resp.Body.Close() 71 | return resp, string(body), err 72 | } 73 | 74 | // // Create will create the pull request on Bitbucket 75 | func (pr *PullRequest) CreateOrUpdate() error { 76 | output.Debug("Preparing to open Bitbucket pull request for %v\n", pr.ProjectAPIURL) 77 | 78 | pullrequestMap := pr.getPullRequestOptions() 79 | output.Debug("%+v\n", pullrequestMap) 80 | pullrequestData, _ := json.Marshal(pullrequestMap) 81 | 82 | url := pr.ProjectAPIURL + "/pullrequests" 83 | output.Debug("Creating pull request at %s", url) 84 | 85 | resp, body, err := pr.request("POST", url, pullrequestData) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | if resp.StatusCode == 201 { 91 | output.Event("Successfully created Bitbucket pull request for %v\n", pr.ProjectAPIURL) 92 | return nil 93 | } 94 | 95 | return fmt.Errorf("Failed to create pull request: %s", body) 96 | } 97 | 98 | func (pr *PullRequest) getPullRequestOptions() map[string]interface{} { 99 | base := pr.Base 100 | if target := pr.GetSetting("bitbucket_destination"); target != nil { 101 | base = target.(string) 102 | } 103 | 104 | pullrequestMap := make(map[string]interface{}) 105 | pullrequestMap["title"] = pr.Title 106 | pullrequestMap["source"] = map[string]interface{}{ 107 | "branch": map[string]string{ 108 | "name": pr.Head, 109 | }, 110 | } 111 | pullrequestMap["destination"] = map[string]interface{}{ 112 | "branch": map[string]string{ 113 | "name": base, 114 | }, 115 | } 116 | pullrequestMap["description"] = pr.Body 117 | 118 | otherFields := []string{ 119 | "close_source_branch", 120 | "reviewers", 121 | } 122 | 123 | for _, f := range otherFields { 124 | if s := pr.GetSetting(fmt.Sprintf("bitbucket_%s", f)); s != nil { 125 | pullrequestMap[f] = s 126 | } 127 | } 128 | 129 | return pullrequestMap 130 | } 131 | -------------------------------------------------------------------------------- /internal/pullrequest/bitbucket/repo.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type BitbucketRepo struct { 8 | apiPassword string 9 | apiUsername string 10 | } 11 | 12 | func NewRepo() *BitbucketRepo { 13 | return &BitbucketRepo{ 14 | apiUsername: getAPIUsername(), 15 | apiPassword: getAPIPassword(), 16 | } 17 | } 18 | 19 | func (repo *BitbucketRepo) CheckRequirements() error { 20 | if repo.apiPassword == "" { 21 | return errors.New("Unable to find Bitbucket API password.\n\nVisit https://docs.dependencies.io/bitbucket for more information.") 22 | } 23 | if repo.apiUsername == "" { 24 | return errors.New("Unable to find Bitbucket API username.\n\nVisit https://docs.dependencies.io/bitbucket for more information.") 25 | } 26 | return nil 27 | } 28 | 29 | func (repo *BitbucketRepo) Autoconfigure() { 30 | } 31 | -------------------------------------------------------------------------------- /internal/pullrequest/bitbucket/utils.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/dropseed/deps/internal/ci/bitbucketpipelines" 8 | ) 9 | 10 | func getAPIPassword() string { 11 | if s := os.Getenv("DEPS_BITBUCKET_PASSWORD"); s != "" { 12 | return s 13 | } 14 | 15 | return "" 16 | } 17 | 18 | func getAPIUsername() string { 19 | if s := os.Getenv("DEPS_BITBUCKET_USERNAME"); s != "" { 20 | return s 21 | } 22 | 23 | return "" 24 | } 25 | 26 | func getProjectAPIURL() (string, error) { 27 | if s := os.Getenv("DEPS_BITBUCKET_REPO_API_URL"); s != "" { 28 | return s, nil 29 | } 30 | 31 | if ciURL := bitbucketpipelines.GetProjectAPIURL(); ciURL != "" { 32 | return ciURL, nil 33 | } 34 | 35 | // TODO otherwise from git remote? 36 | 37 | return "", errors.New("Unable to determine Bitbucket API url for this project") 38 | } 39 | -------------------------------------------------------------------------------- /internal/pullrequest/github/main_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | // import ( 4 | // "os" 5 | // "testing" 6 | 7 | // "github.com/dropseed/deps/internal/pullrequest" 8 | // ) 9 | 10 | // func getPR(dependenciesJSONPath string) (*PullRequest, *pullrequest.Pullrequest) { 11 | // os.Setenv("JOB_ID", "test") 12 | // pr, err := NewPullrequestFromDependenciesJSONPathAndEnv(dependenciesJSONPath) 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // return pr, pr.Pullrequest 17 | // } 18 | 19 | // func TestNewPullRequestFromEnv(t *testing.T) { 20 | // os.Setenv("GITHUB_REPO_FULL_NAME", "dropseed/test") 21 | // os.Setenv("GITHUB_API_TOKEN", "testtoken") 22 | 23 | // pr, prBase := getPR("../../schema/testdata/single_dependency.json") 24 | 25 | // if pr.Pullrequest != prBase { 26 | // t.Error("Pullrequest value incorrect") 27 | // } 28 | 29 | // if pr.RepoFullName != "dropseed/test" { 30 | // t.Error("RepoFullName value incorrect") 31 | // } 32 | 33 | // if pr.APIToken != "testtoken" { 34 | // t.Error("APIToken value incorrect") 35 | // } 36 | // } 37 | 38 | // func TestCreateTestEnv(t *testing.T) { 39 | // pr, _ := getPR("../../schema/testdata/single_dependency.json") 40 | // err := pr.Create() 41 | // if err != nil { 42 | // t.FailNow() 43 | // } 44 | // } 45 | 46 | // func TestCreateProductionEnv(t *testing.T) { 47 | // // this will try to send an actual API call to github.com, and fail 48 | // os.Setenv("DEPENDENCIES_ENV", "production") 49 | // pr, _ := getPR("../../schema/testdata/single_dependency.json") 50 | // err := pr.Create() 51 | // if err == nil { 52 | // t.FailNow() 53 | // } 54 | // } 55 | 56 | // // func TestGetActionsJSON(t *testing.T) { 57 | // // os.Setenv("DEPENDENCIES_ENV", "test") 58 | // // pr, _ := getPR("./testdata/action_dependencies.json") 59 | // // pr.Create() 60 | // // output, err := pr.GetActionsJSON() 61 | // // if err != nil { 62 | // // t.Error(err) 63 | // // return 64 | // // } 65 | // // if output != "{\"PR #0\":{\"dependencies\":{\"manifests\":{\"package.json\":{}}},\"metadata\":{}}}" { 66 | // // t.Errorf("Output doesn't match expected: %v", output) 67 | // // } 68 | // // } 69 | -------------------------------------------------------------------------------- /internal/pullrequest/github/repo.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/dropseed/deps/internal/git" 10 | "github.com/dropseed/deps/internal/output" 11 | ) 12 | 13 | type GitHubRepo struct { 14 | apiToken string 15 | } 16 | 17 | func NewRepo() *GitHubRepo { 18 | return &GitHubRepo{ 19 | apiToken: getAPIToken(), 20 | } 21 | } 22 | 23 | func (repo *GitHubRepo) CheckRequirements() error { 24 | if repo.apiToken == "" { 25 | return errors.New("Unable to find GitHub API token.\n\nVisit https://docs.dependencies.io/github for more information.") 26 | } 27 | return nil 28 | } 29 | 30 | func (repo *GitHubRepo) Autoconfigure() { 31 | output.Debug("Writing GitHub token to ~/.netrc") 32 | hostname := git.GitRemoteHostname() 33 | echo := fmt.Sprintf("echo -e \"machine %s\n login x-access-token\n password %s\" >> ~/.netrc", hostname, repo.apiToken) 34 | cmd := exec.Command("sh", "-c", echo) 35 | cmd.Stdout = os.Stdout 36 | cmd.Stderr = os.Stderr 37 | if err := cmd.Run(); err != nil { 38 | panic(err) 39 | } 40 | } 41 | 42 | // func (repo *GitHubRepo) NewPullrequest(deps *schema.Dependencies, baseBranch string) *PullRequest { 43 | // prBase, err := pullrequest.NewPullrequest(deps) 44 | // if err != nil { 45 | // panic(err) 46 | // } 47 | // prBase.DefaultBaseBranch = baseBranch 48 | 49 | // fullName, err := getRepoFullName() 50 | // if err != nil { 51 | // panic(err) 52 | // } 53 | // parts := strings.Split(fullName, "/") 54 | // owner := parts[0] 55 | // repoName := parts[1] 56 | 57 | // return &PullRequest{ 58 | // Pullrequest: prBase, 59 | // RepoOwnerName: owner, 60 | // RepoName: repoName, 61 | // RepoFullName: fullName, 62 | // APIToken: GetAPIToken(), 63 | // } 64 | // } 65 | -------------------------------------------------------------------------------- /internal/pullrequest/github/testdata/action_dependencies.json: -------------------------------------------------------------------------------- 1 | {"manifests": {"package.json": {}}} 2 | -------------------------------------------------------------------------------- /internal/pullrequest/github/utils.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | 12 | "github.com/bradleyfalzon/ghinstallation" 13 | "github.com/dropseed/deps/internal/git" 14 | ) 15 | 16 | func dereferenceGitHubIssueLinks(body string) (string, error) { 17 | r, err := regexp.Compile("https://github.com/([^/]+/[^/]+/(issues|pull)/\\d+)") 18 | if err != nil { 19 | return "", err 20 | } 21 | sanitized := r.ReplaceAllString(body, "https://www.dependencies.io/github-redirect/$1") 22 | return sanitized, nil 23 | } 24 | 25 | func getRepoFullName() (string, error) { 26 | 27 | // Custom override 28 | if s := os.Getenv("DEPS_GITHUB_REPOSITORY"); s != "" { 29 | return s, nil 30 | } 31 | 32 | // GitHub Actions 33 | if s := os.Getenv("GITHUB_REPOSITORY"); s != "" { 34 | return s, nil 35 | } 36 | 37 | if s := os.Getenv("TRAVIS_REPO_SLUG"); s != "" { 38 | return s, nil 39 | } 40 | 41 | if s := os.Getenv("CIRCLE_PROJECT_USERNAME"); s != "" { 42 | return fmt.Sprintf("%s/%s", s, os.Getenv("CIRCLE_PROJECT_REPONAME")), nil 43 | } 44 | 45 | if s := getRepoFullNameFromRemote(git.GitRemote()); s != "" { 46 | return s, nil 47 | } 48 | 49 | return "", errors.New("Unable to find GitHub repo full name") 50 | } 51 | 52 | func getRepoFullNameFromRemote(remote string) string { 53 | pattern := regexp.MustCompile("([a-zA-Z0-9_-]+\\/[a-zA-Z0-9_-]+)(\\.git)?\\/?$") 54 | matches := pattern.FindStringSubmatch(remote) 55 | if len(matches) > 0 { 56 | return matches[1] 57 | } 58 | return "" 59 | } 60 | 61 | func getAPIToken() string { 62 | if s := os.Getenv("DEPS_GITHUB_TOKEN"); s != "" { 63 | return s 64 | } 65 | 66 | if key := os.Getenv("DEPS_GITHUB_APP_KEY"); key != "" { 67 | // key path 68 | // key raw 69 | // key b64 - only this for now 70 | 71 | keyBytes, err := base64.StdEncoding.DecodeString(key) 72 | if err != nil { 73 | panic("Could not decode base64 DEPS_GITHUB_APP_KEY") 74 | } 75 | 76 | appID, err := strconv.Atoi(os.Getenv("DEPS_GITHUB_APP_ID")) 77 | if err != nil { 78 | panic("Invalid DEPS_GITHUB_APP_ID") 79 | } 80 | installationID, err := strconv.Atoi(os.Getenv("DEPS_GITHUB_APP_INSTALLATION_ID")) 81 | if err != nil { 82 | panic("Invalid DEPS_GITHUB_APP_INSTALLATION_ID") 83 | } 84 | 85 | tr := http.DefaultTransport 86 | itr, err := ghinstallation.New(tr, int64(appID), int64(installationID), keyBytes) 87 | if err != nil { 88 | panic(err) 89 | } 90 | token, err := itr.Token(nil) 91 | if err != nil { 92 | panic(err) 93 | } 94 | return token 95 | } 96 | 97 | return "" 98 | } 99 | 100 | func getAPIBaseURL() string { 101 | if s := os.Getenv("DEPS_GITHUB_API_URL"); s != "" { 102 | return s 103 | } 104 | if s := os.Getenv("GITHUB_API_URL"); s != "" { 105 | // https://docs.github.com/en/actions/learn-github-actions/environment-variables 106 | return s 107 | } 108 | return "https://api.github.com" 109 | } 110 | -------------------------------------------------------------------------------- /internal/pullrequest/github/utils_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "testing" 4 | 5 | func TestNoopDereference(t *testing.T) { 6 | body := "hey this is normal\n\nwith newlines" 7 | cleaned, err := dereferenceGitHubIssueLinks(body) 8 | if err != nil { 9 | t.Error(err) 10 | } 11 | if body != cleaned { 12 | t.FailNow() 13 | } 14 | } 15 | 16 | func TestDereference(t *testing.T) { 17 | body := "hey this is normal\n\n[with](https://github.com/test-org/repo/issues/45) newlines" 18 | cleaned, err := dereferenceGitHubIssueLinks(body) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | if cleaned != "hey this is normal\n\n[with](https://www.dependencies.io/github-redirect/test-org/repo/issues/45) newlines" { 23 | t.Error(cleaned) 24 | } 25 | } 26 | 27 | func TestRepoNameFromRemote(t *testing.T) { 28 | remote := "https://github.com/dropseed/test.git/" 29 | name := getRepoFullNameFromRemote(remote) 30 | if name != "dropseed/test" { 31 | t.Error(name) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/pullrequest/gitlab/main.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/dropseed/deps/internal/config" 14 | "github.com/dropseed/deps/internal/output" 15 | "github.com/dropseed/deps/internal/schemaext" 16 | "github.com/dropseed/deps/pkg/schema" 17 | ) 18 | 19 | // MergeRequest stores additional GitLab specific data 20 | type MergeRequest struct { 21 | Base string 22 | Head string 23 | Title string 24 | Body string 25 | Dependencies *schema.Dependencies 26 | Config *config.Dependency 27 | 28 | ProjectAPIURL string 29 | APIToken string 30 | } 31 | 32 | func NewMergeRequest(base string, head string, deps *schema.Dependencies, cfg *config.Dependency) (*MergeRequest, error) { 33 | apiURL, err := getProjectAPIURL() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &MergeRequest{ 39 | Base: base, 40 | Head: head, 41 | Title: schemaext.TitleForDeps(deps), 42 | Body: schemaext.DescriptionForDeps(deps), 43 | Dependencies: deps, 44 | Config: cfg, 45 | ProjectAPIURL: apiURL, 46 | APIToken: getAPIToken(), 47 | }, nil 48 | } 49 | 50 | func (pr *MergeRequest) GetSetting(name string) interface{} { 51 | return pr.Config.GetSettingForSchema(name, pr.Dependencies) 52 | } 53 | 54 | func (pr *MergeRequest) request(verb string, url string, input []byte) (*http.Response, string, error) { 55 | client := &http.Client{} 56 | 57 | req, err := http.NewRequest(verb, url, bytes.NewBuffer(input)) 58 | if err != nil { 59 | return nil, "", err 60 | } 61 | 62 | req.Header.Add("PRIVATE-TOKEN", pr.APIToken) 63 | req.Header.Add("User-Agent", "deps") 64 | req.Header.Set("Content-Type", "application/json") 65 | 66 | resp, err := client.Do(req) 67 | if err != nil { 68 | return nil, "", err 69 | } 70 | 71 | body, err := ioutil.ReadAll(resp.Body) 72 | resp.Body.Close() 73 | return resp, string(body), err 74 | } 75 | 76 | // // Create will create the merge request on GitLab 77 | func (pr *MergeRequest) CreateOrUpdate() error { 78 | output.Debug("Preparing to open GitLab merge request for %v\n", pr.ProjectAPIURL) 79 | 80 | pullrequestMap := pr.getMergeRequestOptions() 81 | output.Debug("%+v\n", pullrequestMap) 82 | pullrequestData, _ := json.Marshal(pullrequestMap) 83 | 84 | url := pr.ProjectAPIURL + "/merge_requests" 85 | output.Debug("Creating merge request at %s", url) 86 | 87 | resp, body, err := pr.request("POST", url, pullrequestData) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if resp.StatusCode == 201 { 93 | output.Event("Successfully created GitLab merge request for %v\n", pr.ProjectAPIURL) 94 | return nil 95 | } else if resp.StatusCode == 409 { 96 | output.Event("Merge request already exists") 97 | var data map[string][]string 98 | if err := json.Unmarshal([]byte(body), &data); err != nil { 99 | return err 100 | } 101 | 102 | if message, hasMessage := data["message"]; hasMessage { 103 | pattern := regexp.MustCompile("!(\\d+)") 104 | matches := pattern.FindStringSubmatch(message[0]) 105 | // finds !18 and 18... 106 | if len(matches) != 2 { 107 | return errors.New("Unable to find ID for existing merge request to update") 108 | } 109 | mrID := matches[1] 110 | return pr.update(mrID, pullrequestData) 111 | } 112 | } 113 | 114 | return fmt.Errorf("Failed to create merge request: %s", body) 115 | } 116 | 117 | func (pr *MergeRequest) update(iid string, data []byte) error { 118 | url := pr.ProjectAPIURL + "/merge_requests/" + iid 119 | resp, body, err := pr.request("PUT", url, data) 120 | if err != nil { 121 | return err 122 | } 123 | if resp.StatusCode >= 400 { 124 | return fmt.Errorf("Error updating merge request:\n\n%s", body) 125 | } 126 | output.Success("Updated merge request %s", iid) 127 | return nil 128 | } 129 | 130 | func (pr *MergeRequest) getMergeRequestOptions() map[string]interface{} { 131 | base := pr.Base 132 | if target := pr.GetSetting("gitlab_target_branch"); target != nil { 133 | base = target.(string) 134 | } 135 | 136 | pullrequestMap := make(map[string]interface{}) 137 | pullrequestMap["title"] = pr.Title 138 | pullrequestMap["source_branch"] = pr.Head 139 | pullrequestMap["target_branch"] = base 140 | pullrequestMap["description"] = pr.Body 141 | 142 | if labels := pr.GetSetting("gitlab_labels"); labels != nil { 143 | labelStrings := []string{} 144 | for _, l := range labels.([]interface{}) { 145 | labelStrings = append(labelStrings, l.(string)) 146 | } 147 | pullrequestMap["labels"] = strings.Join(labelStrings, ",") 148 | } 149 | 150 | otherFields := []string{ 151 | "assignee_id", 152 | "assignee_ids", 153 | "target_project_id", 154 | "milestone_id", 155 | "remove_source_branch", 156 | "allow_collaboration", 157 | "allow_maintainer_to_push", 158 | "squash", 159 | } 160 | 161 | for _, f := range otherFields { 162 | if s := pr.GetSetting(fmt.Sprintf("gitlab_%s", f)); s != nil { 163 | pullrequestMap[f] = s 164 | } 165 | } 166 | 167 | return pullrequestMap 168 | } 169 | -------------------------------------------------------------------------------- /internal/pullrequest/gitlab/main_test.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dropseed/deps/internal/config" 7 | ) 8 | 9 | // import ( 10 | // "os" 11 | // "testing" 12 | 13 | // "github.com/dropseed/deps/internal/pullrequest" 14 | // ) 15 | 16 | // func getMR(dependenciesJSONPath string) (*MergeRequest, *pullrequest.Pullrequest) { 17 | // os.Setenv("JOB_ID", "test") 18 | // pr, err := NewPullrequestFromDependenciesJSONPathAndEnv(dependenciesJSONPath) 19 | // if err != nil { 20 | // panic(err) 21 | // } 22 | // return pr, pr.Pullrequest 23 | // } 24 | 25 | // func TestNewMergeRequestFromEnv(t *testing.T) { 26 | // os.Setenv("GITLAB_API_URL", "testurl") 27 | // os.Setenv("GITLAB_API_TOKEN", "testtoken") 28 | 29 | // pr, prBase := getMR("../../schema/testdata/single_dependency.json") 30 | 31 | // if pr.Pullrequest != prBase { 32 | // t.Error("Pullrequest value incorrect") 33 | // } 34 | 35 | // if pr.ProjectAPIURL != "testurl" { 36 | // t.Error("ProjectAPIURL value incorrect") 37 | // } 38 | 39 | // if pr.APIToken != "testtoken" { 40 | // t.Error("APIToken value incorrect") 41 | // } 42 | // } 43 | 44 | // func TestCreateTestEnv(t *testing.T) { 45 | // pr, _ := getMR("../../schema/testdata/single_dependency.json") 46 | // err := pr.Create() 47 | // if err != nil { 48 | // t.FailNow() 49 | // } 50 | // } 51 | 52 | // func TestCreateProductionEnv(t *testing.T) { 53 | // // this will try to send an actual API call to github.com, and fail 54 | // os.Setenv("DEPENDENCIES_ENV", "production") 55 | // pr, _ := getMR("../../schema/testdata/single_dependency.json") 56 | // err := pr.Create() 57 | // if err == nil { 58 | // t.FailNow() 59 | // } 60 | // } 61 | 62 | // // func TestGetActionsJSON(t *testing.T) { 63 | // // os.Setenv("DEPENDENCIES_ENV", "test") 64 | // // pr, _ := getMR("./testdata/action_dependencies.json") 65 | // // pr.Create() 66 | // // output, err := pr.GetActionsJSON() 67 | // // if err != nil { 68 | // // t.Error(err) 69 | // // return 70 | // // } 71 | // // if output != "{\"MR !0\":{\"dependencies\":{\"manifests\":{\"package.json\":{}}},\"metadata\":{}}}" { 72 | // // t.Errorf("Output doesn't match expected: %v", output) 73 | // // } 74 | // // } 75 | 76 | func TestMergeRequestOptions(t *testing.T) { 77 | mr := MergeRequest{ 78 | Base: "base", 79 | Head: "head", 80 | Title: "title", 81 | Body: "body", 82 | Dependencies: nil, 83 | Config: &config.Dependency{ 84 | Settings: map[string]interface{}{ 85 | "gitlab_labels": []interface{}{ 86 | "label1", 87 | "label2", 88 | }, 89 | }, 90 | }, 91 | ProjectAPIURL: "url", 92 | APIToken: "token", 93 | } 94 | input := mr.getMergeRequestOptions() 95 | if input["labels"] != "label1,label2" { 96 | t.FailNow() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/pullrequest/gitlab/repo.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/dropseed/deps/internal/git" 11 | "github.com/dropseed/deps/internal/output" 12 | ) 13 | 14 | type GitLabRepo struct { 15 | apiToken string 16 | apiUsername string 17 | } 18 | 19 | func NewRepo() *GitLabRepo { 20 | return &GitLabRepo{ 21 | apiToken: getAPIToken(), 22 | apiUsername: getAPIUsername(), 23 | } 24 | } 25 | 26 | func (repo *GitLabRepo) CheckRequirements() error { 27 | if repo.apiToken == "" { 28 | return errors.New("Unable to find GitLab API token.\n\nVisit https://docs.dependencies.io/gitlab for more information.") 29 | } 30 | if repo.apiUsername == "" { 31 | return errors.New("Unable to find GitLab API username.\n\nVisit https://docs.dependencies.io/gitlab for more information.") 32 | } 33 | return nil 34 | } 35 | 36 | func (repo *GitLabRepo) Autoconfigure() { 37 | remote := git.GitRemote() 38 | if strings.HasPrefix(remote, "https://gitlab-ci-token:") { 39 | parts := strings.SplitN(remote, "@", 2) 40 | keep := parts[1] 41 | updatedRemote := fmt.Sprintf("https://%s:%s@%s", repo.apiUsername, repo.apiToken, keep) 42 | maskedRemote := strings.Replace(updatedRemote, repo.apiToken, "*****", 1) 43 | if cmd := exec.Command("git", "remote", "set-url", "origin", updatedRemote); cmd != nil { 44 | output.Event("Autoconfigure: git remote set-url origin %s", maskedRemote) 45 | cmd.Stdout = os.Stdout 46 | cmd.Stderr = os.Stderr 47 | if err := cmd.Run(); err != nil { 48 | panic(err) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/pullrequest/gitlab/testdata/action_dependencies.json: -------------------------------------------------------------------------------- 1 | {"manifests": {"package.json": {}}} 2 | -------------------------------------------------------------------------------- /internal/pullrequest/gitlab/utils.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/dropseed/deps/internal/ci/gitlabci" 8 | ) 9 | 10 | func getAPIToken() string { 11 | if s := os.Getenv("DEPS_GITLAB_TOKEN"); s != "" { 12 | return s 13 | } 14 | 15 | return "" 16 | } 17 | 18 | func getAPIUsername() string { 19 | if s := os.Getenv("DEPS_GITLAB_USERNAME"); s != "" { 20 | return s 21 | } 22 | 23 | return "gitlab-ci-token" 24 | } 25 | 26 | func getProjectAPIURL() (string, error) { 27 | if s := os.Getenv("DEPS_GITLAB_PROJECT_API_URL"); s != "" { 28 | return s, nil 29 | } 30 | 31 | if gitlabciURL := gitlabci.GetProjectAPIURL(); gitlabciURL != "" { 32 | return gitlabciURL, nil 33 | } 34 | 35 | // TODO otherwise from git remote? 36 | 37 | return "", errors.New("Unable to determine GitLab API url for this project") 38 | } 39 | -------------------------------------------------------------------------------- /internal/pullrequest/main.go: -------------------------------------------------------------------------------- 1 | package pullrequest 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/config" 9 | "github.com/dropseed/deps/internal/git" 10 | "github.com/dropseed/deps/internal/pullrequest/bitbucket" 11 | "github.com/dropseed/deps/internal/pullrequest/github" 12 | "github.com/dropseed/deps/internal/pullrequest/gitlab" 13 | "github.com/dropseed/deps/pkg/schema" 14 | ) 15 | 16 | const GITHUB = "github" 17 | const GITLAB = "gitlab" 18 | const BITBUCKET = "bitbucket" 19 | 20 | // PullrequestAdapter implements the basic Pullrequest functions 21 | type PullrequestAdapter interface { 22 | CreateOrUpdate() error 23 | GetSetting(string) interface{} 24 | } 25 | 26 | type RepoAdapter interface { 27 | CheckRequirements() error 28 | Autoconfigure() 29 | // NewPullrequest(*schema.Dependencies, string) PullrequestAdapter 30 | } 31 | 32 | func NewRepo() RepoAdapter { 33 | gitHost := gitHost() 34 | 35 | if gitHost == GITHUB { 36 | return github.NewRepo() 37 | } 38 | 39 | if gitHost == GITLAB { 40 | return gitlab.NewRepo() 41 | } 42 | 43 | if gitHost == BITBUCKET { 44 | return bitbucket.NewRepo() 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func NewPullrequest(base string, head string, deps *schema.Dependencies, cfg *config.Dependency) (PullrequestAdapter, error) { 51 | gitHost := gitHost() 52 | 53 | if gitHost == GITHUB { 54 | return github.NewPullRequest(base, head, deps, cfg) 55 | } 56 | 57 | if gitHost == GITLAB { 58 | return gitlab.NewMergeRequest(base, head, deps, cfg) 59 | } 60 | 61 | if gitHost == BITBUCKET { 62 | return bitbucket.NewPullRequest(base, head, deps, cfg) 63 | } 64 | 65 | return nil, errors.New("Repo not found or not supported") 66 | } 67 | 68 | func gitHost() string { 69 | // or can maybe tell from github actions env var too or gitlab pipeline, but both should have remote as well 70 | if override := os.Getenv("DEPS_GIT_HOST"); override != "" { 71 | return override 72 | } 73 | 74 | remote := git.GitRemote() 75 | 76 | // TODO https://user:pass@ 77 | 78 | if strings.HasPrefix(remote, "https://github.com/") || strings.HasPrefix(remote, "git@github.com:") { 79 | return GITHUB 80 | } 81 | 82 | if strings.HasPrefix(remote, "https://gitlab.com/") || strings.HasPrefix(remote, "git@gitlab.com:") { 83 | return GITLAB 84 | } 85 | 86 | if strings.HasPrefix(remote, "https://bitbucket.org") || strings.HasPrefix(remote, "git@bitbucket.org:") { 87 | return BITBUCKET 88 | } 89 | 90 | // More generic matching (github.example.com, etc. but could also accidently match gitlab.example.com/org/github-api) 91 | 92 | if strings.Contains(remote, "github") { 93 | return GITHUB 94 | } 95 | 96 | if strings.Contains(remote, "gitlab") { 97 | return GITLAB 98 | } 99 | 100 | if strings.Contains(remote, "bitbucket") { 101 | return BITBUCKET 102 | } 103 | 104 | return "" 105 | } 106 | -------------------------------------------------------------------------------- /internal/pullrequest/main_test.go: -------------------------------------------------------------------------------- 1 | package pullrequest 2 | 3 | // func TestNewPullrequestFromEnv(t *testing.T) { 4 | // config := config.Config{} 5 | // // config.LoadFlags() 6 | // config.LoadEnvSettings() 7 | // 8 | // os.Setenv("GIT_BRANCH", "tester") 9 | // 10 | // pr := NewPullrequestFromEnv("branch-name", "pr title", "pr body", &config) 11 | // 12 | // if pr.DefaultBaseBranch != "tester" { 13 | // t.Error("DefaultBaseBranch value incorrect") 14 | // } 15 | // 16 | // if pr.Branch != "branch-name" { 17 | // t.Error("Branch value incorrect") 18 | // } 19 | // 20 | // if pr.Title != "pr title" { 21 | // t.Error("Title value incorrect") 22 | // } 23 | // 24 | // if pr.Body != "pr body" { 25 | // t.Error("Body value incorrect") 26 | // } 27 | // 28 | // if pr.Config != &config { 29 | // t.Error("Config value incorrect") 30 | // } 31 | // } 32 | -------------------------------------------------------------------------------- /internal/runner/ci_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dropseed/deps/pkg/schema" 8 | ) 9 | 10 | func checkRender(jsonName, template, expected string) error { 11 | deps, err := schema.NewDependenciesFromJSONPath(fmt.Sprintf("./testdata/%s.json", jsonName)) 12 | if err != nil { 13 | panic(err) 14 | } 15 | message, err := renderCommitMessage(deps, template) 16 | if err != nil { 17 | return err 18 | } 19 | if message != expected { 20 | return fmt.Errorf("Message does not match: %s\n---\n%s\n", message, expected) 21 | } 22 | return nil 23 | } 24 | 25 | func TestCommitMessageSubjectTemplates(t *testing.T) { 26 | if err := checkRender("single_dependency", "{{.Subject}}", "Update pullrequest from 0.1.0 to 0.3.0"); err != nil { 27 | t.Error(err) 28 | } 29 | if err := checkRender("single_dependency", "[deps] {{.Subject}}", "[deps] Update pullrequest from 0.1.0 to 0.3.0"); err != nil { 30 | t.Error(err) 31 | } 32 | if err := checkRender("single_dependency", "{{.Subject}} (skip ci)", "Update pullrequest from 0.1.0 to 0.3.0 (skip ci)"); err != nil { 33 | t.Error(err) 34 | } 35 | if err := checkRender("single_dependency", " {{.Subject}} ", "Update pullrequest from 0.1.0 to 0.3.0"); err != nil { 36 | t.Error(err) 37 | } 38 | if err := checkRender("single_dependency", "{{.Subject}}\n\nChangelog: updated", "Update pullrequest from 0.1.0 to 0.3.0\n\nChangelog: updated"); err != nil { 39 | t.Error(err) 40 | } 41 | } 42 | 43 | func TestCommitMessageSubjectBodyTemplates(t *testing.T) { 44 | if err := checkRender("single_dependency", "{{.SubjectAndBody}}", "Update pullrequest from 0.1.0 to 0.3.0"); err != nil { 45 | t.Error(err) 46 | } 47 | if err := checkRender("two_dependencies", "{{.SubjectAndBody}}", "Update requirements.txt (pullrequest and requests)\n\n- `pullrequest` in `requirements.txt` from 0.1.0 to 0.3.0\n- `requests` in `requirements.txt` from 0.1.0 to 0.3.0"); err != nil { 48 | t.Error(err) 49 | } 50 | if err := checkRender("single_lockfile", "{{.SubjectAndBody}}", "Update yarn.lock (postcss-cli and tailwindcss)\n\n- `yarn.lock` was updated (including 2 direct and 44 transitive dependencies)\n - `postcss-cli` was updated from 6.1.2 to 6.1.3\n - `tailwindcss` was updated from 1.0.1 to 1.1.2"); err != nil { 51 | t.Error(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/runner/local.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/dropseed/deps/internal/config" 8 | "github.com/dropseed/deps/internal/git" 9 | "github.com/dropseed/deps/internal/output" 10 | 11 | "github.com/manifoldco/promptui" 12 | ) 13 | 14 | // Local runs a full interactive update process 15 | func Local() error { 16 | if git.HasStagedChanges() { 17 | return errors.New("You can't have staged changes while running this command. Please commit or unstage them.") 18 | } 19 | 20 | if git.IsDirty() { 21 | output.Warning("You have uncommitted changes! We are going to stage them so that we can tell the difference between changes. They will be unstaged when this command exits.\n") 22 | git.Add() 23 | } 24 | 25 | cfg, err := config.FindOrInfer() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | allUpdates, err := collectUpdates(cfg, []string{}, []string{}) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | if git.HasStagedChanges() || git.IsDirty() { 36 | output.Debug("Restoring the state of your repo before updates were collected") 37 | git.Checkout(".") 38 | git.Unstage() 39 | } 40 | 41 | newUpdates, _, _, err := organizeUpdates(allUpdates) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if err := newUpdates.prompt(); err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (updates Updates) prompt() error { 54 | for { 55 | refs := map[int]string{} 56 | items := []string{} 57 | 58 | updateIndex := 0 59 | for _, update := range updates { 60 | if !update.completed { 61 | items = append(items, update.title) 62 | refs[updateIndex] = update.id 63 | updateIndex++ 64 | } 65 | } 66 | 67 | if len(items) < 1 { 68 | // No updates left 69 | break 70 | } 71 | 72 | items = append(items, "Done") 73 | 74 | prompt := promptui.Select{ 75 | Label: fmt.Sprintf("Choose an update to make"), 76 | Items: items, 77 | } 78 | 79 | fmt.Println() 80 | i, _, err := prompt.Run() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if i+1 == len(items) { 86 | // Chose to quit 87 | break 88 | } 89 | 90 | update := updates[refs[i]] 91 | if _, err := update.runner.Act(update.dependencies); err != nil { 92 | return err 93 | } 94 | update.completed = true 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/runner/main.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/dropseed/deps/internal/component" 8 | "github.com/dropseed/deps/internal/config" 9 | "github.com/dropseed/deps/internal/output" 10 | ) 11 | 12 | func organizeUpdates(updates Updates) (Updates, Updates, Updates, error) { 13 | newUpdates := Updates{} // PRs for these 14 | outdatedUpdates := Updates{} // lockfile update on these? 15 | existingUpdates := Updates{} 16 | 17 | for _, update := range updates { 18 | if update.exists() { 19 | existingUpdates.addUpdate(update) 20 | } else if outdated := update.outdatedBranch(); outdated != "" { 21 | update.branch = outdated // change the branch to the existing match 22 | outdatedUpdates.addUpdate(update) 23 | } else { 24 | newUpdates.addUpdate(update) 25 | } 26 | } 27 | 28 | if len(outdatedUpdates) > 0 { 29 | fmt.Println() 30 | output.Event("%d outdated updates", len(outdatedUpdates)) 31 | outdatedUpdates.printOverview() 32 | } 33 | 34 | if len(existingUpdates) > 0 { 35 | fmt.Println() 36 | output.Event("%d existing updates", len(existingUpdates)) 37 | existingUpdates.printOverview() 38 | } 39 | 40 | if len(newUpdates) > 0 { 41 | fmt.Println() 42 | output.Event("%d new updates to be made", len(newUpdates)) 43 | newUpdates.printOverview() 44 | } 45 | 46 | return newUpdates, outdatedUpdates, existingUpdates, nil 47 | } 48 | 49 | func collectUpdates(cfg *config.Config, types []string, paths []string) (Updates, error) { 50 | if len(types) > 0 { 51 | output.Event("Only collecting types: %s", strings.Join(types, ", ")) 52 | } 53 | typesMap := map[string]bool{} 54 | for _, t := range types { 55 | if strings.HasPrefix(t, "!") { 56 | typesMap[t[1:]] = false 57 | } else { 58 | typesMap[t] = true 59 | } 60 | } 61 | 62 | if len(paths) > 0 { 63 | output.Event("Only collecting paths: %s", strings.Join(paths, ", ")) 64 | } 65 | pathsMap := map[string]bool{} 66 | for _, p := range paths { 67 | if strings.HasPrefix(p, "!") { 68 | pathsMap[p[1:]] = false 69 | } else { 70 | pathsMap[p] = true 71 | } 72 | } 73 | 74 | updates := Updates{} 75 | 76 | for index, dependencyConfig := range cfg.Dependencies { 77 | 78 | if enabled, ok := typesMap[dependencyConfig.Type]; len(typesMap) > 0 && (!ok || !enabled) { 79 | continue 80 | } 81 | 82 | if enabled, ok := pathsMap[dependencyConfig.Path]; len(pathsMap) > 0 && (!ok || !enabled) { 83 | continue 84 | } 85 | 86 | runner, err := component.NewRunnerFromString(dependencyConfig.Type) 87 | if err != nil { 88 | return nil, err 89 | } 90 | env, err := dependencyConfig.Environ() 91 | if err != nil { 92 | return nil, err 93 | } 94 | runner.Index = index 95 | runner.Env = env 96 | 97 | if err := runner.Install(); err != nil { 98 | return nil, err 99 | } 100 | 101 | dependencies, err := runner.Collect(dependencyConfig.Path) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | depUpdates, err := newUpdatesFromDependencies(dependencies, dependencyConfig) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if len(depUpdates) > 0 { 112 | for _, update := range depUpdates { 113 | // Store this for use later 114 | update.runner = runner 115 | updates.addUpdate(update) 116 | } 117 | } 118 | } 119 | 120 | return updates, nil 121 | } 122 | -------------------------------------------------------------------------------- /internal/runner/testdata/single_dependency.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifests": { 3 | "/": { 4 | "current": { 5 | "dependencies": { 6 | "pullrequest": { 7 | "source": "go", 8 | "constraint": "0.1.0" 9 | } 10 | } 11 | }, 12 | "updated": { 13 | "dependencies": { 14 | "pullrequest": { 15 | "source": "go", 16 | "constraint": "0.3.0" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/runner/testdata/two_dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifests": { 3 | "requirements.txt": { 4 | "current": { 5 | "dependencies": { 6 | "pullrequest": { 7 | "source": "go", 8 | "constraint": "0.1.0" 9 | }, 10 | "requests": { 11 | "source": "pip", 12 | "constraint": "0.1.0" 13 | } 14 | } 15 | }, 16 | "updated": { 17 | "dependencies": { 18 | "pullrequest": { 19 | "source": "go", 20 | "constraint": "0.3.0" 21 | }, 22 | "requests": { 23 | "source": "pip", 24 | "constraint": "0.3.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/runner/update.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dropseed/deps/internal/schemaext" 7 | 8 | "github.com/dropseed/deps/internal/component" 9 | "github.com/dropseed/deps/internal/config" 10 | "github.com/dropseed/deps/internal/git" 11 | "github.com/dropseed/deps/pkg/schema" 12 | ) 13 | 14 | // Update contains relevant data for a potential dependency update 15 | type Update struct { 16 | dependencies *schema.Dependencies 17 | dependencyConfig *config.Dependency 18 | completed bool 19 | runner *component.Runner 20 | id string 21 | title string 22 | branch string 23 | } 24 | 25 | func NewUpdate(deps *schema.Dependencies, cfg *config.Dependency) *Update { 26 | if err := deps.Validate(); err != nil { 27 | panic(err) 28 | } 29 | 30 | updateID := schemaext.UpdateIDForDeps(deps) 31 | uniqueID := schemaext.UniqueIDForDeps(deps) 32 | branch := git.GetBranchName(fmt.Sprintf("%s-%s", updateID, uniqueID)) 33 | 34 | update := Update{ 35 | dependencies: deps, 36 | dependencyConfig: cfg, 37 | id: updateID, 38 | title: schemaext.TitleForDeps(deps), 39 | branch: branch, 40 | } 41 | 42 | return &update 43 | } 44 | 45 | func (update *Update) exists() bool { 46 | b := git.BranchMatching(update.branch) 47 | return b != "" 48 | } 49 | 50 | func (update *Update) outdatedBranch() string { 51 | // Assumes exists() was already checked for an exact match, 52 | // so this checks ANY branches that match the prefix ID 53 | return git.BranchMatching(update.branchPrefix()) 54 | } 55 | 56 | func (update *Update) branchPrefix() string { 57 | return git.GetBranchName(update.id) 58 | } 59 | -------------------------------------------------------------------------------- /internal/runner/updates.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "github.com/dropseed/deps/internal/config" 5 | "github.com/dropseed/deps/internal/output" 6 | "github.com/dropseed/deps/pkg/schema" 7 | ) 8 | 9 | type Updates map[string]*Update 10 | 11 | func (updates Updates) add(deps *schema.Dependencies, cfg *config.Dependency) { 12 | update := NewUpdate(deps, cfg) 13 | updates.addUpdate(update) 14 | } 15 | 16 | func (updates Updates) addUpdate(update *Update) { 17 | updates[update.id] = update 18 | } 19 | 20 | func (updates Updates) removeUpdate(update *Update) { 21 | delete(updates, update.id) 22 | } 23 | 24 | func (updates Updates) printOverview() { 25 | if len(updates) < 1 { 26 | output.Success("No updates found") 27 | } 28 | 29 | for _, update := range updates { 30 | output.Event("[%s] %s", update.id, update.title) 31 | } 32 | } 33 | 34 | func newUpdatesFromDependencies(dependencies *schema.Dependencies, dependencyConfig *config.Dependency) (Updates, error) { 35 | updates := Updates{} 36 | 37 | if *dependencyConfig.LockfileUpdates.Enabled { 38 | output.Debug("Filtering lockfile updates") 39 | for path, lockfile := range dependencies.Lockfiles { 40 | // output.Debug("%s has updates: %t", path, lockfile.HasUpdates()) 41 | if !lockfile.HasUpdates() { 42 | continue 43 | } 44 | 45 | // All lockfile updates are split out individually 46 | 47 | updateDependencies := schema.Dependencies{ 48 | Lockfiles: map[string]*schema.Lockfile{ 49 | path: lockfile, 50 | }, 51 | } 52 | 53 | updates.add(&updateDependencies, dependencyConfig) 54 | } 55 | } else { 56 | output.Event("Lockfile updates disbled") 57 | } 58 | 59 | if *dependencyConfig.ManifestUpdates.Enabled { 60 | output.Debug("Filtering manifest updates") 61 | for path, manifest := range dependencies.Manifests { 62 | // output.Debug("%s has updates: %t", path, manifest.HasUpdates()) 63 | if !manifest.HasUpdates() { 64 | continue 65 | } 66 | 67 | filteredGroups, err := dependencyConfig.ManifestUpdates.FilteredDependencyGroups(manifest.Updated.Dependencies) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | for _, groupDeps := range filteredGroups { 73 | 74 | updateDependencies := schema.Dependencies{ 75 | Manifests: map[string]*schema.Manifest{ 76 | path: &schema.Manifest{ 77 | LockfilePath: manifest.LockfilePath, 78 | Current: &schema.ManifestVersion{ 79 | Dependencies: map[string]*schema.ManifestDependency{}, 80 | }, 81 | Updated: &schema.ManifestVersion{ 82 | Dependencies: map[string]*schema.ManifestDependency{}, 83 | }, 84 | }, 85 | }, 86 | } 87 | 88 | for name, dep := range groupDeps { 89 | updateDependencies.Manifests[path].Current.Dependencies[name] = manifest.Current.Dependencies[name] 90 | updateDependencies.Manifests[path].Updated.Dependencies[name] = dep 91 | } 92 | 93 | updates.add(&updateDependencies, dependencyConfig) 94 | } 95 | } 96 | } else { 97 | output.Event("Manifest updates disbled") 98 | } 99 | 100 | return updates, nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/runner/updates_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | // import ( 4 | // "strings" 5 | // "testing" 6 | 7 | // "github.com/dropseed/deps/internal/config" 8 | // "github.com/dropseed/deps/pkg/schema" 9 | // ) 10 | 11 | // func getDepConfig() *config.Dependency { 12 | // content := `version: 3 13 | // dependencies: 14 | // - type: js 15 | // ` 16 | 17 | // config, err := config.NewConfigFromReader(strings.NewReader(content), nil) 18 | // if err != nil { 19 | // panic(err) 20 | // } 21 | // config.Compile() 22 | // return config.Dependencies[0] 23 | // } 24 | 25 | // func TestNoDependencies(t *testing.T) { 26 | // dependencies, err := schema.NewDependenciesFromJSONPath("./testdata/no_dependencies.json") 27 | // if err != nil { 28 | // t.Error(err) 29 | // } 30 | 31 | // updates, err := newUpdatesFromDependencies(dependencies, getDepConfig()) 32 | // if err != nil { 33 | // t.Error(err) 34 | // } 35 | 36 | // if len(updates) != 0 { 37 | // t.FailNow() 38 | // } 39 | // } 40 | 41 | // func TestLockfileUpdate(t *testing.T) { 42 | // dependencies, err := schema.NewDependenciesFromJSONPath("./testdata/single_lockfile.json") 43 | // if err != nil { 44 | // t.Error(err) 45 | // } 46 | 47 | // updates, err := newUpdatesFromDependencies(dependencies, getDepConfig()) 48 | // if err != nil { 49 | // t.Error(err) 50 | // } 51 | 52 | // if len(updates) != 1 { 53 | // t.FailNow() 54 | // } 55 | 56 | // if updates[0].summary != "- `yarn.lock` was updated (including 5 updated direct dependencies)" { 57 | // t.FailNow() 58 | // } 59 | // } 60 | 61 | // // func TestManifestUpdates(t *testing.T) { 62 | // // dependencies, err := schema.NewDependenciesFromJSONPath("./testdata/two_dependencies.json") 63 | // // if err != nil { 64 | // // t.Error(err) 65 | // // } 66 | 67 | // // updates, err := newUpdatesFromDependencies(dependencies, getDepConfig()) 68 | // // if err != nil { 69 | // // t.Error(err) 70 | // // } 71 | 72 | // // if len(updates) != 2 { 73 | // // fmt.Println(len(updates)) 74 | // // t.FailNow() 75 | // // } 76 | 77 | // // if updates[0].summary != "- `pullrequest` in `requirements.txt` from \"0.1.0\" to \"0.3.0\"" { 78 | // // fmt.Println(updates[0].summary) 79 | // // t.FailNow() 80 | // // } 81 | 82 | // // if updates[1].summary != "- `requests` in `requirements.txt` from \"1.0.0\" to \"3.0.0\"" { 83 | // // fmt.Println(updates[1].summary) 84 | // // t.FailNow() 85 | // // } 86 | // // } 87 | -------------------------------------------------------------------------------- /internal/schemaext/id.go: -------------------------------------------------------------------------------- 1 | package schemaext 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | 8 | "github.com/dropseed/deps/pkg/schema" 9 | ) 10 | 11 | func UpdateIDForDeps(dependencies *schema.Dependencies) string { 12 | truncated := schema.Dependencies{ 13 | // TODO if type is important to separate updates between components, 14 | // then can add Dependencies.Type and use that too 15 | Lockfiles: map[string]*schema.Lockfile{}, 16 | Manifests: map[string]*schema.Manifest{}, 17 | } 18 | 19 | if dependencies.HasLockfiles() { 20 | for name := range dependencies.Lockfiles { 21 | // Only care about the filename 22 | truncated.Lockfiles[name] = nil 23 | } 24 | } 25 | 26 | if dependencies.HasManifests() { 27 | for name, manifest := range dependencies.Manifests { 28 | if !manifest.HasUpdates() { 29 | continue 30 | } 31 | 32 | // Only care about the filename + dependency names 33 | truncatedManifest := &schema.Manifest{ 34 | Updated: &schema.ManifestVersion{ 35 | Dependencies: map[string]*schema.ManifestDependency{}, 36 | }, 37 | } 38 | for dep := range manifest.Updated.Dependencies { 39 | truncatedManifest.Updated.Dependencies[dep] = nil 40 | } 41 | 42 | truncated.Manifests[name] = truncatedManifest 43 | } 44 | } 45 | 46 | return getShortMD5(truncated) 47 | } 48 | 49 | func UniqueIDForDeps(dependencies *schema.Dependencies) string { 50 | return getShortMD5(dependencies) 51 | } 52 | 53 | func getShortMD5(i interface{}) string { 54 | out, err := json.Marshal(i) 55 | if err != nil { 56 | panic(err) 57 | } 58 | sum := md5.Sum(out) 59 | str := hex.EncodeToString(sum[:]) 60 | short := str[:7] 61 | return short 62 | } 63 | -------------------------------------------------------------------------------- /internal/schemaext/lockfile.go: -------------------------------------------------------------------------------- 1 | package schemaext 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/dropseed/deps/pkg/schema" 8 | ) 9 | 10 | // LockfileChanges stores data about what changes were made to a lockfile 11 | type LockfileChanges struct { 12 | Updated []string 13 | Added []string 14 | Removed []string 15 | } 16 | 17 | func getSummaryLinesForLockfiles(lockfiles map[string]*schema.Lockfile) ([]string, error) { 18 | summaries := make([]string, 0, len(lockfiles)) 19 | 20 | // iterate using the sorted keys instead of unpredictable map 21 | keys := []string{} 22 | for path := range lockfiles { 23 | keys = append(keys, path) 24 | } 25 | sort.Strings(keys) 26 | 27 | for _, lockfilePath := range keys { 28 | lockfile := lockfiles[lockfilePath] 29 | s, err := getSummaryLineForLockfile(lockfile, lockfilePath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | summaries = append(summaries, s) 34 | } 35 | return summaries, nil 36 | } 37 | 38 | func lockfileChangesByType(lockfile *schema.Lockfile) map[string]*LockfileChanges { 39 | changesByType := map[string]*LockfileChanges{} 40 | 41 | for name, dep := range lockfile.Current.Dependencies { 42 | depType := "direct" 43 | if dep.IsTransitive { 44 | depType = "transitive" 45 | } 46 | 47 | _, ok := changesByType[depType] 48 | if !ok { 49 | changesByType[depType] = &LockfileChanges{} 50 | } 51 | changesForType := changesByType[depType] 52 | 53 | if updatedDep, found := lockfile.Updated.Dependencies[name]; !found { 54 | changesForType.Removed = append(changesForType.Removed, name) 55 | } else { 56 | if dep.Version.Name != updatedDep.Version.Name { 57 | changesForType.Updated = append(changesForType.Updated, name) 58 | } 59 | } 60 | } 61 | 62 | for name, dep := range lockfile.Updated.Dependencies { 63 | if _, found := lockfile.Current.Dependencies[name]; !found { 64 | depType := "direct" 65 | if dep.IsTransitive { 66 | depType = "transitive" 67 | } 68 | 69 | _, ok := changesByType[depType] 70 | if !ok { 71 | changesByType[depType] = &LockfileChanges{} 72 | } 73 | changesForType := changesByType[depType] 74 | 75 | changesForType.Added = append(changesForType.Added, name) 76 | } 77 | } 78 | 79 | return changesByType 80 | } 81 | 82 | func getSummaryLineForLockfile(lockfile *schema.Lockfile, lockfilePath string) (string, error) { 83 | changesByType := lockfileChangesByType(lockfile) 84 | 85 | subitems := "" 86 | 87 | numTransitive := 0 88 | numDirect := 0 89 | 90 | if transitive, found := changesByType["transitive"]; found && len(transitive.Updated) > 0 { 91 | numTransitive = len(transitive.Updated) 92 | } 93 | 94 | if direct, found := changesByType["direct"]; found && len(direct.Updated) > 0 { 95 | numDirect = len(direct.Updated) 96 | 97 | sort.Strings(direct.Updated) // sort first to get predictable order 98 | for _, name := range direct.Updated { 99 | currentDep := lockfile.Current.Dependencies[name] 100 | dep := lockfile.Updated.Dependencies[name] 101 | subitems += fmt.Sprintf("\n - `%s` was updated from %s to %s", name, currentDep.Version.Name, dep.Version.Name) 102 | } 103 | } 104 | 105 | parens := fmt.Sprintf(" (including %d direct and %d transitive dependencies)", numDirect, numTransitive) 106 | 107 | return fmt.Sprintf("- `%s` was updated%s%s", lockfilePath, parens, subitems), nil 108 | } 109 | 110 | func getShortOverviewForLockfile(lockfile *schema.Lockfile) string { 111 | changesByType := lockfileChangesByType(lockfile) 112 | 113 | if direct, found := changesByType["direct"]; found && len(direct.Updated) > 0 { 114 | numDirect := len(direct.Updated) 115 | 116 | sort.Strings(direct.Updated) // sort first to get predictable order 117 | 118 | if len(direct.Updated) == 1 { 119 | return direct.Updated[0] 120 | } 121 | 122 | if len(direct.Updated) == 2 { 123 | return fmt.Sprintf("%s and %s", direct.Updated[0], direct.Updated[1]) 124 | } 125 | 126 | return fmt.Sprintf("%s, %s, and %d more", direct.Updated[0], direct.Updated[1], numDirect-2) 127 | } 128 | 129 | if transitive, found := changesByType["transitive"]; found && len(transitive.Updated) > 0 { 130 | numTransitive := len(transitive.Updated) 131 | if numTransitive == 1 { 132 | return fmt.Sprintf("%d transitive dependency", numTransitive) 133 | } else { 134 | return fmt.Sprintf("%d transitive dependencies", numTransitive) 135 | } 136 | } 137 | 138 | return "" 139 | } 140 | -------------------------------------------------------------------------------- /internal/schemaext/main.go: -------------------------------------------------------------------------------- 1 | package schemaext 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/pkg/schema" 9 | ) 10 | 11 | const maxBodyLength = 65535 12 | 13 | func TitleForDeps(s *schema.Dependencies) string { 14 | 15 | lockfiles := map[string]*schema.Lockfile{} 16 | manifests := map[string]*schema.Manifest{} 17 | 18 | for name, lockfile := range s.Lockfiles { 19 | if lockfile.HasUpdates() { 20 | lockfiles[name] = lockfile 21 | } 22 | } 23 | for name, manifest := range s.Manifests { 24 | if manifest.HasUpdates() { 25 | manifests[name] = manifest 26 | } 27 | } 28 | 29 | foundLockfiles := len(lockfiles) > 0 30 | foundManifests := len(manifests) > 0 31 | 32 | if foundLockfiles && foundManifests { 33 | lfPlural := "lockfiles" 34 | if len(lockfiles) == 1 { 35 | lfPlural = "lockfile" 36 | } 37 | mPlural := "manifests" 38 | if len(manifests) == 1 { 39 | mPlural = "manifest" 40 | } 41 | return fmt.Sprintf("Update %v %v and %v %v", len(lockfiles), lfPlural, len(manifests), mPlural) 42 | } 43 | 44 | if foundLockfiles { 45 | lockfilePaths := []string{} 46 | for k := range lockfiles { 47 | lockfilePaths = append(lockfilePaths, k) 48 | } 49 | sort.Strings(lockfilePaths) 50 | 51 | if len(lockfilePaths) == 1 { 52 | if shortOverview := getShortOverviewForLockfile(lockfiles[lockfilePaths[0]]); shortOverview != "" { 53 | return fmt.Sprintf("Update %v (%s)", lockfilePaths[0], shortOverview) 54 | } else { 55 | return fmt.Sprintf("Update %v", lockfilePaths[0]) 56 | } 57 | } 58 | return fmt.Sprintf("Update lockfiles: %v", strings.Join(lockfilePaths, ", ")) 59 | } 60 | 61 | if foundManifests { 62 | 63 | manifestPaths := make([]string, 0, len(manifests)) 64 | for k := range manifests { 65 | manifestPaths = append(manifestPaths, k) 66 | } 67 | 68 | if len(manifestPaths) == 1 { 69 | 70 | manifestPath := manifestPaths[0] 71 | manifest := manifests[manifestPath] 72 | dependencies := manifest.Updated.Dependencies 73 | dependencyNames := make([]string, 0, len(dependencies)) 74 | for k := range dependencies { 75 | dependencyNames = append(dependencyNames, k) 76 | } 77 | 78 | if len(dependencyNames) == 1 { 79 | name := dependencyNames[0] 80 | dep := dependencies[name] 81 | installed := manifest.Current.Dependencies[name].Constraint 82 | updated := dep.Constraint 83 | inManifest := "" 84 | if manifestPath != "" && manifestPath != "." && manifestPath != "/" { 85 | inManifest = fmt.Sprintf(" in %s", manifestPath) 86 | } 87 | return fmt.Sprintf("Update %s%s from %s to %s", dependencyNameForDisplay(name), inManifest, installed, updated) 88 | } 89 | 90 | if shortOverview := getShortOverviewForManifest(manifest); shortOverview != "" { 91 | return fmt.Sprintf("Update %s (%s)", manifestPath, shortOverview) 92 | } else { 93 | return fmt.Sprintf("Update %s", manifestPath) 94 | } 95 | } 96 | 97 | // More than 1 manifest 98 | return fmt.Sprintf("Update dependencies in %v", strings.Join(manifestPaths, ", ")) 99 | } 100 | 101 | return "" 102 | } 103 | 104 | func DescriptionForDeps(s *schema.Dependencies) string { 105 | summaryHeader := "The following dependencies have been updated by [deps](https://www.dependencies.io/):" 106 | summaryLines := DescriptionItemsForDeps(s) 107 | final := summaryHeader + "\n\n" + summaryLines + "\n" 108 | 109 | if len(final) > maxBodyLength { 110 | final = final[:maxBodyLength] 111 | } 112 | 113 | return final 114 | } 115 | 116 | func DescriptionItemsForDeps(s *schema.Dependencies) string { 117 | lockfiles := map[string]*schema.Lockfile{} 118 | manifests := map[string]*schema.Manifest{} 119 | 120 | for name, lockfile := range s.Lockfiles { 121 | if lockfile.HasUpdates() { 122 | lockfiles[name] = lockfile 123 | } 124 | } 125 | for name, manifest := range s.Manifests { 126 | if manifest.HasUpdates() { 127 | manifests[name] = manifest 128 | } 129 | } 130 | 131 | foundLockfiles := len(lockfiles) > 0 132 | foundManifests := len(manifests) > 0 133 | 134 | if !foundLockfiles && !foundManifests { 135 | return "" 136 | } 137 | 138 | summaryLines := []string{} 139 | 140 | if foundLockfiles { 141 | lines, err := getSummaryLinesForLockfiles(lockfiles) 142 | if err != nil { 143 | panic(err) 144 | } 145 | summaryLines = append(summaryLines, lines...) 146 | } 147 | 148 | if foundManifests { 149 | lines, err := getSummaryLinesForManifests(manifests) 150 | if err != nil { 151 | panic(err) 152 | } 153 | summaryLines = append(summaryLines, lines...) 154 | } 155 | 156 | return strings.Join(summaryLines, "\n") 157 | } 158 | -------------------------------------------------------------------------------- /internal/schemaext/manifest.go: -------------------------------------------------------------------------------- 1 | package schemaext 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/dropseed/deps/pkg/schema" 8 | ) 9 | 10 | func getSummaryLinesForManifests(manifests map[string]*schema.Manifest) ([]string, error) { 11 | summaries := make([]string, 0, len(manifests)) 12 | 13 | // iterate using the sorted keys instead of unpredictable map 14 | keys := []string{} 15 | for path := range manifests { 16 | keys = append(keys, path) 17 | } 18 | sort.Strings(keys) 19 | 20 | for _, manifestPath := range keys { 21 | manifest := manifests[manifestPath] 22 | // summary, err := manifest.GetSummaryLine(manifestPath) 23 | // if err != nil { 24 | // return nil, err 25 | // } 26 | // summaries = append(summaries, summary) 27 | // iterate using the sorted keys instead of unpredictable map 28 | keys := []string{} 29 | for name := range manifest.Updated.Dependencies { 30 | keys = append(keys, name) 31 | } 32 | sort.Strings(keys) 33 | 34 | for _, dependencyName := range keys { 35 | s, err := getSummaryLineForDependencyName(manifest, dependencyName, manifestPath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | summaries = append(summaries, s) 40 | } 41 | } 42 | return summaries, nil 43 | } 44 | 45 | func getSummaryLineForDependencyName(manifest *schema.Manifest, name, manifestPath string) (string, error) { 46 | currentDependency := manifest.Current.Dependencies[name] 47 | updatedDependency := manifest.Updated.Dependencies[name] 48 | inManifest := "" 49 | if manifestPath != "" { 50 | inManifest = fmt.Sprintf(" in `%s`", manifestPath) 51 | } 52 | return fmt.Sprintf("- `%s`%s from %s to %s", dependencyNameForDisplay(name), inManifest, currentDependency.Constraint, updatedDependency.Constraint), nil 53 | } 54 | 55 | func getShortOverviewForManifest(manifest *schema.Manifest) string { 56 | names := []string{} 57 | for name := range manifest.Updated.Dependencies { 58 | names = append(names, name) 59 | } 60 | sort.Strings(names) 61 | 62 | if len(names) == 0 { 63 | return "" 64 | } 65 | 66 | if len(names) == 1 { 67 | return names[0] 68 | } 69 | 70 | if len(names) == 2 { 71 | return fmt.Sprintf("%s and %s", names[0], names[1]) 72 | } 73 | 74 | return fmt.Sprintf("%s, %s, and %d more", names[0], names[1], len(names)-2) 75 | } 76 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/single_body.txt: -------------------------------------------------------------------------------- 1 | The following dependencies have been updated by [deps](https://www.dependencies.io/): 2 | 3 | - `pullrequest` in `/` from 0.1.0 to 0.3.0 4 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/single_body_empty_manifest.txt: -------------------------------------------------------------------------------- 1 | The following dependencies have been updated by [deps](https://www.dependencies.io/): 2 | 3 | - `dropseed/pullrequest` from 0.1.0 to 0.3.0 4 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/single_dependency.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifests": { 3 | "/": { 4 | "current": { 5 | "dependencies": { 6 | "pullrequest": { 7 | "source": "go", 8 | "constraint": "0.1.0" 9 | } 10 | } 11 | }, 12 | "updated": { 13 | "dependencies": { 14 | "pullrequest": { 15 | "source": "go", 16 | "constraint": "0.3.0" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/single_dependency_empty_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifests": { 3 | "": { 4 | "current": { 5 | "dependencies": { 6 | "https://github.com/dropseed/pullrequest.git": { 7 | "source": "go", 8 | "constraint": "0.1.0" 9 | } 10 | } 11 | }, 12 | "updated": { 13 | "dependencies": { 14 | "https://github.com/dropseed/pullrequest.git": { 15 | "source": "go", 16 | "constraint": "0.3.0" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/single_lockfile.txt: -------------------------------------------------------------------------------- 1 | The following dependencies have been updated by [deps](https://www.dependencies.io/): 2 | 3 | - `yarn.lock` was updated (including 2 direct and 44 transitive dependencies) 4 | - `postcss-cli` was updated from 6.1.2 to 6.1.3 5 | - `tailwindcss` was updated from 1.0.1 to 1.1.2 6 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/two_body.txt: -------------------------------------------------------------------------------- 1 | The following dependencies have been updated by [deps](https://www.dependencies.io/): 2 | 3 | - `pullrequest` in `requirements.txt` from 0.1.0 to 0.3.0 4 | - `requests` in `requirements.txt` from 0.1.0 to 0.3.0 5 | -------------------------------------------------------------------------------- /internal/schemaext/testdata/two_dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifests": { 3 | "requirements.txt": { 4 | "current": { 5 | "dependencies": { 6 | "pullrequest": { 7 | "source": "go", 8 | "constraint": "0.1.0" 9 | }, 10 | "requests": { 11 | "source": "pip", 12 | "constraint": "0.1.0" 13 | } 14 | } 15 | }, 16 | "updated": { 17 | "dependencies": { 18 | "pullrequest": { 19 | "source": "go", 20 | "constraint": "0.3.0" 21 | }, 22 | "requests": { 23 | "source": "pip", 24 | "constraint": "0.3.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/schemaext/utils.go: -------------------------------------------------------------------------------- 1 | package schemaext 2 | 3 | import "strings" 4 | 5 | func dependencyNameForDisplay(name string) string { 6 | 7 | prefixes := []string{ 8 | "https://github.com/", 9 | "https://gitlab.com/", 10 | } 11 | 12 | for _, prefix := range prefixes { 13 | if strings.HasPrefix(name, prefix) { 14 | name = name[len(prefix):] 15 | } 16 | } 17 | 18 | suffixes := []string{ 19 | ".git/", 20 | ".git", 21 | } 22 | 23 | for _, suffix := range suffixes { 24 | if strings.HasSuffix(name, suffix) { 25 | name = name[:len(name)-len(suffix)] 26 | } 27 | } 28 | 29 | return name 30 | } 31 | -------------------------------------------------------------------------------- /internal/test/compare.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/dropseed/deps/internal/output" 9 | "github.com/sergi/go-diff/diffmatchpatch" 10 | ) 11 | 12 | // const LOOSEHOLDER = "testloose" 13 | 14 | func schemasMatchExactly(given, expected interface{}) (bool, error) { 15 | givenOut, err := json.MarshalIndent(given, "", " ") 16 | if err != nil { 17 | return false, err 18 | } 19 | expectedOut, err := json.MarshalIndent(expected, "", " ") 20 | if err != nil { 21 | return false, err 22 | } 23 | 24 | givenOutString := string(givenOut) 25 | expectedOutString := string(expectedOut) 26 | 27 | if givenOutString != expectedOutString { 28 | printDiff(givenOutString, expectedOutString) 29 | return false, nil 30 | } 31 | 32 | return true, nil 33 | } 34 | 35 | func printDiff(given, expected string) { 36 | output.Error(strings.Repeat("=", 80)) 37 | dmp := diffmatchpatch.New() 38 | diffs := dmp.DiffMain(given, expected, false) 39 | fmt.Println(dmp.DiffPrettyText(diffs)) 40 | output.Error(strings.Repeat("=", 80)) 41 | } 42 | 43 | // func schemasMatchLoosely(given, expected interface{}) (bool, error) { 44 | // if gd, gok := given.(*schema.Dependencies); gok { 45 | // if ed, ok := expected.(*schema.Dependencies); ok { 46 | // cleanSchemaDependencies(gd, ed) 47 | // } 48 | // } 49 | // return schemasMatchExactly(given, expected) 50 | // } 51 | 52 | // func cleanSchemaDependencies(given, expected *schema.Dependencies) { 53 | // output.Debug("Cleaning parsed manifest dependencies for loose comparison") 54 | // // for p, m := range expected.Manifests { 55 | // // for depName, dep := range m.Current.Dependencies { 56 | // // // Truncate the parsed available versions to the same length as the expected 57 | // // // so that new versions can be added onto the end, but nothing else 58 | // // if givenDep, ok := given.Manifests[p].Current.Dependencies[depName]; ok { 59 | // // if execute.Verbosity > 0 { 60 | // // output.Debug("Truncating %s availble from %d to %d", depName, len(givenDep.Available), len(dep.Available)) 61 | // // } 62 | // // givenDep.Available = givenDep.Available[:len(dep.Available)] 63 | // // } 64 | // // } 65 | // // } 66 | 67 | // output.Debug("Cleaning lockfile dependencies for loose comparison") 68 | 69 | // for _, l := range expected.Lockfiles { 70 | // if lfv := l.Current; lfv != nil { 71 | // cleanSchemaLockfileVersion(lfv) 72 | // } 73 | // if lfv := l.Updated; lfv != nil { 74 | // cleanSchemaLockfileVersion(lfv) 75 | // } 76 | // } 77 | // for _, l := range given.Lockfiles { 78 | // if lfv := l.Current; lfv != nil { 79 | // cleanSchemaLockfileVersion(lfv) 80 | // } 81 | // if lfv := l.Updated; lfv != nil { 82 | // cleanSchemaLockfileVersion(lfv) 83 | // } 84 | // } 85 | // } 86 | 87 | // func cleanSchemaLockfileVersion(lfv *schema.LockfileVersion) { 88 | // lfv.Fingerprint = LOOSEHOLDER 89 | // for depName, dep := range lfv.Dependencies { 90 | // if dep.IsTransitive { 91 | // delete(lfv.Dependencies, depName) 92 | // } else { 93 | // dep.Version.Name = LOOSEHOLDER 94 | // // dep.Version.Constraint = LOOSEHOLDER 95 | // } 96 | // } 97 | // } 98 | -------------------------------------------------------------------------------- /internal/test/config.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path" 8 | "reflect" 9 | "regexp" 10 | 11 | "github.com/dropseed/deps/internal/config" 12 | "github.com/dropseed/deps/internal/filefinder" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type Config struct { 17 | Tests []*Test `yaml:"tests"` 18 | path string 19 | } 20 | 21 | func (c *Config) compile() { 22 | for _, test := range c.Tests { 23 | test.config = c 24 | if test.UserConfig == nil { 25 | test.UserConfig = &config.Dependency{} 26 | } 27 | // Set default data paths 28 | if test.Collect == nil { 29 | test.Collect = &TestPhase{ 30 | Input: test.Data, 31 | Output: test.Data, 32 | } 33 | } 34 | if test.Act == nil { 35 | test.Act = &TestPhase{ 36 | Input: test.Data, 37 | Output: test.Data, 38 | } 39 | } 40 | test.UserConfig.Compile() 41 | } 42 | } 43 | 44 | func (c *Config) joinPath(s string) string { 45 | return path.Join(path.Dir(c.path), s) 46 | } 47 | 48 | type Test struct { 49 | Name string `yaml:"name"` 50 | Repo string `yaml:"repo"` 51 | UserConfig *config.Dependency `yaml:"user_config,omitempty"` 52 | config *Config 53 | Diff string `yaml:"diff,omitempty"` 54 | DiffArgs []string `yaml:"diff_args,omitempty"` 55 | Collect *TestPhase `yaml:"collect,omitempty"` 56 | Act *TestPhase `yaml:"act,omitempty"` 57 | Data string `yaml:"data"` 58 | } 59 | 60 | type TestPhase struct { 61 | Input string `yaml:"input,omitempty"` 62 | Output string `yaml:"output"` 63 | } 64 | 65 | func (t *Test) displayName() string { 66 | return t.Name 67 | } 68 | 69 | // NewConfigFromPath loads a Config from a file 70 | func NewConfigFromPath(path string) (*Config, error) { 71 | f, err := os.Open(path) 72 | defer f.Close() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | cfg, err := NewConfigFromReader(f) 78 | if cfg != nil { 79 | cfg.path = path 80 | } 81 | return cfg, err 82 | } 83 | 84 | func NewConfigFromReader(reader io.Reader) (*Config, error) { 85 | cfg := &Config{} 86 | decoder := yaml.NewDecoder(reader) 87 | decoder.SetDefaultMapType(reflect.TypeOf(map[string]interface{}{})) 88 | if err := decoder.Decode(cfg); err != nil { 89 | return nil, err 90 | } 91 | 92 | cfg.compile() 93 | 94 | return cfg, nil 95 | } 96 | 97 | func findTestConfigs(dir string) ([]*Config, error) { 98 | patterns := map[string]*regexp.Regexp{ 99 | "tests_config": regexp.MustCompile("^deps_tests?\\.ya?ml$"), 100 | } 101 | configPaths := filefinder.DeepFindInDir(dir, patterns, 4) 102 | 103 | if len(configPaths) < 1 { 104 | return nil, errors.New("no test config files found") 105 | } 106 | 107 | configs := []*Config{} 108 | for p, _ := range configPaths { 109 | config, err := NewConfigFromPath(p) 110 | if err != nil { 111 | return nil, err 112 | } 113 | configs = append(configs, config) 114 | } 115 | 116 | return configs, nil 117 | } 118 | -------------------------------------------------------------------------------- /internal/version/main.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | var ( 6 | version = "dev" 7 | commit = "none" 8 | date = "unknown" 9 | ) 10 | 11 | var WithMeta = fmt.Sprintf("%v\ncommit %v\nbuilt at %v", version, commit, date) 12 | -------------------------------------------------------------------------------- /pkg/schema/dependencies.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | ) 8 | 9 | type Dependencies struct { 10 | Lockfiles map[string]*Lockfile `json:"lockfiles,omitempty"` 11 | Manifests map[string]*Manifest `json:"manifests,omitempty"` 12 | } 13 | 14 | // NewDependenciesFromJSONPath loads Dependencies from a JSON file path 15 | func NewDependenciesFromJSONPath(path string) (*Dependencies, error) { 16 | fileContent, err := ioutil.ReadFile(path) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return NewDependenciesFromJSONContent(fileContent) 21 | } 22 | 23 | // NewDependenciesFromJSONContent creates a Dependencies instance with Unmarshalled JSON data 24 | func NewDependenciesFromJSONContent(content []byte) (*Dependencies, error) { 25 | deps := Dependencies{} 26 | decoder := json.NewDecoder(bytes.NewReader(content)) 27 | decoder.DisallowUnknownFields() 28 | if err := decoder.Decode(&deps); err != nil { 29 | return nil, err 30 | } 31 | 32 | if err := deps.Validate(); err != nil { 33 | return nil, err 34 | } 35 | 36 | return &deps, nil 37 | } 38 | 39 | func (s *Dependencies) Validate() error { 40 | for _, lockfile := range s.Lockfiles { 41 | if err := lockfile.Validate(); err != nil { 42 | return err 43 | } 44 | } 45 | for _, manifest := range s.Manifests { 46 | if err := manifest.Validate(); err != nil { 47 | return err 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (s *Dependencies) HasLockfiles() bool { 55 | return s.Lockfiles != nil && len(s.Lockfiles) > 0 56 | } 57 | 58 | func (s *Dependencies) HasManifests() bool { 59 | return s.Manifests != nil && len(s.Manifests) > 0 60 | } 61 | -------------------------------------------------------------------------------- /pkg/schema/dependency.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // Dependency contains fields and functions common to lockfiles and manifests 4 | type Dependency struct { 5 | Source string `json:"source"` 6 | Repo string `json:"repo,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/schema/lockfile.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type Lockfile struct { 8 | Current *LockfileVersion `json:"current"` 9 | Updated *LockfileVersion `json:"updated,omitempty"` 10 | } 11 | 12 | type LockfileVersion struct { 13 | Dependencies map[string]*LockfileDependency `json:"dependencies"` 14 | Fingerprint string `json:"fingerprint"` 15 | } 16 | 17 | // Dependency stores data for a manifest or lockfile dependency (some fields will be empty) 18 | type LockfileDependency struct { 19 | // Constraint string `json:"constraint,omitempty"` 20 | Version *Version `json:"version"` 21 | IsTransitive bool `json:"is_transitive,omitempty"` 22 | *Dependency 23 | } 24 | 25 | func (lockfile *Lockfile) Validate() error { 26 | if lockfile.Current != nil { 27 | if err := lockfile.Current.Validate(); err != nil { 28 | return err 29 | } 30 | } else { 31 | return errors.New("lockfile.current is required") 32 | } 33 | 34 | if lockfile.Updated != nil { 35 | if err := lockfile.Updated.Validate(); err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (lockfile *Lockfile) HasUpdates() bool { 44 | return lockfile.Updated != nil && len(lockfile.Updated.Dependencies) > 0 45 | } 46 | 47 | func (lv *LockfileVersion) Validate() error { 48 | if lv.Fingerprint == "" { 49 | return errors.New("lockfile fingerprint is required") 50 | } 51 | 52 | for _, dependency := range lv.Dependencies { 53 | if err := dependency.Validate(); err != nil { 54 | return err 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (ld *LockfileDependency) Validate() error { 62 | if ld.Version != nil { 63 | if err := ld.Version.Validate(); err != nil { 64 | return err 65 | } 66 | } else { 67 | return errors.New("lockfile dependency.version is required") 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/schema/manifest.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Manifest contains manifest data 8 | type Manifest struct { 9 | Current *ManifestVersion `json:"current"` 10 | // TODO remove? 11 | LockfilePath string `json:"lockfile_path,omitempty"` 12 | Updated *ManifestVersion `json:"updated,omitempty"` 13 | } 14 | 15 | // ManifestVersion constains data for a manifest at a specific point in time 16 | type ManifestVersion struct { 17 | Dependencies map[string]*ManifestDependency `json:"dependencies"` 18 | } 19 | 20 | type ManifestDependency struct { 21 | Constraint string `json:"constraint"` 22 | *Dependency 23 | } 24 | 25 | func (manifest *Manifest) Validate() error { 26 | if manifest.Current != nil { 27 | if err := manifest.Current.Validate(); err != nil { 28 | return err 29 | } 30 | } else { 31 | return errors.New("manifest.current is requried") 32 | } 33 | 34 | if manifest.Updated != nil { 35 | if err := manifest.Updated.Validate(); err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (manifest *Manifest) HasUpdates() bool { 44 | return manifest.Updated != nil && len(manifest.Updated.Dependencies) > 0 45 | } 46 | 47 | func (mv *ManifestVersion) Validate() error { 48 | for _, dependency := range mv.Dependencies { 49 | if err := dependency.Validate(); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | func (md *ManifestDependency) Validate() error { 56 | if md.Constraint == "" { 57 | return errors.New("manifest dependency constraint is required") 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/schema/version.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "errors" 4 | 5 | type Version struct { 6 | Name string `json:"name"` 7 | Link string `json:"link,omitempty"` 8 | // License string `json:"license,omitempty"` 9 | // or nested (name, url) 10 | } 11 | 12 | func (v *Version) Validate() error { 13 | if v.Name == "" { 14 | return errors.New("dependency name is requried") 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /scripts/generate-dist: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | goreleaser release --skip-publish --skip-validate --rm-dist 3 | -------------------------------------------------------------------------------- /scripts/generate-install-script: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | godownloader .goreleaser.yml > docs/content/install.sh 3 | -------------------------------------------------------------------------------- /scripts/go-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | go install ./cmd/deps 3 | echo "Installed deps" 4 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | go mod download -------------------------------------------------------------------------------- /scripts/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | ./scripts/run 3 | # ./test 4 | -------------------------------------------------------------------------------- /scripts/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | go run ./cmd/deps $@ 3 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | go test ./... 3 | --------------------------------------------------------------------------------