├── .codeclimate.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitlab-ci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin ├── build └── test ├── chat ├── chat.go └── slack.go ├── cmd ├── codereview.go ├── complete.go ├── issue.go ├── issues.go ├── label.go ├── pullrequest.go ├── revise.go ├── root.go └── start.go ├── common └── common.go ├── go.mod ├── go.sum ├── issuetracking ├── github.go ├── github_test.go ├── issuetracking.go ├── jira.go └── testdata │ ├── github_issue_v3.json │ ├── github_issues_v3.json │ └── github_user_v3.json ├── main.go ├── scm ├── github.go └── scm.go ├── service ├── bash.go ├── github.go └── mocks │ └── GitHubService.go ├── testutils └── testutils.go ├── util ├── confirm.go ├── openbrowser.go └── validateparam.go └── versioncontrol ├── git.go └── versioncontrol.go /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This is our default .CodeClimate.yml, broken out by language. Uncomment the 2 | # sections at the bottom that apply to your project. ACTION comments indicate 3 | # places where config might need to be tweaked. 4 | 5 | version: "2" 6 | 7 | plugins: 8 | 9 | # --------------- 10 | # Cross-language plugins. Should always be on. 11 | 12 | duplication: # Looks for similar and identical code blocks 13 | enabled: true 14 | config: 15 | languages: 16 | go: 17 | java: 18 | javascript: 19 | php: 20 | python: 21 | python_version: 3 # ACTION Comment this out if using Python 2 22 | ruby: 23 | swift: 24 | typescript: 25 | 26 | fixme: # Flags any FIXME, TODO, BUG, XXX, HACK comments so they can be fixed 27 | enabled: true 28 | config: 29 | strings: 30 | - FIXME 31 | - TODO 32 | - HACK 33 | - XXX 34 | - BUG 35 | 36 | # --------------- 37 | # Commonly-used languages - run time is minimal and all of these will work 38 | # whether files of that language are found or not. In general, leave uncommented 39 | 40 | # Markdown 41 | markdownlint: 42 | enabled: true 43 | 44 | # Go 45 | gofmt: 46 | enabled: true 47 | golint: 48 | enabled: true 49 | govet: 50 | enabled: true 51 | 52 | # Ruby 53 | flog: 54 | enabled: true 55 | reek: 56 | enabled: true 57 | rubocop: 58 | enabled: true 59 | channel: rubocop-0-79 # As of March 10, 2020, rubocop 0.80.1 is the latest 60 | # However, it does not work with CodeClimate - throws 61 | # an Invalid JSON error. 62 | # ACTION uncomment bundler-audit below if using Gemfile/Gemfile.lock 63 | # ACTION uncomment brakeman below if using Rails 64 | 65 | # Shell scripts 66 | shellcheck: 67 | enabled: true 68 | 69 | # --------------- 70 | # Other languages - will work with or without language files present. Again, 71 | # runtime is minimal, so OK to leave uncommented. 72 | 73 | # CoffeeScript 74 | coffeelint: 75 | enabled: true 76 | 77 | # CSS 78 | csslint: 79 | enabled: true 80 | 81 | # Groovy 82 | codenarc: 83 | enabled: true 84 | 85 | # Java 86 | pmd: 87 | enabled: true 88 | sonar-java: 89 | enabled: true 90 | config: 91 | sonar.java.source: "7" # ACTION set this to the major version of Java used 92 | # ACTION uncomment checkstyle below if Java code exists in repo 93 | 94 | # Node.js 95 | nodesecurity: 96 | enabled: true 97 | # ACTION uncomment eslint below if JavaScript already exists and .eslintrc 98 | # file exists in repo 99 | 100 | # PHP 101 | phan: 102 | enabled: true 103 | config: 104 | file_extensions: "php" 105 | phpcodesniffer: 106 | enabled: true 107 | config: 108 | file_extensions: "php,inc,lib" 109 | # Using Wordpress standards as our one PHP repo is a Wordpress theme 110 | standards: "PSR1,PSR2,WordPress,WordPress-Core,WordPress-Extra" 111 | phpmd: 112 | enabled: true 113 | config: 114 | file_extensions: "php,inc,lib" 115 | rulesets: "cleancode,codesize,controversial,naming,unusedcode" 116 | sonar-php: 117 | enabled: true 118 | 119 | # Python 120 | bandit: 121 | enabled: true 122 | pep8: 123 | enabled: true 124 | radon: 125 | enabled: true 126 | # config: 127 | # python_version: 2 # ACTION Uncomment these 2 lines if using Python 2 128 | sonar-python: 129 | enabled: true 130 | 131 | # --------------- 132 | # Configuration Required Language specific - these will error and abort the 133 | # codeclimate run if they are turned on and certain files or configuration are 134 | # missing. Should be commented out unless the project already includes the 135 | # necessary files that the linter looks at 136 | 137 | # Ruby - requires presence of Gemfile and Gemfile.lock 138 | # bundler-audit: 139 | # enabled: true 140 | 141 | # Rails - requires detecting a Rails application 142 | # brakeman: 143 | # enabled: true 144 | 145 | # Chef - requires detecting a cookbook 146 | # foodcritic: 147 | # enabled: true 148 | 149 | # Java - might require Java code? Errored when run without 150 | # checkstyle: 151 | # enabled: true 152 | 153 | # JavaScript - requires an eslintrc to be created and added to project 154 | # eslint: 155 | # enabled: true 156 | # channel: "eslint-6" 157 | 158 | # --------------- 159 | # List any files/folders to exclude from checking. Wildcards accepted. Leave 160 | # commented if no files to exclude as an empty array will error 161 | exclude_patterns: 162 | - ".gitignore" 163 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cyberark/community-and-integrations-team @conjurinc/community-and-integrations-team @conjurdemos/community-and-integrations-team @cyberark/conjur-core-team @conjurinc/conjur-core-team @conjurdemos/conjur-core-team 2 | 3 | # Changes to .trivyignore require Security Architect approval 4 | .trivyignore @cyberark/security-architects @conjurinc/security-architects @conjurdemos/security-architects 5 | 6 | # Changes to .codeclimate.yml require Quality Architect approval 7 | .codeclimate.yml @cyberark/quality-architects @conjurinc/quality-architects @conjurdemos/quality-architects 8 | 9 | # Changes to SECURITY.md require Security Architect approval 10 | SECURITY.md @cyberark/security-architects @conjurinc/security-architects @conjurdemos/security-architects 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Create a bug report to help us improve 4 | title: '' 5 | labels: component/dev-tools, kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected Results 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Actual Results (including error logs, if applicable) 24 | A clear and concise description of what actually did happen. 25 | 26 | ## Reproducible 27 | * [ ] Always 28 | * [ ] Sometimes 29 | * [ ] Non-Reproducible 30 | 31 | ## Version/Tag number 32 | What version of the product are you running? Any version info that you can share is helpful. 33 | For example, you might give the version from Docker logs, the Docker tag, a specific download URL, 34 | the output of the `/info` route, etc. 35 | 36 | ## Environment setup 37 | Can you describe the environment in which this product is running? Is it running on a VM / in a container / in a cloud? 38 | Which cloud provider? Which container orchestrator (including version)? 39 | The more info you can share about your runtime environment, the better we may be able to reproduce the issue. 40 | 41 | ## Additional Information 42 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: kind/enhancement, component/dev-tools 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 12 | A clear and concise description of what the problem is. Ex.`I would like to see [...] because [...]`. 13 | Please include the intended use case and what the feature would improve on so that we can prioritize 14 | the feature accordingly. 15 | 16 | ## Describe the solution you would like 17 | 18 | A clear and concise description of what the desired end result(s) would be. 19 | 20 | ## Describe alternatives you have considered 21 | 22 | A clear and concise description of any alternative solutions or features that may be related to this that 23 | you have considered. 24 | 25 | ## Additional context 26 | 27 | Add any other context information about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | - _What's changed? Why were these changes made?_ 3 | - _How should the reviewer approach this PR, especially if manual tests are required?_ 4 | - _Are there relevant screenshots you can add to the PR description?_ 5 | 6 | ### What ticket does this PR close? 7 | Resolves #[relevant GitHub issues, eg 76] 8 | 9 | ### Checklists 10 | 11 | #### Change log 12 | - [ ] The CHANGELOG has been updated, or 13 | - [ ] This PR does not include user-facing changes and doesn't require a CHANGELOG update 14 | 15 | #### Test coverage 16 | - [ ] This PR includes new unit and integration tests to go with the code changes, or 17 | - [ ] The changes in this PR do not require tests 18 | 19 | #### Documentation 20 | - [ ] Docs (e.g. `README`s) were updated in this PR, and/or there is a follow-on issue to update docs, or 21 | - [ ] This PR does not require updating any documentation -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dev-flow 2 | vendor/ 3 | dist/ 4 | 5 | # Vim swapfiles 6 | *.sw[po] 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: tmaier/docker-compose:18 2 | 3 | services: 4 | - docker:dind 5 | 6 | variables: 7 | DOCKER_HOST: tcp://docker:2375/ # https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-executor 8 | DOCKER_DRIVER: overlay2 9 | 10 | stages: 11 | - build 12 | - test 13 | 14 | before_script: 15 | - docker info 16 | - apk add --no-cache bash make git curl 17 | 18 | build: 19 | stage: build 20 | script: 21 | - ./bin/build 22 | artifacts: 23 | paths: 24 | - dist/*.tar.gz 25 | - dist/*.zip 26 | - dist/*.rb 27 | - dist/*.deb 28 | - dist/*.rpm 29 | - dist/*.txt # shasum 30 | 31 | test: 32 | stage: test 33 | script: 34 | - ./bin/test 35 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # all available options: https://goreleaser.com/customization/ 2 | project_name: dev-flow 3 | 4 | before: 5 | hooks: 6 | - go mod download 7 | 8 | builds: 9 | - binary: dev-flow 10 | env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - darwin # MacOS 14 | - windows 15 | - linux 16 | goarch: 17 | - amd64 18 | ldflags: [] 19 | main: ./main.go 20 | 21 | archive: 22 | name_template: "{{.ProjectName}}-{{.Os}}-{{.Arch}}" 23 | format_overrides: 24 | - goos: windows 25 | format: zip 26 | files: 27 | - none* # only package the binary - not defaults: readme, license, changelog 28 | 29 | checksum: 30 | name_template: 'SHA256SUMS.txt' 31 | 32 | brew: 33 | description: CLI for standardizing and automating common development tasks 34 | homepage: https://github.com/cyberark/dev-flow 35 | url_template: https://github.com/cyberark/dev-flow/releases/download/v{{.Version}}/summon-conjur-darwin-amd64.tar.gz 36 | install: | 37 | bin.install "dev-flow" 38 | test: | 39 | system "#{bin}/dev-flow", "-h" 40 | 41 | tap: 42 | owner: cyberark 43 | name: homebrew-tools 44 | skip_upload: true 45 | 46 | nfpm: 47 | name_template: "{{.ProjectName}}" 48 | vendor: CyberArk 49 | homepage: https://github.com/cyberark/dev-flow 50 | maintainer: John Tuttle 51 | 52 | description: CLI for standardizing and automating common development tasks 53 | license: Apache2.0 54 | formats: 55 | - deb 56 | - rpm 57 | bindir: /usr/local/bin 58 | 59 | release: 60 | disable: true 61 | prerelease: auto 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## 0.1.0 - 2017-08-27 10 | 11 | First release. 12 | 13 | [Unreleased]: https://github.com/cyberark/conjur/compare/v0.1.0...HEAD -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For general contribution and community guidelines, please see the [community repo](https://github.com/cyberark/community). 4 | 5 | ## Contributing Workflow 6 | 7 | 1. [Fork the project](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 8 | 2. [Clone your fork](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) 9 | 3. Make local changes to your fork by editing files 10 | 3. [Commit your changes](https://help.github.com/en/github/managing-files-in-a-repository/adding-a-file-to-a-repository-using-the-command-line) 11 | 4. [Push your local changes to the remote server](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) 12 | 5. [Create new Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) 13 | 14 | From here your pull request will be reviewed and once you've responded to all 15 | feedback it will be merged into the project. Congratulations, you're a 16 | contributor! 17 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ### Install dependencies: 4 | 5 | ``` 6 | dep ensure 7 | ``` 8 | 9 | ### Build binaries: 10 | 11 | ``` 12 | ./bin/build 13 | ``` 14 | 15 | ### Run tests: 16 | 17 | ``` 18 | ./bin/test 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright (c) 2020 CyberArk Software Ltd. All rights reserved. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dev-flow 2 | 3 | The dev-flow CLI is a tool for standardizing and automating common development 4 | tasks. It currently only supports GitHub for managing issues and pull requests, 5 | but is built to be easily extendible for additional tooling. 6 | 7 | [![GitHub release](https://img.shields.io/github/release/cyberark/dev-flow.svg)](https://github.com/cyberark/dev-flow/releases/latest) 8 | 9 | [![pipeline status](https://gitlab.com/cyberark/dev-flow/badges/master/pipeline.svg)](https://gitlab.com/cyberark/dev-flow/pipelines) 10 | [![Maintainability](https://api.codeclimate.com/v1/badges/2fbe5ba2a5ac283854f0/maintainability)](https://codeclimate.com/github/cyberark/dev-flow/maintainability) 11 | 12 | --- 13 | 14 | ## Setup 15 | 16 | ### Install Golang 17 | 18 | If you haven't already, follow the Go [installation instructions](https://golang.org/doc/install#install). 19 | 20 | ### Install dev-flow 21 | 22 | Install `dev-flow` like so: 23 | 24 | ``` 25 | go get github.com/cyberark/dev-flow 26 | cd $GOPATH/src/github.com/cyberark/dev-flow 27 | go install 28 | ``` 29 | 30 | ### Provide a GitHub Access Token 31 | 32 | `dev-flow` makes heavy use of GitHub and requires that a GitHub access token be 33 | provided in the `GITHUB_ACCESS_TOKEN` environment variable. The following setup 34 | describes one way to provide this token securely using the OSX keychain. 35 | 36 | 1. Create a [GitHub access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) 37 | if you haven't already. 38 | 39 | 1. Install [Summon](https://github.com/cyberark/summon) and the [summon-keyring](https://github.com/conjurinc/summon-keyring) provider. 40 | 41 | 1. Store the GitHub access token in your OSX keychain: 42 | 43 | ``` 44 | $ security add-generic-password -s "summon" -a "github/access_token" -w "insert-token-here" 45 | ``` 46 | 47 | 1. Create `~/.df-secrets.yml` to store a reference to your token: 48 | 49 | ``` 50 | GITHUB_ACCESS_TOKEN: !var github/access_token 51 | ``` 52 | 53 | 1. Create an alias to run `dev-flow` with Summon: 54 | 55 | ``` 56 | alias df='summon -p keyring.py -f ~/.df-secrets.yml dev-flow' 57 | ``` 58 | 59 | That's it! You should now be able to use that alias to run `dev-flow` with the 60 | secrets it needs. 61 | 62 | ### Provide a Slack API Token 63 | 64 | `dev-flow` can be configured to deliver notifications via Slack bot to users 65 | involved with an issue when the state of an issue changes. To enable these 66 | notifications, you must provide the API token for a bot in the `SLACK_API_TOKEN` 67 | environment variable. 68 | 69 | 1. Obtain the token for your Slack org's `dev-flow` app or, if need be, [create an app](https://api.slack.com/slack-apps) 70 | yourself and retrieve its API token. 71 | 72 | 1. Store the Slack API token in your OSX keychain: 73 | 74 | ``` 75 | $ security add-generic-password -s "summon" -a "slack/api_token" -w "insert-token-here" 76 | ``` 77 | 78 | 1. Add the API token to `~/.df-secrets.yml`: 79 | 80 | ``` 81 | SLACK_API_TOKEN: !var slack/api_token 82 | ``` 83 | 84 | `dev-flow` should now be able to send messages to users when their attention is 85 | needed on an issue. 86 | 87 | ### Configure Labels 88 | 89 | `dev-flow` can apply labels during the lifecycle of a story. You can provide the 90 | names of these labels by creating `~/.df-config.yml` like so: 91 | 92 | ``` 93 | labels: 94 | in_progress: 'in progress' 95 | in_review: 'review' 96 | ``` 97 | 98 | You must create these labels in your issue tracker before using them as 99 | `dev-flow` will not create them automatically. 100 | 101 | ## Usage 102 | 103 | Once `dev-flow` is installed, the following commands can be run from the root directory of a source-controlled project: 104 | 105 | - `issues`: list open issues. 106 | - `issue [issue-key]`: open issue in browser. 107 | - `start [issue-num]`: create branch, assign issue to self and update labels. 108 | - `pullrequest` (`pr`): create pull request for current branch into `master`. 109 | - `codereview [username]` (`cr`): create pull request into `master` and assign issue to user. 110 | - `revise`: reject pull request and assign issue back to pull request creator. 111 | - `complete`: merge pull request and (optionally) delete remote and local branches. 112 | 113 | ## Sample Workflow 114 | 115 | Alice and Bob both work on the CoolProject team at CoolOrg. They recently 116 | installed and configured `dev-flow` to automate some of the repetitive tasks 117 | that they must perform on a daily basis when contributing to CoolProject. 118 | 119 | Alice just finished wrapping up her most recent task and decides it's time to 120 | find her next one. She takes a look at the current issues in the CoolProject 121 | repository: 122 | 123 | ``` 124 | $ df issues 125 | 52 - DRY up all the things (unassigned) [needs info] 126 | 67 - something or other needs tests (bob) [feature, in progress] 127 | 45 - fix this crazy bug! (unassigned) [bug, ready] 128 | ``` 129 | 130 | "That last one sounds like a fun challenge", Alice thinks to herself. Let's get 131 | more detail. She runs the `issue` command to open the issue in her browser: 132 | 133 | ``` 134 | $ df issue 45 135 | ``` 136 | 137 | After reading over the issue, She rolls up her sleeves and begins working on the issue: 138 | 139 | ``` 140 | $ df start 45 141 | Assigned issue 45 to user alice. 142 | Added label 'in progress' to issue 45. 143 | ... 144 | [45--fix-this-crazy-bug b6a3fba] Issue 45 Started. 145 | Branch '45--fix-this-crazy-bug' set up to track remote branch '45--fix-this-crazy-bug' from 'origin'. 146 | Issue started! You are now working in branch: 45--fix-this-crazy-bug 147 | ``` 148 | 149 | Just like that, she has a local branch with an automatically generated name set 150 | up to track a remote branch. Not only that but the issue has been labeled to 151 | play nicely with Waffle. How convenient! 152 | 153 | She proceeds to fix the crazy bug, commiting her work when necessary. Finally 154 | it's time to have someone review her work. She knows Bob is involved with the 155 | current project so she creates a pull request for him to review: 156 | 157 | ``` 158 | $ df cr bob 159 | ``` 160 | 161 | Meanwhile, Bob is sitting at his desk typing away when he suddenly receives a 162 | Slack notification from his friendly neighborhood `dev-flow` bot: 163 | 164 | ``` 165 | alice has requested your review on https://github.com/coolorg/coolproject/pull/97 166 | ``` 167 | 168 | Bob has a few minutes to spend checking out the pull request, so he opens the 169 | handy link in the Slack message and reviews Alice's work. His review includes 170 | a few suggested changes, so he kicks it back her way: 171 | 172 | ``` 173 | $ git checkout 45--fix-this-crazy-bug 174 | $ df revise 175 | ``` 176 | 177 | Now it's Alice's turn to get a visit from `dev-flow` bot: 178 | 179 | ``` 180 | bob has requested changes on https://github.com/coolorg/coolproject/pull/97 181 | ``` 182 | 183 | She opens the link to read Bob's feedback and takes a few minutes to make the 184 | requested changes. Afterwards, she passes the story back to Bob: 185 | 186 | ``` 187 | $ df cr bob 188 | ``` 189 | 190 | Bob once again receives a notification from `dev-flow` bot and opens the link in 191 | his browser to verify the requested changes. Satisfied, he approves and merges 192 | the story into `master`: 193 | 194 | ``` 195 | $ df complete 196 | Are you sure you want to merge 45--fix-this-crazy-bug [y/n]: y 197 | Merged 45--fix-this-crazy-bug into master 198 | ... 199 | Delete remote branch 45--fix-this-crazy-bug [y/n]: y 200 | Remote branch deleted. 201 | ... 202 | Delete local branch 45--fix-this-crazy-bug [y/n]: y 203 | Deleted branch 45--fix-this-crazy-bug (was 2f3579e). 204 | Local branch deleted. 205 | ``` 206 | 207 | With Alice's story merged and his own working environment nice and clean, Bob 208 | can continue on his merry way. Meanwhile, Alice receives one last notification 209 | from `dev-flow` bot to let her know that her story has been merged: 210 | 211 | ``` 212 | bob has merged your pull request https://github.com/coolorg/coolproject/pull/97 213 | ``` 214 | 215 | "Thanks, dev-flow bot!", thinks Alice, before she continues with her day. 216 | 217 | ### Contributing 218 | 219 | We welcome contributions of all kinds to this repository. For instructions on 220 | how to get started and descriptions of our development workflows, please see our 221 | [contributing guide](CONTRIBUTING.md). 222 | 223 | ## License 224 | 225 | This repository is licensed under Apache License 2.0 - see [`LICENSE`](LICENSE) for more details. 226 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the CyberArk Conjur 4 | suite of tools and products. 5 | 6 | * [Reporting a Bug](#reporting-a-bug) 7 | * [Disclosure Policy](#disclosure-policy) 8 | * [Comments on this Policy](#comments-on-this-policy) 9 | 10 | ## Reporting a Bug 11 | 12 | The CyberArk Conjur team and community take all security bugs in the Conjur suite seriously. 13 | Thank you for improving the security of the Conjur suite. We appreciate your efforts and 14 | responsible disclosure and will make every effort to acknowledge your 15 | contributions. 16 | 17 | Report security bugs by emailing the lead maintainers at security@conjur.org. 18 | 19 | The maintainers will acknowledge your email within 2 business days. Subsequently, we will 20 | send a more detailed response within 2 business days of our acknowledgement indicating 21 | the next steps in handling your report. After the initial reply to your report, the security 22 | team will endeavor to keep you informed of the progress towards a fix and full 23 | announcement, and may ask for additional information or guidance. 24 | 25 | Report security bugs in third-party modules to the person or team maintaining 26 | the module. 27 | 28 | ## Disclosure Policy 29 | 30 | When the security team receives a security bug report, they will assign it to a 31 | primary handler. This person will coordinate the fix and release process, 32 | involving the following steps: 33 | 34 | * Confirm the problem and determine the affected versions. 35 | * Audit code to find any potential similar problems. 36 | * Prepare fixes for all releases still under maintenance. These fixes will be 37 | released as fast as possible. 38 | 39 | ## Comments on this Policy 40 | 41 | If you have suggestions on how this process could be improved please submit a 42 | pull request. 43 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | BIN_NAME="dev-flow" 4 | GORELEASER_IMAGE="goreleaser/goreleaser:latest-cgo" 5 | 6 | GORELEASER_ARGS='--rm-dist --skip-validate' 7 | if [ "$CI_COMMIT_REF_NAME" != "master" ]; then 8 | GORELEASER_ARGS="$GORELEASER_ARGS --snapshot" 9 | fi 10 | 11 | git fetch --tags # jenkins does not do this automatically yet 12 | 13 | docker pull "${GORELEASER_IMAGE}" 14 | 15 | echo "> Building and packaging binaries" 16 | docker run --rm -t \ 17 | -v "$PWD:/${BIN_NAME}" \ 18 | -w "/${BIN_NAME}" \ 19 | "${GORELEASER_IMAGE}" ${GORELEASER_ARGS} 20 | 21 | goos='linux' # uname -s | tr '[:upper:]' '[:lower:]' 22 | goarch="amd64" 23 | 24 | cp "dist/${goos}_${goarch}/${BIN_NAME}" . # for following test stages 25 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | TEST_CMD='apk add --no-cache git build-base && go build && ./dev-flow -h' 4 | 5 | docker run --rm -t \ 6 | -v "$PWD:/dev-flow" \ 7 | -w "/dev-flow" \ 8 | golang:1.11-alpine sh -c "${TEST_CMD}" 9 | -------------------------------------------------------------------------------- /chat/chat.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type ChatClient interface { 8 | DirectMessage(string, string) 9 | } 10 | 11 | func GetClient() ChatClient { 12 | if os.Getenv("SLACK_API_TOKEN") != "" { 13 | return Slack{} 14 | } else { 15 | return nil 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chat/slack.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/nlopes/slack" 8 | ) 9 | 10 | type Slack struct{} 11 | 12 | func (s Slack) client() *slack.Client { 13 | return slack.New(os.Getenv("SLACK_API_TOKEN")) 14 | } 15 | 16 | func (s Slack) getUserID(userRealName string) (string) { 17 | users, err := s.getAllUsers() 18 | 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | var userID string 24 | 25 | for _, slackUser := range users { 26 | if slackUser.RealName == userRealName { 27 | //fmt.Printf("%+v\n", slackUser) 28 | userID = slackUser.ID 29 | } 30 | } 31 | 32 | return userID 33 | } 34 | 35 | func (s Slack) DirectMessage(userRealName string, message string) { 36 | userID := s.getUserID(userRealName) 37 | 38 | client := s.client() 39 | 40 | _, _, channelID, err := client.OpenIMChannel(userID) 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | params := slack.PostMessageParameters{ 47 | Username: "dev-flow", 48 | AsUser: true, 49 | } 50 | 51 | client.PostMessage(channelID, message, params) 52 | } 53 | 54 | func (s Slack) getAllUsers() (results []slack.User, err error) { 55 | // The Slack API may require pagination in the future, in which case 56 | // this limit of 0 will no longer work. 57 | up := s.client().GetUsersPaginated( 58 | slack.GetUsersOptionPresence(false), 59 | slack.GetUsersOptionLimit(0), 60 | ) 61 | 62 | up, err = up.Next(context.Background()) 63 | 64 | results = append(results, up.Users...) 65 | 66 | return results, up.Failure(err) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/codereview.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/cyberark/dev-flow/chat" 11 | "github.com/cyberark/dev-flow/issuetracking" 12 | "github.com/cyberark/dev-flow/scm" 13 | "github.com/cyberark/dev-flow/util" 14 | "github.com/cyberark/dev-flow/versioncontrol" 15 | ) 16 | 17 | var LinkTypeCodereview string = "close" 18 | 19 | var codereviewCmd = &cobra.Command{ 20 | Use: "codereview [reviewer]", 21 | Aliases: []string{"cr"}, 22 | Short: "Creates a pull request and assigns a reviewer.", 23 | Args: cobra.MinimumNArgs(1), 24 | Run: func(cmd *cobra.Command, args []string) { 25 | util.ValidateStringParam( 26 | "link-type", 27 | LinkTypeCodereview, 28 | []string{ "close", "connect" }, 29 | ) 30 | 31 | reviewer := args[0] 32 | 33 | vc := versioncontrol.GetClient() 34 | repo, err := vc.Repo() 35 | 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | 40 | branchName, err := vc.CurrentBranch() 41 | 42 | if err != nil { 43 | log.Fatalln(err) 44 | } 45 | 46 | it := issuetracking.GetClient(repo) 47 | issueKey := issuetracking.GetIssueKeyFromBranchName(branchName) 48 | issue, err := it.GetIssue(issueKey) 49 | 50 | if err != nil { 51 | log.Fatalln(err) 52 | } 53 | 54 | err = it.RemoveIssueLabel(issue.Number, viper.GetString("labels.start")) 55 | 56 | if err != nil { 57 | log.Println(err) 58 | } 59 | 60 | err = it.AddIssueLabel(issue.Number, viper.GetString("labels.codereview")) 61 | 62 | if err != nil { 63 | log.Println(err) 64 | } 65 | 66 | scm := scm.GetClient(repo) 67 | pr := scm.GetPullRequest(branchName) 68 | 69 | if pr != nil { 70 | fmt.Println("Pull request already exists for branch", branchName) 71 | } else { 72 | pr = scm.CreatePullRequest(*issue, LinkTypeCodereview) 73 | } 74 | 75 | scm.AssignPullRequestReviewer(pr, reviewer) 76 | 77 | chat := chat.GetClient() 78 | 79 | if chat != nil { 80 | login, err := it.GetCurrentUserLogin() 81 | 82 | if err != nil { 83 | log.Fatalln(err) 84 | } 85 | 86 | userRealName, err := it.GetUserRealName(reviewer) 87 | 88 | if err != nil { 89 | log.Fatalln(err) 90 | } 91 | 92 | chat.DirectMessage( 93 | userRealName, 94 | fmt.Sprintf("%v has requested your review on %v", login, pr.URL), 95 | ) 96 | } 97 | 98 | if util.Confirm("Open pull request in browser?") { 99 | util.Openbrowser(pr.URL) 100 | } 101 | }, 102 | } 103 | 104 | func init() { 105 | rootCmd.AddCommand(codereviewCmd) 106 | 107 | codereviewCmd.Flags().StringVarP( 108 | &LinkTypeCodereview, 109 | "link-type", 110 | "l", 111 | "close", 112 | "The type of link to create with the associated issue.", 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/complete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/cyberark/dev-flow/chat" 11 | "github.com/cyberark/dev-flow/issuetracking" 12 | "github.com/cyberark/dev-flow/scm" 13 | "github.com/cyberark/dev-flow/util" 14 | "github.com/cyberark/dev-flow/versioncontrol" 15 | ) 16 | 17 | var MergeMethod string = "rebase" 18 | 19 | var completeCmd = &cobra.Command{ 20 | Use: "complete", 21 | Short: "Squash merges the story branch and completes the issue.", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | util.ValidateStringParam( 24 | "merge-method", 25 | MergeMethod, 26 | []string{ "rebase", "squash", "merge" }, 27 | ) 28 | 29 | vc := versioncontrol.GetClient() 30 | repo, err := vc.Repo() 31 | 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | 36 | branchName, err := vc.CurrentBranch() 37 | 38 | if err != nil { 39 | log.Fatalln(err) 40 | } 41 | 42 | scm := scm.GetClient(repo) 43 | pr := scm.GetPullRequest(branchName) 44 | 45 | if pr == nil { 46 | err := fmt.Sprintf("No pull request found for branch %s", branchName) 47 | log.Fatalln(err) 48 | } 49 | 50 | if !pr.Mergeable { 51 | err := "Pull request not mergeable. Check for conflicts." 52 | log.Fatalln(err) 53 | } 54 | 55 | if !util.Confirm(fmt.Sprintf("Are you sure you want to merge %v into %v?", branchName, pr.Base)) { 56 | log.Fatalln("Pull request not merged.") 57 | } 58 | 59 | success := scm.MergePullRequest(pr, MergeMethod) 60 | 61 | it := issuetracking.GetClient(repo) 62 | issueKey := issuetracking.GetIssueKeyFromBranchName(branchName) 63 | issue, err := it.GetIssue(issueKey) 64 | 65 | if err != nil { 66 | log.Fatalln(err) 67 | } 68 | 69 | if success { 70 | fmt.Printf("Merged %v into %v\n", branchName, pr.Base) 71 | } else { 72 | err := "Merge failed" 73 | log.Fatalln(err) 74 | } 75 | 76 | err = it.AssignIssue(issue.Number, pr.Creator) 77 | 78 | if err != nil { 79 | log.Fatalln(err) 80 | } 81 | 82 | chat := chat.GetClient() 83 | 84 | if chat != nil { 85 | userRealName, err := it.GetUserRealName(pr.Creator) 86 | 87 | if err != nil { 88 | log.Fatalln(err) 89 | } 90 | 91 | login, err := it.GetCurrentUserLogin() 92 | 93 | if err != nil { 94 | log.Fatalln(err) 95 | } 96 | 97 | chat.DirectMessage( 98 | userRealName, 99 | fmt.Sprintf("%v has merged your pull request %v", login, pr.URL), 100 | ) 101 | } 102 | 103 | err = it.RemoveIssueLabel(issue.Number, viper.GetString("labels.codereview")) 104 | 105 | if err != nil { 106 | log.Println(err) 107 | } 108 | 109 | err = it.AddIssueLabel(issue.Number, viper.GetString("labels.complete")) 110 | 111 | if err != nil { 112 | log.Println(err) 113 | } 114 | 115 | vc.CheckoutAndPull(pr.Base) 116 | 117 | if util.Confirm(fmt.Sprintf("Delete remote branch %v", branchName)) { 118 | vc.DeleteRemoteBranch(branchName) 119 | fmt.Println("Remote branch deleted.") 120 | } 121 | 122 | if util.Confirm(fmt.Sprintf("Delete local branch %v", branchName)) { 123 | vc.DeleteLocalBranch(branchName) 124 | fmt.Println("Local branch deleted.") 125 | } 126 | }, 127 | } 128 | 129 | func init() { 130 | rootCmd.AddCommand(completeCmd) 131 | 132 | completeCmd.Flags().StringVarP( 133 | &MergeMethod, 134 | "merge-method", 135 | "m", 136 | "rebase", 137 | "Merge method to use (rebase, squash, or merge). Defaults to rebase.", 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /cmd/issue.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/cyberark/dev-flow/issuetracking" 9 | "github.com/cyberark/dev-flow/util" 10 | "github.com/cyberark/dev-flow/versioncontrol" 11 | ) 12 | 13 | var issueCmd = &cobra.Command{ 14 | Use: "issue", 15 | Short: "Open the specified issue.", 16 | Long: "Open the specified issue.", 17 | Args: cobra.MinimumNArgs(1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | issueKey := args[0] 20 | 21 | vc := versioncontrol.GetClient() 22 | repo, err := vc.Repo() 23 | 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | 28 | it := issuetracking.GetClient(repo) 29 | issue, err := it.GetIssue(issueKey) 30 | 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | 35 | util.Openbrowser(issue.URL) 36 | }, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(issueCmd) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/issues.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/cyberark/dev-flow/issuetracking" 11 | "github.com/cyberark/dev-flow/versioncontrol" 12 | ) 13 | 14 | var issuesCmd = &cobra.Command{ 15 | Use: "issues", 16 | Short: "Lists open, unassigned issues on the current repository.", 17 | Long: "Lists open, unassigned issues on the current repository.", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | vc := versioncontrol.GetClient() 20 | repo, err := vc.Repo() 21 | 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | it := issuetracking.GetClient(repo) 27 | 28 | issues, err := it.GetIssues() 29 | 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | 34 | for _, issue := range issues { 35 | assignee := "unassigned" 36 | 37 | if issue.Assignee != "" { 38 | assignee = issue.Assignee 39 | } 40 | 41 | fmt.Printf( 42 | "%v - %v (%v) [%v] \n", 43 | issue.Number, 44 | issue.Title, 45 | assignee, 46 | strings.Join(issue.Labels, ", "), 47 | ) 48 | } 49 | }, 50 | } 51 | 52 | func init() { 53 | rootCmd.AddCommand(issuesCmd) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/label.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/cyberark/dev-flow/issuetracking" 10 | "github.com/cyberark/dev-flow/versioncontrol" 11 | ) 12 | 13 | var IssueKey string 14 | 15 | var labelCmd = &cobra.Command{ 16 | Use: "label", 17 | Short: "Add a label to an issue.", 18 | Long: "Apply a label to an issue.", 19 | Args: cobra.MinimumNArgs(1), 20 | Run: func(cmd *cobra.Command, args []string) { 21 | label := args[0] 22 | 23 | vc := versioncontrol.GetClient() 24 | repo, err := vc.Repo() 25 | 26 | if err != nil { 27 | log.Fatalln(err) 28 | } 29 | 30 | if IssueKey == "" { 31 | fmt.Println("No issue key provided, retrieving from branch.") 32 | 33 | branchName, err := vc.CurrentBranch() 34 | 35 | if err != nil { 36 | log.Fatalln(err) 37 | } 38 | 39 | IssueKey = issuetracking.GetIssueKeyFromBranchName(branchName) 40 | } 41 | 42 | if IssueKey == "" { 43 | log.Fatalln("No issue key provided") 44 | } 45 | 46 | it := issuetracking.GetClient(repo) 47 | issue, err := it.GetIssue(IssueKey) 48 | 49 | if err != nil { 50 | log.Fatalln(err) 51 | } 52 | 53 | err = it.AddIssueLabel(issue.Number, label) 54 | 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | }, 59 | } 60 | 61 | func init() { 62 | rootCmd.AddCommand(labelCmd) 63 | 64 | labelCmd.Flags().StringVarP( 65 | &IssueKey, 66 | "issue-key", 67 | "i", 68 | "", 69 | "The key of the issue to which the label should be added.", 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/pullrequest.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/cyberark/dev-flow/issuetracking" 10 | "github.com/cyberark/dev-flow/scm" 11 | "github.com/cyberark/dev-flow/util" 12 | "github.com/cyberark/dev-flow/versioncontrol" 13 | ) 14 | 15 | var LinkTypePullrequest string = "close" 16 | 17 | var pullrequestCmd = &cobra.Command{ 18 | Use: "pullrequest", 19 | Aliases: []string{"pr"}, 20 | Short: "Creates a pull request for your branch.", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | util.ValidateStringParam( 23 | "link-type", 24 | LinkTypePullrequest, 25 | []string{ "close", "connect" }, 26 | ) 27 | 28 | vc := versioncontrol.GetClient() 29 | repo, err := vc.Repo() 30 | 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | 35 | branchName, err := vc.CurrentBranch() 36 | 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | 41 | scm := scm.GetClient(repo) 42 | pr := scm.GetPullRequest(branchName) 43 | 44 | if pr != nil { 45 | fmt.Println("Pull request already exists for branch", branchName) 46 | } else { 47 | issueKey := issuetracking.GetIssueKeyFromBranchName(branchName) 48 | issue, err := issuetracking.GetClient(repo).GetIssue(issueKey) 49 | 50 | if err != nil { 51 | log.Fatalln(err) 52 | } 53 | 54 | pr = scm.CreatePullRequest(*issue, LinkTypePullrequest) 55 | } 56 | 57 | if util.Confirm("Open pull request in browser?") { 58 | util.Openbrowser(pr.URL) 59 | } 60 | }, 61 | } 62 | 63 | func init() { 64 | rootCmd.AddCommand(pullrequestCmd) 65 | 66 | pullrequestCmd.Flags().StringVarP( 67 | &LinkTypePullrequest, 68 | "link-type", 69 | "l", 70 | "close", 71 | "The type of link to create with the associated issue.", 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/revise.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/cyberark/dev-flow/chat" 10 | "github.com/cyberark/dev-flow/issuetracking" 11 | "github.com/cyberark/dev-flow/scm" 12 | "github.com/cyberark/dev-flow/versioncontrol" 13 | ) 14 | 15 | var reviseCmd = &cobra.Command{ 16 | Use: "revise", 17 | Short: "Rejects a PR and assigns it back to the implementor.", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | vc := versioncontrol.GetClient() 20 | repo, err := vc.Repo() 21 | 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | branchName, err := vc.CurrentBranch() 27 | 28 | if err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | issueKey := issuetracking.GetIssueKeyFromBranchName(branchName) 33 | 34 | scm := scm.GetClient(repo) 35 | pr := scm.GetPullRequest(branchName) 36 | 37 | // TODO: This won't work when the issue tracker != the scm 38 | // for example Jira vs GitHub 39 | it := issuetracking.GetClient(repo) 40 | issue, err := it.GetIssue(issueKey) 41 | 42 | if err != nil { 43 | log.Fatalln(err) 44 | } 45 | 46 | err = it.AssignIssue(issue.Number, pr.Creator) 47 | 48 | if err != nil { 49 | log.Fatalln(err) 50 | } 51 | 52 | chat := chat.GetClient() 53 | 54 | if chat != nil { 55 | userRealName, err := it.GetUserRealName(pr.Creator) 56 | 57 | if err != nil { 58 | log.Fatalln(err) 59 | } 60 | 61 | login, err := it.GetCurrentUserLogin() 62 | 63 | if err != nil { 64 | log.Fatalln(err) 65 | } 66 | 67 | chat.DirectMessage( 68 | userRealName, 69 | fmt.Sprintf("%v has requested changes on %v", login, pr.URL), 70 | ) 71 | } 72 | }, 73 | } 74 | 75 | func init() { 76 | rootCmd.AddCommand(reviseCmd) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | homedir "github.com/mitchellh/go-homedir" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var cfgFile string 13 | 14 | // rootCmd represents the base command when called without any subcommands 15 | var rootCmd = &cobra.Command{ 16 | Use: "dev-flow", 17 | Short: "A brief description of your application", 18 | Long: `A longer description that spans multiple lines and likely contains 19 | examples and usage of using your application. For example: 20 | 21 | Cobra is a CLI library for Go that empowers applications. 22 | This application is a tool to generate the needed files 23 | to quickly create a Cobra application.`, 24 | // Uncomment the following line if your bare application 25 | // has an action associated with it: 26 | // Run: func(cmd *cobra.Command, args []string) { }, 27 | } 28 | 29 | // Execute adds all child commands to the root command and sets flags appropriately. 30 | // This is called by main.main(). It only needs to happen once to the rootCmd. 31 | func Execute() { 32 | if err := rootCmd.Execute(); err != nil { 33 | log.Fatalln(err) 34 | } 35 | } 36 | 37 | func init() { 38 | cobra.OnInitialize(initConfig) 39 | 40 | // Here you will define your flags and configuration settings. 41 | // Cobra supports persistent flags, which, if defined here, 42 | // will be global for your application. 43 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dev-flow.yaml)") 44 | 45 | // Cobra also supports local flags, which will only run 46 | // when this action is called directly. 47 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 48 | } 49 | 50 | // initConfig reads in config file and ENV variables if set. 51 | func initConfig() { 52 | if cfgFile != "" { 53 | // Use config file from the flag. 54 | viper.SetConfigFile(cfgFile) 55 | } else { 56 | // Find home directory. 57 | home, err := homedir.Dir() 58 | if err != nil { 59 | log.Fatalln(err) 60 | } 61 | 62 | viper.SetConfigType("yaml") 63 | viper.SetConfigFile(fmt.Sprintf("%s/.df-config.yml", home)) 64 | } 65 | 66 | viper.AutomaticEnv() // read in environment variables that match 67 | 68 | // If a config file is found, read it in. 69 | if err := viper.ReadInConfig(); err == nil { 70 | //fmt.Println("Using config file:", viper.ConfigFileUsed()) 71 | } else { 72 | //fmt.Println(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/start.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/cyberark/dev-flow/issuetracking" 11 | "github.com/cyberark/dev-flow/versioncontrol" 12 | ) 13 | 14 | var startCmd = &cobra.Command{ 15 | Use: "start", 16 | Short: "Creates a remote branch for the specified issue", 17 | Args: cobra.ExactArgs(1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | issueKey := args[0] 20 | 21 | vc := versioncontrol.GetClient() 22 | repo, err := vc.Repo() 23 | 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | 28 | it := issuetracking.GetClient(repo) 29 | issue, err := it.GetIssue(issueKey) 30 | 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | 35 | login, err := it.GetCurrentUserLogin() 36 | 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | 41 | err = it.AssignIssue(issue.Number, login) 42 | 43 | if err != nil { 44 | log.Fatalln(err) 45 | } 46 | 47 | fmt.Printf("Assigned issue %v to user %v.\n", issue.Number, login) 48 | 49 | err = it.AddIssueLabel(issue.Number, viper.GetString("labels.start")) 50 | 51 | if err != nil { 52 | log.Println(err) 53 | } 54 | 55 | vc.CheckoutAndPull("master") 56 | 57 | branchName := issue.BranchName() 58 | 59 | isRemote, err := vc.IsRemoteBranch(branchName) 60 | 61 | if err != nil { 62 | log.Fatalln(err) 63 | } 64 | 65 | if isRemote { 66 | output, err := vc.CheckoutAndPull(branchName) 67 | 68 | if err != nil { 69 | log.Fatalln(err) 70 | } 71 | 72 | log.Println(output) 73 | } else { 74 | output, err := vc.InitBranch(issue.Number, branchName) 75 | 76 | if err != nil { 77 | log.Fatalln(err) 78 | } 79 | 80 | log.Println(output) 81 | } 82 | 83 | fmt.Println("Issue started! You are now working in branch:", branchName) 84 | }, 85 | } 86 | 87 | func init() { 88 | rootCmd.AddCommand(startCmd) 89 | } 90 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type Issue struct { 10 | URL string 11 | Number int 12 | Title string 13 | Assignee string 14 | Labels []string 15 | } 16 | 17 | func (issue Issue) String() string { 18 | return fmt.Sprintf("%v - %v", issue.Number, issue.Title) 19 | } 20 | 21 | func (issue Issue) BranchName() string { 22 | title := issue.Title 23 | title = strings.ToLower(title) 24 | title = strings.TrimSpace(title) 25 | 26 | re := regexp.MustCompile(`[^\w\s]`) 27 | title = re.ReplaceAllString(title, "$1$1") 28 | 29 | title = strings.Replace(title, " ", "-", -1) 30 | 31 | return fmt.Sprintf("%v--%v", issue.Number, title) 32 | } 33 | 34 | func (issue Issue) HasLabel(label string) bool { 35 | for _, issueLabel := range issue.Labels { 36 | if issueLabel == label { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | type Repo struct { 44 | Owner string 45 | Name string 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyberark/dev-flow 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.1 // indirect 5 | github.com/fsnotify/fsnotify v1.4.7 // indirect 6 | github.com/golang/protobuf v1.2.0 // indirect 7 | github.com/google/go-github v17.0.0+incompatible 8 | github.com/google/go-querystring v1.0.0 // indirect 9 | github.com/gorilla/websocket v1.3.0 // indirect 10 | github.com/hashicorp/hcl v1.0.0 // indirect 11 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 12 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect 13 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect 14 | github.com/magiconair/properties v1.8.0 // indirect 15 | github.com/mitchellh/go-homedir v1.0.0 16 | github.com/mitchellh/mapstructure v1.0.0 // indirect 17 | github.com/nlopes/slack v0.3.0 18 | github.com/pelletier/go-toml v1.2.0 // indirect 19 | github.com/spf13/afero v1.1.1 // indirect 20 | github.com/spf13/cast v1.2.0 // indirect 21 | github.com/spf13/cobra v0.0.3 22 | github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect 23 | github.com/spf13/pflag v1.0.2 // indirect 24 | github.com/spf13/viper v1.1.0 25 | github.com/stretchr/testify v1.3.0 26 | github.com/vektra/mockery v0.0.0-20181123154057-e78b021dcbb5 // indirect 27 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect 28 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect 30 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 // indirect 31 | golang.org/x/text v0.3.0 // indirect 32 | google.golang.org/appengine v1.1.0 // indirect 33 | gopkg.in/yaml.v2 v2.2.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 9 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 10 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 11 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 12 | github.com/gorilla/websocket v1.3.0 h1:r/LXc0VJIMd0rCMsc6DxgczaQtoCwCLatnfXmSYcXx8= 13 | github.com/gorilla/websocket v1.3.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 14 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 15 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 16 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 17 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 18 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= 19 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= 20 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 h1:MNApn+Z+fIT4NPZopPfCc1obT6aY3SVM6DOctz1A9ZU= 21 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= 22 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 23 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 24 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 25 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 26 | github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I= 27 | github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 28 | github.com/nlopes/slack v0.3.0 h1:jCxvaS8wC4Bb1jnbqZMjCDkOOgy4spvQWcrw/TF0L0E= 29 | github.com/nlopes/slack v0.3.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= 30 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 31 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I= 34 | github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 35 | github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= 36 | github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= 37 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 38 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 39 | github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 h1:kJI9pPzfsULT/72wy7mxkRQZPtKWgFdCA2RTGZ4v8/E= 40 | github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 41 | github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= 42 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 43 | github.com/spf13/viper v1.1.0 h1:V7OZpY8i3C1x/pDmU0zNNlfVoDz112fSYvtWMjjS3f4= 44 | github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 47 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 49 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 50 | github.com/vektra/mockery v0.0.0-20181123154057-e78b021dcbb5 h1:Xim2mBRFdXzXmKRO8DJg/FJtn/8Fj9NOEpO6+WuMPmk= 51 | github.com/vektra/mockery v0.0.0-20181123154057-e78b021dcbb5/go.mod h1:ppEjwdhyy7Y31EnHRDm1JkChoC7LXIJ7Ex0VYLWtZtQ= 52 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= 53 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 55 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 57 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 h1:GqwDwfvIpC33dK9bA1fD+JiDUNsuAiQiEkpHqUKze4o= 59 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/tools v0.0.0-20181112210238-4b1f3b6b1646 h1:JEEoTsNEpPwxsebhPLC6P2jNr+6RFZLY4elUBVcMb+I= 63 | golang.org/x/tools v0.0.0-20181112210238-4b1f3b6b1646/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 64 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 67 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 68 | -------------------------------------------------------------------------------- /issuetracking/github.go: -------------------------------------------------------------------------------- 1 | package issuetracking 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/google/go-github/github" 7 | 8 | "github.com/cyberark/dev-flow/common" 9 | "github.com/cyberark/dev-flow/service" 10 | ) 11 | 12 | type GitHub struct { 13 | Repo common.Repo 14 | GitHubService service.GitHubService 15 | } 16 | 17 | func toCommonIssue(ghIssue *github.Issue) common.Issue { 18 | var assignee string = "" 19 | 20 | if ghIssue.Assignee != nil { 21 | assignee = *ghIssue.Assignee.Login 22 | } 23 | 24 | ghLabels := ghIssue.Labels 25 | labels := make([]string, len(ghLabels)) 26 | 27 | for i, ghLabel := range ghLabels { 28 | labels[i] = *ghLabel.Name 29 | } 30 | 31 | return common.Issue{ 32 | URL: *ghIssue.HTMLURL, 33 | Number: *ghIssue.Number, 34 | Title: *ghIssue.Title, 35 | Assignee: assignee, 36 | Labels: labels, 37 | } 38 | } 39 | 40 | func (gh GitHub) getUser(login string) (*github.User, error) { 41 | return gh.GitHubService.GetUser(login) 42 | } 43 | 44 | func (gh GitHub) GetCurrentUserLogin() (string, error) { 45 | ghUser, err := gh.getUser("") 46 | 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | return *ghUser.Login, nil 52 | } 53 | 54 | func (gh GitHub) GetUserRealName(login string) (string, error) { 55 | ghUser, err := gh.getUser(login) 56 | 57 | if err != nil { 58 | return "", err 59 | } 60 | 61 | return *ghUser.Name, nil 62 | } 63 | 64 | func (gh GitHub) GetIssues() ([]common.Issue, error) { 65 | ghIssues, err := gh.GitHubService.GetIssues(gh.Repo) 66 | 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | var issues []common.Issue 72 | 73 | for _, ghIssue := range ghIssues { 74 | issue := toCommonIssue(ghIssue) 75 | issues = append(issues, issue) 76 | } 77 | 78 | return issues, nil 79 | } 80 | 81 | func (gh GitHub) GetIssue(issueKey string) (*common.Issue, error) { 82 | issueNum, err := strconv.Atoi(issueKey) 83 | 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | ghIssue, err := gh.GitHubService.GetIssue(gh.Repo, issueNum) 89 | 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | issue := toCommonIssue(ghIssue) 95 | 96 | return &issue, nil 97 | } 98 | 99 | func (gh GitHub) AssignIssue(issueNum int, login string) error { 100 | err := gh.GitHubService.AssignIssue(gh.Repo, issueNum, login) 101 | 102 | if err != nil { 103 | return err 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (gh GitHub) AddIssueLabel(issueNum int, labelName string) error { 110 | err := gh.GitHubService.AddLabelToIssue(gh.Repo, issueNum, labelName) 111 | 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (gh GitHub) RemoveIssueLabel(issueNum int, labelName string) error { 120 | err := gh.GitHubService.RemoveLabelForIssue(gh.Repo, issueNum, labelName) 121 | 122 | if err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /issuetracking/github_test.go: -------------------------------------------------------------------------------- 1 | package issuetracking_test 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/google/go-github/github" 11 | 12 | "github.com/cyberark/dev-flow/common" 13 | "github.com/cyberark/dev-flow/issuetracking" 14 | "github.com/cyberark/dev-flow/service/mocks" 15 | "github.com/cyberark/dev-flow/testutils" 16 | "github.com/cyberark/dev-flow/versioncontrol" 17 | ) 18 | 19 | func TestGetCurrentUserLogin(t *testing.T) { 20 | tests := map[string]struct{ 21 | result string 22 | err error 23 | }{ 24 | "success": { result: "octocat", err: nil }, 25 | "propagates error": { result: "", err: errors.New("an error") }, 26 | } 27 | 28 | for _, test := range tests { 29 | var user *github.User = nil 30 | 31 | if test.result != "" { 32 | user = &github.User{} 33 | testutils.LoadFixture("testdata/github_user_v3.json", user) 34 | } 35 | 36 | mockService := &mocks.GitHubService{} 37 | mockService.On("GetUser", "").Return(user, test.err) 38 | 39 | client := issuetracking.GitHub{ 40 | GitHubService: mockService, 41 | } 42 | 43 | login, err := client.GetCurrentUserLogin() 44 | 45 | assert.Equal(t, test.result, login) 46 | assert.Equal(t, test.err, err) 47 | } 48 | } 49 | 50 | func TestGetUserRealName(t *testing.T) { 51 | tests := map[string]struct{ 52 | result string 53 | err error 54 | }{ 55 | "success": { result: "monalisa octocat", err: nil }, 56 | "propagates error": { result: "", err: errors.New("an error") }, 57 | } 58 | 59 | for _, test := range tests { 60 | var user *github.User = nil 61 | 62 | if test.result != "" { 63 | user = &github.User{} 64 | testutils.LoadFixture("testdata/github_user_v3.json", user) 65 | } 66 | 67 | mockService := &mocks.GitHubService{} 68 | mockService.On("GetUser", "octocat").Return(user, test.err) 69 | 70 | client := issuetracking.GitHub{ 71 | GitHubService: mockService, 72 | } 73 | 74 | login, err := client.GetUserRealName("octocat") 75 | 76 | assert.Equal(t, test.result, login) 77 | assert.Equal(t, test.err, err) 78 | } 79 | } 80 | 81 | func TestGetIssues(t *testing.T) { 82 | tests := map[string]struct{ 83 | result []common.Issue 84 | err error 85 | }{ 86 | "success": { 87 | result: []common.Issue{ 88 | { 89 | URL: "https://github.com/octocat/Hello-World/issues/1347", 90 | Number: 1347, 91 | Title: "Found a bug", 92 | Assignee: "octocat", 93 | Labels: []string{"bug"}, 94 | }, 95 | }, 96 | err: nil, 97 | }, 98 | "propagates error": { result: nil, err: errors.New("an error") }, 99 | } 100 | 101 | for _, test := range tests { 102 | var ghIssuesPtrs []*github.Issue 103 | 104 | if test.result != nil { 105 | ghIssues := make([]github.Issue, 0) 106 | 107 | testutils.LoadFixture("testdata/github_issues_v3.json", &ghIssues) 108 | 109 | for i := 0; i < len(ghIssues); i++ { 110 | ghIssuesPtrs = append(ghIssuesPtrs, &ghIssues[i]) 111 | } 112 | } 113 | 114 | repo := common.Repo{ 115 | Owner: "test-owner", 116 | Name: "test-name", 117 | } 118 | 119 | mockService := &mocks.GitHubService{} 120 | mockService.On("GetIssues", repo).Return(ghIssuesPtrs, test.err) 121 | 122 | client := issuetracking.GitHub{ 123 | Repo: repo, 124 | GitHubService: mockService, 125 | } 126 | 127 | issues, err := client.GetIssues() 128 | 129 | assert.Equal(t, test.result, issues) 130 | assert.Equal(t, test.err, err) 131 | } 132 | } 133 | 134 | func TestGetIssue(t *testing.T) { 135 | tests := map[string]struct{ 136 | result *common.Issue 137 | err error 138 | }{ 139 | "success": { 140 | result: &common.Issue{ 141 | URL: "https://github.com/octocat/Hello-World/issues/1347", 142 | Number: 1347, 143 | Title: "Found a bug", 144 | Assignee: "octocat", 145 | Labels: []string{"bug"}, 146 | }, 147 | err: nil, 148 | }, 149 | "propagates error": { result: nil, err: errors.New("an error") }, 150 | } 151 | 152 | for _, test := range tests { 153 | var ghIssue *github.Issue 154 | 155 | if test.result != nil { 156 | testutils.LoadFixture("testdata/github_issue_v3.json", &ghIssue) 157 | } 158 | 159 | repo := common.Repo{ 160 | Owner: "test-owner", 161 | Name: "test-name", 162 | } 163 | issueNum := 1347 164 | 165 | mockService := &mocks.GitHubService{} 166 | mockService.On("GetIssue", repo, issueNum).Return(ghIssue, test.err) 167 | 168 | client := issuetracking.GitHub{ 169 | Repo: repo, 170 | GitHubService: mockService, 171 | } 172 | 173 | issue, err := client.GetIssue(strconv.Itoa(issueNum)) 174 | 175 | assert.Equal(t, test.result, issue) 176 | assert.Equal(t, test.err, err) 177 | } 178 | } 179 | 180 | func TestAssignissue(t *testing.T) { 181 | tests := map[string]struct{ 182 | err error 183 | }{ 184 | "success": { err: nil }, 185 | "propagates error": { err: errors.New("an error") }, 186 | } 187 | 188 | for _, test := range tests { 189 | repo := common.Repo{ 190 | Owner: "test-owner", 191 | Name: "test-name", 192 | } 193 | issueNum := 123 194 | login := "octocat" 195 | 196 | mockService := &mocks.GitHubService{} 197 | mockService.On("AssignIssue", repo, issueNum, login).Return(test.err) 198 | 199 | client := issuetracking.GitHub{ 200 | Repo: repo, 201 | GitHubService: mockService, 202 | } 203 | 204 | err := client.AssignIssue(issueNum, login) 205 | 206 | assert.Equal(t, test.err, err) 207 | } 208 | } 209 | 210 | func TestAddIssueLabel(t *testing.T) { 211 | tests := map[string]struct{ 212 | err error 213 | }{ 214 | "success": { err: nil }, 215 | "propagates error": { err: errors.New("an error") }, 216 | } 217 | 218 | for _, test := range tests { 219 | repo := common.Repo{ 220 | Owner: "test-owner", 221 | Name: "test-name", 222 | } 223 | issueNum := 123 224 | labelName := "test" 225 | 226 | mockService := &mocks.GitHubService{} 227 | mockService.On("AddLabelToIssue", repo, issueNum, labelName).Return(test.err) 228 | 229 | client := issuetracking.GitHub{ 230 | Repo: repo, 231 | GitHubService: mockService, 232 | } 233 | 234 | err := client.AddIssueLabel(issueNum, labelName) 235 | 236 | assert.Equal(t, test.err, err) 237 | } 238 | } 239 | 240 | func TestRemoveIssueLabel(t *testing.T) { 241 | tests := map[string]struct{ 242 | err error 243 | }{ 244 | "success": { err: nil }, 245 | "propagates error": { err: errors.New("an error") }, 246 | } 247 | 248 | for _, test := range tests { 249 | repo := common.Repo{ 250 | Owner: "test-owner", 251 | Name: "test-name", 252 | } 253 | issueNum := 123 254 | labelName := "test" 255 | 256 | mockService := &mocks.GitHubService{} 257 | mockService.On("RemoveLabelForIssue", repo, issueNum, labelName).Return(test.err) 258 | 259 | client := issuetracking.GitHub{ 260 | Repo: repo, 261 | GitHubService: mockService, 262 | } 263 | 264 | err := client.RemoveIssueLabel(issueNum, labelName) 265 | 266 | assert.Equal(t, test.err, err) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /issuetracking/issuetracking.go: -------------------------------------------------------------------------------- 1 | package issuetracking 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/cyberark/dev-flow/common" 7 | "github.com/cyberark/dev-flow/service" 8 | ) 9 | 10 | type IssueTrackingClient interface { 11 | GetCurrentUserLogin() (string, error) 12 | GetUserRealName(string) (string, error) 13 | GetIssues() ([]common.Issue, error) 14 | GetIssue(string) (*common.Issue, error) 15 | AssignIssue(int, string) error 16 | AddIssueLabel(int, string) error 17 | RemoveIssueLabel(int, string) error 18 | } 19 | 20 | func GetClient(repo common.Repo) IssueTrackingClient { 21 | return GitHub{ 22 | Repo: repo, 23 | GitHubService: service.GitHub{}, 24 | } 25 | } 26 | 27 | func GetIssueKeyFromBranchName(branchName string) string { 28 | return strings.Split(branchName, "--")[0] 29 | } 30 | -------------------------------------------------------------------------------- /issuetracking/jira.go: -------------------------------------------------------------------------------- 1 | package issuetracking 2 | 3 | type Jira struct{} 4 | -------------------------------------------------------------------------------- /issuetracking/testdata/github_issue_v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "node_id": "MDU6SXNzdWUx", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 5 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 6 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 8 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 9 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 10 | "number": 1347, 11 | "state": "open", 12 | "title": "Found a bug", 13 | "body": "I'm having a problem with this.", 14 | "user": { 15 | "login": "octocat", 16 | "id": 1, 17 | "node_id": "MDQ6VXNlcjE=", 18 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/octocat", 21 | "html_url": "https://github.com/octocat", 22 | "followers_url": "https://api.github.com/users/octocat/followers", 23 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 27 | "organizations_url": "https://api.github.com/users/octocat/orgs", 28 | "repos_url": "https://api.github.com/users/octocat/repos", 29 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/octocat/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | { 36 | "id": 208045946, 37 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 38 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 39 | "name": "bug", 40 | "description": "Something isn't working", 41 | "color": "f29513", 42 | "default": true 43 | } 44 | ], 45 | "assignee": { 46 | "login": "octocat", 47 | "id": 1, 48 | "node_id": "MDQ6VXNlcjE=", 49 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 50 | "gravatar_id": "", 51 | "url": "https://api.github.com/users/octocat", 52 | "html_url": "https://github.com/octocat", 53 | "followers_url": "https://api.github.com/users/octocat/followers", 54 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 55 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 56 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 57 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 58 | "organizations_url": "https://api.github.com/users/octocat/orgs", 59 | "repos_url": "https://api.github.com/users/octocat/repos", 60 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 61 | "received_events_url": "https://api.github.com/users/octocat/received_events", 62 | "type": "User", 63 | "site_admin": false 64 | }, 65 | "assignees": [ 66 | { 67 | "login": "octocat", 68 | "id": 1, 69 | "node_id": "MDQ6VXNlcjE=", 70 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 71 | "gravatar_id": "", 72 | "url": "https://api.github.com/users/octocat", 73 | "html_url": "https://github.com/octocat", 74 | "followers_url": "https://api.github.com/users/octocat/followers", 75 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 76 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 77 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 78 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 79 | "organizations_url": "https://api.github.com/users/octocat/orgs", 80 | "repos_url": "https://api.github.com/users/octocat/repos", 81 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 82 | "received_events_url": "https://api.github.com/users/octocat/received_events", 83 | "type": "User", 84 | "site_admin": false 85 | } 86 | ], 87 | "milestone": { 88 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 89 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 90 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 91 | "id": 1002604, 92 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 93 | "number": 1, 94 | "state": "open", 95 | "title": "v1.0", 96 | "description": "Tracking milestone for version 1.0", 97 | "creator": { 98 | "login": "octocat", 99 | "id": 1, 100 | "node_id": "MDQ6VXNlcjE=", 101 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 102 | "gravatar_id": "", 103 | "url": "https://api.github.com/users/octocat", 104 | "html_url": "https://github.com/octocat", 105 | "followers_url": "https://api.github.com/users/octocat/followers", 106 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 107 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 108 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 109 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 110 | "organizations_url": "https://api.github.com/users/octocat/orgs", 111 | "repos_url": "https://api.github.com/users/octocat/repos", 112 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 113 | "received_events_url": "https://api.github.com/users/octocat/received_events", 114 | "type": "User", 115 | "site_admin": false 116 | }, 117 | "open_issues": 4, 118 | "closed_issues": 8, 119 | "created_at": "2011-04-10T20:09:31Z", 120 | "updated_at": "2014-03-03T18:58:10Z", 121 | "closed_at": "2013-02-12T13:22:01Z", 122 | "due_on": "2012-10-09T23:39:01Z" 123 | }, 124 | "locked": true, 125 | "active_lock_reason": "too heated", 126 | "comments": 0, 127 | "pull_request": { 128 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 129 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 130 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 131 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 132 | }, 133 | "closed_at": null, 134 | "created_at": "2011-04-22T13:33:48Z", 135 | "updated_at": "2011-04-22T13:33:48Z", 136 | "closed_by": { 137 | "login": "octocat", 138 | "id": 1, 139 | "node_id": "MDQ6VXNlcjE=", 140 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 141 | "gravatar_id": "", 142 | "url": "https://api.github.com/users/octocat", 143 | "html_url": "https://github.com/octocat", 144 | "followers_url": "https://api.github.com/users/octocat/followers", 145 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 146 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 147 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 148 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 149 | "organizations_url": "https://api.github.com/users/octocat/orgs", 150 | "repos_url": "https://api.github.com/users/octocat/repos", 151 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 152 | "received_events_url": "https://api.github.com/users/octocat/received_events", 153 | "type": "User", 154 | "site_admin": false 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /issuetracking/testdata/github_issues_v3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "node_id": "MDU6SXNzdWUx", 5 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 6 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 7 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 8 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 9 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 10 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 11 | "number": 1347, 12 | "state": "open", 13 | "title": "Found a bug", 14 | "body": "I'm having a problem with this.", 15 | "user": { 16 | "login": "octocat", 17 | "id": 1, 18 | "node_id": "MDQ6VXNlcjE=", 19 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 20 | "gravatar_id": "", 21 | "url": "https://api.github.com/users/octocat", 22 | "html_url": "https://github.com/octocat", 23 | "followers_url": "https://api.github.com/users/octocat/followers", 24 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 25 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 26 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 27 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 28 | "organizations_url": "https://api.github.com/users/octocat/orgs", 29 | "repos_url": "https://api.github.com/users/octocat/repos", 30 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 31 | "received_events_url": "https://api.github.com/users/octocat/received_events", 32 | "type": "User", 33 | "site_admin": false 34 | }, 35 | "labels": [ 36 | { 37 | "id": 208045946, 38 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 39 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 40 | "name": "bug", 41 | "description": "Something isn't working", 42 | "color": "f29513", 43 | "default": true 44 | } 45 | ], 46 | "assignee": { 47 | "login": "octocat", 48 | "id": 1, 49 | "node_id": "MDQ6VXNlcjE=", 50 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 51 | "gravatar_id": "", 52 | "url": "https://api.github.com/users/octocat", 53 | "html_url": "https://github.com/octocat", 54 | "followers_url": "https://api.github.com/users/octocat/followers", 55 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 56 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 57 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 58 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 59 | "organizations_url": "https://api.github.com/users/octocat/orgs", 60 | "repos_url": "https://api.github.com/users/octocat/repos", 61 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 62 | "received_events_url": "https://api.github.com/users/octocat/received_events", 63 | "type": "User", 64 | "site_admin": false 65 | }, 66 | "assignees": [ 67 | { 68 | "login": "octocat", 69 | "id": 1, 70 | "node_id": "MDQ6VXNlcjE=", 71 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 72 | "gravatar_id": "", 73 | "url": "https://api.github.com/users/octocat", 74 | "html_url": "https://github.com/octocat", 75 | "followers_url": "https://api.github.com/users/octocat/followers", 76 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 77 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 78 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 79 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 80 | "organizations_url": "https://api.github.com/users/octocat/orgs", 81 | "repos_url": "https://api.github.com/users/octocat/repos", 82 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 83 | "received_events_url": "https://api.github.com/users/octocat/received_events", 84 | "type": "User", 85 | "site_admin": false 86 | } 87 | ], 88 | "milestone": { 89 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 90 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 91 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 92 | "id": 1002604, 93 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 94 | "number": 1, 95 | "state": "open", 96 | "title": "v1.0", 97 | "description": "Tracking milestone for version 1.0", 98 | "creator": { 99 | "login": "octocat", 100 | "id": 1, 101 | "node_id": "MDQ6VXNlcjE=", 102 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 103 | "gravatar_id": "", 104 | "url": "https://api.github.com/users/octocat", 105 | "html_url": "https://github.com/octocat", 106 | "followers_url": "https://api.github.com/users/octocat/followers", 107 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 108 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 109 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 110 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 111 | "organizations_url": "https://api.github.com/users/octocat/orgs", 112 | "repos_url": "https://api.github.com/users/octocat/repos", 113 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 114 | "received_events_url": "https://api.github.com/users/octocat/received_events", 115 | "type": "User", 116 | "site_admin": false 117 | }, 118 | "open_issues": 4, 119 | "closed_issues": 8, 120 | "created_at": "2011-04-10T20:09:31Z", 121 | "updated_at": "2014-03-03T18:58:10Z", 122 | "closed_at": "2013-02-12T13:22:01Z", 123 | "due_on": "2012-10-09T23:39:01Z" 124 | }, 125 | "locked": true, 126 | "active_lock_reason": "too heated", 127 | "comments": 0, 128 | "pull_request": { 129 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 130 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 131 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 132 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 133 | }, 134 | "closed_at": null, 135 | "created_at": "2011-04-22T13:33:48Z", 136 | "updated_at": "2011-04-22T13:33:48Z", 137 | "repository": { 138 | "id": 1296269, 139 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 140 | "name": "Hello-World", 141 | "full_name": "octocat/Hello-World", 142 | "owner": { 143 | "login": "octocat", 144 | "id": 1, 145 | "node_id": "MDQ6VXNlcjE=", 146 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 147 | "gravatar_id": "", 148 | "url": "https://api.github.com/users/octocat", 149 | "html_url": "https://github.com/octocat", 150 | "followers_url": "https://api.github.com/users/octocat/followers", 151 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 152 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 153 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 154 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 155 | "organizations_url": "https://api.github.com/users/octocat/orgs", 156 | "repos_url": "https://api.github.com/users/octocat/repos", 157 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 158 | "received_events_url": "https://api.github.com/users/octocat/received_events", 159 | "type": "User", 160 | "site_admin": false 161 | }, 162 | "private": false, 163 | "html_url": "https://github.com/octocat/Hello-World", 164 | "description": "This your first repo!", 165 | "fork": false, 166 | "url": "https://api.github.com/repos/octocat/Hello-World", 167 | "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", 168 | "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}", 169 | "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", 170 | "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}", 171 | "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", 172 | "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}", 173 | "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}", 174 | "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", 175 | "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}", 176 | "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors", 177 | "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments", 178 | "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads", 179 | "events_url": "http://api.github.com/repos/octocat/Hello-World/events", 180 | "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks", 181 | "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", 182 | "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", 183 | "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", 184 | "git_url": "git:github.com/octocat/Hello-World.git", 185 | "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", 186 | "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", 187 | "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}", 188 | "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", 189 | "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}", 190 | "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages", 191 | "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges", 192 | "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}", 193 | "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", 194 | "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}", 195 | "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}", 196 | "ssh_url": "git@github.com:octocat/Hello-World.git", 197 | "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers", 198 | "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", 199 | "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers", 200 | "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription", 201 | "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags", 202 | "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams", 203 | "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", 204 | "clone_url": "https://github.com/octocat/Hello-World.git", 205 | "mirror_url": "git:git.example.com/octocat/Hello-World", 206 | "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks", 207 | "svn_url": "https://svn.github.com/octocat/Hello-World", 208 | "homepage": "https://github.com", 209 | "language": null, 210 | "forks_count": 9, 211 | "stargazers_count": 80, 212 | "watchers_count": 80, 213 | "size": 108, 214 | "default_branch": "master", 215 | "open_issues_count": 0, 216 | "is_template": true, 217 | "topics": [ 218 | "octocat", 219 | "atom", 220 | "electron", 221 | "api" 222 | ], 223 | "has_issues": true, 224 | "has_projects": true, 225 | "has_wiki": true, 226 | "has_pages": false, 227 | "has_downloads": true, 228 | "archived": false, 229 | "disabled": false, 230 | "pushed_at": "2011-01-26T19:06:43Z", 231 | "created_at": "2011-01-26T19:01:12Z", 232 | "updated_at": "2011-01-26T19:14:43Z", 233 | "permissions": { 234 | "admin": false, 235 | "push": false, 236 | "pull": true 237 | }, 238 | "allow_rebase_merge": true, 239 | "template_repository": null, 240 | "allow_squash_merge": true, 241 | "allow_merge_commit": true, 242 | "subscribers_count": 42, 243 | "network_count": 0 244 | } 245 | } 246 | ] 247 | -------------------------------------------------------------------------------- /issuetracking/testdata/github_user_v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "octocat", 3 | "id": 1, 4 | "node_id": "MDQ6VXNlcjE=", 5 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/octocat", 8 | "html_url": "https://github.com/octocat", 9 | "followers_url": "https://api.github.com/users/octocat/followers", 10 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 14 | "organizations_url": "https://api.github.com/users/octocat/orgs", 15 | "repos_url": "https://api.github.com/users/octocat/repos", 16 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/octocat/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "monalisa octocat", 21 | "company": "GitHub", 22 | "blog": "https://github.com/blog", 23 | "location": "San Francisco", 24 | "email": "octocat@github.com", 25 | "hireable": false, 26 | "bio": "There once was...", 27 | "public_repos": 2, 28 | "public_gists": 1, 29 | "followers": 20, 30 | "following": 0, 31 | "created_at": "2008-01-14T04:33:35Z", 32 | "updated_at": "2008-01-14T04:33:35Z" 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/cyberark/dev-flow/cmd" 7 | ) 8 | 9 | func main() { 10 | log.SetPrefix("INFO: ") 11 | log.SetFlags(log.Llongfile) 12 | 13 | cmd.Execute() 14 | } 15 | -------------------------------------------------------------------------------- /scm/github.go: -------------------------------------------------------------------------------- 1 | package scm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/go-github/github" 7 | 8 | "github.com/cyberark/dev-flow/common" 9 | "github.com/cyberark/dev-flow/service" 10 | ) 11 | 12 | type GitHub struct{ 13 | Repo common.Repo 14 | } 15 | 16 | func newGitHubClient() service.GitHub { 17 | return service.GitHub{} 18 | } 19 | 20 | func (gh GitHub) GetPullRequest(branchName string) *PullRequest { 21 | ghPullRequests, err := newGitHubClient().GetPullRequests(gh.Repo, branchName) 22 | 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | var pullRequestNum *int 28 | 29 | for _, ghPullRequest := range ghPullRequests { 30 | if *ghPullRequest.Head.Ref == branchName { 31 | pullRequestNum = ghPullRequest.Number 32 | } 33 | } 34 | 35 | var pullRequest *PullRequest 36 | 37 | if pullRequestNum != nil { 38 | // We call List and then Get because the Mergeable field is only 39 | // returned when retrieving single issues. 40 | ghPullRequest, err := newGitHubClient().GetPullRequest(gh.Repo, *pullRequestNum) 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | pullRequest = gh.toCommonPullRequest(ghPullRequest) 47 | } 48 | 49 | return pullRequest 50 | } 51 | 52 | func (gh GitHub) CreatePullRequest(issue common.Issue, linkType string) *PullRequest { 53 | base := "master" 54 | head := issue.BranchName() 55 | title := issue.Title 56 | body := fmt.Sprintf("%s #%v", linkType, issue.Number) 57 | 58 | newPullRequest := &github.NewPullRequest{ 59 | Base: &base, 60 | Head: &head, 61 | Title: &title, 62 | Body: &body, 63 | } 64 | 65 | ghPullRequest, err := newGitHubClient().CreatePullRequest(gh.Repo, newPullRequest) 66 | 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | err = newGitHubClient().AssignIssue(gh.Repo, *ghPullRequest.Number, issue.Assignee) 72 | 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | return gh.toCommonPullRequest(ghPullRequest) 78 | } 79 | 80 | func (gh GitHub) AssignPullRequestReviewer(pr *PullRequest, reviewer string) { 81 | err := newGitHubClient().RequestReviewer(gh.Repo, pr.Number, reviewer) 82 | 83 | if err != nil { 84 | panic(err) 85 | } 86 | } 87 | 88 | func (gh GitHub) MergePullRequest(pr *PullRequest, mergeMethod string) bool { 89 | merged, err := newGitHubClient().MergePullRequest(gh.Repo, pr.Number, mergeMethod) 90 | 91 | if err != nil { 92 | panic(err) 93 | } 94 | 95 | return merged 96 | } 97 | 98 | func (gh GitHub) toCommonPullRequest(ghpr *github.PullRequest) *PullRequest { 99 | mergeable := false 100 | 101 | if ghpr.Mergeable != nil { 102 | mergeable = *ghpr.Mergeable 103 | } 104 | 105 | return &PullRequest{ 106 | Number: *ghpr.Number, 107 | Creator: *ghpr.User.Login, 108 | Base: *ghpr.Base.Ref, 109 | Mergeable: mergeable, 110 | URL: *ghpr.HTMLURL, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /scm/scm.go: -------------------------------------------------------------------------------- 1 | package scm 2 | 3 | import ( 4 | "github.com/cyberark/dev-flow/common" 5 | ) 6 | 7 | type SourceControlManagementClient interface { 8 | GetPullRequest(string) *PullRequest 9 | CreatePullRequest(common.Issue, string) *PullRequest 10 | AssignPullRequestReviewer(*PullRequest, string) 11 | MergePullRequest(*PullRequest, string) bool 12 | } 13 | 14 | func (pr PullRequest) String() string { 15 | return pr.URL 16 | } 17 | 18 | func GetClient(repo common.Repo) SourceControlManagementClient { 19 | return GitHub{ 20 | Repo: repo, 21 | } 22 | } 23 | 24 | type PullRequest struct { 25 | Number int 26 | Creator string 27 | Base string 28 | Mergeable bool 29 | URL string 30 | } 31 | -------------------------------------------------------------------------------- /service/bash.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | type Bash struct{} 8 | 9 | func (bash Bash) RunCommand(cmd string) (string, error) { 10 | stdout, err := exec.Command("bash", "-c", cmd).Output() 11 | output := string(stdout) 12 | 13 | if err != nil { 14 | return output, err 15 | } 16 | 17 | return output, nil 18 | } 19 | -------------------------------------------------------------------------------- /service/github.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "golang.org/x/oauth2" 9 | 10 | "github.com/google/go-github/github" 11 | 12 | "github.com/cyberark/dev-flow/common" 13 | ) 14 | 15 | type GitHubService interface { 16 | GetUser(string) (*github.User, error) 17 | GetIssues(common.Repo) ([]*github.Issue, error) 18 | GetIssue(repo common.Repo, issueNum int) (*github.Issue, error) 19 | AssignIssue(repo common.Repo, issueNum int, assigneeLogin string) (error) 20 | GetLabel(repo common.Repo, name string) (*github.Label, error) 21 | AddLabelToIssue(repo common.Repo, issueNum int, labelName string) (error) 22 | RemoveLabelForIssue(repo common.Repo, issueNum int, labelName string) (error) 23 | GetPullRequests(repo common.Repo, branchName string) ([]*github.PullRequest, error) 24 | GetPullRequest(repo common.Repo, pullRequestNum int) (*github.PullRequest, error) 25 | } 26 | 27 | type GitHub struct{} 28 | 29 | func newClient() *github.Client { 30 | ts := oauth2.StaticTokenSource( 31 | &oauth2.Token{AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN")}, 32 | ) 33 | tc := oauth2.NewClient(context.Background(), ts) 34 | 35 | return github.NewClient(tc) 36 | } 37 | 38 | func (gh GitHub) GetUser(login string) (*github.User, error) { 39 | user, _, err := newClient().Users.Get(context.Background(), login) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return user, nil 46 | } 47 | 48 | func (gh GitHub) GetIssues(repo common.Repo) ([]*github.Issue, error) { 49 | issues, _, err := newClient().Issues.ListByRepo( 50 | context.Background(), 51 | repo.Owner, 52 | repo.Name, 53 | nil, 54 | ) 55 | 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return issues, nil 61 | } 62 | 63 | func (gh GitHub) GetIssue(repo common.Repo, issueNum int) (*github.Issue, error) { 64 | issue, _, err := newClient().Issues.Get( 65 | context.Background(), 66 | repo.Owner, 67 | repo.Name, 68 | issueNum, 69 | ) 70 | 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return issue, nil 76 | } 77 | 78 | func (gh GitHub) AssignIssue(repo common.Repo, issueNum int, assigneeLogin string) (error) { 79 | _, _, err := newClient().Issues.AddAssignees( 80 | context.Background(), 81 | repo.Owner, 82 | repo.Name, 83 | issueNum, 84 | []string{assigneeLogin}, 85 | ) 86 | 87 | if err != nil { 88 | return err 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (gh GitHub) GetLabel(repo common.Repo, name string) (*github.Label, error) { 95 | label, _, err := newClient().Issues.GetLabel( 96 | context.Background(), 97 | repo.Owner, 98 | repo.Name, 99 | name, 100 | ) 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return label, nil 107 | } 108 | 109 | func (gh GitHub) AddLabelToIssue(repo common.Repo, issueNum int, labelName string) (error) { 110 | _, _, err := newClient().Issues.AddLabelsToIssue( 111 | context.Background(), 112 | repo.Owner, 113 | repo.Name, 114 | issueNum, 115 | []string{labelName}, 116 | ) 117 | 118 | if err != nil { 119 | return err 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func (gh GitHub) RemoveLabelForIssue(repo common.Repo, issueNum int, labelName string) (error) { 126 | _, err := newClient().Issues.RemoveLabelForIssue( 127 | context.Background(), 128 | repo.Owner, 129 | repo.Name, 130 | issueNum, 131 | labelName, 132 | ) 133 | 134 | if err != nil { 135 | return err 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (gh GitHub) GetPullRequests(repo common.Repo, branchName string) ([]*github.PullRequest, error) { 142 | opts := &github.PullRequestListOptions{ 143 | State: "open", 144 | Base: "master", 145 | Head: fmt.Sprintf("%v:%v", repo.Owner, branchName), 146 | } 147 | 148 | pullRequests, _, err := newClient().PullRequests.List( 149 | context.Background(), 150 | repo.Owner, 151 | repo.Name, 152 | opts, 153 | ) 154 | 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | return pullRequests, nil 160 | } 161 | 162 | func (gh GitHub) GetPullRequest(repo common.Repo, pullRequestNum int) (*github.PullRequest, error) { 163 | pullRequest, _, err := newClient().PullRequests.Get( 164 | context.Background(), 165 | repo.Owner, 166 | repo.Name, 167 | pullRequestNum, 168 | ) 169 | 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return pullRequest, nil 175 | } 176 | 177 | func (gh GitHub) CreatePullRequest(repo common.Repo, newPullRequest *github.NewPullRequest) (*github.PullRequest, error) { 178 | pullRequest, _, err := newClient().PullRequests.Create( 179 | context.Background(), 180 | repo.Owner, 181 | repo.Name, 182 | newPullRequest, 183 | ) 184 | 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | return pullRequest, nil 190 | } 191 | 192 | func (gh GitHub) RequestReviewer(repo common.Repo, pullRequestNum int, reviewerLogin string) (error) { 193 | reviewersRequest := github.ReviewersRequest{ 194 | Reviewers: []string{reviewerLogin}, 195 | } 196 | 197 | _, _, err := newClient().PullRequests.RequestReviewers( 198 | context.Background(), 199 | repo.Owner, 200 | repo.Name, 201 | pullRequestNum, 202 | reviewersRequest, 203 | ) 204 | 205 | if err != nil { 206 | return err 207 | } 208 | 209 | return nil 210 | } 211 | 212 | func (gh GitHub) MergePullRequest(repo common.Repo, pullRequestNum int, mergeMethod string) (bool, error) { 213 | pullRequestOptions := &github.PullRequestOptions{ 214 | MergeMethod: mergeMethod, 215 | } 216 | 217 | mergeResult, _, err := newClient().PullRequests.Merge( 218 | context.Background(), 219 | repo.Owner, 220 | repo.Name, 221 | pullRequestNum, 222 | "", 223 | pullRequestOptions, 224 | ) 225 | 226 | if err != nil { 227 | return false, err 228 | } 229 | 230 | return *mergeResult.Merged, nil 231 | } 232 | -------------------------------------------------------------------------------- /service/mocks/GitHubService.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import github "github.com/google/go-github/github" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | import versioncontrol "github.com/cyberark/dev-flow/versioncontrol" 9 | 10 | // GitHubService is an autogenerated mock type for the GitHubService type 11 | type GitHubService struct { 12 | mock.Mock 13 | } 14 | 15 | // AddLabelToIssue provides a mock function with given fields: repo, issueNum, labelName 16 | func (_m *GitHubService) AddLabelToIssue(repo versioncontrol.Repo, issueNum int, labelName string) error { 17 | ret := _m.Called(repo, issueNum, labelName) 18 | 19 | var r0 error 20 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, int, string) error); ok { 21 | r0 = rf(repo, issueNum, labelName) 22 | } else { 23 | r0 = ret.Error(0) 24 | } 25 | 26 | return r0 27 | } 28 | 29 | // AssignIssue provides a mock function with given fields: repo, issueNum, assigneeLogin 30 | func (_m *GitHubService) AssignIssue(repo versioncontrol.Repo, issueNum int, assigneeLogin string) error { 31 | ret := _m.Called(repo, issueNum, assigneeLogin) 32 | 33 | var r0 error 34 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, int, string) error); ok { 35 | r0 = rf(repo, issueNum, assigneeLogin) 36 | } else { 37 | r0 = ret.Error(0) 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // GetIssue provides a mock function with given fields: repo, issueNum 44 | func (_m *GitHubService) GetIssue(repo versioncontrol.Repo, issueNum int) (*github.Issue, error) { 45 | ret := _m.Called(repo, issueNum) 46 | 47 | var r0 *github.Issue 48 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, int) *github.Issue); ok { 49 | r0 = rf(repo, issueNum) 50 | } else { 51 | if ret.Get(0) != nil { 52 | r0 = ret.Get(0).(*github.Issue) 53 | } 54 | } 55 | 56 | var r1 error 57 | if rf, ok := ret.Get(1).(func(versioncontrol.Repo, int) error); ok { 58 | r1 = rf(repo, issueNum) 59 | } else { 60 | r1 = ret.Error(1) 61 | } 62 | 63 | return r0, r1 64 | } 65 | 66 | // GetIssues provides a mock function with given fields: _a0 67 | func (_m *GitHubService) GetIssues(_a0 versioncontrol.Repo) ([]*github.Issue, error) { 68 | ret := _m.Called(_a0) 69 | 70 | var r0 []*github.Issue 71 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo) []*github.Issue); ok { 72 | r0 = rf(_a0) 73 | } else { 74 | if ret.Get(0) != nil { 75 | r0 = ret.Get(0).([]*github.Issue) 76 | } 77 | } 78 | 79 | var r1 error 80 | if rf, ok := ret.Get(1).(func(versioncontrol.Repo) error); ok { 81 | r1 = rf(_a0) 82 | } else { 83 | r1 = ret.Error(1) 84 | } 85 | 86 | return r0, r1 87 | } 88 | 89 | // GetLabel provides a mock function with given fields: repo, name 90 | func (_m *GitHubService) GetLabel(repo versioncontrol.Repo, name string) (*github.Label, error) { 91 | ret := _m.Called(repo, name) 92 | 93 | var r0 *github.Label 94 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, string) *github.Label); ok { 95 | r0 = rf(repo, name) 96 | } else { 97 | if ret.Get(0) != nil { 98 | r0 = ret.Get(0).(*github.Label) 99 | } 100 | } 101 | 102 | var r1 error 103 | if rf, ok := ret.Get(1).(func(versioncontrol.Repo, string) error); ok { 104 | r1 = rf(repo, name) 105 | } else { 106 | r1 = ret.Error(1) 107 | } 108 | 109 | return r0, r1 110 | } 111 | 112 | // GetPullRequest provides a mock function with given fields: repo, pullRequestNum 113 | func (_m *GitHubService) GetPullRequest(repo versioncontrol.Repo, pullRequestNum int) (*github.PullRequest, error) { 114 | ret := _m.Called(repo, pullRequestNum) 115 | 116 | var r0 *github.PullRequest 117 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, int) *github.PullRequest); ok { 118 | r0 = rf(repo, pullRequestNum) 119 | } else { 120 | if ret.Get(0) != nil { 121 | r0 = ret.Get(0).(*github.PullRequest) 122 | } 123 | } 124 | 125 | var r1 error 126 | if rf, ok := ret.Get(1).(func(versioncontrol.Repo, int) error); ok { 127 | r1 = rf(repo, pullRequestNum) 128 | } else { 129 | r1 = ret.Error(1) 130 | } 131 | 132 | return r0, r1 133 | } 134 | 135 | // GetPullRequests provides a mock function with given fields: repo, branchName 136 | func (_m *GitHubService) GetPullRequests(repo versioncontrol.Repo, branchName string) ([]*github.PullRequest, error) { 137 | ret := _m.Called(repo, branchName) 138 | 139 | var r0 []*github.PullRequest 140 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, string) []*github.PullRequest); ok { 141 | r0 = rf(repo, branchName) 142 | } else { 143 | if ret.Get(0) != nil { 144 | r0 = ret.Get(0).([]*github.PullRequest) 145 | } 146 | } 147 | 148 | var r1 error 149 | if rf, ok := ret.Get(1).(func(versioncontrol.Repo, string) error); ok { 150 | r1 = rf(repo, branchName) 151 | } else { 152 | r1 = ret.Error(1) 153 | } 154 | 155 | return r0, r1 156 | } 157 | 158 | // GetUser provides a mock function with given fields: _a0 159 | func (_m *GitHubService) GetUser(_a0 string) (*github.User, error) { 160 | ret := _m.Called(_a0) 161 | 162 | var r0 *github.User 163 | if rf, ok := ret.Get(0).(func(string) *github.User); ok { 164 | r0 = rf(_a0) 165 | } else { 166 | if ret.Get(0) != nil { 167 | r0 = ret.Get(0).(*github.User) 168 | } 169 | } 170 | 171 | var r1 error 172 | if rf, ok := ret.Get(1).(func(string) error); ok { 173 | r1 = rf(_a0) 174 | } else { 175 | r1 = ret.Error(1) 176 | } 177 | 178 | return r0, r1 179 | } 180 | 181 | // RemoveLabelForIssue provides a mock function with given fields: repo, issueNum, labelName 182 | func (_m *GitHubService) RemoveLabelForIssue(repo versioncontrol.Repo, issueNum int, labelName string) error { 183 | ret := _m.Called(repo, issueNum, labelName) 184 | 185 | var r0 error 186 | if rf, ok := ret.Get(0).(func(versioncontrol.Repo, int, string) error); ok { 187 | r0 = rf(repo, issueNum, labelName) 188 | } else { 189 | r0 = ret.Error(0) 190 | } 191 | 192 | return r0 193 | } 194 | -------------------------------------------------------------------------------- /testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func loadJsonFile(path string) []byte { 11 | file, err := os.Open(path) 12 | defer file.Close() 13 | 14 | if err != nil { 15 | panic("Failed to load fixture.") 16 | } 17 | 18 | bytes, _ := ioutil.ReadAll(file) 19 | 20 | return bytes 21 | } 22 | 23 | func LoadFixture(path string, obj interface{}) { 24 | bytes := loadJsonFile(path) 25 | 26 | err := json.Unmarshal(bytes, obj) 27 | 28 | if err != nil { 29 | panic("Failed to unmarshal json.") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /util/confirm.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func Confirm(prompt string) bool { 12 | r := bufio.NewReader(os.Stdin) 13 | 14 | tries := 3 15 | 16 | for ; tries > 0; tries-- { 17 | fmt.Printf("%v [y/n]: ", prompt) 18 | 19 | res, err := r.ReadString('\n') 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | if len(res) < 2 { 25 | continue 26 | } 27 | 28 | return strings.ToLower(strings.TrimSpace(res))[0] == 'y' 29 | } 30 | 31 | return false 32 | } 33 | -------------------------------------------------------------------------------- /util/openbrowser.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os/exec" 7 | "runtime" 8 | ) 9 | 10 | func Openbrowser(url string) { 11 | var err error 12 | 13 | switch runtime.GOOS { 14 | case "linux": 15 | err = exec.Command("xdg-open", url).Start() 16 | case "windows": 17 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 18 | case "darwin": 19 | err = exec.Command("open", url).Start() 20 | default: 21 | err = fmt.Errorf("unsupported platform") 22 | } 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /util/validateparam.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func ValidateStringParam(paramName string, paramValue string, validValues []string) { 9 | validValueMap := make(map[string]bool) 10 | for _, validValue := range validValues { 11 | validValueMap[validValue] = true 12 | } 13 | 14 | if !validValueMap[paramValue] { 15 | err := fmt.Sprintf( 16 | "Invalid value '%s' for param %s. Must be one of %v.", 17 | paramValue, 18 | paramName, 19 | validValues, 20 | ) 21 | log.Fatalln(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /versioncontrol/git.go: -------------------------------------------------------------------------------- 1 | package versioncontrol 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/cyberark/dev-flow/common" 8 | "github.com/cyberark/dev-flow/service" 9 | ) 10 | 11 | type Git struct{ 12 | BashService service.Bash 13 | } 14 | 15 | func (git Git) Repo() (common.Repo, error) { 16 | cmd := "git remote show origin -n | grep h.URL | sed 's/.*://;s/.git$//'" 17 | 18 | repoSlug, err := git.BashService.RunCommand(cmd) 19 | 20 | if err != nil { 21 | return common.Repo{}, err 22 | } 23 | 24 | repoSlugSplit := strings.Split(strings.TrimSpace(repoSlug), "/") 25 | 26 | repo := common.Repo { 27 | Owner: repoSlugSplit[0], 28 | Name: repoSlugSplit[1], 29 | } 30 | 31 | return repo, nil 32 | } 33 | 34 | func (git Git) CurrentBranch() (string, error) { 35 | cmd := "git branch | grep \\* | cut -d ' ' -f2" 36 | 37 | branch, err := git.BashService.RunCommand(cmd) 38 | 39 | if err != nil { 40 | return branch, err 41 | } 42 | 43 | return strings.TrimSpace(branch), nil 44 | } 45 | 46 | func (git Git) Pull() (string, error) { 47 | return git.BashService.RunCommand("git pull") 48 | } 49 | 50 | func (git Git) CheckoutAndPull(branchName string) (string, error) { 51 | cmd := fmt.Sprintf("git checkout %v", branchName) 52 | checkoutOutput, err := git.BashService.RunCommand(cmd) 53 | 54 | output := "" 55 | output += checkoutOutput 56 | 57 | if err != nil { 58 | return output, err 59 | } 60 | 61 | pullOutput, err := git.Pull() 62 | 63 | output += pullOutput 64 | 65 | if err != nil { 66 | return output, err 67 | } 68 | 69 | return output, nil 70 | } 71 | 72 | func (git Git) IsRemoteBranch(branchName string) (bool, error) { 73 | repo, err := git.Repo() 74 | 75 | if err != nil { 76 | return false, err 77 | } 78 | 79 | repoSlug := fmt.Sprintf("%v/%v", repo.Owner, repo.Name) 80 | 81 | cmd := fmt.Sprintf("git ls-remote --heads git@github.com:%v.git %v", repoSlug, branchName) 82 | 83 | output, err := git.BashService.RunCommand(cmd) 84 | 85 | if err != nil { 86 | return false, err 87 | } 88 | 89 | return output != "", nil 90 | } 91 | 92 | func (git Git) InitBranch(issueNum int, branchName string) (string, error) { 93 | cmd := fmt.Sprintf("git checkout -b %v", branchName) 94 | checkoutOutput, err := git.BashService.RunCommand(cmd) 95 | 96 | output := "" 97 | output += checkoutOutput 98 | 99 | if err != nil { 100 | return output, err 101 | } 102 | 103 | cmd = fmt.Sprintf("git push --set-upstream origin %v", branchName) 104 | pushOutput, err := git.BashService.RunCommand(cmd) 105 | 106 | output += pushOutput 107 | 108 | if err != nil { 109 | return output, err 110 | } 111 | 112 | return output, nil 113 | } 114 | 115 | func (git Git) DeleteRemoteBranch(branchName string) (string, error) { 116 | cmd := fmt.Sprintf("git push origin --delete %v", branchName) 117 | return git.BashService.RunCommand(cmd) 118 | } 119 | 120 | func (git Git) DeleteLocalBranch(branchName string) (string, error) { 121 | cmd := fmt.Sprintf("git branch -D %v", branchName) 122 | return git.BashService.RunCommand(cmd) 123 | } 124 | -------------------------------------------------------------------------------- /versioncontrol/versioncontrol.go: -------------------------------------------------------------------------------- 1 | package versioncontrol 2 | 3 | import ( 4 | "github.com/cyberark/dev-flow/common" 5 | ) 6 | 7 | type VersionControlClient interface { 8 | Repo() (common.Repo, error) 9 | CurrentBranch() (string, error) 10 | Pull() (string, error) 11 | CheckoutAndPull(string) (string, error) 12 | IsRemoteBranch(string) (bool, error) 13 | InitBranch(int, string) (string, error) 14 | DeleteRemoteBranch(string) (string, error) 15 | DeleteLocalBranch(string) (string, error) 16 | } 17 | 18 | func GetClient() VersionControlClient { 19 | return Git{} 20 | } 21 | 22 | type Branch struct { 23 | Name *string 24 | } 25 | --------------------------------------------------------------------------------