├── .eclipse └── org.eclipse.jdt.ui.prefs ├── .github ├── dco.yml └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── CODE_OF_CONDUCT.adoc ├── CONTRIBUTING.adoc ├── Dockerfile ├── LICENSE.txt ├── README.adoc ├── action.yaml ├── build.gradle ├── config └── checkstyle │ ├── checkstyle-suppressions.xml │ └── checkstyle.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── io │ └── spring │ └── githubchangeloggenerator │ ├── Application.java │ ├── ApplicationProperties.java │ ├── ChangelogGenerator.java │ ├── ChangelogSection.java │ ├── ChangelogSections.java │ ├── CommandProcessor.java │ ├── MilestoneReference.java │ ├── SelectIssues.java │ ├── github │ ├── payload │ │ ├── Issue.java │ │ ├── Label.java │ │ ├── Milestone.java │ │ ├── PullRequest.java │ │ ├── User.java │ │ └── package-info.java │ └── service │ │ ├── GitHubProperties.java │ │ ├── GitHubService.java │ │ ├── Page.java │ │ ├── Repository.java │ │ └── package-info.java │ └── package-info.java └── test ├── java └── io │ └── spring │ └── githubchangeloggenerator │ ├── ApplicationPropertiesTests.java │ ├── ChangelogGeneratorTests.java │ ├── ChangelogSectionsTests.java │ └── github │ ├── payload │ ├── IssueJsonTests.java │ └── MilestoneJsonTests.java │ └── service │ └── GitHubServiceTests.java └── resources ├── Sample-Release-Notes.md └── io └── spring └── githubchangeloggenerator ├── github ├── payload │ ├── issue.json │ └── milestone.json └── service │ ├── closed-issues-for-milestone-page-1.json │ ├── closed-issues-for-milestone-page-2.json │ ├── issue.json │ └── milestones.json ├── output-with-all-contributors-excluded ├── output-with-bot-contributors ├── output-with-custom-contributors-title ├── output-with-duplicate-contributors ├── output-with-excluded-contributors ├── output-with-ignored-labels ├── output-with-issues-only ├── output-with-more-than-two-contributors ├── output-with-multiple-external-link ├── output-with-no-bugs ├── output-with-no-enhancements ├── output-with-no-prs ├── output-with-one-external-link ├── output-with-pull-requests-only ├── output-with-title-sorted-issues ├── output-without-issue-links └── test-application.yml /.eclipse/org.eclipse.jdt.ui.prefs: -------------------------------------------------------------------------------- 1 | cleanup.add_default_serial_version_id=true 2 | cleanup.add_generated_serial_version_id=false 3 | cleanup.add_missing_annotations=true 4 | cleanup.add_missing_deprecated_annotations=true 5 | cleanup.add_missing_methods=false 6 | cleanup.add_missing_nls_tags=false 7 | cleanup.add_missing_override_annotations=true 8 | cleanup.add_missing_override_annotations_interface_methods=true 9 | cleanup.add_serial_version_id=false 10 | cleanup.always_use_blocks=true 11 | cleanup.always_use_parentheses_in_expressions=false 12 | cleanup.always_use_this_for_non_static_field_access=true 13 | cleanup.always_use_this_for_non_static_method_access=false 14 | cleanup.convert_functional_interfaces=false 15 | cleanup.convert_to_enhanced_for_loop=false 16 | cleanup.correct_indentation=false 17 | cleanup.format_source_code=true 18 | cleanup.format_source_code_changes_only=false 19 | cleanup.insert_inferred_type_arguments=false 20 | cleanup.make_local_variable_final=false 21 | cleanup.make_parameters_final=false 22 | cleanup.make_private_fields_final=false 23 | cleanup.make_type_abstract_if_missing_method=false 24 | cleanup.make_variable_declarations_final=false 25 | cleanup.never_use_blocks=false 26 | cleanup.never_use_parentheses_in_expressions=true 27 | cleanup.organize_imports=true 28 | cleanup.qualify_static_field_accesses_with_declaring_class=false 29 | cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true 30 | cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true 31 | cleanup.qualify_static_member_accesses_with_declaring_class=true 32 | cleanup.qualify_static_method_accesses_with_declaring_class=false 33 | cleanup.remove_private_constructors=true 34 | cleanup.remove_redundant_type_arguments=true 35 | cleanup.remove_trailing_whitespaces=true 36 | cleanup.remove_trailing_whitespaces_all=true 37 | cleanup.remove_trailing_whitespaces_ignore_empty=false 38 | cleanup.remove_unnecessary_casts=true 39 | cleanup.remove_unnecessary_nls_tags=false 40 | cleanup.remove_unused_imports=true 41 | cleanup.remove_unused_local_variables=false 42 | cleanup.remove_unused_private_fields=true 43 | cleanup.remove_unused_private_members=false 44 | cleanup.remove_unused_private_methods=true 45 | cleanup.remove_unused_private_types=true 46 | cleanup.sort_members=false 47 | cleanup.sort_members_all=false 48 | cleanup.use_anonymous_class_creation=false 49 | cleanup.use_blocks=true 50 | cleanup.use_blocks_only_for_return_and_throw=false 51 | cleanup.use_lambda=true 52 | cleanup.use_parentheses_in_expressions=false 53 | cleanup.use_this_for_non_static_field_access=true 54 | cleanup.use_this_for_non_static_field_access_only_if_necessary=false 55 | cleanup.use_this_for_non_static_method_access=false 56 | cleanup.use_this_for_non_static_method_access_only_if_necessary=true 57 | cleanup.use_type_arguments=false 58 | cleanup_profile=_Spring Cleanup Conventions 59 | cleanup_settings_version=2 60 | eclipse.preferences.version=1 61 | editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true 62 | formatter_profile=_Spring Java Conventions 63 | formatter_settings_version=13 64 | org.eclipse.jdt.ui.exception.name=e 65 | org.eclipse.jdt.ui.gettersetter.use.is=true 66 | org.eclipse.jdt.ui.ignorelowercasenames=true 67 | org.eclipse.jdt.ui.importorder=java;javax;;io.spring;\#; 68 | org.eclipse.jdt.ui.javadoc=true 69 | org.eclipse.jdt.ui.keywordthis=false 70 | org.eclipse.jdt.ui.ondemandthreshold=9999 71 | org.eclipse.jdt.ui.overrideannotation=true 72 | org.eclipse.jdt.ui.staticondemandthreshold=9999 73 | org.eclipse.jdt.ui.text.custom_code_templates= 74 | sp_cleanup.add_default_serial_version_id=true 75 | sp_cleanup.add_generated_serial_version_id=false 76 | sp_cleanup.add_missing_annotations=true 77 | sp_cleanup.add_missing_deprecated_annotations=true 78 | sp_cleanup.add_missing_methods=false 79 | sp_cleanup.add_missing_nls_tags=false 80 | sp_cleanup.add_missing_override_annotations=true 81 | sp_cleanup.add_missing_override_annotations_interface_methods=true 82 | sp_cleanup.add_serial_version_id=false 83 | sp_cleanup.always_use_blocks=true 84 | sp_cleanup.always_use_parentheses_in_expressions=true 85 | sp_cleanup.always_use_this_for_non_static_field_access=true 86 | sp_cleanup.always_use_this_for_non_static_method_access=false 87 | sp_cleanup.convert_to_enhanced_for_loop=false 88 | sp_cleanup.correct_indentation=false 89 | sp_cleanup.format_source_code=true 90 | sp_cleanup.format_source_code_changes_only=false 91 | sp_cleanup.make_local_variable_final=false 92 | sp_cleanup.make_parameters_final=false 93 | sp_cleanup.make_private_fields_final=false 94 | sp_cleanup.make_type_abstract_if_missing_method=false 95 | sp_cleanup.make_variable_declarations_final=false 96 | sp_cleanup.never_use_blocks=false 97 | sp_cleanup.never_use_parentheses_in_expressions=false 98 | sp_cleanup.on_save_use_additional_actions=true 99 | sp_cleanup.organize_imports=true 100 | sp_cleanup.qualify_static_field_accesses_with_declaring_class=false 101 | sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true 102 | sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true 103 | sp_cleanup.qualify_static_member_accesses_with_declaring_class=true 104 | sp_cleanup.qualify_static_method_accesses_with_declaring_class=false 105 | sp_cleanup.remove_private_constructors=true 106 | sp_cleanup.remove_redundant_modifiers=true 107 | sp_cleanup.remove_redundant_semicolons=true 108 | sp_cleanup.remove_redundant_type_arguments=true 109 | sp_cleanup.remove_trailing_whitespaces=true 110 | sp_cleanup.remove_trailing_whitespaces_all=true 111 | sp_cleanup.remove_trailing_whitespaces_ignore_empty=false 112 | sp_cleanup.remove_unnecessary_casts=true 113 | sp_cleanup.remove_unnecessary_nls_tags=false 114 | sp_cleanup.remove_unused_imports=true 115 | sp_cleanup.remove_unused_local_variables=false 116 | sp_cleanup.remove_unused_private_fields=true 117 | sp_cleanup.remove_unused_private_members=false 118 | sp_cleanup.remove_unused_private_methods=true 119 | sp_cleanup.remove_unused_private_types=true 120 | sp_cleanup.sort_members=false 121 | sp_cleanup.sort_members_all=false 122 | sp_cleanup.use_blocks=true 123 | sp_cleanup.use_blocks_only_for_return_and_throw=false 124 | sp_cleanup.use_parentheses_in_expressions=false 125 | sp_cleanup.use_this_for_non_static_field_access=true 126 | sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false 127 | sp_cleanup.use_this_for_non_static_method_access=false 128 | sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true 129 | -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | jobs: 9 | build: 10 | name: 'Build' 11 | runs-on: 'ubuntu-latest' 12 | steps: 13 | - name: Set up Java 14 | uses: actions/setup-java@5896cecc08fd8a1fbdfaf517e29b571164b031f7 # v4.2.0 15 | with: 16 | distribution: 'liberica' 17 | java-version: 17 18 | - name: Check out code 19 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 20 | - name: Set up Gradle 21 | uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # 3.1.0 22 | with: 23 | cache-read-only: false 24 | - name: Build 25 | id: build 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+.[0-9]+.[0-9]+ 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | jobs: 9 | release: 10 | name: 'Release' 11 | runs-on: 'ubuntu-latest' 12 | steps: 13 | - name: Set up Java 14 | uses: actions/setup-java@5896cecc08fd8a1fbdfaf517e29b571164b031f7 # v4.2.0 15 | with: 16 | distribution: 'liberica' 17 | java-version: 17 18 | - name: Check out code 19 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 20 | - name: Set up Gradle 21 | uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # 3.1.0 22 | with: 23 | cache-read-only: false 24 | - name: Build 25 | run: ./gradlew build 26 | - name: Get milestone from tag 27 | id: milestone-from-tag 28 | run: | 29 | milestone=$(echo ${{ github.ref_name }} | cut -c 2-) 30 | echo "milestone=$milestone" >> $GITHUB_OUTPUT 31 | - name: Generate changelog 32 | uses: ./ 33 | with: 34 | milestone: ${{ steps.milestone-from-tag.outputs.milestone }} 35 | - name: Release 36 | env: 37 | GITHUB_TOKEN: ${{ github.token }} 38 | run: gh release create ${{ github.ref_name }} --notes-file=changelog.md build/libs/github-changelog-generator.jar 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | .gradle/ 3 | /bin/ 4 | /out/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.adoc: -------------------------------------------------------------------------------- 1 | = Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open 4 | and welcoming community, we pledge to respect all people who contribute through reporting 5 | issues, posting feature requests, updating documentation, submitting pull requests or 6 | patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free experience for 9 | everyone, regardless of level of experience, gender, gender identity and expression, 10 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, 11 | religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 24 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 25 | Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors 26 | that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing this project. Project 30 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 31 | from the project team. 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will 38 | be reviewed and investigated and will result in a response that is deemed necessary and 39 | appropriate to the circumstances. Maintainers are obligated to maintain confidentiality 40 | with regard to the reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the 43 | https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at 44 | https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Contributing to GitHub Release Notes Generator 2 | 3 | This project is released under the Apache 2.0 license. If you would like to contribute 4 | something, or simply want to hack on the code this document should help you get started. 5 | 6 | 7 | 8 | == Code of Conduct 9 | This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of 10 | conduct]. By participating, you are expected to uphold this code. Please report 11 | unacceptable behavior to spring-code-of-conduct@pivotal.io. 12 | 13 | 14 | 15 | == Include a Signed Off By Trailer 16 | All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. 17 | For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. 18 | 19 | 20 | 21 | == Code Conventions and Housekeeping 22 | None of these is essential for a pull request, but they will all help. They can also be 23 | added after the original pull request but before a merge. 24 | 25 | * We use the https://github.com/spring-io/spring-javaformat/[Spring JavaFormat] project 26 | to apply code formatting conventions. 27 | * The build includes checkstyle rules for many of our code conventions. Run 28 | `./gradlew check` if you want to check you changes are compliant. 29 | * Make sure all new `.java` files to have a simple Javadoc class comment with at least an 30 | `@author` tag identifying you, and preferably at least a paragraph on what the class is 31 | for. 32 | * Add the ASF license header comment to all new `.java` files (copy from existing files 33 | in the project) 34 | * Add yourself as an `@author` to the `.java` files that you modify substantially (more 35 | than cosmetic changes). 36 | * Add some Javadocs. 37 | * A few unit tests would help a lot as well -- someone has to do it. 38 | * If no-one else is using your branch, please rebase it against the current master (or 39 | other target branch in the main project). 40 | * When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], 41 | if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit 42 | message (where `XXXX` is the issue number). -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:8.5.0-jdk17-alpine as build 2 | COPY src /app/src/ 3 | COPY config /app/config/ 4 | COPY build.gradle settings.gradle gradle.properties /app/ 5 | RUN cd /app && gradle -Dorg.gradle.welcome=never --no-daemon bootJar 6 | 7 | FROM ghcr.io/bell-sw/liberica-openjre-debian:17.0.10-13 8 | COPY --from=build /app/build/libs/github-changelog-generator.jar /opt/action/github-changelog-generator.jar 9 | ENTRYPOINT ["java", "-jar", "/opt/action/github-changelog-generator.jar"] 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | https://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.adoc: -------------------------------------------------------------------------------- 1 | = GitHub Changelog Generator 2 | 3 | A changelog generator for GitHub Issues, available as a https://github.com/spring-io/github-changelog-generator/releases[release jar] or for use as a GitHub Action. 4 | 5 | 6 | 7 | == Running as a Release Jar 8 | 9 | The changelog generator requires Java 17 or later. 10 | To generate a markdown changelog using a https://github.com/spring-io/github-changelog-generator/releases[release jar], follow these steps: 11 | 12 | - Download a https://github.com/spring-io/github-changelog-generator/releases[release jar]. 13 | - Run `java -jar github-changelog-generator.jar --changelog.repository=/` 14 | 15 | To increase https://developer.github.com/v3/?#rate-limiting[GitHub's rate limits], you can also use `--github-token=` to provide an access token that is used for authentication. 16 | 17 | For more advanced configuration options, <>. 18 | 19 | == Using as a GitHub Action 20 | 21 | 22 | 23 | === Configuration 24 | 25 | 26 | 27 | ==== Required Inputs 28 | 29 | - `milestone`: Milestone for which the changelog should be generated 30 | 31 | 32 | 33 | === Optional Inputs 34 | 35 | - `changelog-file`: Path of the file to which the changelog should be written. 36 | Defaults to `changelog.md` 37 | - `config-file`: Path to a changelog generator configuration file, relative to `GITHUB_WORKSPACE`. 38 | <> for details of the advanced options that can be configured using this file. 39 | - `repository`: Repository for which a changelog should be generated. Defaults to the workflow's repository. 40 | - `token`: Token for authentication with GitHub. 41 | Authenticating increases GitHub rate limits. 42 | 43 | 44 | === Minimal Example 45 | 46 | [source,yaml,indent=0] 47 | ---- 48 | steps: 49 | - name: Generate Changelog 50 | uses: spring-io/github-changelog-generator@ 51 | with: 52 | milestone: '1.0.0' 53 | ---- 54 | 55 | 56 | 57 | == Advanced Configuration 58 | 59 | A YAML configuration file can be used to provide more complex configuration, either when running as a release jar or as a GitHub action. 60 | 61 | 62 | 63 | === Customizing Sections 64 | 65 | By default, the changelog will contain the following sections: 66 | 67 | |=== 68 | |Title |Label Text 69 | 70 | |":star: New Features" 71 | |"enhancement" 72 | 73 | |":lady_beetle: Bug Fixes" 74 | |"regression" or "bug" 75 | 76 | |":notebook_with_decorative_cover: Documentation" 77 | |"documentation" 78 | 79 | |":hammer: Dependency Upgrades" 80 | |"dependency-upgrade" 81 | |=== 82 | 83 | The title is in https://guides.github.com/features/mastering-markdown[Markdown] format and emoji like ":star:" can be used. 84 | If you want something different then you can add `sections` YAML: 85 | 86 | [source,yaml] 87 | ---- 88 | changelog: 89 | sections: 90 | - title: "Enhancements" 91 | labels: ["new"] 92 | - title: "Bugs" 93 | labels: ["fix"] 94 | ---- 95 | 96 | By default, adding sections will replace the default sections. 97 | To add sections after the defaults, add the following configuration: 98 | 99 | [source, yaml] 100 | ---- 101 | changelog: 102 | add-sections: true 103 | ---- 104 | 105 | You can also customize the contributors title using the following: 106 | 107 | [source,yaml] 108 | ---- 109 | changelog: 110 | contributors: 111 | title: "Contributors" 112 | ---- 113 | 114 | You can add external links such as release notes for quick access using the following: 115 | 116 | [source,yaml] 117 | ---- 118 | changelog: 119 | external_links: 120 | - name: "Release Notes" 121 | location: "https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes" 122 | ---- 123 | 124 | 125 | 126 | ==== Showing Issues in Multiple Sections 127 | 128 | Unless otherwise configured, issues will only appear in the first matching section. 129 | For example, if you have an issue labeled with `enhancement` and `documentation` then it will only appear in the "New Features" section. 130 | 131 | If you want an issue to appear in multiple sections, use the `group` property. 132 | Groups allow you to create logical groupings of related sections. 133 | An issue may only appear once in any given group. 134 | 135 | For example, you might define the following: 136 | 137 | [source,yaml] 138 | ---- 139 | changelog: 140 | sections: 141 | - title: "Highlights" 142 | labels: ["noteworthy"] 143 | group: "highlights" 144 | - title: "Enhancements" 145 | labels: ["new"] 146 | - title: "Bugs" 147 | labels: ["fix"] 148 | ---- 149 | 150 | This will create two distinct groups, "highlights" and "default" (which is used if no `group` property is specified). 151 | An issue labeled with `new` and `noteworthy` will appear in both the "Highlights" and "Enhancements" section. 152 | 153 | 154 | 155 | === Excluding Issues 156 | 157 | Issues and pull requests can be excluded from the changelog by configuring exclusions. 158 | You can ignore all items that have certain labels using `changelog.issues.exclude.labels`. 159 | For example: 160 | 161 | [source,yaml] 162 | ---- 163 | changelog: 164 | issues: 165 | exclude: 166 | labels: ["wontfix", "question", "duplicate", "invalid"] 167 | ---- 168 | 169 | 170 | 171 | === Excluding Contributors 172 | 173 | Contributors whose username ends with `[bot]`, such as `dependabot[bot]` and `github-actions[bot]`, are automatically excluded. 174 | If you have other contributors that you want to be excluded (perhaps core team members), you can set the following: 175 | 176 | [source,yaml] 177 | ---- 178 | changelog: 179 | contributors: 180 | exclude: 181 | names: ["coremember"] 182 | ---- 183 | 184 | You can also use `*` if you want to drop the contributors section entirely. 185 | 186 | 187 | 188 | === Sorting Issues 189 | 190 | By default, issues are sorted by their "created" date. 191 | If you want to order them by title instead you can set `changelog.issues.sort` to `title`. 192 | It's also set the property on section configuration if you want ordering per section: 193 | 194 | [source,yaml] 195 | ---- 196 | changelog: 197 | sections: 198 | - title: "Bugs" 199 | labels: ["bug"] 200 | - title: "Dependency Upgrades" 201 | labels: ["dependency"] 202 | sort: "title" 203 | ---- 204 | 205 | 206 | 207 | === Following Ported Issues 208 | 209 | If an issue is forward-ported or backward-ported between milestones, you might have separate issues in each milestone that reference the original issue. 210 | To credit a contributor in the changelog for every milestone that includes a forward or backward port of the issue that was resolved, configure the labels that are used to identify ported issues. 211 | The body of a ported issue should contain a comment with a reference to the original issue, which is extracted using a regular expression with exactly one capture group for the original issue number. 212 | 213 | [source,yaml] 214 | ---- 215 | changelog: 216 | issues: 217 | ports: 218 | - label: "status: forward-port" 219 | bodyExpression: 'Forward port of issue #(\d+).*' 220 | - label: "status: backport" 221 | bodyExpression: 'Back port of issue #(\d+).*' 222 | ---- 223 | 224 | 225 | 226 | === Disabling Generation of Links to Each Issue 227 | 228 | By default, each entry in the changelog will include a link back to the issue or PR on GitHub. 229 | The generation of these links can be disabled: 230 | 231 | [source,yaml] 232 | ---- 233 | changelog: 234 | issues: 235 | generate_links: false 236 | ---- 237 | 238 | 239 | 240 | == License 241 | 242 | This project is Open Source software released under the 243 | https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license]. 244 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: 'GitHub Changelog Generator' 2 | description: 'Generates a changelog from the closed issues in a GitHub milestone' 3 | inputs: 4 | repository: 5 | description: 'Repository for which a changelog should be generated' 6 | required: false 7 | default: ${{ github.repository}} 8 | milestone: 9 | description: 'Milestone for which the changelog should be generated' 10 | required: true 11 | token: 12 | description: 'Optional token for authentication with GitHub. Authenticating increases GitHub rate limits' 13 | required: false 14 | config-file: 15 | description: 'Path to a changelog generator configuration file, relative to GITHUB_WORKSPACE' 16 | required: false 17 | changelog-file: 18 | description: 'Path of the file to which the changelog should be written' 19 | default: changelog.md 20 | runs: 21 | using: 'docker' 22 | image: 'Dockerfile' 23 | args: 24 | - ${{ inputs.milestone }} 25 | - ${{ inputs.changelog-file }} 26 | - --changelog.repository=${{ inputs.repository }} 27 | - --github.token=${{ inputs.token }} 28 | - ${{ inputs.config-file != null && format('--spring.config.location=file:/github/workspace/{0}', inputs.config-file) || '' }} 29 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "checkstyle" 3 | id "java" 4 | id "org.springframework.boot" version "3.4.4" 5 | id "io.spring.javaformat" version "$javaFormatVersion" 6 | } 7 | 8 | java { 9 | sourceCompatibility "17" 10 | targetCompatibility "17" 11 | } 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | configurations { 18 | checkstyle { 19 | resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") { 20 | select("com.google.guava:guava:0") 21 | } 22 | } 23 | } 24 | 25 | checkstyle { 26 | toolVersion = "10.12.7" 27 | } 28 | 29 | dependencies { 30 | annotationProcessor(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) 31 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 32 | 33 | checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}") 34 | checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:$javaFormatVersion") 35 | 36 | implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) 37 | implementation("org.springframework:spring-web") 38 | implementation("org.springframework.boot:spring-boot-starter-json") 39 | 40 | testImplementation("org.springframework.boot:spring-boot-starter-test") 41 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 42 | } 43 | 44 | tasks.named("test") { 45 | useJUnitPlatform() 46 | } 47 | 48 | tasks.named("jar") { 49 | enabled = false 50 | } 51 | 52 | tasks.named("bootJar") { 53 | archiveVersion = "" 54 | } -------------------------------------------------------------------------------- /config/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | javaFormatVersion=0.0.41 2 | version=0.0.13-SNAPSHOT 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-io/github-changelog-generator/925ed0a45f3ede77449f704b628fbb77b4178a90/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "github-changelog-generator" -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 22 | 23 | /** 24 | * GitHub changelog generator. 25 | * 26 | * @author Madhura Bhave 27 | */ 28 | @SpringBootApplication 29 | @ConfigurationPropertiesScan 30 | public class Application { 31 | 32 | public static void main(String[] args) { 33 | SpringApplication.run(Application.class, args); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.Set; 22 | import java.util.regex.Pattern; 23 | 24 | import org.springframework.boot.context.properties.ConfigurationProperties; 25 | import org.springframework.boot.context.properties.bind.DefaultValue; 26 | import org.springframework.util.Assert; 27 | 28 | import io.spring.githubchangeloggenerator.github.service.Repository; 29 | 30 | /** 31 | * Configuration properties for the GitHub repo. 32 | * 33 | * @author Madhura Bhave 34 | * @author Phillip Webb 35 | * @author Mahendra Bishnoi 36 | * @author Gary Russell 37 | * @author Steven Sheehy 38 | */ 39 | @ConfigurationProperties(prefix = "changelog") 40 | public class ApplicationProperties { 41 | 42 | /** 43 | * GitHub repository to use in the form "owner/repository". 44 | */ 45 | private final Repository repository; 46 | 47 | /** 48 | * The way that milestones are referenced. Supports "title", "id". 49 | */ 50 | private final MilestoneReference milestoneReference; 51 | 52 | /** 53 | * Section definitions in the order that they should appear. 54 | */ 55 | private final List
sections; 56 | 57 | /** 58 | * Settings specific to issues. 59 | */ 60 | private final Issues issues; 61 | 62 | /** 63 | * Settings specific to contributors. 64 | */ 65 | private final Contributors contributors; 66 | 67 | /** 68 | * Settings specific to external links. 69 | */ 70 | private final List externalLinks; 71 | 72 | /** 73 | * True to add sections to default instead of replacing. 74 | */ 75 | private final boolean addSections; 76 | 77 | public ApplicationProperties(Repository repository, @DefaultValue("title") MilestoneReference milestoneReference, 78 | List
sections, Issues issues, Contributors contributors, List externalLinks, 79 | @DefaultValue("false") boolean addSections) { 80 | Assert.notNull(repository, "Repository must not be null"); 81 | this.repository = repository; 82 | this.milestoneReference = milestoneReference; 83 | this.sections = (sections != null) ? sections : Collections.emptyList(); 84 | this.issues = (issues != null) ? issues : new Issues(null, null, null, true); 85 | this.contributors = (contributors != null) ? contributors : new Contributors(null, null); 86 | this.externalLinks = (externalLinks != null) ? externalLinks : Collections.emptyList(); 87 | this.addSections = addSections; 88 | } 89 | 90 | public Repository getRepository() { 91 | return this.repository; 92 | } 93 | 94 | public MilestoneReference getMilestoneReference() { 95 | return this.milestoneReference; 96 | } 97 | 98 | public List
getSections() { 99 | return this.sections; 100 | } 101 | 102 | public Issues getIssues() { 103 | return this.issues; 104 | } 105 | 106 | public Contributors getContributors() { 107 | return this.contributors; 108 | } 109 | 110 | public List getExternalLinks() { 111 | return this.externalLinks; 112 | } 113 | 114 | public boolean isAddSections() { 115 | return this.addSections; 116 | } 117 | 118 | /** 119 | * Properties for a single changelog section. 120 | */ 121 | public static class Section { 122 | 123 | /** 124 | * Title of the section. 125 | */ 126 | private final String title; 127 | 128 | /** 129 | * Group used to bound the contained issues. Issues appear in the first section of 130 | * each group. 131 | */ 132 | private final String group; 133 | 134 | /** 135 | * Sort order for issues within this section. 136 | */ 137 | private final IssueSort sort; 138 | 139 | /** 140 | * Labels used to identify if an issue is for the section. 141 | */ 142 | private final Set labels; 143 | 144 | /** 145 | * Whether issues, pull requests or both should be included in this section. 146 | */ 147 | private final IssueType type; 148 | 149 | public Section(String title, @DefaultValue("default") String group, IssueSort sort, Set labels, 150 | @DefaultValue("any") IssueType type) { 151 | this.title = title; 152 | this.group = (group != null) ? group : "default"; 153 | this.sort = sort; 154 | this.labels = labels; 155 | this.type = type; 156 | } 157 | 158 | public String getTitle() { 159 | return this.title; 160 | } 161 | 162 | public String getGroup() { 163 | return this.group; 164 | } 165 | 166 | public IssueSort getSort() { 167 | return this.sort; 168 | } 169 | 170 | public Set getLabels() { 171 | return this.labels; 172 | } 173 | 174 | public IssueType getType() { 175 | return this.type; 176 | } 177 | 178 | } 179 | 180 | /** 181 | * Properties relating to issues. 182 | */ 183 | public static class Issues { 184 | 185 | /** 186 | * The issue sort order. 187 | */ 188 | private final IssueSort sort; 189 | 190 | /** 191 | * Issue exclusions. 192 | */ 193 | private final IssuesExclude exclude; 194 | 195 | /** 196 | * Identification of issues that are a forward-port or back-port of another issue. 197 | */ 198 | private final Set ports; 199 | 200 | /** 201 | * Whether to generate a link to each issue in the changelog. 202 | */ 203 | private final boolean generateLinks; 204 | 205 | public Issues(IssueSort sort, IssuesExclude exclude, Set ports, 206 | @DefaultValue("true") boolean generateLinks) { 207 | this.sort = sort; 208 | this.exclude = (exclude != null) ? exclude : new IssuesExclude(null); 209 | this.ports = (ports != null) ? ports : Collections.emptySet(); 210 | this.generateLinks = generateLinks; 211 | } 212 | 213 | public IssueSort getSort() { 214 | return this.sort; 215 | } 216 | 217 | public IssuesExclude getExcludes() { 218 | return this.exclude; 219 | } 220 | 221 | public Set getPorts() { 222 | return this.ports; 223 | } 224 | 225 | public boolean isGenerateLinks() { 226 | return this.generateLinks; 227 | } 228 | 229 | } 230 | 231 | /** 232 | * Issues exclude. 233 | */ 234 | public static class IssuesExclude { 235 | 236 | /** 237 | * Labels used to exclude issues. 238 | */ 239 | private final Set labels; 240 | 241 | public IssuesExclude(Set labels) { 242 | this.labels = (labels != null) ? labels : Collections.emptySet(); 243 | } 244 | 245 | public Set getLabels() { 246 | return this.labels; 247 | } 248 | 249 | } 250 | 251 | /** 252 | * Properties related to identification of ported issues. 253 | */ 254 | public static class PortedIssue { 255 | 256 | /** 257 | * Label used to identify a ported issue. 258 | */ 259 | private final String label; 260 | 261 | /** 262 | * Regular expression used to extract the upstream or downstream issue ID from the 263 | * body of a ported issue. 264 | */ 265 | private final Pattern bodyExpression; 266 | 267 | public PortedIssue(String label, String bodyExpression) { 268 | this.label = label; 269 | this.bodyExpression = Pattern.compile(bodyExpression); 270 | } 271 | 272 | public String getLabel() { 273 | return this.label; 274 | } 275 | 276 | public Pattern getBodyExpression() { 277 | return this.bodyExpression; 278 | } 279 | 280 | } 281 | 282 | /** 283 | * Properties relating to constructors. 284 | */ 285 | public static class Contributors { 286 | 287 | /** 288 | * Title for the contributors section. 289 | */ 290 | private final String title; 291 | 292 | /** 293 | * Contributor exclusions. 294 | */ 295 | private final ContributorsExclude exclude; 296 | 297 | public Contributors(String title, ContributorsExclude exclude) { 298 | this.title = title; 299 | this.exclude = (exclude != null) ? exclude : new ContributorsExclude(null); 300 | } 301 | 302 | public String getTitle() { 303 | return this.title; 304 | } 305 | 306 | public ContributorsExclude getExclude() { 307 | return this.exclude; 308 | } 309 | 310 | } 311 | 312 | /** 313 | * Contributors exclude. 314 | */ 315 | public static class ContributorsExclude { 316 | 317 | /** 318 | * Contributor names to exclude. 319 | */ 320 | private final Set names; 321 | 322 | public ContributorsExclude(Set names) { 323 | this.names = (names != null) ? names : Collections.emptySet(); 324 | } 325 | 326 | public Set getNames() { 327 | return this.names; 328 | } 329 | 330 | } 331 | 332 | /** 333 | * Properties for a single external link. 334 | */ 335 | public static class ExternalLink { 336 | 337 | /** 338 | * Name to be shown for an external link. 339 | */ 340 | private final String name; 341 | 342 | /** 343 | * URL for an external link. 344 | */ 345 | private final String location; 346 | 347 | public ExternalLink(String name, String location) { 348 | this.name = name; 349 | this.location = location; 350 | } 351 | 352 | public String getName() { 353 | return this.name; 354 | } 355 | 356 | public String getLocation() { 357 | return this.location; 358 | } 359 | 360 | } 361 | 362 | public enum IssueSort { 363 | 364 | /** 365 | * Sort by the created date. 366 | */ 367 | CREATED, 368 | 369 | /** 370 | * Sort by the title. 371 | */ 372 | TITLE 373 | 374 | } 375 | 376 | /** 377 | * The type of changelog entry. 378 | */ 379 | public enum IssueType { 380 | 381 | /** 382 | * Either issue or pull requests. 383 | */ 384 | ANY, 385 | 386 | /** 387 | * GitHub issue. 388 | */ 389 | ISSUE, 390 | 391 | /** 392 | * GitHub pull request. 393 | */ 394 | PULL_REQUEST 395 | 396 | } 397 | 398 | } 399 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/ChangelogGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.io.File; 20 | import java.io.FileWriter; 21 | import java.io.IOException; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.Comparator; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | import java.util.stream.Collectors; 32 | 33 | import org.springframework.stereotype.Component; 34 | import org.springframework.util.FileCopyUtils; 35 | 36 | import io.spring.githubchangeloggenerator.ApplicationProperties.ExternalLink; 37 | import io.spring.githubchangeloggenerator.ApplicationProperties.IssueSort; 38 | import io.spring.githubchangeloggenerator.ApplicationProperties.PortedIssue; 39 | import io.spring.githubchangeloggenerator.github.payload.Issue; 40 | import io.spring.githubchangeloggenerator.github.payload.Label; 41 | import io.spring.githubchangeloggenerator.github.payload.User; 42 | import io.spring.githubchangeloggenerator.github.service.GitHubService; 43 | import io.spring.githubchangeloggenerator.github.service.Repository; 44 | 45 | /** 46 | * Generates a changelog markdown file which includes bug fixes, enhancements and 47 | * contributors for a given milestone. 48 | * 49 | * @author Madhura Bhave 50 | * @author Phillip Webb 51 | * @author Mahendra Bishnoi 52 | */ 53 | @Component 54 | public class ChangelogGenerator { 55 | 56 | private static final Comparator TITLE_COMPARATOR = Comparator.comparing(Issue::getTitle, 57 | String.CASE_INSENSITIVE_ORDER); 58 | 59 | private static final List escapes = Arrays.asList(gitHubUserMentions(), htmlTags(), markdownStyling()); 60 | 61 | private final GitHubService service; 62 | 63 | private final Repository repository; 64 | 65 | private final MilestoneReference milestoneReference; 66 | 67 | private final IssueSort sort; 68 | 69 | private final Set excludeLabels; 70 | 71 | private final Set portedIssues; 72 | 73 | private final Set excludeContributors; 74 | 75 | private final String contributorsTitle; 76 | 77 | private final ChangelogSections sections; 78 | 79 | private final List externalLinks; 80 | 81 | private final boolean generateLinks; 82 | 83 | public ChangelogGenerator(GitHubService service, ApplicationProperties properties) { 84 | this.service = service; 85 | this.repository = properties.getRepository(); 86 | this.milestoneReference = properties.getMilestoneReference(); 87 | this.sort = properties.getIssues().getSort(); 88 | this.excludeLabels = properties.getIssues().getExcludes().getLabels(); 89 | this.excludeContributors = properties.getContributors().getExclude().getNames(); 90 | this.contributorsTitle = properties.getContributors().getTitle(); 91 | this.sections = new ChangelogSections(properties); 92 | this.portedIssues = properties.getIssues().getPorts(); 93 | this.externalLinks = properties.getExternalLinks(); 94 | this.generateLinks = properties.getIssues().isGenerateLinks(); 95 | } 96 | 97 | /** 98 | * Generates a file at the given path which includes bug fixes, enhancements and 99 | * contributors for the given milestone. 100 | * @param milestone the milestone to generate the changelog for 101 | * @param path the path to the file 102 | * @throws IOException if writing to file failed 103 | */ 104 | public void generate(String milestone, String path) throws IOException { 105 | int milestoneNumber = resolveMilestoneReference(milestone); 106 | List issues = getIssues(milestoneNumber); 107 | String content = generateContent(issues); 108 | writeContentToFile(content, path); 109 | } 110 | 111 | private List getIssues(int milestoneNumber) { 112 | List issues = new ArrayList<>(this.service.getIssuesForMilestone(milestoneNumber, this.repository)); 113 | issues.removeIf(this::isExcluded); 114 | return issues; 115 | } 116 | 117 | private boolean isExcluded(Issue issue) { 118 | return issue.getLabels().stream().anyMatch(this::isExcluded); 119 | } 120 | 121 | private boolean isExcluded(Label label) { 122 | return this.excludeLabels.contains(label.getName()); 123 | } 124 | 125 | private int resolveMilestoneReference(String milestone) { 126 | return switch (this.milestoneReference) { 127 | case TITLE -> this.service.getMilestoneNumber(milestone, this.repository); 128 | case ID -> Integer.parseInt(milestone); 129 | }; 130 | } 131 | 132 | private String generateContent(List issues) { 133 | StringBuilder content = new StringBuilder(); 134 | addSectionContent(content, this.sections.collate(issues)); 135 | Set contributors = getContributors(issues); 136 | if (!contributors.isEmpty()) { 137 | addContributorsContent(content, contributors); 138 | } 139 | if (!this.externalLinks.isEmpty()) { 140 | addExternalLinksContent(content, this.externalLinks); 141 | } 142 | return content.toString(); 143 | } 144 | 145 | private void addSectionContent(StringBuilder content, Map> sectionIssues) { 146 | sectionIssues.forEach((section, issues) -> { 147 | sort(section.getSort(), issues); 148 | content.append((content.length() != 0) ? String.format("%n") : ""); 149 | content.append("## ").append(section).append(String.format("%n%n")); 150 | issues.stream().map(this::getFormattedIssue).forEach(content::append); 151 | }); 152 | } 153 | 154 | private void sort(IssueSort sort, List issues) { 155 | sort = (sort != null) ? sort : this.sort; 156 | if (sort == IssueSort.TITLE) { 157 | issues.sort(TITLE_COMPARATOR); 158 | } 159 | } 160 | 161 | private String getFormattedIssue(Issue issue) { 162 | String title = issue.getTitle(); 163 | for (Escape escape : escapes) { 164 | title = escape.apply(title); 165 | } 166 | return (this.generateLinks) ? String.format("- %s %s%n", title, getLinkToIssue(issue)) 167 | : String.format("- %s%n", title); 168 | } 169 | 170 | private String getLinkToIssue(Issue issue) { 171 | return "[#" + issue.getNumber() + "]" + "(" + issue.getUrl() + ")"; 172 | } 173 | 174 | private Set getContributors(List issues) { 175 | if (this.excludeContributors.contains("*")) { 176 | return Collections.emptySet(); 177 | } 178 | return issues.stream() 179 | .map(this::getPortedReferenceIssue) 180 | .filter((issue) -> issue.getPullRequest() != null) 181 | .map(Issue::getUser) 182 | .filter(this::isIncludedContributor) 183 | .collect(Collectors.toSet()); 184 | } 185 | 186 | private Issue getPortedReferenceIssue(Issue issue) { 187 | for (PortedIssue portedIssue : this.portedIssues) { 188 | List labelNames = issue.getLabels().stream().map(Label::getName).toList(); 189 | if (labelNames.contains(portedIssue.getLabel())) { 190 | Pattern pattern = portedIssue.getBodyExpression(); 191 | Matcher matcher = pattern.matcher(issue.getBody()); 192 | if (matcher.matches()) { 193 | String issueNumber = matcher.group(1); 194 | Issue referencedIssue = this.service.getIssue(issueNumber, this.repository); 195 | if (referencedIssue != null) { 196 | return getPortedReferenceIssue(referencedIssue); 197 | } 198 | } 199 | } 200 | } 201 | return issue; 202 | } 203 | 204 | private boolean isIncludedContributor(User user) { 205 | String name = user.getName(); 206 | return !this.excludeContributors.contains(name) && !name.endsWith("[bot]"); 207 | } 208 | 209 | private void addContributorsContent(StringBuilder content, Set contributors) { 210 | content.append(String.format("%n## ")); 211 | content.append((this.contributorsTitle != null) ? this.contributorsTitle : ":heart: Contributors"); 212 | content.append(String.format("%n%nThank you to all the contributors who worked on this release:%n%n")); 213 | content.append(formatContributors(contributors)); 214 | } 215 | 216 | private String formatContributors(Set contributors) { 217 | List names = contributors.stream().map(User::getName).map((name) -> "@" + name).sorted().toList(); 218 | StringBuilder formatted = new StringBuilder(); 219 | String separator = (names.size() > 2) ? ", " : " "; 220 | for (int i = 0; i < names.size(); i++) { 221 | if (i > 0) { 222 | formatted.append(separator); 223 | if (i == names.size() - 1) { 224 | formatted.append("and "); 225 | } 226 | } 227 | formatted.append(names.get(i)); 228 | } 229 | return formatted.toString(); 230 | } 231 | 232 | private void addExternalLinksContent(StringBuilder content, List externalLinks) { 233 | content.append(String.format("%n## ")); 234 | content.append(String.format("External Links%n%n")); 235 | externalLinks.stream().map(this::formatExternalLinks).forEach(content::append); 236 | } 237 | 238 | private String formatExternalLinks(ExternalLink externalLink) { 239 | return String.format("- [%s](%s)%n", externalLink.getName(), externalLink.getLocation()); 240 | } 241 | 242 | private void writeContentToFile(String content, String path) throws IOException { 243 | File file = new File(path).getAbsoluteFile(); 244 | File parent = file.getParentFile(); 245 | if (parent != null) { 246 | parent.mkdirs(); 247 | } 248 | FileCopyUtils.copy(content, new FileWriter(file)); 249 | } 250 | 251 | private static Escape gitHubUserMentions() { 252 | return new PatternEscape(Pattern.compile("(^|[^\\w`])(@[\\w-]+)"), "$1`$2`"); 253 | } 254 | 255 | private static Escape htmlTags() { 256 | return new PatternEscape(Pattern.compile("(^|[^\\w`])(<[\\w\\-/<>]+>)"), "$1`$2`"); 257 | } 258 | 259 | private static Escape markdownStyling() { 260 | return (input) -> { 261 | boolean withinBackticks = false; 262 | char previous = ' '; 263 | StringBuilder result = new StringBuilder(input.length()); 264 | for (char c : input.toCharArray()) { 265 | if (!withinBackticks && previous != '\\' && (c == '*' || c == '_' || c == '~')) { 266 | result.append('\\'); 267 | } 268 | result.append(c); 269 | if (c == '`') { 270 | withinBackticks = !withinBackticks; 271 | } 272 | previous = c; 273 | } 274 | return result.toString(); 275 | }; 276 | } 277 | 278 | private interface Escape { 279 | 280 | String apply(String input); 281 | 282 | } 283 | 284 | private static final class PatternEscape implements Escape { 285 | 286 | private final Pattern pattern; 287 | 288 | private final String replacement; 289 | 290 | private PatternEscape(Pattern pattern, String replacement) { 291 | this.pattern = pattern; 292 | this.replacement = replacement; 293 | } 294 | 295 | @Override 296 | public String apply(String input) { 297 | return this.pattern.matcher(input).replaceAll(this.replacement); 298 | } 299 | 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/ChangelogSection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.util.function.Predicate; 20 | 21 | import org.springframework.util.Assert; 22 | 23 | import io.spring.githubchangeloggenerator.ApplicationProperties.IssueSort; 24 | import io.spring.githubchangeloggenerator.github.payload.Issue; 25 | 26 | /** 27 | * A single section of a changelog report. 28 | * 29 | * @author Phillip Webb 30 | * @author Steven Sheehy 31 | */ 32 | class ChangelogSection { 33 | 34 | private final String title; 35 | 36 | private final String group; 37 | 38 | private final IssueSort sort; 39 | 40 | private Predicate filter; 41 | 42 | ChangelogSection(String title, String group, IssueSort sort, Predicate filter) { 43 | Assert.hasText(title, "Title must not be empty"); 44 | Assert.notNull(filter, "Filter must not be null"); 45 | this.title = title; 46 | this.group = group; 47 | this.sort = sort; 48 | this.filter = filter; 49 | } 50 | 51 | String getGroup() { 52 | return this.group; 53 | } 54 | 55 | IssueSort getSort() { 56 | return this.sort; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return this.title; 62 | } 63 | 64 | boolean isMatchFor(Issue issue) { 65 | return this.filter.test(issue); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/ChangelogSections.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.Comparator; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Set; 26 | import java.util.SortedMap; 27 | import java.util.TreeMap; 28 | import java.util.function.Predicate; 29 | import java.util.stream.Collectors; 30 | 31 | import org.springframework.util.CollectionUtils; 32 | 33 | import io.spring.githubchangeloggenerator.github.payload.Issue; 34 | 35 | /** 36 | * Manages sections of the changelog report. 37 | * 38 | * @author Phillip Webb 39 | * @author Gary Russell 40 | * @author Steven Sheehy 41 | */ 42 | class ChangelogSections { 43 | 44 | private static final List DEFAULT_SECTIONS; 45 | static { 46 | List sections = new ArrayList<>(); 47 | add(sections, ":star: New Features", "enhancement"); 48 | add(sections, ":lady_beetle: Bug Fixes", "bug", "regression"); 49 | add(sections, ":notebook_with_decorative_cover: Documentation", "documentation"); 50 | add(sections, ":hammer: Dependency Upgrades", "dependency-upgrade"); 51 | DEFAULT_SECTIONS = Collections.unmodifiableList(sections); 52 | } 53 | 54 | private static void add(List sections, String title, String... labelNameContent) { 55 | sections.add(new ChangelogSection(title, null, null, SelectIssues.withLabelNamesContaining(labelNameContent))); 56 | } 57 | 58 | private final List sections; 59 | 60 | ChangelogSections(ApplicationProperties properties) { 61 | this.sections = adapt(properties); 62 | } 63 | 64 | private List adapt(ApplicationProperties properties) { 65 | List propertySections = properties.getSections(); 66 | if (CollectionUtils.isEmpty(propertySections)) { 67 | return DEFAULT_SECTIONS; 68 | } 69 | List customSections = propertySections.stream().map(this::adapt).collect(Collectors.toList()); 70 | if (properties.isAddSections()) { 71 | List merged = new ArrayList<>(DEFAULT_SECTIONS); 72 | merged.addAll(customSections); 73 | return merged; 74 | } 75 | return customSections; 76 | } 77 | 78 | private ChangelogSection adapt(ApplicationProperties.Section section) { 79 | Predicate filter = SelectIssues.withLabelNamesContaining(section.getLabels()); 80 | filter = filter.and(SelectIssues.withType(section.getType())); 81 | return new ChangelogSection(section.getTitle(), section.getGroup(), section.getSort(), filter); 82 | } 83 | 84 | Map> collate(List issues) { 85 | SortedMap> collated = new TreeMap<>(Comparator.comparing(this.sections::indexOf)); 86 | for (Issue issue : issues) { 87 | List sections = getSections(issue); 88 | for (ChangelogSection section : sections) { 89 | collated.computeIfAbsent(section, (key) -> new ArrayList<>()); 90 | collated.get(section).add(issue); 91 | } 92 | } 93 | return collated; 94 | } 95 | 96 | private List getSections(Issue issue) { 97 | List result = new ArrayList<>(); 98 | Set groupClaims = new HashSet<>(); 99 | for (ChangelogSection section : this.sections) { 100 | if (section.isMatchFor(issue) && groupClaims.add(section.getGroup())) { 101 | result.add(section); 102 | } 103 | } 104 | return result; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/CommandProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.io.IOException; 20 | import java.util.List; 21 | 22 | import org.springframework.boot.ApplicationArguments; 23 | import org.springframework.boot.ApplicationRunner; 24 | import org.springframework.stereotype.Component; 25 | import org.springframework.util.Assert; 26 | 27 | /** 28 | * {@link ApplicationRunner} that triggers the generation of the changelog based on 29 | * application arguments. 30 | * 31 | * @author Madhura Bhave 32 | */ 33 | @Component 34 | public class CommandProcessor implements ApplicationRunner { 35 | 36 | private final ChangelogGenerator generator; 37 | 38 | public CommandProcessor(ChangelogGenerator generator) { 39 | this.generator = generator; 40 | } 41 | 42 | @Override 43 | public void run(ApplicationArguments args) throws IOException { 44 | run(args.getNonOptionArgs()); 45 | } 46 | 47 | private void run(List args) throws IOException { 48 | String milestone = args.get(0); 49 | String path = args.get(1); 50 | run(milestone, path); 51 | } 52 | 53 | private void run(String milestone, String path) throws IOException { 54 | Assert.hasLength(milestone, "Milestone must be specified"); 55 | Assert.hasLength(path, "Path must be specified"); 56 | this.generator.generate(milestone, path); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/MilestoneReference.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | /** 20 | * The way that milestones are references. 21 | * 22 | * @author Phillip Webb 23 | */ 24 | public enum MilestoneReference { 25 | 26 | /** 27 | * Resolve using the milestone title. 28 | */ 29 | TITLE, 30 | 31 | /** 32 | * Resolve using the milestone ID. 33 | */ 34 | ID 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/SelectIssues.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator; 18 | 19 | import java.util.Collection; 20 | import java.util.Set; 21 | import java.util.function.Predicate; 22 | 23 | import io.spring.githubchangeloggenerator.ApplicationProperties.IssueType; 24 | import io.spring.githubchangeloggenerator.github.payload.Issue; 25 | import io.spring.githubchangeloggenerator.github.payload.Label; 26 | 27 | /** 28 | * Utility to select issues. 29 | * 30 | * @author Phillip Webb 31 | * @author Steven Sheehy 32 | */ 33 | final class SelectIssues { 34 | 35 | private SelectIssues() { 36 | } 37 | 38 | static Predicate withLabelNamesContaining(String... nameContent) { 39 | return withLabelNamesContaining(Set.of(nameContent)); 40 | } 41 | 42 | static Predicate withLabelNamesContaining(Collection nameContent) { 43 | return (issue) -> issue.getLabels() 44 | .stream() 45 | .map(Label::getName) 46 | .anyMatch((name) -> nameContent.stream().anyMatch(name::contains)); 47 | } 48 | 49 | static Predicate withType(IssueType type) { 50 | return (issue) -> { 51 | return switch (type) { 52 | case ANY -> true; 53 | case ISSUE -> issue.getPullRequest() == null; 54 | case PULL_REQUEST -> issue.getPullRequest() != null; 55 | }; 56 | }; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/spring/githubchangeloggenerator/github/payload/Issue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.githubchangeloggenerator.github.payload; 18 | 19 | import java.util.List; 20 | 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | 23 | /** 24 | * Details of a GitHub issue. 25 | * 26 | * @author Madhura Bhave 27 | */ 28 | public class Issue { 29 | 30 | private final String number; 31 | 32 | private final String title; 33 | 34 | private final User user; 35 | 36 | private final List