diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cd6a1eb29e2265c840ba856175447315e137f545 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +secrets/ +bin/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1b6269ea1d104397bf4f9efccfb7d16da4c72edf --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,25 @@ +include: + - project: ci/docker + file: '.kaniko-build-push.gitlab-ci.yaml' + ref: v0.x + +variables: + CI_REGISTRY_USER: sykeben + +stages: + - build + - push + +release-binaries: + stage: push + image: + name: goreleaser/goreleaser + entrypoint: [''] + only: + - tags + variables: + # Disable shallow cloning so that goreleaser can diff between tags to + # generate a changelog. + GIT_DEPTH: 0 + script: + - goreleaser release --clean diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000000000000000000000000000000000000..32c3196193d37e646eca8785156bffb4a4a85602 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,12 @@ +builds: + - ldflags: + - -s -w -X sykesdev.ca/generate-a-changelog/cmd.version={{.Env.CI_COMMIT_TAG}} + - -s -w -X sykesdev.ca/generate-a-changelog/cmd.commitSha={{.Env.CI_COMMIT_SHA}} + - -s -w -X sykesdev.ca/generate-a-changelog/cmd.targetOs={{.Os}} + - -s -w -X sykesdev.ca/generate-a-changelog/cmd.targetArch={{.Arch}} + - -s -w -X sykesdev.ca/generate-a-changelog/cmd.buildstamp={{.CommitTimestamp}} + +gitlab_urls: + api: https://gitlab.sykesdev.ca/api/v4/ + download: https://gitlab.sykesdev.ca + skip_tls_verify: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..4a56f1c463dde8c6814a502675ad8b48d53b7576 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +<!-- BEGIN_CHANGELOG_ENTRIES --> +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0+001] - 2023-05-18 +### Added +* feat(baseline): first commit with some basic components complete +* feat(git): implemented basic changelog content function from git +* feat(docker): added docker config and ci for binary and image deployments +* feat(cli): added cli command implementation with flags +### Fixed +* fix(git): fixed some formatting and ensured duplicate tag references are not added +* fix(changelog/generate): resolved some issues for release datestamps and tagging +### Changed +* initial commit +* update(checkpoint): structural changes +* refactor(changelog): moved changelog logic to correct package and pulled git functionality into its own pkg +* chore(checks/formatting): applied formatting and syntax checks +* docs(changelog): updated changelog with new content + + +<!-- END_CHANGELOG_ENTRIES --> diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..d27585d4a5d7eeadb5d409c4def1b83ec5219eab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +## Contributing + +Contributions are welcome. Simply create an Issue or PR diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0667e6b546f3acea8a22b8ceb6a49e7a5681fc38 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.19 as builder +ARG VERSION +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the source code +COPY main.go main.go +COPY cmd/ cmd/ +COPY pkg/ pkg/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags="-s -w -X sykesdev.ca/generate-a-changelog/cmd.version=${VERSION} -X sykesdev.ca/generate-a-changelog/cmd.commitSha=${VERSION} -X sykesdev.ca/generate-a-changelog/cmd.targetOs=$(uname -s) -X sykesdev.ca/generate-a-changelog/cmd.targetArch=$(uname -m) -X sykesdev.ca/generate-a-changelog/cmd.buildstamp=$(date +%s)" -o gac main.go + +# use scratch to limit attack surface-area +FROM scratch +WORKDIR / +COPY --from=builder /workspace/gac . +USER 65532:65532 + +ENTRYPOINT ["/gac"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ed8658979be8fe488f5f35ae953134908c48f07b --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000000000000000000000000000000000000..956827814564ae59a33aedcfe945e7a152d3d5b9 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,171 @@ +## General Process + +check for `--full` flag (generates a full changelog for all tags and all commits): +for each tag: + - get commits between tag and previous tag (or begining if none) + - add version header with commit time (format: [vX.Y.Z] - YYYY-MM-DD) + for each commit: + - sort by commit type: + breaking -> "Breaking Changes" + feat -> "Added" + fix -> "Fixed" + update,refactor,chore,others -> "Changed" + - generate release object +- generate blob containing formatted changelog contents +- write contents to file + +check for `--tag TAG` flag which must contain a valid semver version string and would cause generation of only the a release with changes for the specified tag. **This tag does not need to exist**. If the tag does not exist, will use the current commit and get everything beneath since last tagged release OR first commit. + +otherwise, insert latest release only at the top + + + + + + + +## EXAMPLE OUTPUT BELOW: + +# Changelog + +All notable changes to this project will be documented in this file. + + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.1.0] - 2023-05-17 +### Changed +* docs(examples): updated documentation to contain new config options + +## [3.0.0] - 2023-05-17 +### Fixed +* fix(release/config): typo on release config +* fix(broadcast/updates): resolved issue where broadcast was updating more than just the snippets part +### Changed +* chore: cleaned up a number of structures and functions throughout and added a version command +* update(stream/youtube): added better configuration for youtube broadcast creation +* update(thumbnails): added thumbnail upload and attachment logic +* chore(cleanup): small refactor to publisher helpers to make more readable +* update(stream/youtube): added better configuration for youtube broadcast creation +* merge branch 'update-more-youtube-config-options' into 'master' update(stream/youtube): added better configuration for youtube broadcast creation see merge request standalone-projects/yls!13 + +## [2.0.2] - 2023-05-17 +### Fixed +* fix(docker): added ldflags to docker build as well +* fix(docker): ldflags on docker build +### Changed +* docs(install/binary): added single use installation command for binary install +* docs(install/binary): simplified install command +* docs(prereq): update added prerequisites to the readme +* docs(prereq): update added prerequisites to the readme +* chore: cleaned up a number of structures and functions throughout and added a version command +* docs(install): updated install command for robustness +* test: test build 01 +* merge branch 'chore-cleanup-simplify' into 'master' chore: cleaned up a number of structures and functions throughout and added a version command see merge request standalone-projects/yls!12 + +## [2.0.1] - 2023-05-17 +### Fixed +* fix(client/token): added edge-case check for token expiry only if no refresh token present + +## [2.0.0] - 2023-05-17 +### Changed +* update(optimization): minor improvement by initializing youtube service as part of upload builder component +* docs: default publisher should be commented out in config example +* docs(formatting): improved formatting on config example +* update(pub/wordpress): extended customization for wordpress publisher metadata +* update(optimization): minor improvement by initializing youtube service as part of upload builder component +* merge branch 'patch-init-youtube-service-in-component' into 'master' update(optimization): minor improvement by initializing youtube service as part of upload builder component see merge request standalone-projects/yls!11 + +## [1.0.0] - 2023-05-17 +### Changed +* refactor(checkpoint): began rework of publisher logic +* refactor(checkpoint): some additional improvements but may require further rework +* refactor(checkpoint): some additional improvements but may require further rework +* refactor(pub/config): applied a number of config improvements for publishers and streams +* docs: updated config example +* refactor(stream): re-organized file structure for stream package +* docs: updated docs for new release +* refactor better publisher config +* merge branch 'refactor-better-publisher-config' into 'master' refactor better publisher config see merge request standalone-projects/yls!10 + +## [0.2.7] - 2023-05-17 +### Added +* feat(embed): added embed link to stream logging +### Fixed +* fix(token/expire): removed token expiry notifications as this is handled transparently by the http client +### Changed +* merge branch 'chore-rem-expiry-notifications' into 'master' fix(token/expire): removed token expiry notifications as this is handled transparently by the http client see merge request standalone-projects/yls!9 + +## [0.2.6] - 2023-05-17 +### Fixed +* fix(auth/config): fixed token warnings instead of error when only access token is expired + config read tags for unmarshal +### Changed +* chore(various): made several improvements to logging configuration +* update(catch-up): got changes from master +* merge branch 'feat-add-embed-url' into 'master' feat(embed): added embed link to stream logging see merge request standalone-projects/yls!8 + +## [0.2.5] - 2023-05-17 +### Added +* feat(login): added additional login command to make containerized deployments a little easier +### Fixed +* fix(now): added accidentally removed now run code from start +* fix(viper): removed viper deps and references from logs +* fix(start): fixed image build by removing unsupported join on unmarshal error +### Changed +* docs(docker): added instructions for installation including docker +* merge branch 'docs-add-docker-instructions' into 'master' docs(docker): added instructions for installation including docker see merge request standalone-projects/yls!4 +* update(chk): added some more sa testing code +* update(96ae02): merge changes from origin +* chore(various): made several improvements to logging configuration +* merge branch 'chore-minor-improvements' into 'master' chore(various): made several improvements to logging configuration see merge request standalone-projects/yls!7 + +## [0.2.4] - 2023-05-17 +### Changed +* ci(release): every tagged version should automatically run release + +## [0.2.3] - 2023-05-17 +### Changed +* docs(readme): updated usage docs to reflect new functionality +* merge branch 'docs-usage-update' into 'master' docs(readme): updated usage docs to reflect new functionality see merge request standalone-projects/cog-yls!3 + +## [0.2.2] - 2023-05-17 +### Changed +* merge branch 'fix-ci-options' into 'master' ci(gitlab): fix ci/cd options for project see merge request standalone-projects/cog-yls!2 +* ci: re-point ci base to new location for docker-ci +* ci: change project path for ci includes + +## [0.2.1] - 2023-05-17 +### Changed +* ci(gitlab): fix ci/cd options for project + +## [0.2.0] - 2023-05-17 +### Added +* feat(embed): added embed link to stream logging +* feat(wp): added publisher support starting with wordpress concrete +### Changed +* merge branch 'feat-add-wordpress-auto' into 'master' feat(wp): added publisher support starting with wordpress concrete see merge request standalone-projects/cog-yls!1 + +## [0.1.2] - 2023-05-17 +### Added +* feat(start): implemented a --now flag to allow for single-use +### Fixed +* fix(start/shutdown): more appropriately wait for any running jobs to complete before process closing +### Changed +* docs(usage): updated command usage in docs +* merge branch 'feat-add-now-flag' into 'master' feat(start): implemented a --now flag to allow for single-use see merge request standalone-projects/yls!3 + +## [0.1.1] - 2023-05-17 +### Changed +* refactor(cleanup): cleaned up a bunch of messy stuff, added share link and refactored flags +* merge branch 'chore-cleanup' into 'master' refactor(cleanup): cleaned up a bunch of messy stuff, added share link and refactored flags see merge request standalone-projects/yls!2 + +## [0.1.0] - 2023-05-17 +### Added +* feat(baseline): initial implementation +* feat(release): added go release for binaries and a dockerfile +### Fixed +* fix(config/goreleaser): added appropriate config for goreleaser on private gl +### Changed +* initial commit +* merge branch 'feat-initial-implementation' into 'master' feat(baseline): initial implementation see merge request trash_lab/youtube_livestream_scheduler!1 +* docs(examples): updated example configs diff --git a/README.md b/README.md index 11b7ebb756c27beb3380fa43436495632e24eadf..69afa812dcfba10e666142da7293515db5aee507 100644 --- a/README.md +++ b/README.md @@ -1,92 +1 @@ # generate-a-changelog - - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://gitlab.sykesdev.ca/standalone-projects/generate-a-changelog.git -git branch -M master -git push -uf origin master -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://gitlab.sykesdev.ca/standalone-projects/generate-a-changelog/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000000000000000000000000000000000000..c35dcbc320ea9f458e894cff3af49719bbbc0c00 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,11 @@ +version: '3' + +tasks: + go:build: + cmds: + - go build -o ./bin/gac -v + go:run: + deps: + - go:build + cmds: + - ./bin/gac release diff --git a/cmd/helper.go b/cmd/helper.go new file mode 100644 index 0000000000000000000000000000000000000000..b76cf5816988a91150bccaa1a24477effb207f55 --- /dev/null +++ b/cmd/helper.go @@ -0,0 +1,18 @@ +package cmd + +func stringInSlice(s string, ss []string) bool { + for i := range ss { + if s == ss[i] { + return true + } + } + return false +} + +func keys[T, V comparable](m map[T]V) []T { + var keys []T + for key := range m { + keys = append(keys, key) + } + return keys +} diff --git a/cmd/release.go b/cmd/release.go new file mode 100644 index 0000000000000000000000000000000000000000..75b459a89e831460f43d9776598d84f5f46e051a --- /dev/null +++ b/cmd/release.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + + "github.com/Masterminds/semver" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "sykesdev.ca/generate-a-changelog/pkg/changelog" + "sykesdev.ca/generate-a-changelog/pkg/data" + "sykesdev.ca/generate-a-changelog/pkg/writer" +) + +var ( + generateAll bool + noHeader bool + empty bool + printOnly bool + + tagName string + outFile string + sortDirection string +) + +var releaseCmd = &cobra.Command{ + Use: "release", + Short: "generates a formatted CHANGELOG file based on the keepachangelog and conventional-commits standards", + Run: func(cmd *cobra.Command, args []string) { + // default variables + projectPath, err := os.Getwd() + if err != nil { + log.Fatal().Err(err).Msg("could not get current work directory") + } + + // get context directory / project path from args + if len(args) > 0 { + projectPath = args[0] + } + + log.Debug().Str("context", projectPath).Msg("selected running context") + + if outFile == "" { + outFile = filepath.Join(projectPath, "CHANGELOG.md") + log.Debug().Str("outFile", outFile).Msg("no output file detected, using default") + } + + var nextReleaseTag *semver.Version = nil + if tagName != "" { + nextReleaseTag, err = semver.NewVersion(tagName) + if err != nil { + log.Fatal().Err(err).Msg("failed to parse tag from provided string") + } + log.Debug().Str("nextRelease", nextReleaseTag.Original()).Msg("adding commits after latest existing tag to next release") + } + + clog, err := changelog.New(&changelog.GeneratorOptions{ + DisableHeader: noHeader, + PullFromGit: !empty, + NextReleaseTag: nextReleaseTag, + ReleasesSorted: data.SortOrderFromString(sortDirection), + ProjectPath: projectPath, + }) + if err != nil { + log.Fatal().Err(err).Msg("failed to generate CHANGELOG") + } + log.Debug().Int("release_count", len(clog.Releases)).Msg("CHANGELOG data has been generated successfully!") + + var buf bytes.Buffer + clog.Render(&buf) + if printOnly { + w := &writer.StdoutWriter{} + w.Write(buf.Bytes()) + } else { + w, err := writer.NewFileWriter(&writer.FileWriterConfig{ + Path: outFile, + Mode: "replace", + Template: `<!-- BEGIN_CHANGELOG_ENTRIES --> +{{ .Content }} +<!-- END_CHANGELOG_ENTRIES -->`, + }) + if err != nil { + log.Fatal().Err(err).Msg("unable to configure file writer interface") + } + + _, err = w.Write(buf.Bytes()) + if err != nil { + log.Fatal().Str("file", outFile).Err(err).Msg("unable to write CHANGELOG to file") + } + } + + log.Info().Int("release_count", len(clog.Releases)).Int("bytes_total", len(buf.Bytes())).Msg("CHANGELOG data has been generated successfully!") + }, +} + +func init() { + releaseCmd.PersistentFlags().BoolVarP(&generateAll, "all", "a", false, "(optional) generate all changelog entries for the repository (WARNING: this can be slow if the repository is large)") + releaseCmd.PersistentFlags().BoolVar(&noHeader, "header", false, "(optional) do not prepend the keepachangelog header to the generated changelog content") + releaseCmd.PersistentFlags().BoolVar(&empty, "empty", false, "(optional) generate an empty changelog with only a header") + releaseCmd.PersistentFlags().BoolVarP(&printOnly, "print-only", "p", false, "(optional) print the resulting CHANGELOG directly to the terminal instead of writing to a file") + + releaseCmd.PersistentFlags().StringVarP(&tagName, "tag", "t", "", "(optional) the name of the next planned tagged release") + + releaseCmd.PersistentFlags().StringVarP(&outFile, "outfile", "o", "", "(optional) the name of the file to write the CHANGELOG to. If none is provided, the CHANGELOG will be written to the current directory as CHANGELOG.md") + releaseCmd.PersistentFlags().StringVarP(&sortDirection, "sort-direction", "s", "ASC", "(optional) the direction to sort changelog entries.") + + rootCmd.AddCommand(releaseCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000000000000000000000000000000000000..4e057b7b5814558ec182c074d4b1d43230e4fc71 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var ( + debugMode bool + logLevel string + + configFile string +) + +var rootCmd = &cobra.Command{ + Use: "gac", + Short: "generates or updates a CHANGELOG.md file based on the conventional-commits and keepachangelog standards", +} + +func init() { + cobra.OnInitialize(initLogger) + cobra.EnableCaseInsensitive = true + + rootCmd.PersistentFlags().BoolVarP(&debugMode, "debug", "d", false, "specifies whether generate-a-changelog should be run in debug mode") + rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "INFO", "specifies a logging level if not 'INFO'. Can be one of 'INFO', 'WARN', 'ERROR', 'FATAL', 'PANIC'") + rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", ".generate-a-changelog.yaml", "The path to a configuration file that specifies how generate-a-changelog should behave. A config file is not necessary for operation") +} + +func initLogger() { + levelMap := map[string]zerolog.Level{ + "DEBUG": zerolog.DebugLevel, + "INFO": zerolog.InfoLevel, + "WARN": zerolog.WarnLevel, + "ERROR": zerolog.ErrorLevel, + "FATAL": zerolog.FatalLevel, + "PANIC": zerolog.PanicLevel, + } + + if debugMode { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + return + } + + if !stringInSlice(strings.ToUpper(logLevel), keys(levelMap)) { + log.Fatal().Msg(fmt.Sprintf("invalid log-level specified. must be one of [%s]", strings.Join(keys(levelMap), ", "))) + } + + zerolog.SetGlobalLevel(levelMap[strings.ToUpper(logLevel)]) +} + +func Execute() error { + return rootCmd.Execute() +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000000000000000000000000000000000000..ce05ceaa09021ac6383d6c59e3fc40ce11ff7b7c --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var version = "development" +var commitSha string +var targetOs string +var targetArch string +var buildstamp string + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "shows version information for generate-a-changelog", + Run: func(cmd *cobra.Command, args []string) { + type vinfo struct { + Version string `json:"version"` + Commit string `json:"commit_sha"` + TargetOS string `json:"target_os"` + TargetArch string `json:"target_arch"` + Timestamp string `json:"build_timestamp"` + } + + i := &vinfo{ + Version: version, + Commit: commitSha, + TargetOS: targetOs, + TargetArch: targetArch, + Timestamp: buildstamp, + } + v, err := json.MarshalIndent(i, "", " ") + if err != nil { + log.Fatal().Err(err).Msg("failed to get version information for generate-a-changelog") + } + + fmt.Println(string(v)) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..f8fc030d7e6048dc58a5068222f4a4728517fe7c --- /dev/null +++ b/go.mod @@ -0,0 +1,45 @@ +module sykesdev.ca/generate-a-changelog + +go 1.20 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/Masterminds/sprig v2.22.0+incompatible + github.com/rs/zerolog v1.29.1 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/cloudflare/circl v1.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/skeema/knownhosts v1.1.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/net v0.10.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + +require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/go-git/go-git/v5 v5.6.1 + github.com/google/uuid v1.3.0 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..1e63ad7b6921acfbcc4f2d27fe73b139329354cf --- /dev/null +++ b/go.sum @@ -0,0 +1,186 @@ +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= +github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..f255c8074bdca188cce8548b1e5ac60ef2560cf0 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/rs/zerolog/log" + "sykesdev.ca/generate-a-changelog/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + log.Fatal().Err(err).Msg("failed to run entrypoint code for root command") + } +} diff --git a/pkg/changelog/change.go b/pkg/changelog/change.go new file mode 100644 index 0000000000000000000000000000000000000000..bc1fe7c816285d4c217ca64100a446953ab1b77a --- /dev/null +++ b/pkg/changelog/change.go @@ -0,0 +1,14 @@ +package changelog + +import ( + "fmt" + "io" +) + +type Change struct { + Message string +} + +func (c *Change) Render(w io.Writer) { + io.WriteString(w, fmt.Sprintf("* %s\n", c.Message)) +} diff --git a/pkg/changelog/changelist.go b/pkg/changelog/changelist.go new file mode 100644 index 0000000000000000000000000000000000000000..8295205dd49321d3e0f13cf6c3ff8477b3cdc026 --- /dev/null +++ b/pkg/changelog/changelist.go @@ -0,0 +1,62 @@ +package changelog + +import ( + "fmt" + "io" + "strings" +) + +type ChangeType uint8 + +const ( + ChangeTypeBreaking ChangeType = iota + ChangeTypeAdded + ChangeTypeFixed + ChangeTypeRemoved + ChangeTypeChanged +) + +func changeTypeFromString(c string) ChangeType { + switch strings.ToLower(c) { + case "breaking": + return ChangeTypeBreaking + case "feat": + return ChangeTypeAdded + case "fix": + return ChangeTypeFixed + case "remove": + return ChangeTypeRemoved + default: + return ChangeTypeChanged + } +} + +func (ct *ChangeType) String() string { + switch *ct { + case ChangeTypeBreaking: + return "BREAKING CHANGE" + case ChangeTypeAdded: + return "Added" + case ChangeTypeFixed: + return "Fixed" + case ChangeTypeRemoved: + return "Removed" + } + return "Changed" +} + +type ChangeList struct { + Type ChangeType + Changes []*Change +} + +func newChangeList(ct ChangeType) *ChangeList { + return &ChangeList{Type: ct} +} + +func (c *ChangeList) Render(w io.Writer) { + io.WriteString(w, fmt.Sprintf("### %s\n", c.Type.String())) + for _, change := range c.Changes { + change.Render(w) + } +} diff --git a/pkg/changelog/changelog.go b/pkg/changelog/changelog.go new file mode 100644 index 0000000000000000000000000000000000000000..334bd2113fd3883c51bd6bdaa581a9fa44f18754 --- /dev/null +++ b/pkg/changelog/changelog.go @@ -0,0 +1,188 @@ +package changelog + +import ( + "errors" + "io" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/Masterminds/semver" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/rs/zerolog/log" + "sykesdev.ca/generate-a-changelog/pkg/data" +) + +type GeneratorOptions struct { + DisableHeader bool + PullFromGit bool + + NextReleaseTag *semver.Version + ReleasesSorted data.SortOrder + + ProjectPath string +} + +type Changelog struct { + Header string + Releases []*Release +} + +func New(options ...*GeneratorOptions) (*Changelog, error) { + opts := &GeneratorOptions{ + DisableHeader: false, + PullFromGit: false, + NextReleaseTag: nil, + ReleasesSorted: data.SortDescending, + ProjectPath: ".", + } + + if len(options) > 0 { + opts = options[0] + } + + if opts.PullFromGit { + repo, err := git.PlainOpen(filepath.Clean(opts.ProjectPath)) + if err != nil { + return nil, err + } + return newFromGit(repo, opts.DisableHeader, opts.NextReleaseTag, opts.ReleasesSorted) + } + + return newEmpty(opts.DisableHeader) +} + +func newEmpty(disableHeader bool) (*Changelog, error) { + c := Changelog{ + Releases: []*Release{}, + } + + if !disableHeader { + c.Header = `All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).` + } + + return &c, nil +} + +func newFromGit(repo *git.Repository, disableHeader bool, nextRelease *semver.Version, releasesSortOrder data.SortOrder) (*Changelog, error) { + chlog, err := newEmpty(disableHeader) + if err != nil { + return nil, err + } + + // get all tags (sorted, asc) + tags, err := data.SortedTags(repo) + if err != nil { + return nil, err + } + + // get all commits (sorted, asc) + commits, err := data.SortedCommits(repo) + if err != nil { + return nil, err + } + + // compile regex for getting change type from commmit message + re := regexp.MustCompile(`^[A-Za-z]+`) + + // len(tags) == 0: gather all commits and add them to release with the nextRelease tag + if len(tags) == 0 { + if nextRelease != nil { + for _, c := range commits { + ctMatch := re.FindStringSubmatch(c.Message) + if len(ctMatch) == 0 { + continue + } + ct := changeTypeFromString(ctMatch[0]) + chlog.AddChangeToRelease(nextRelease, time.Now(), ct, c) + } + return chlog, nil + } + + return nil, errors.New("no existing tags or next release tags have been identified/discovered") + } + + // current tag => next tag from iterator + current := 0 + + // work up from oldest commit + for _, commit := range commits { + ctMatch := re.FindStringSubmatch(commit.Message) + if len(ctMatch) == 0 { + continue + } + ct := changeTypeFromString(ctMatch[0]) + + // when we reach the end of existing tags: add any remaining commits to the nextRelease + // then return the changelog + if current >= len(tags) && nextRelease != nil { + chlog.AddChangeToRelease(nextRelease, time.Now(), ct, commit) + return chlog, nil + } + + tag := tags[current] + version, err := semver.NewVersion(tag.Name) + if err != nil { + log.Warn().Err(err).Msg("unable to get valid version for tag. will not add changes to this tag") + continue + } + chlog.AddChangeToRelease(version, tag.Tagger.When, ct, commit) + + // when the current tag points to the current commit, we have reached the end of the tagged release + if cc, _ := tag.Commit(); cc.Hash.String() == commit.Hash.String() { + current++ + } + } + + return chlog, nil +} + +func (c *Changelog) Release(tag *semver.Version) *Release { + for _, r := range c.Releases { + if r.Tag.Equal(tag) { + return r + } + } + + return nil +} + +func (c *Changelog) AddChangeToRelease(tag *semver.Version, tagTime time.Time, ct ChangeType, commit *object.Commit) { + r := c.Release(tag) + if r == nil { + r = &Release{Tag: tag, Date: tagTime.Format("2006-01-02")} + c.Releases = append([]*Release{r}, c.Releases...) + } + + changelist := r.ChangeList(ct) + if changelist == nil { + changelist = newChangeList(ct) + r.Changes = append(r.Changes, changelist) + } + + change := &Change{ + Message: strings.ReplaceAll(strings.ToLower(commit.Message), "\n", " "), + } + changelist.Changes = append(changelist.Changes, change) +} + +func (c *Changelog) Render(w io.Writer) { + io.WriteString(w, "# Changelog\n") + if header := strings.TrimSpace(c.Header); header != "" { + io.WriteString(w, "\n") + io.WriteString(w, header) + io.WriteString(w, "\n") + } + + for _, r := range c.Releases { + io.WriteString(w, "\n") + r.Render(w) + } + + io.WriteString(w, "\n") +} diff --git a/pkg/changelog/release.go b/pkg/changelog/release.go new file mode 100644 index 0000000000000000000000000000000000000000..89b5a92082270d45c74e9fe206d9315ccbc8b3c3 --- /dev/null +++ b/pkg/changelog/release.go @@ -0,0 +1,40 @@ +package changelog + +import ( + "fmt" + "io" + "sort" + + "github.com/Masterminds/semver" +) + +type Release struct { + Tag *semver.Version + Date string + Changes []*ChangeList +} + +func (r *Release) ChangeList(ct ChangeType) *ChangeList { + for _, c := range r.Changes { + if c.Type == ct { + return c + } + } + + return nil +} + +func (r *Release) sortChanges() *Release { + sort.Slice(r.Changes, func(i, j int) bool { + return r.Changes[i].Type < r.Changes[j].Type + }) + + return r +} + +func (r *Release) Render(w io.Writer) { + io.WriteString(w, fmt.Sprintf("## [%s] - %s\n", r.Tag.String(), r.Date)) + for _, c := range r.sortChanges().Changes { + c.Render(w) + } +} diff --git a/pkg/data/git.go b/pkg/data/git.go new file mode 100644 index 0000000000000000000000000000000000000000..7f300e6de92999828e02884aec59fd2f6fcf408e --- /dev/null +++ b/pkg/data/git.go @@ -0,0 +1,61 @@ +package data + +import ( + "sort" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func SortedCommits(repo *git.Repository) ([]*object.Commit, error) { + commitObjects, err := repo.CommitObjects() + if err != nil { + return nil, err + } + commits := []*object.Commit{} + commitObjects.ForEach(func(c *object.Commit) error { + commits = append(commits, c) + return nil + }) + + // sort all commits in ascending chronological order + sort.SliceStable(commits, func(i, j int) bool { + return commits[i].Author.When.Before(commits[j].Author.When) + }) + + return commits, nil +} + +func SortedTags(repo *git.Repository) ([]*object.Tag, error) { + tagObjects, err := repo.TagObjects() + if err != nil { + return nil, err + } + tags := []*object.Tag{} + tagObjects.ForEach(func(t *object.Tag) error { + if exists, index := tagInSlice(t, tags); exists { + tags[index] = t + return nil + } + + tags = append(tags, t) + return nil + }) + + // sort all tags in ascending chronological order + sort.SliceStable(tags, func(i, j int) bool { + return tags[i].Tagger.When.Before(tags[j].Tagger.When) + }) + + return tags, nil +} + +func tagInSlice(t *object.Tag, tl []*object.Tag) (bool, int) { + for ind, tag := range tl { + if tag.Name == t.Name { + return true, ind + } + } + + return false, 0 +} diff --git a/pkg/data/sorting.go b/pkg/data/sorting.go new file mode 100644 index 0000000000000000000000000000000000000000..b68455892b6f830a86ac8df40808948a167f64f3 --- /dev/null +++ b/pkg/data/sorting.go @@ -0,0 +1,21 @@ +package data + +import "strings" + +type SortOrder uint8 + +const ( + SortAscending SortOrder = iota + SortDescending +) + +func SortOrderFromString(order string) SortOrder { + switch strings.ToLower(order) { + case "asc": + return SortAscending + case "desc": + return SortDescending + default: + return SortDescending + } +} diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 0000000000000000000000000000000000000000..f656b6d35ab9bc020a0052c1cc2ba86707b8f42d --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,190 @@ +package writer + +import ( + "bytes" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/rs/zerolog/log" +) + +// Writer is an interface that describes responsibilities of the a CHANGELOG writer +type Writer interface { + Write(b []byte) (int, error) +} + +// WriterConfig is the root-level CHANGELOG writer config +type WriterConfig struct { + Stdout bool `yaml:"stdout,omitempty"` + File *FileWriterConfig `yaml:"file,omitempty"` +} + +func (w *WriterConfig) GetWriter() (Writer, error) { + if w.Stdout { + return &StdoutWriter{}, nil + } + + if w.File != nil { + return NewFileWriter(w.File) + } + + return nil, errors.New("no writer configuration present") +} + +// StdoutWriter is used to write CHANGELOG directly to stdout +// This mode is useful for debugging/troubleshooting config issues +type StdoutWriter struct{} + +// Write cotent to stdout +func (s *StdoutWriter) Write(b []byte) (int, error) { + return os.Stdout.WriteString(string(b) + "\n") +} + +// FileWriterMode defines the behavior of the FileWriter for writing content to output file +type FileWriterMode uint8 + +const ( + // WriterModeReplace forces replacement of entire file on each write + WriterModeReplace FileWriterMode = iota + // WriterModeInject will attempt to inject the new CHANGELOG entry into the existing file + WriterModeInject +) + +// FileWriterConfig is used to configure a writer that writes CHANGELOG entries directly to a file +type FileWriterConfig struct { + Path string `yaml:"path,omitempty"` + Mode string `yaml:"mode,omitempty"` + Template string `yaml:"template,omitempty"` + BeginTag string `yaml:"beginTag,omitempty"` + EndTag string `yaml:"endTag,omitempty"` +} + +func NewFileWriter(cfg *FileWriterConfig) (*FileWriter, error) { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("could not get current working directory for CHANGELOG. %e", err) + } + changelogPath := path.Join(cwd, "CHANGELOG.md") + + if cfg.Path != "" { + changelogPath = cfg.Path + } + + mode := WriterModeReplace + if cfg.Mode != "" { + switch strings.ToLower(strings.TrimSpace(cfg.Mode)) { + case "replace": + mode = WriterModeReplace + case "inject": + mode = WriterModeInject + default: + return nil, fmt.Errorf("an invalid writer mode was supplied. %s", cfg.Mode) + } + } + + beginTag := "<!-- BEGIN_CHANGELOG_ENTRIES -->" + endTag := "<!-- END_CHANGELOG_ENTRIES -->" + + if cfg.BeginTag != "" { + beginTag = cfg.BeginTag + } + + if cfg.EndTag != "" { + endTag = cfg.EndTag + } + + return &FileWriter{ + path: changelogPath, + mode: mode, + template: cfg.Template, + beginTag: beginTag, + endTag: endTag, + }, nil +} + +type FileWriter struct { + path string + mode FileWriterMode + template string + beginTag string + endTag string +} + +func (fw *FileWriter) Write(b []byte) (int, error) { + if fw.template == "" { + if fw.mode == WriterModeReplace { + return fw.write(fw.path, b) + } + + return 0, errors.New("template is missing") + } + + buff, err := fw.apply(b) + if err != nil { + return 0, err + } + + if fw.mode == WriterModeReplace { + return fw.write(fw.path, buff.Bytes()) + } + + content, err := os.ReadFile(filepath.Clean(fw.path)) + if err != nil { + return fw.write(fw.path, buff.Bytes()) + } + + if len(content) == 0 { + return fw.write(fw.path, buff.Bytes()) + } + + return fw.inject(fw.path, string(content), buff.String()) +} + +func (fw *FileWriter) apply(b []byte) (bytes.Buffer, error) { + type content struct { + Content string + } + + var buff bytes.Buffer + tmpl := template.Must(template.New("content").Funcs(sprig.FuncMap()).Parse(fw.template)) + err := tmpl.ExecuteTemplate(&buff, "content", content{string(b)}) + + return buff, err +} + +func (fw *FileWriter) inject(out string, content string, generated string) (int, error) { + before := strings.Index(content, fw.beginTag) + after := strings.Index(content, fw.endTag) + + // no tags found in file, append all to end of file + if before < 0 && after < 0 { + return fw.write(out, []byte(content+"\n"+generated)) + } + + // beginTag missing from file + if before < 0 { + return 0, errors.New("begin tag is missing") + } + + generated = content[:before] + generated + + // endTag is before the beginTag + if after < before { + return 0, errors.New("end tag is before the begin tag") + } + + generated += content[after+len(fw.endTag):] + + return fw.write(out, []byte(generated)) +} + +func (fw *FileWriter) write(out string, b []byte) (int, error) { + log.Debug().Str("outputFile", out).Msg("writing CHANGELOG to file") + return len(b), os.WriteFile(out, b, 0644) +}