├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── auto-release.yml │ ├── auto-tag-main.yml │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal └── semver │ ├── convcommits.go │ └── semver.go └── main.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'bug:' 5 | labels: bug, needs triage 6 | assignees: koozz 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 21 | 22 | **Expected behavior** 23 | 24 | 25 | **Additional context** 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'feature:' 5 | labels: feature, needs triage 6 | assignees: koozz 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Acceptance criteria** 20 | 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily 8 | - package-ecosystem: gomod 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto release 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_run: 8 | workflows: ["Auto tag main"] 9 | types: 10 | - completed 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | release: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Pre-compile GitHub extension 25 | uses: cli/gh-extension-precompile@v2.0.0 26 | with: 27 | go_version: "1.19" 28 | -------------------------------------------------------------------------------- /.github/workflows/auto-tag-main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto tag main 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | tag: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: SemVer 19 | id: semver 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | run: | 23 | gh extension install koozz/gh-semver 24 | gh semver -action 25 | - name: Tag 26 | run: | 27 | if [[ -z $(git tag -l ${{ steps.semver.outputs.version }}) ]]; then 28 | git tag ${{ steps.semver.outputs.version }} 29 | git push --tags 30 | fi 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | schedule: 10 | - cron: "40 17 * * 0" 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: go 29 | - name: Autobuild 30 | uses: github/codeql-action/autobuild@v3 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v3 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /gh-semver 2 | /gh-semver.exe 3 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | # gh-semver 2 | 3 | This GitHub CLI extension can be used determine the [semantic version] to 4 | release. 5 | 6 | First it will search all tags and traverse up and down the git log to find the 7 | latest tag and inspect all commits made since against the [conventional commits] 8 | standard v1.0.0. 9 | 10 | ## Prerequisites 11 | 12 | * [GitHub commandline interface] 13 | * **Repository cloned with full depth**, a shallow clone cannot be traversed. 14 | 15 | ## Usage (commandline) 16 | 17 | Install the extension by running: 18 | 19 | ```bash 20 | gh extension install koozz/gh-semver 21 | ``` 22 | 23 | Run this extension with its keyword (default is semver): 24 | 25 | ```bash 26 | gh semver 27 | ``` 28 | 29 | View more options with: 30 | 31 | ```bash 32 | gh semver -help 33 | ``` 34 | 35 | In case of a newer version, upgrade by running: 36 | 37 | ```bash 38 | gh extension upgrade koozz/gh-semver 39 | ``` 40 | 41 | ## Usage (GitHub Actions) 42 | 43 | This extension can be used in a [GitHub Actions] workflow to determine the next 44 | semantic version. 45 | 46 | In your workflow; 47 | 48 | * make sure the checkout has `fetch-depth: 0` 49 | * install the extension 50 | * call the extension 51 | * use the version as you see fit 52 | 53 | ```yaml 54 | # ... 55 | 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v3 59 | with: 60 | fetch-depth: 0 61 | - name: SemVer 62 | id: semver 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | run: | 66 | gh extension install koozz/gh-semver 67 | gh semver -action 68 | - name: Tag 69 | run: | 70 | git tag ${{ steps.semver.outputs.version }} 71 | 72 | # ... 73 | ``` 74 | 75 | Or let the extension create the tag: 76 | 77 | ```yaml 78 | # ... 79 | 80 | steps: 81 | - name: Checkout repository 82 | uses: actions/checkout@v3 83 | with: 84 | fetch-depth: 0 85 | - name: SemVer 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | run: | 89 | gh extension install koozz/gh-semver 90 | gh semver -tag 91 | git push --tags 92 | 93 | # ... 94 | ``` 95 | 96 | Example can be found in [.github/workflows/auto-tag-main.yml][workflow] 97 | 98 | ## Roadmap 99 | 100 | Things on the roadmap: 101 | 102 | * Semantic Versioning(-ish) for modules in a mono-repo; 103 | * Limit versioning to a directory within the repository. 104 | * Prefix the version with a module name. 105 | * Tests to make the software more robust and reliable. 106 | 107 | ## Issues? 108 | 109 | Help me out and describe the [issue] as complete a possible. 110 | 111 | ## License 112 | 113 | Apache 2.0 114 | 115 | 116 | [conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/ 117 | [GitHub Actions]: https://docs.github.com/en/actions 118 | [GitHub commandline interface]: https://cli.github.com/ 119 | [issue]: https://github.com/koozz/gh-semver/issues/new/choose 120 | [semantic version]: https://semver.org/ 121 | [workflow]: .github/workflows/auto-tag-main.yml 122 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/koozz/gh-semver 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cli/go-gh v1.2.1 7 | github.com/go-git/go-git/v5 v5.14.0 8 | ) 9 | 10 | require ( 11 | dario.cat/mergo v1.0.0 // indirect 12 | github.com/Microsoft/go-winio v0.6.2 // indirect 13 | github.com/ProtonMail/go-crypto v1.1.5 // indirect 14 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 15 | github.com/cli/safeexec v1.0.1 // indirect 16 | github.com/cli/shurcooL-graphql v0.0.3 // indirect 17 | github.com/cloudflare/circl v1.6.0 // indirect 18 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 19 | github.com/emirpasic/gods v1.18.1 // indirect 20 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 21 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 22 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 23 | github.com/henvic/httpretty v0.1.0 // indirect 24 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 25 | github.com/kevinburke/ssh_config v1.2.0 // indirect 26 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 27 | github.com/mattn/go-isatty v0.0.19 // indirect 28 | github.com/mattn/go-runewidth v0.0.14 // indirect 29 | github.com/muesli/termenv v0.15.1 // indirect 30 | github.com/pjbgf/sha1cd v0.3.2 // indirect 31 | github.com/rivo/uniseg v0.4.4 // indirect 32 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 33 | github.com/skeema/knownhosts v1.3.1 // indirect 34 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect 35 | github.com/xanzy/ssh-agent v0.3.3 // indirect 36 | golang.org/x/crypto v0.36.0 // indirect 37 | golang.org/x/net v0.38.0 // indirect 38 | golang.org/x/sys v0.31.0 // indirect 39 | golang.org/x/term v0.30.0 // indirect 40 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 41 | gopkg.in/warnings.v0 v0.1.2 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 4 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= 8 | github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 12 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 13 | github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o= 14 | github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM= 15 | github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= 16 | github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 17 | github.com/cli/shurcooL-graphql v0.0.3 h1:CtpPxyGDs136/+ZeyAfUKYmcQBjDlq5aqnrDCW5Ghh8= 18 | github.com/cli/shurcooL-graphql v0.0.3/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= 19 | github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= 20 | github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 21 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 22 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 27 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 28 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 29 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 30 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 31 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 32 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 33 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 34 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 35 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= 36 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 37 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 38 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 39 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 40 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 41 | github.com/henvic/httpretty v0.1.0 h1:Htk66UUEbXTD4JR0qJZaw8YAMKw+9I24ZZOnDe/ti+E= 42 | github.com/henvic/httpretty v0.1.0/go.mod h1:ViEsly7wgdugYtymX54pYp6Vv2wqZmNHayJ6q8tlKCc= 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 44 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 45 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 46 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 49 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 53 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 54 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 55 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 57 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 58 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 59 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= 60 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 61 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 62 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 63 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 64 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 65 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 69 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 70 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 71 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 72 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 73 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 74 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 75 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 76 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 79 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 80 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 81 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= 82 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= 83 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 84 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 85 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 86 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 87 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 88 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 89 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 90 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 91 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 92 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 93 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 94 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 95 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 96 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 97 | golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 98 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 99 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 100 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 101 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 102 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 103 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 114 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 115 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 116 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 117 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 118 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 119 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 120 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 121 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 122 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 123 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 124 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 125 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 126 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 127 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 128 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 129 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 135 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 136 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 137 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 138 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 140 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 141 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 142 | -------------------------------------------------------------------------------- /internal/semver/convcommits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Scott Leggett (https://github.com/smlx/ccv) 2 | // Copyright 2022 Jan van den Berg 3 | // 4 | // modifications 5 | // - added VersionBump struct 6 | // - changed to own SemVer struct 7 | // - added extended information (if not on main branch) 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | package semver 21 | 22 | import ( 23 | "fmt" 24 | "regexp" 25 | "strings" 26 | 27 | "github.com/cli/go-gh" 28 | "github.com/go-git/go-git/v5" 29 | "github.com/go-git/go-git/v5/plumbing" 30 | "github.com/go-git/go-git/v5/plumbing/object" 31 | ) 32 | 33 | type ConventionalCommits struct { 34 | gitRepo *git.Repository 35 | majorRegex *regexp.Regexp 36 | minorRegex *regexp.Regexp 37 | patchRegex *regexp.Regexp 38 | filterPath string 39 | prefix string 40 | } 41 | 42 | type VersionBump struct { 43 | major bool 44 | minor bool 45 | patch bool 46 | } 47 | 48 | func NewConventionalCommits(repo *git.Repository, filterPath, prefix string) *ConventionalCommits { 49 | return &ConventionalCommits{ 50 | gitRepo: repo, 51 | majorRegex: regexp.MustCompile(`^(fix|feat)(\(.+\))?!: |BREAKING CHANGE: `), 52 | minorRegex: regexp.MustCompile(`^feat(\(.+\))?: `), 53 | patchRegex: regexp.MustCompile(`^fix(\(.+\))?: `), 54 | filterPath: filterPath, 55 | prefix: prefix, 56 | } 57 | } 58 | 59 | // SemVer returns the calculated next semantic version 60 | func (cc *ConventionalCommits) SemVer() (*SemVer, error) { 61 | tags, err := cc.gitRepo.Tags() 62 | if err != nil { 63 | return nil, fmt.Errorf("couldn't get tags: %w", err) 64 | } 65 | 66 | // map relevant tags to commit hashes 67 | tagRefs := map[string]string{} 68 | err = tags.ForEach(func(ref *plumbing.Reference) error { 69 | if cc.prefix == "" || strings.HasPrefix(ref.Name().Short(), cc.prefix) { 70 | var sha plumbing.Hash 71 | annotatedTag, _ := cc.gitRepo.TagObject(ref.Hash()) 72 | if annotatedTag != nil { 73 | sha = annotatedTag.Target 74 | } else { 75 | sha = ref.Hash() 76 | } 77 | tagRefs[sha.String()] = ref.Name().Short() 78 | } 79 | return nil 80 | }) 81 | if err != nil { 82 | return nil, fmt.Errorf("couldn't iterate tags: %w", err) 83 | } 84 | 85 | // no existing tags 86 | if len(tagRefs) == 0 { 87 | return NewSemVer(0, 1, 0), nil 88 | } 89 | 90 | // traverse main branch to find latest version 91 | latestMain, mainVersionBump, err := cc.traverse(tagRefs, git.LogOrderDFS) 92 | if err != nil { 93 | return nil, fmt.Errorf("couldn't walk commits on main: %w", err) 94 | } 95 | mainBranch, err := cc.getMainBranch() 96 | if err != nil { 97 | return nil, fmt.Errorf("couldn't figure out main branch: %w", err) 98 | } 99 | latestMain.SetBranch(mainBranch) 100 | 101 | // traverse current branch to find latest version 102 | latestBranch, branchVersionBump, err := cc.traverse(tagRefs, git.LogOrderDFSPost) 103 | if err != nil { 104 | return nil, fmt.Errorf("couldn't walk commits on branch: %w", err) 105 | } 106 | head, err := cc.gitRepo.Head() 107 | if err != nil { 108 | return nil, fmt.Errorf("couldn't get head: %w", err) 109 | } 110 | latestBranch.SetBranch(head.Name().Short()) 111 | 112 | // might be in detached head state 113 | if latestMain == nil && latestBranch == nil { 114 | return nil, fmt.Errorf("tags exist in the repository, but not in ancestors of HEAD") 115 | } 116 | 117 | // figure out the latest version in either parent 118 | var latestVersion *SemVer 119 | if latestMain == nil { 120 | latestVersion = latestBranch 121 | } else if latestBranch == nil { 122 | latestVersion = latestMain 123 | } else if latestMain.GreaterThan(latestBranch) { 124 | latestVersion = latestMain 125 | } else { 126 | latestVersion = latestBranch 127 | } 128 | 129 | // figure out the highest increment in either parent 130 | var newVersion SemVer 131 | switch { 132 | case mainVersionBump.major || branchVersionBump.major: 133 | newVersion = latestVersion.IncMajor() 134 | case mainVersionBump.minor || branchVersionBump.minor: 135 | newVersion = latestVersion.IncMinor() 136 | case mainVersionBump.patch || branchVersionBump.patch: 137 | newVersion = latestVersion.IncPatch() 138 | default: 139 | newVersion = *latestVersion 140 | } 141 | 142 | // drop extended information for main branch 143 | if latestBranch.SameBranch(latestMain) { 144 | newVersion.Ext = nil 145 | } 146 | return &newVersion, nil 147 | } 148 | 149 | func (cc *ConventionalCommits) traverse(tagRefs map[string]string, order git.LogOrder) (*SemVer, *VersionBump, error) { 150 | versionBump := &VersionBump{} 151 | 152 | var stopIter error = fmt.Errorf("stop commit iteration") 153 | var latestTag string 154 | 155 | var commitDistance uint64 = 0 156 | var commitHash string = "" 157 | 158 | // walk commit hashes back from HEAD via main 159 | commits, err := cc.gitRepo.Log(&git.LogOptions{Order: order}) 160 | if err != nil { 161 | return nil, versionBump, fmt.Errorf("couldn't get commits: %w", err) 162 | } 163 | 164 | err = commits.ForEach(func(commit *object.Commit) error { 165 | if commitHash == "" { 166 | commitHash = commit.Hash.String() 167 | } 168 | 169 | if latestTag = tagRefs[commit.Hash.String()]; latestTag != "" { 170 | return stopIter 171 | } 172 | commitDistance += 1 173 | 174 | if relevant := cc.isRelevantCommit(commit); relevant { 175 | // analyze commit message 176 | if cc.patchRegex.MatchString(commit.Message) { 177 | versionBump.patch = true 178 | } 179 | if cc.minorRegex.MatchString(commit.Message) { 180 | versionBump.minor = true 181 | } 182 | if cc.majorRegex.MatchString(commit.Message) { 183 | versionBump.major = true 184 | } 185 | } 186 | return err 187 | }) 188 | if err != nil && err != stopIter { 189 | return nil, versionBump, fmt.Errorf("couldn't determine latest tag: %w", err) 190 | } 191 | 192 | // not tagged yet. this can happen if we are on a branch with no tags. 193 | if latestTag == "" { 194 | return nil, versionBump, nil 195 | } 196 | 197 | // parse 198 | latestVersion, err := ParseSemVer(latestTag) 199 | if err != nil { 200 | return nil, versionBump, fmt.Errorf("couldn't parse tag '%v': %w", latestTag, err) 201 | } 202 | 203 | // set extended information 204 | latestVersion.SetBranch("") 205 | latestVersion.SetCommitDistance(commitDistance) 206 | latestVersion.SetCommitHash(commitHash) 207 | return latestVersion, versionBump, nil 208 | } 209 | 210 | func (cc *ConventionalCommits) isRelevantCommit(commit *object.Commit) bool { 211 | // With no filtering, each commit is relevant 212 | if cc.prefix == "" { 213 | return true 214 | } 215 | 216 | // Filter on path 217 | fileIter, err := commit.Files() 218 | if err != nil { 219 | return true 220 | } 221 | 222 | var relevant = false 223 | fileIter.ForEach(func(file *object.File) error { 224 | if !relevant && strings.HasPrefix(file.Name, cc.filterPath) { 225 | relevant = true 226 | } 227 | return nil 228 | }) 229 | return relevant 230 | } 231 | 232 | func (cc *ConventionalCommits) getMainBranch() (string, error) { 233 | args := []string{"repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"} 234 | stdOut, _, err := gh.Exec(args...) 235 | if err != nil { 236 | return "", err 237 | } 238 | 239 | return strings.TrimSpace(stdOut.String()), nil 240 | } 241 | -------------------------------------------------------------------------------- /internal/semver/semver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Jan van den Berg 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package semver 15 | 16 | import ( 17 | "fmt" 18 | "regexp" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | type SemVer struct { 24 | Prefix string 25 | LeadingV string 26 | Major uint64 27 | Minor uint64 28 | Patch uint64 29 | Ext *SemVerExtended 30 | } 31 | 32 | type SemVerExtended struct { 33 | Branch string 34 | CommitDistance uint64 35 | CommitHash string 36 | } 37 | 38 | var branchStripCharacters = regexp.MustCompile(`[^0-9A-Za-z-]`) 39 | 40 | func NewSemVer(major, minor, patch uint64) *SemVer { 41 | return &SemVer{ 42 | Prefix: "", 43 | LeadingV: "v", 44 | Major: major, 45 | Minor: minor, 46 | Patch: patch, 47 | Ext: nil, 48 | } 49 | } 50 | 51 | func ParseSemVer(input string) (*SemVer, error) { 52 | re, err := regexp.Compile(`(?P.+-)?(?Pv)?(?P\d+)\.(?P\d+)\.(?P\d+)(?P-(?P\w+)\.(?P\d+)\.(?P\w+))?`) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | semver := NewSemVer(0, 0, 0) 58 | matches := re.FindStringSubmatch(input) 59 | 60 | if matches[re.SubexpIndex("v")] == "v" { 61 | semver.LeadingV = "v" 62 | } 63 | 64 | major, err := strconv.ParseUint(matches[re.SubexpIndex("major")], 10, 32) 65 | if err != nil { 66 | return nil, fmt.Errorf("error parsing major; %v", err) 67 | } 68 | semver.Major = major 69 | 70 | minor, err := strconv.ParseUint(matches[re.SubexpIndex("minor")], 10, 32) 71 | if err != nil { 72 | return nil, fmt.Errorf("error parsing minor; %v", err) 73 | } 74 | semver.Minor = minor 75 | 76 | patch, err := strconv.ParseUint(matches[re.SubexpIndex("patch")], 10, 32) 77 | if err != nil { 78 | return nil, fmt.Errorf("error parsing patch; %v", err) 79 | } 80 | semver.Patch = patch 81 | 82 | if matches[re.SubexpIndex("extended")] != "" { 83 | branch := matches[re.SubexpIndex("branch")] 84 | commitDistance, err := strconv.ParseUint(matches[re.SubexpIndex("commit_distance")], 10, 32) 85 | if err != nil { 86 | return nil, fmt.Errorf("error parsing commit distance; %v", err) 87 | } 88 | commitHash := matches[re.SubexpIndex("commit_hash")] 89 | 90 | semver.Ext = &SemVerExtended{branch, commitDistance, commitHash} 91 | } 92 | return semver, nil 93 | } 94 | 95 | func (s *SemVer) GreaterThan(other *SemVer) bool { 96 | return s.Major > other.Major || 97 | (s.Major == other.Major && s.Minor > other.Minor) || 98 | (s.Major == other.Major && s.Minor == other.Minor && s.Patch > other.Patch) 99 | } 100 | 101 | func (s *SemVer) SameBranch(other *SemVer) bool { 102 | return s.Ext != nil && other.Ext != nil && s.Ext.Branch == other.Ext.Branch 103 | } 104 | 105 | func (s *SemVer) IncMajor() SemVer { 106 | return SemVer{ 107 | Prefix: s.Prefix, 108 | LeadingV: s.LeadingV, 109 | Major: s.Major + 1, 110 | Minor: 0, 111 | Patch: 0, 112 | Ext: s.Ext, 113 | } 114 | } 115 | 116 | func (s *SemVer) IncMinor() SemVer { 117 | return SemVer{ 118 | Prefix: s.Prefix, 119 | LeadingV: s.LeadingV, 120 | Major: s.Major, 121 | Minor: s.Minor + 1, 122 | Patch: 0, 123 | Ext: s.Ext, 124 | } 125 | } 126 | 127 | func (s *SemVer) IncPatch() SemVer { 128 | return SemVer{ 129 | Prefix: s.Prefix, 130 | LeadingV: s.LeadingV, 131 | Major: s.Major, 132 | Minor: s.Minor, 133 | Patch: s.Patch + 1, 134 | Ext: s.Ext, 135 | } 136 | } 137 | 138 | func (s *SemVer) SetBranch(branch string) SemVer { 139 | if s.Ext == nil { 140 | s.Ext = &SemVerExtended{"", 0, ""} 141 | } 142 | s.Ext.Branch = branch 143 | 144 | return *s 145 | } 146 | 147 | func (s *SemVer) SetCommitDistance(commitDistance uint64) SemVer { 148 | if s.Ext == nil { 149 | s.Ext = &SemVerExtended{"", 0, ""} 150 | } 151 | s.Ext.CommitDistance = commitDistance 152 | 153 | return *s 154 | } 155 | 156 | func (s *SemVer) SetCommitHash(commitHash string) SemVer { 157 | if s.Ext == nil { 158 | s.Ext = &SemVerExtended{"", 0, ""} 159 | } 160 | if len(commitHash) >= 7 { 161 | s.Ext.CommitHash = commitHash[0:7] 162 | } else { 163 | s.Ext.CommitHash = commitHash 164 | } 165 | 166 | return *s 167 | } 168 | 169 | func (s *SemVer) PrintTag(release bool) string { 170 | var version string 171 | if release || s.Ext == nil { 172 | version = fmt.Sprintf("%s%d.%d.%d", s.LeadingV, s.Major, s.Minor, s.Patch) 173 | } else { 174 | branch := branchStripCharacters.ReplaceAllString(s.Ext.Branch, "") 175 | version = fmt.Sprintf("%s%d.%d.%d-%s.%d.%s", s.LeadingV, s.Major, s.Minor, s.Patch, branch, s.Ext.CommitDistance, s.Ext.CommitHash) 176 | } 177 | if s.Prefix != "" { 178 | return strings.Join([]string{s.Prefix, version}, "-") 179 | } else { 180 | return version 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Jan van den Berg 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "os" 20 | 21 | "github.com/go-git/go-git/v5" 22 | "github.com/koozz/gh-semver/internal/semver" 23 | ) 24 | 25 | func main() { 26 | var ( 27 | action bool 28 | filterPath string 29 | prefix string 30 | release bool 31 | tag bool 32 | ) 33 | flag.BoolVar(&action, "action", false, "GitHub Action output format named 'version'") 34 | flag.StringVar(&filterPath, "filter-path", "", "The path to filter commits (in case of a mono-repo)") 35 | flag.StringVar(&prefix, "prefix", "", "The prefix of the tag (in case of a mono-repo)") 36 | flag.BoolVar(&release, "release", false, "Force release tag") 37 | flag.BoolVar(&tag, "tag", false, "Commit the tag") 38 | flag.Parse() 39 | 40 | // open current repository 41 | repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true}) 42 | if err != nil { 43 | fmt.Fprintf(os.Stderr, "couldn't open git repository: %v\n", err) 44 | os.Exit(1) 45 | } 46 | 47 | tagVersion := calculateSemVer(repo, filterPath, prefix, action, release) 48 | if tag { 49 | gitTag(repo, tagVersion) 50 | } 51 | 52 | format := "%s\n" 53 | if action { 54 | format = "::set-output name=version::%s\n" 55 | } 56 | fmt.Printf(format, tagVersion) 57 | } 58 | 59 | func calculateSemVer(repo *git.Repository, filterPath, prefix string, action, release bool) string { 60 | conventionalCommits := semver.NewConventionalCommits(repo, filterPath, prefix) 61 | nextVersion, err := conventionalCommits.SemVer() 62 | if err != nil { 63 | fmt.Fprintf(os.Stderr, "error: %v", err) 64 | os.Exit(1) 65 | } 66 | nextVersion.Prefix = prefix 67 | 68 | return nextVersion.PrintTag(release) 69 | } 70 | 71 | func gitTag(repo *git.Repository, tagVersion string) { 72 | if _, err := repo.Tag(tagVersion); err != nil { 73 | headRef, err := repo.Head() 74 | if err != nil { 75 | fmt.Fprintf(os.Stderr, "error determining tag: %v\n", err) 76 | os.Exit(1) 77 | } 78 | if _, err = repo.CreateTag(tagVersion, headRef.Hash(), &git.CreateTagOptions{ 79 | Message: tagVersion, 80 | }); err != nil { 81 | fmt.Fprintf(os.Stderr, "error creating tag: %v\v", err) 82 | os.Exit(1) 83 | } 84 | } 85 | } 86 | --------------------------------------------------------------------------------