├── .gitignore ├── README.md ├── pom.xml ├── release.md └── src ├── it ├── settings.xml ├── test-multi-module │ ├── README.md │ ├── child1 │ │ ├── pom.xml │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── jutzig │ │ │ │ └── maven │ │ │ │ └── test │ │ │ │ └── HelloWorld.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── jutzig │ │ │ └── maven │ │ │ └── test │ │ │ └── HelloWorldTest.java │ ├── child2 │ │ ├── pom.xml │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── jutzig │ │ │ │ └── maven │ │ │ │ └── test │ │ │ │ └── HelloWorld.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── jutzig │ │ │ └── maven │ │ │ └── test │ │ │ └── HelloWorldTest.java │ ├── invoker.properties │ ├── pom.xml │ └── verify.groovy └── test-release │ ├── README.md │ ├── invoker.properties │ ├── pom.xml │ ├── src │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── jutzig │ │ │ └── maven │ │ │ └── test │ │ │ └── HelloWorld.java │ └── test │ │ └── java │ │ └── de │ │ └── jutzig │ │ └── maven │ │ └── test │ │ └── HelloWorldTest.java │ └── verify.groovy ├── main └── java │ └── de │ └── jutzig │ └── github │ └── release │ └── plugin │ └── UploadMojo.java └── test └── java └── de └── jutzig └── github └── release └── plugin └── UploadMojoTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse # 2 | /.classpath 3 | /.project 4 | /.settings/ 5 | 6 | # Idea # 7 | /.idea 8 | /*.iml 9 | 10 | # Maven # 11 | /target/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | github-release-plugin 2 | ===================== 3 | 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.jutzig/github-release-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/de.jutzig/github-release-plugin) 5 | 6 | uses the github release api to upload files 7 | 8 | To use the plugin you need to configure it in your `pom.xml` like so 9 | 10 | ``` 11 | 12 | de.jutzig 13 | github-release-plugin 14 | 1.5.0 15 | 16 | Description of your release 17 | 1.0 Final 18 | ${project.version} 19 | 20 | 23 | 24 | 25 | ${project.build.directory} 26 | 27 | ${project.artifactId}*.tar.gz 28 | ${project.artifactId}*.zip 29 | 30 | 31 | 32 | 33 | 34 | ``` 35 | 36 | Unless otherwise specified, the plugin will upload the main artifact of your project and take the github repository url from the `` section. 37 | 38 | By default, the plugin will look for your github credentials in your maven `settings.xml`. Credentials can be privated as an API token, or username and password. Example 39 | ``` 40 | 41 | 42 | github 43 | API_TOKEN 44 | GITHUB_USERNAME 45 | GITHUB_PASSWORD 46 | 47 | 48 | ``` 49 | 50 | These credentials can be overridden by setting `username` and `password` as system properties. 51 | 52 | Additional Parameters: 53 | 54 | * `-Dgithub.draft=true` creates the release in draft state 55 | * `-Dgithub.commitish=release/1.0.0` allows to specify a commitsh 56 | * `-Dgithub.apitoken=API_TOKEN` allows to set github api token instead of providing it in settings.xml 57 | * `-Dgithub.username=GITHUB_USERNAME` allows to set github username instead of providing it in settings.xml 58 | * `-Dgithub.password=GITHUB_PASSWORD` allows to set github password instead of providing it in settings.xml 59 | 60 | The plugin is available on Maven central 61 | 62 | ## Note on release description 63 | The easiest way of getting a good release description (aka release notes or changelog) is to defer its generation to the GitHub. So if you completely leave out 64 | the `` configuration parameter, then GitHub will generate a meaningful description for you. Otherwise, if description _is_ supplied, it will be 65 | prepended to one generated by GitHub. 66 | 67 | Below is an example of how the auto-generated release notes by GitHub could look like: 68 | 69 | Screenshot of autogenerated GitHub release notes 70 | 71 | You can read how to customize release notes format in [GitHub documentation][github-release-notes-format]. 72 | 73 | ## Note on the GitHub API endpoints 74 | The endpoint for GitHub API is inferred from `` connection string. When missing, it by default would use the public endpoint at https://api.github.com. 75 | If you want to upload to a GitHub enterprise instance, then a respective `` connection string must be specified. 76 | 77 | 78 | [github-release-notes-format]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | de.jutzig 4 | github-release-plugin 5 | maven-plugin 6 | 1.6.1-SNAPSHOT 7 | github-release-plugin 8 | Maven plugin to create github releases and upload assets to the release 9 | https://github.com/jutzig/github-release-plugin 10 | 11 | 12 | 13 | The Apache License, Version 2.0 14 | http://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | 20 | jutzig 21 | Johannes Utzig 22 | jutzig.dev@gmail.com 23 | https://github.com/jutzig 24 | 25 | 26 | 27 | 28 | https://github.com/jutzig/github-release-plugin 29 | scm:git:https://github.com/jutzig/github-release-plugin.git 30 | scm:git:https://github.com/jutzig/github-release-plugin.git 31 | HEAD 32 | 33 | 34 | 35 | 36 | ossrh 37 | https://oss.sonatype.org/content/repositories/snapshots 38 | 39 | 40 | ossrh 41 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 42 | 43 | 44 | 45 | 46 | 3.6.0 47 | 48 | 49 | 50 | UTF-8 51 | 1.8 52 | ${java.version} 53 | ${java.version} 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-plugin-plugin 61 | 3.6.4 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-javadoc-plugin 66 | 3.1.1 67 | 68 | -Xdoclint:none 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-invoker-plugin 74 | 3.2.1 75 | 76 | false 77 | clean 78 | verify 79 | src/it/settings.xml 80 | ${project.build.directory}/it 81 | true 82 | true 83 | 84 | 85 | 86 | integration-tests-with-poms 87 | 88 | install 89 | run 90 | 91 | 92 | none 93 | 94 | 95 | 96 | 97 | 98 | org.codehaus.groovy 99 | groovy-all 100 | 2.4.5 101 | 102 | 103 | 104 | 105 | 106 | maven-release-plugin 107 | 2.5.3 108 | 109 | true 110 | false 111 | release 112 | deploy 113 | 114 | 115 | 116 | org.apache.maven.scm 117 | maven-scm-provider-gitexe 118 | 1.8.1 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | release 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-source-plugin 133 | 3.1.0 134 | 135 | 136 | attach-sources 137 | 138 | jar-no-fork 139 | 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-javadoc-plugin 146 | 3.1.1 147 | 148 | 149 | attach-javadocs 150 | 151 | jar 152 | 153 | 154 | 155 | 156 | 157 | org.apache.maven.plugins 158 | maven-gpg-plugin 159 | 1.6 160 | 161 | 162 | sign-artifacts 163 | verify 164 | 165 | sign 166 | 167 | 168 | 169 | 170 | 171 | org.sonatype.plugins 172 | nexus-staging-maven-plugin 173 | 1.6.8 174 | true 175 | 176 | ossrh 177 | https://oss.sonatype.org/ 178 | true 179 | 180 | 181 | 182 | de.jutzig 183 | github-release-plugin 184 | 1.6.0 185 | 186 | 187 | github-upload 188 | deploy 189 | 190 | release 191 | 192 | false 193 | 194 | v${project.version} 195 | v${project.version} 196 | ${project.build.directory}/${project.artifactId}-${project.version}.jar 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | org.kohsuke 209 | github-api 210 | 1.318 211 | 212 | 213 | 214 | org.apache.maven 215 | maven-plugin-api 216 | 3.6.2 217 | provided 218 | 219 | 220 | org.apache.maven.plugin-tools 221 | maven-plugin-annotations 222 | 3.6.0 223 | provided 224 | 225 | 226 | org.apache.maven 227 | maven-core 228 | 3.5.4 229 | provided 230 | 231 | 232 | org.codehaus.plexus 233 | plexus-utils 234 | 3.2.1 235 | 236 | 237 | 238 | org.junit.jupiter 239 | junit-jupiter 240 | 5.10.1 241 | test 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | Release: 2 | 3 | * see http://central.sonatype.org/pages/apache-maven.html 4 | * mvn versions:set -DnewVersion=1.2.3 5 | * mvn clean deploy -P release -------------------------------------------------------------------------------- /src/it/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | it-repo 7 | 8 | true 9 | 10 | 11 | 12 | local.central 13 | @localRepositoryUrl@ 14 | 15 | true 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 23 | 24 | local.central 25 | @localRepositoryUrl@ 26 | 27 | true 28 | 29 | 30 | true 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/it/test-multi-module/README.md: -------------------------------------------------------------------------------- 1 | # How this works 2 | 3 | * The `maven-invoker-plugin` uses it's own `settings.xml` which is merged with the one from `~/.m2/settings.xml`. 4 | * The credentials for deploying too Github are under the `~/.m2/settings.xml`, so as to not having to add them 5 | as sources. 6 | * The `repositoryId` and expected `serverId` in the `~/.m2/settings.xml` file are as follows 7 | ``` 8 | jutzig/github-release-plugin 9 | jutzig/github-release-plugin 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child1/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | de.jutzig 7 | test-parent 8 | @project.version@ 9 | 10 | child1 11 | 12 | 13 | 14 | 15 | src/test/resources 16 | true 17 | 18 | 19 | 20 | 21 | 22 | @project.groupId@ 23 | @project.artifactId@ 24 | @project.version@ 25 | 26 | 27 | Test artifact ${project.version}. 28 | ${project.version} 29 | ${project.version} 30 | 31 | true 32 | jutzig/github-release-plugin 33 | false 34 | 35 | 36 | 37 | ${project.build.directory} 38 | 39 | ${project.artifactId}*.jar 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | package 48 | 49 | release 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child1/src/main/java/de/jutzig/maven/test/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | public class HelloWorld 4 | { 5 | 6 | public static void main(String[] args) 7 | { 8 | System.out.println("Hello, world!"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child1/src/test/java/de/jutzig/maven/test/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | public class HelloWorldTest 10 | { 11 | 12 | @Test 13 | public void connectionTest() throws IOException 14 | { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | de.jutzig 7 | test-parent 8 | @project.version@ 9 | 10 | child2 11 | 12 | 13 | 14 | 15 | src/test/resources 16 | true 17 | 18 | 19 | 20 | 21 | 22 | @project.groupId@ 23 | @project.artifactId@ 24 | @project.version@ 25 | 26 | 27 | Test artifact ${project.version}. 28 | ${project.version} 29 | ${project.version} 30 | 31 | false 32 | jutzig/github-release-plugin 33 | false 34 | 35 | 36 | 37 | ${project.build.directory} 38 | 39 | ${project.artifactId}*.jar 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | package 48 | 49 | release 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child2/src/main/java/de/jutzig/maven/test/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | public class HelloWorld 4 | { 5 | 6 | public static void main(String[] args) 7 | { 8 | System.out.println("Hello, world!"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/it/test-multi-module/child2/src/test/java/de/jutzig/maven/test/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | public class HelloWorldTest 10 | { 11 | 12 | @Test 13 | public void connectionTest() throws IOException 14 | { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/it/test-multi-module/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals=deploy -Dmaven.deploy.skip=true -e -X 2 | -------------------------------------------------------------------------------- /src/it/test-multi-module/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | de.jutzig 6 | test-parent 7 | @project.version@ 8 | pom 9 | 10 | 11 | scm:git:git@github.com:jutzig/github-release-plugin.git 12 | scm:git:git@github.com:jutzig/github-release-plugin.git 13 | https://github.com/jutzig/github-release-plugin/tree/${project.scm.tag} 14 | master 15 | 16 | 17 | 18 | 1.8 19 | 1.8 20 | 21 | 22 | 23 | 24 | child1 25 | child2 26 | 27 | 28 | 29 | 30 | junit 31 | junit 32 | 4.11 33 | test 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/it/test-multi-module/verify.groovy: -------------------------------------------------------------------------------- 1 | // TODO: Add a proper check, but, basically, if the invoker build fails, that would be enough of a failure. 2 | return true 3 | -------------------------------------------------------------------------------- /src/it/test-release/README.md: -------------------------------------------------------------------------------- 1 | # How this works 2 | 3 | * The `maven-invoker-plugin` uses it's own `settings.xml` which is merged with the one from `~/.m2/settings.xml`. 4 | * The credentials for deploying too Github are under the `~/.m2/settings.xml`, so as to not having to add them 5 | as sources. 6 | * The `repositoryId` and expected `serverId` in the `~/.m2/settings.xml` file are as follows 7 | ``` 8 | jutzig/github-release-plugin 9 | jutzig/github-release-plugin 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /src/it/test-release/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals=deploy -Dmaven.deploy.skip=true -e -X 2 | -------------------------------------------------------------------------------- /src/it/test-release/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | de.jutzig 6 | test-project 7 | @project.version@ 8 | 9 | 10 | 11 | junit 12 | junit 13 | 4.11 14 | test 15 | 16 | 17 | 18 | 19 | scm:git:git@github.com:jutzig/github-release-plugin.git 20 | scm:git:git@github.com:jutzig/github-release-plugin.git 21 | https://github.com/jutzig/github-release-plugin/tree/${project.scm.tag} 22 | master 23 | 24 | 25 | 26 | 1.8 27 | 1.8 28 | 29 | 30 | 31 | 32 | 33 | src/test/resources 34 | true 35 | 36 | 37 | 38 | 39 | 40 | @project.groupId@ 41 | @project.artifactId@ 42 | @project.version@ 43 | 44 | 45 | Test artifact ${project.version}. 46 | ${project.version} 47 | ${project.version} 48 | 49 | true 50 | jutzig/github-release-plugin 51 | jutzig/github-release-plugin 52 | true 53 | 54 | 55 | 56 | ${project.build.directory} 57 | 58 | ${project.artifactId}*.jar 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | package 67 | 68 | release 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/it/test-release/src/main/java/de/jutzig/maven/test/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | public class HelloWorld 4 | { 5 | 6 | public static void main(String[] args) 7 | { 8 | System.out.println("Hello, world!"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/it/test-release/src/test/java/de/jutzig/maven/test/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.maven.test; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | public class HelloWorldTest 10 | { 11 | 12 | @Test 13 | public void connectionTest() throws IOException 14 | { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/it/test-release/verify.groovy: -------------------------------------------------------------------------------- 1 | // TODO: Add a proper check, but, basically, if the invoker build fails, that would be enough of a failure. 2 | return true 3 | -------------------------------------------------------------------------------- /src/main/java/de/jutzig/github/release/plugin/UploadMojo.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.github.release.plugin; 2 | 3 | /* 4 | * Copyright 2001-2005 The Apache Software Foundation. 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 | * http://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 | import java.io.File; 20 | import java.io.IOException; 21 | import java.text.MessageFormat; 22 | import java.util.List; 23 | import java.util.regex.Matcher; 24 | import java.util.regex.Pattern; 25 | 26 | import org.apache.commons.lang3.StringUtils; 27 | import org.apache.maven.model.FileSet; 28 | import org.apache.maven.model.Scm; 29 | import org.apache.maven.plugin.AbstractMojo; 30 | import org.apache.maven.plugin.MojoExecutionException; 31 | import org.apache.maven.plugins.annotations.Component; 32 | import org.apache.maven.plugins.annotations.LifecyclePhase; 33 | import org.apache.maven.plugins.annotations.Mojo; 34 | import org.apache.maven.plugins.annotations.Parameter; 35 | import org.apache.maven.project.MavenProject; 36 | import org.apache.maven.settings.Server; 37 | import org.apache.maven.settings.Settings; 38 | import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; 39 | import org.apache.maven.settings.crypto.SettingsDecrypter; 40 | import org.apache.maven.settings.crypto.SettingsDecryptionResult; 41 | import org.codehaus.plexus.PlexusConstants; 42 | import org.codehaus.plexus.PlexusContainer; 43 | import org.codehaus.plexus.component.annotations.Requirement; 44 | import org.codehaus.plexus.component.repository.exception.ComponentLookupException; 45 | import org.codehaus.plexus.context.Context; 46 | import org.codehaus.plexus.context.ContextException; 47 | import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable; 48 | import org.codehaus.plexus.util.FileUtils; 49 | import org.kohsuke.github.GHAsset; 50 | import org.kohsuke.github.GHRelease; 51 | import org.kohsuke.github.GHReleaseBuilder; 52 | import org.kohsuke.github.GHRepository; 53 | import org.kohsuke.github.GitHub; 54 | import org.kohsuke.github.GitHubBuilder; 55 | import org.kohsuke.github.PagedIterable; 56 | 57 | /** 58 | * Goal which attaches a file to a GitHub release 59 | */ 60 | @Mojo(name = "release", defaultPhase = LifecyclePhase.DEPLOY) 61 | public class UploadMojo extends AbstractMojo implements Contextualizable{ 62 | 63 | private static final String PUBLIC_GITUHB_API_ENDPOINT = "https://api.github.com"; 64 | /** 65 | * Server id for github access. 66 | */ 67 | @Parameter(defaultValue = "github") 68 | private String serverId; 69 | 70 | /** 71 | * The tag name this release is based on. 72 | */ 73 | @Parameter(defaultValue = "${project.version}") 74 | private String tag; 75 | 76 | /** 77 | * The name of the release 78 | */ 79 | @Parameter(property = "relase.name") 80 | private String releaseName; 81 | 82 | /** 83 | * The release description 84 | */ 85 | @Parameter 86 | private String description; 87 | 88 | /** 89 | * The commitish to use 90 | */ 91 | @Parameter(property = "github.commitish") 92 | private String commitish; 93 | 94 | /** 95 | * Whether or not the release should be draft 96 | */ 97 | @Parameter(property = "github.draft") 98 | private Boolean draft; 99 | 100 | /** 101 | * The github id of the project. By default initialized from the project scm connection 102 | */ 103 | @Parameter(property = "release.repositoryId", defaultValue = "${project.scm.connection}", required = true) 104 | private String repositoryId; 105 | 106 | /** 107 | * The Maven settings 108 | */ 109 | @Parameter(defaultValue = "${settings}", readonly = true, required = true) 110 | private Settings settings; 111 | 112 | /** 113 | * The file to upload to the release. Default is ${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging} (the main artifact) 114 | */ 115 | @Parameter(property = "release.artifact", defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}") 116 | private String artifact; 117 | 118 | /** 119 | * A specific fileSet rule to select files and directories for upload to the release. 120 | */ 121 | @Parameter 122 | private FileSet fileSet; 123 | 124 | /** 125 | * A list of fileSet rules to select files and directories for upload to the release. 126 | */ 127 | @Parameter 128 | private List fileSets; 129 | 130 | /** 131 | * Flag to indicate to overwrite the asset in the release if it already exists. Default is false 132 | */ 133 | @Parameter(defaultValue = "false") 134 | private Boolean overwriteArtifact; 135 | 136 | /** 137 | * Flag to indicate whether to remove releases, if they exist, before re-creating them. Default is false 138 | */ 139 | @Parameter(defaultValue = "false") 140 | private Boolean deleteRelease; 141 | 142 | @Requirement 143 | private PlexusContainer container; 144 | 145 | /** 146 | * If this is a prerelease. Will be set by default according to ${project.version} (see {@link #guessPreRelease(String)}. 147 | */ 148 | @Parameter 149 | private Boolean prerelease; 150 | 151 | /** 152 | * Fail plugin execution if release already exists. 153 | */ 154 | @Parameter(defaultValue = "false") 155 | private Boolean failOnExistingRelease; 156 | 157 | @Component 158 | private MavenProject project; 159 | 160 | public void execute() throws MojoExecutionException { 161 | if(releaseName==null) 162 | releaseName = tag; 163 | if(prerelease==null) 164 | prerelease = guessPreRelease(tag); 165 | repositoryId = computeRepositoryId(repositoryId); 166 | GHRelease release = null; 167 | try { 168 | GitHub gitHub = createGithub(serverId); 169 | GHRepository repository = gitHub.getRepository(repositoryId); 170 | release = findRelease(repository,releaseName); 171 | if (release != null) { 172 | String message = "Release " + releaseName + " already exists. Not creating"; 173 | 174 | if (failOnExistingRelease) { 175 | throw new MojoExecutionException(message); 176 | } 177 | 178 | if (deleteRelease) { 179 | getLog().info("Removing existing release " + release.getName() + "..."); 180 | 181 | release.delete(); 182 | 183 | getLog().info("Release " + release.getName() + " removed successfully."); 184 | release = null; 185 | } else { 186 | getLog().info(message); 187 | } 188 | } 189 | if (release == null) { 190 | getLog().info("Creating release "+releaseName); 191 | GHReleaseBuilder builder = repository.createRelease(tag); 192 | builder.generateReleaseNotes(true); 193 | if(description!=null) { 194 | builder.body(description); 195 | } 196 | if (commitish!=null) { 197 | builder.commitish(commitish); 198 | } 199 | if (draft!=null) { 200 | builder.draft(draft); 201 | } 202 | 203 | builder.prerelease(prerelease); 204 | builder.name(releaseName); 205 | release = builder.create(); 206 | } 207 | } catch (IOException e) { 208 | getLog().error(e); 209 | throw new MojoExecutionException("Failed to create release", e); 210 | } 211 | 212 | try { 213 | if(artifact != null && !artifact.trim().isEmpty()) { 214 | File asset = new File(artifact); 215 | if(asset.exists()) { 216 | uploadAsset(release, asset); 217 | } 218 | } 219 | 220 | if(fileSet != null) 221 | uploadAssets(release, fileSet); 222 | 223 | if(fileSets != null) 224 | for (FileSet set : fileSets) 225 | uploadAssets(release, set); 226 | 227 | } catch (IOException e) { 228 | 229 | getLog().error(e); 230 | throw new MojoExecutionException("Failed to upload assets", e); 231 | } 232 | } 233 | 234 | private void uploadAsset(GHRelease release, File asset) throws IOException { 235 | getLog().info("Processing asset "+asset.getPath()); 236 | 237 | List existingAssets = release.getAssets(); 238 | for ( GHAsset a : existingAssets ){ 239 | if (a.getName().equals( asset.getName() )){ 240 | if(overwriteArtifact) { 241 | getLog().info(" Deleting existing asset"); 242 | a.delete(); 243 | } 244 | else 245 | { 246 | getLog().warn("Asset "+asset.getName()+" already exists. Skipping"); 247 | return; 248 | } 249 | } 250 | } 251 | 252 | getLog().info(" Upload asset"); 253 | // for some reason this doesn't work currently 254 | release.uploadAsset(asset, "application/zip"); 255 | } 256 | 257 | private void uploadAssets(GHRelease release, FileSet fileset) throws IOException { 258 | List assets = FileUtils.getFiles( 259 | new File(fileset.getDirectory()), 260 | StringUtils.join(fileset.getIncludes(), ','), 261 | StringUtils.join(fileset.getExcludes(), ',') 262 | ); 263 | for (File asset : assets) 264 | uploadAsset(release, asset); 265 | } 266 | 267 | private GHRelease findRelease(GHRepository repository, String releaseNameToFind) throws IOException { 268 | PagedIterable releases = repository.listReleases(); 269 | for (GHRelease ghRelease : releases) { 270 | if (releaseNameToFind.equals(ghRelease.getName())) { 271 | return ghRelease; 272 | } 273 | } 274 | return null; 275 | } 276 | 277 | /** 278 | * @see SCM URL Format 279 | */ 280 | private static final Pattern REPOSITORY_PATTERN = Pattern.compile( 281 | "^(scm:git[:|])?" + //Maven prefix for git SCM 282 | "(https?://[\\w\\d.-]+/|git@[\\w\\d.-]+:)" + //GitHub prefix for HTTP/HTTPS/SSH/Subversion scheme 283 | "([^/]+/[^/\\.]+)" + //Repository ID 284 | "(\\.git)?" + //Optional suffix ".git" 285 | "(/.*)?$" //Optional child project path 286 | , Pattern.CASE_INSENSITIVE); 287 | 288 | public static String computeRepositoryId(String id) { 289 | Matcher matcher = REPOSITORY_PATTERN.matcher(id); 290 | if (matcher.matches()) { 291 | return matcher.group(3); 292 | } else { 293 | return id; 294 | } 295 | } 296 | 297 | static String computeGithubApiEndpoint(Scm scm) { 298 | if (scm == null || StringUtils.isEmpty(scm.getConnection())) { 299 | return PUBLIC_GITUHB_API_ENDPOINT; 300 | } 301 | Matcher matcher = REPOSITORY_PATTERN.matcher(scm.getConnection()); 302 | if (!matcher.matches()) { 303 | return PUBLIC_GITUHB_API_ENDPOINT; 304 | } 305 | String githubApiEndpoint = matcher.group(2); 306 | if (githubApiEndpoint.contains("github.com")) { 307 | return PUBLIC_GITUHB_API_ENDPOINT; 308 | } 309 | 310 | if (githubApiEndpoint.startsWith("git@")) { 311 | // According to the regex pattern above, the matched group would be in a form of git@hostname: 312 | githubApiEndpoint = githubApiEndpoint.substring(4, githubApiEndpoint.length() - 1); 313 | } 314 | 315 | githubApiEndpoint = StringUtils.removeEnd(githubApiEndpoint, "/"); 316 | if (!githubApiEndpoint.startsWith("http")) { 317 | githubApiEndpoint = "https://" + githubApiEndpoint; 318 | } 319 | // See https://docs.github.com/en/enterprise-server@3.10/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#schema 320 | return githubApiEndpoint + "/api/v3"; 321 | } 322 | 323 | public GitHub createGithub(String serverId) throws MojoExecutionException, IOException { 324 | String usernameProperty = System.getProperty("username"); 325 | String passwordProperty = System.getProperty("password"); 326 | String githubApiEndpoint = computeGithubApiEndpoint(project.getScm()); 327 | GitHubBuilder gitHubBuilder = new GitHubBuilder().withEndpoint(githubApiEndpoint); 328 | if(usernameProperty!=null && passwordProperty!=null) 329 | { 330 | getLog().debug("Using server credentials from system properties 'username' and 'password'"); 331 | return gitHubBuilder.withPassword(usernameProperty, passwordProperty).build(); 332 | } 333 | 334 | Server server = getServer(settings, serverId); 335 | if (server == null) 336 | throw new MojoExecutionException(MessageFormat.format("Server ''{0}'' not found in settings", serverId)); 337 | 338 | getLog().debug(MessageFormat.format("Using ''{0}'' server credentials", serverId)); 339 | 340 | try { 341 | SettingsDecrypter settingsDecrypter = container.lookup(SettingsDecrypter.class); 342 | SettingsDecryptionResult result = settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server)); 343 | server = result.getServer(); 344 | } catch (ComponentLookupException cle) { 345 | throw new MojoExecutionException("Unable to lookup SettingsDecrypter: " + cle.getMessage(), cle); 346 | } 347 | 348 | String serverUsername = System.getProperty("github.username", server.getUsername()); 349 | String serverPassword = System.getProperty("github.password", server.getPassword()); 350 | String serverAccessToken = System.getProperty("github.apitoken", server.getPrivateKey()); 351 | if (StringUtils.isNotEmpty(serverUsername) && StringUtils.isNotEmpty(serverPassword)) 352 | return gitHubBuilder.withPassword(serverUsername, serverPassword).build(); 353 | else if (StringUtils.isNotEmpty(serverAccessToken)) 354 | return gitHubBuilder.withOAuthToken(serverAccessToken).build(); 355 | else 356 | throw new MojoExecutionException("Configuration for server " + serverId + " has no login credentials"); 357 | } 358 | 359 | /** 360 | * Get server with given id 361 | * 362 | * @param settings 363 | * @param serverId 364 | * must be non-null and non-empty 365 | * @return server or null if none matching 366 | */ 367 | protected Server getServer(final Settings settings, final String serverId) { 368 | if (settings == null) 369 | return null; 370 | List servers = settings.getServers(); 371 | if (servers == null || servers.isEmpty()) 372 | return null; 373 | 374 | for (Server server : servers) 375 | if (serverId.equals(server.getId())) 376 | return server; 377 | return null; 378 | } 379 | 380 | public void contextualize(Context context) throws ContextException { 381 | container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY ); 382 | } 383 | 384 | /** 385 | * Guess if a version defined in POM should be considered as {@link #prerelease}. 386 | */ 387 | static boolean guessPreRelease(String version) { 388 | boolean preRelease = version.endsWith("-SNAPSHOT") 389 | || StringUtils.containsIgnoreCase(version, "-alpha") 390 | || StringUtils.containsIgnoreCase(version, "-beta") 391 | || StringUtils.containsIgnoreCase(version, "-RC") 392 | || StringUtils.containsIgnoreCase(version, ".RC") 393 | || StringUtils.containsIgnoreCase(version, ".M") 394 | || StringUtils.containsIgnoreCase(version, ".BUILD_SNAPSHOT"); 395 | return preRelease; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/test/java/de/jutzig/github/release/plugin/UploadMojoTest.java: -------------------------------------------------------------------------------- 1 | package de.jutzig.github.release.plugin; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import org.apache.maven.model.Scm; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.*; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class UploadMojoTest { 13 | @ParameterizedTest(name = "{0} should resolve to {1} repository id") 14 | @CsvSource({ 15 | // Public 16 | "scm:git:https://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 17 | "scm:git|https://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 18 | "https://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 19 | 20 | "scm:git:http://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 21 | "scm:git|http://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 22 | "http://github.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 23 | 24 | "scm:git:git@github.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 25 | "scm:git|git@github.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 26 | "git@github.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 27 | 28 | "scm:git:https://github.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 29 | "scm:git|https://github.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 30 | "https://github.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 31 | 32 | "scm:git:http://github.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin", 33 | "scm:git|http://github.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin", 34 | "http://github.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin", 35 | 36 | // Enterprise 37 | "scm:git:https://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 38 | "scm:git|https://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 39 | "https://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 40 | 41 | "scm:git:http://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 42 | "scm:git|http://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 43 | "http://github.acme.com/jutzig/github-release-plugin.git, jutzig/github-release-plugin", 44 | 45 | "scm:git:git@github.acme.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 46 | "scm:git|git@github.acme.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 47 | "git@github.acme.com:jutzig/github-release-plugin.git, jutzig/github-release-plugin", 48 | 49 | "scm:git:https://github.acme.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 50 | "scm:git|https://github.acme.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 51 | "https://github.acme.com/jutzig/github-release-plugin, jutzig/github-release-plugin", 52 | 53 | "scm:git:http://github.acme.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin", 54 | "scm:git|http://github.acme.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin", 55 | "http://github.acme.com/jutzig/github-release-plugin.git/child, jutzig/github-release-plugin" 56 | }) 57 | void testComputeRepositoryId(String scmString, String expectedRepositoryId) { 58 | assertEquals(expectedRepositoryId, UploadMojo.computeRepositoryId(scmString)); 59 | } 60 | 61 | @ParameterizedTest(name = "{0} should resolve to {1} endpoint") 62 | @MethodSource("scmFixture") 63 | void testGithubEndpoint(Scm scm, String expectedEndpoint) { 64 | assertEquals(expectedEndpoint, UploadMojo.computeGithubApiEndpoint(scm)); 65 | } 66 | 67 | @Test 68 | void testGuessPreRelease() { 69 | assertTrue(UploadMojo.guessPreRelease("1.0-SNAPSHOT")); 70 | assertTrue(UploadMojo.guessPreRelease("1.0-alpha")); 71 | assertTrue(UploadMojo.guessPreRelease("1.0-alpha-1")); 72 | assertTrue(UploadMojo.guessPreRelease("1.0-beta")); 73 | assertTrue(UploadMojo.guessPreRelease("1.0-beta-1")); 74 | assertTrue(UploadMojo.guessPreRelease("1.0-RC")); 75 | assertTrue(UploadMojo.guessPreRelease("1.0-RC1")); 76 | assertTrue(UploadMojo.guessPreRelease("1.0-rc1")); 77 | assertTrue(UploadMojo.guessPreRelease("1.0-rc-1")); 78 | 79 | assertFalse(UploadMojo.guessPreRelease("1")); 80 | assertFalse(UploadMojo.guessPreRelease("1.0")); 81 | } 82 | 83 | private static Stream scmFixture() { 84 | return Stream.of( 85 | // Public GitHub 86 | Arguments.of(scmWithConnectionString("scm:git:https://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 87 | Arguments.of(scmWithConnectionString("scm:git|https://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 88 | Arguments.of(scmWithConnectionString("https://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 89 | 90 | Arguments.of(scmWithConnectionString("scm:git:http://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 91 | Arguments.of(scmWithConnectionString("scm:git|http://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 92 | Arguments.of(scmWithConnectionString("http://github.com/jutzig/github-release-plugin.git"), "https://api.github.com"), 93 | 94 | Arguments.of(scmWithConnectionString("scm:git:git@github.com:jutzig/github-release-plugin.git"), "https://api.github.com"), 95 | Arguments.of(scmWithConnectionString("scm:git|git@github.com:jutzig/github-release-plugin.git"), "https://api.github.com"), 96 | Arguments.of(scmWithConnectionString("git@github.com:jutzig/github-release-plugin.git"), "https://api.github.com"), 97 | 98 | Arguments.of(scmWithConnectionString("scm:git:https://github.com/jutzig/github-release-plugin"), "https://api.github.com"), 99 | Arguments.of(scmWithConnectionString("scm:git|https://github.com/jutzig/github-release-plugin"), "https://api.github.com"), 100 | Arguments.of(scmWithConnectionString("https://github.com/jutzig/github-release-plugin"), "https://api.github.com"), 101 | 102 | Arguments.of(scmWithConnectionString("scm:git:http://github.com/jutzig/github-release-plugin.git/child"), "https://api.github.com"), 103 | Arguments.of(scmWithConnectionString("scm:git|http://github.com/jutzig/github-release-plugin.git/child"), "https://api.github.com"), 104 | Arguments.of(scmWithConnectionString("http://github.com/jutzig/github-release-plugin.git/child"), "https://api.github.com"), 105 | 106 | // GitHub Enterprise 107 | Arguments.of(scmWithConnectionString("scm:git:https://github.acme.com/jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 108 | Arguments.of(scmWithConnectionString("scm:git|https://github.acme.com/jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 109 | Arguments.of(scmWithConnectionString("https://github.acme.com/jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 110 | 111 | Arguments.of(scmWithConnectionString("scm:git:http://github.acme.com/jutzig/github-release-plugin.git"), "http://github.acme.com/api/v3"), 112 | Arguments.of(scmWithConnectionString("scm:git|http://github.acme.com/jutzig/github-release-plugin.git"), "http://github.acme.com/api/v3"), 113 | Arguments.of(scmWithConnectionString("http://github.acme.com/jutzig/github-release-plugin.git"), "http://github.acme.com/api/v3"), 114 | 115 | Arguments.of(scmWithConnectionString("scm:git:git@github.acme.com:jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 116 | Arguments.of(scmWithConnectionString("scm:git|git@github.acme.com:jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 117 | Arguments.of(scmWithConnectionString("git@github.acme.com:jutzig/github-release-plugin.git"), "https://github.acme.com/api/v3"), 118 | 119 | Arguments.of(scmWithConnectionString("scm:git:https://github.acme.com/jutzig/github-release-plugin"), "https://github.acme.com/api/v3"), 120 | Arguments.of(scmWithConnectionString("scm:git|https://github.acme.com/jutzig/github-release-plugin"), "https://github.acme.com/api/v3"), 121 | Arguments.of(scmWithConnectionString("https://github.acme.com/jutzig/github-release-plugin"), "https://github.acme.com/api/v3"), 122 | 123 | Arguments.of(scmWithConnectionString("scm:git:http://github.acme.com/jutzig/github-release-plugin.git/child"), "http://github.acme.com/api/v3"), 124 | Arguments.of(scmWithConnectionString("scm:git|http://github.acme.com/jutzig/github-release-plugin.git/child"), "http://github.acme.com/api/v3"), 125 | Arguments.of(scmWithConnectionString("http://github.acme.com/jutzig/github-release-plugin.git/child"), "http://github.acme.com/api/v3"), 126 | 127 | // Fallback to public 128 | Arguments.of(null, "https://api.github.com") 129 | ); 130 | } 131 | 132 | private static Scm scmWithConnectionString(String connection) { 133 | Scm scm = new Scm(); 134 | scm.setConnection(connection); 135 | return scm; 136 | } 137 | } 138 | --------------------------------------------------------------------------------