diff --git a/README.md b/README.md index dc7c254..ac60640 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ To use the plugin you need to configure it in your `pom.xml` like so Description of your release 1.0 Final ${project.version} - + @@ -48,4 +48,27 @@ These credentials can be overridden by setting `username` and `password` as syst Thanks to a contribution from rowanseymour you can also use your API token by adding it as `` to your server definition in the `settings.xml`. +## GitHub Enterprise users + +It is also possible to upload to GitHub Enterprise. For that, two additional options are available +in the configuration: `githubHostname`, `githubApiUrlPrefix` and `githubApiUploadUrlPrefix`. They can be used like this: + +``` + + de.jutzig + github-release-plugin + 1.1.1 + + Description of your release + 1.0 Final + ${project.version} + + github-enterprise.domain + http://github-enterprise.domain/api/v3 + https://github-enterprise.domain/api/v3 + ... + + +``` + The plugin is available on Maven central diff --git a/pom.xml b/pom.xml index 819898f..cfae67b 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,14 @@ + + org.apache.maven.plugins + maven-plugin-plugin + 3.3 + + github-release + + @@ -154,20 +162,32 @@ org.kohsuke github-api - 1.49 + 1.75-SNAPSHOT org.apache.maven maven-plugin-api - 3.1.0 + 3.3.9 provided org.apache.maven maven-core - 3.1.0 + 3.3.9 provided + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.3 + provided + + + org.apache.maven + maven-compat + 3.3.9 + test + junit @@ -175,5 +195,19 @@ 4.11 test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + test + + + + commons-io + commons-io + 2.4 + test + diff --git a/src/main/java/de/jutzig/github/release/plugin/UploadMojo.java b/src/main/java/de/jutzig/github/release/plugin/UploadMojo.java index 2c2652d..55f59d1 100644 --- a/src/main/java/de/jutzig/github/release/plugin/UploadMojo.java +++ b/src/main/java/de/jutzig/github/release/plugin/UploadMojo.java @@ -29,6 +29,10 @@ import org.apache.maven.model.FileSet; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; @@ -50,100 +54,133 @@ /** * Goal which attaches a file to a GitHub release - * - * @goal release - * - * @phase deploy */ +@Mojo( name="release", defaultPhase = LifecyclePhase.DEPLOY) public class UploadMojo extends AbstractMojo implements Contextualizable{ + private final static String DEFAULT_GITHUBHOSTNAME = "github.com"; + private final static String DEFAULT_GITAPIURLPREFIX = "https://api.github.com"; + /** + * + * //@parameter default-value="github" expression="github" * Server id for github access. - * - * @parameter default-value="github" expression="github" */ - private String serverId; + @Parameter( defaultValue = "github") + private String serverId; /** * The tag name this release is based on. - * - * @parameter expression="${project.version}" + * + * //@parameter expression="${project.version}" */ + @Parameter( property = "project.version" ) private String tag; /** * The name of the release - * - * @parameter expression="${release.name}" + * + * //@parameter expression="${release.name}" */ - private String releaseName; + @Parameter( property = "release.name" ) + private String releaseName; /** * The release description - * - * @parameter expression="${project.description}" + * + * //@parameter expression="${project.description}" */ + @Parameter( property = "project.description" ) private String description; /** * The github id of the project. By default initialized from the project scm connection - * - * @parameter default-value="${project.scm.connection}" expression="${release.repositoryId}" + * + * //@parameter default-value="${project.scm.connection}" expression="${release.repositoryId}" * @required */ + @Parameter( defaultValue = "${project.scm.connection}", property = "release.repositoryId") private String repositoryId; /** * The Maven settings * - * @parameter expression="${settings} + * //@parameter expression="${settings} */ + @Parameter( defaultValue = "${settings}", readonly = true) private Settings settings; /** * The Maven session * - * @parameter expression="${session}" + * //@parameter expression="${session}" */ + @Parameter( defaultValue = "${session}", readonly = true) private MavenSession session; /** * The file to upload to the release. Default is ${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging} (the main artifact) * - * @parameter default-value="${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}" expression="${release.artifact}" + * //@parameter default-value="${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}" expression="${release.artifact}" */ + @Parameter( defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}", + property = "release.artifact") private String artifact; /** * A specific fileSet rule to select files and directories for upload to the release. * - * @parameter + * //@parameter */ private FileSet fileSet; /** * A list of fileSet rules to select files and directories for upload to the release. * - * @parameter + * //@parameter */ private List fileSets; /** * Flag to indicate to overwrite the asset in the release if it already exists. Default is false * - * @parameter default-value=false + * //@parameter default-value=false */ + @Parameter( defaultValue = "false" ) private Boolean overwriteArtifact; + /** + * Github (Enterprise) hostname. Default is 'github.com' + * + * ////@parameter default-value="github.com" + */ + @Parameter( defaultValue = DEFAULT_GITHUBHOSTNAME ) + private String githubHostname; + + /** + * Github (Enterprise) API URL (prefix). Default is 'https://api.github.com' + */ + @Parameter( defaultValue = DEFAULT_GITAPIURLPREFIX ) + private String githubApiUrlPrefix; + + /** + * URL prefix to upload the release to Github. Default is 'https://uploads.github.com/' + * + * ////@parameter default-value="https://uploads.github.com/" + */ + @Parameter( defaultValue = "https://uploads.github.com/") + private String githubApiUploadUrlPrefix; + @Requirement private PlexusContainer container; /** * If this is a prerelease. By default it will use true if the tag ends in -SNAPSHOT * - * @parameter - * + * //@parameter + * */ + @Parameter private Boolean prerelease; public void execute() throws MojoExecutionException { @@ -188,7 +225,7 @@ public void execute() throws MojoExecutionException { uploadAssets(release, set); } catch (IOException e) { - + getLog().error(e); throw new MojoExecutionException("Failed to upload assets", e); } @@ -197,14 +234,12 @@ public void execute() throws MojoExecutionException { private void uploadAsset(GHRelease release, File asset) throws IOException { getLog().info("Processing asset "+asset.getPath()); - URL url = new URL(MessageFormat.format("https://uploads.github.com/repos/{0}/releases/{1}/assets?name={2}",repositoryId,Long.toString(release.getId()),asset.getName())); - List existingAssets = release.getAssets(); for ( GHAsset a : existingAssets ){ if (a.getName().equals( asset.getName() )){ if(overwriteArtifact) { getLog().info(" Deleting existing asset"); - a.delete(); + a.delete(); } else { @@ -216,7 +251,7 @@ private void uploadAsset(GHRelease release, File asset) throws IOException { getLog().info(" Upload asset"); // for some reason this doesn't work currently - release.uploadAsset(asset, "application/zip"); + release.uploadAsset(asset, "application/zip", ensureNoTrailingSlash(githubApiUploadUrlPrefix)); } private void uploadAssets(GHRelease release, FileSet fileset) throws IOException { @@ -239,17 +274,18 @@ private GHRelease findRelease(GHRepository repository, String releaseName2) thro return null; } - /** - * @see SCM URL Format - */ - private static final Pattern REPOSITORY_PATTERN = Pattern.compile( + public String computeRepositoryId(String id) { + + String githubHostnameForRegexp = githubHostname.replace(".", "\\."); + + final Pattern REPOSITORY_PATTERN = Pattern.compile( "^(scm:git[:|])?" + //Maven prefix for git SCM - "(https?://github\\.com/|git@github\\.com:)" + //GitHub prefix for HTTP/HTTPS/SSH/Subversion scheme + "(https?://" + githubHostnameForRegexp + "/|git@" + githubHostnameForRegexp + ":)" + //GitHub prefix for HTTP/HTTPS/SSH/Subversion scheme "([^/]+/[^/]*?)" + //Repository ID "(\\.git)?$" //Optional suffix ".git" - , Pattern.CASE_INSENSITIVE); + , Pattern.CASE_INSENSITIVE); + getLog().debug("using following repository pattern: " + REPOSITORY_PATTERN.toString()); - public static String computeRepositoryId(String id) { Matcher matcher = REPOSITORY_PATTERN.matcher(id); if (matcher.matches()) { return matcher.group(3); @@ -263,10 +299,10 @@ public GitHub createGithub(String serverId) throws MojoExecutionException, IOExc String passwordProperty = System.getProperty("password"); if(usernameProperty!=null && passwordProperty!=null) { - getLog().debug("Using server credentials from system properties 'username' and 'password'"); + getLog().debug("Using server credentials from system properties 'username' and 'password'"); return GitHub.connectUsingPassword(usernameProperty, passwordProperty); } - + Server server = getServer(settings, serverId); if (server == null) throw new MojoExecutionException(MessageFormat.format("Server ''{0}'' not found in settings", serverId)); @@ -285,16 +321,18 @@ public GitHub createGithub(String serverId) throws MojoExecutionException, IOExc String serverPassword = server.getPassword(); String serverAccessToken = server.getPrivateKey(); if (StringUtils.isNotEmpty(serverUsername) && StringUtils.isNotEmpty(serverPassword)) - return GitHub.connectUsingPassword(serverUsername, serverPassword); + return DEFAULT_GITHUBHOSTNAME.equals(githubHostname) ? GitHub.connectUsingPassword(serverUsername, serverPassword) + : GitHub.connectToEnterprise(githubApiUrlPrefix, serverUsername, serverPassword); else if (StringUtils.isNotEmpty(serverAccessToken)) - return GitHub.connectUsingOAuth(serverAccessToken); + return DEFAULT_GITHUBHOSTNAME.equals(githubHostname) ? GitHub.connectUsingOAuth(serverAccessToken) + : GitHub.connectToEnterprise(githubApiUrlPrefix, serverAccessToken); else throw new MojoExecutionException("Configuration for server " + serverId + " has no login credentials"); } /** * Get server with given id - * + * * @param settings * @param serverId * must be non-null and non-empty @@ -316,4 +354,17 @@ protected Server getServer(final Settings settings, final String serverId) { public void contextualize(Context context) throws ContextException { container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY ); } + + /** + * @param str + * @return trimmed str minus the trailing '/', if any + */ + String ensureNoTrailingSlash(String str) { + if(str == null || str.isEmpty()) { + return str; + } + String strTrimmed = str.trim(); + return strTrimmed.endsWith("/") ? strTrimmed.substring(0, strTrimmed.length() - 1) : strTrimmed; + } + } diff --git a/src/main/resources/META-INF/plexus/components.xml b/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 0000000..8bad9f0 --- /dev/null +++ b/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,33 @@ + + + + + org.apache.maven.artifact.handler.ArtifactHandler + github-release + org.apache.maven.artifact.handler.DefaultArtifactHandler + + jar + jar + jar + java + + + + + org.apache.maven.lifecycle.mapping.LifecycleMapping + github-release + org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping + + + de.jutzig:github-release-plugin:initialize + de.jutzig:github-release-plugin:validate-project + org.apache.maven.plugins:maven-resources-plugin:resources + de.jutzig:github-release-plugin:validate-application + de.jutzig:github-release-plugin:package + org.apache.maven.plugins:maven-install-plugin:install + org.apache.maven.plugins:maven-deploy-plugin:deploy + + + + + diff --git a/src/test/java/de/jutzig/github/release/plugin/UploadMojoTest.java b/src/test/java/de/jutzig/github/release/plugin/UploadMojoTest.java index e2c2e5b..f5da71f 100644 --- a/src/test/java/de/jutzig/github/release/plugin/UploadMojoTest.java +++ b/src/test/java/de/jutzig/github/release/plugin/UploadMojoTest.java @@ -2,42 +2,131 @@ import org.junit.Before; import org.junit.Test; +import org.junit.Rule; +import static org.junit.Assert.*; import java.util.HashMap; import java.util.Map; +import java.io.File; -import static org.junit.Assert.assertEquals; +import org.apache.maven.plugin.testing.MojoRule; +import org.apache.maven.plugin.testing.WithoutMojo; +import org.apache.maven.plugin.testing.resources.TestResources; public class UploadMojoTest { - private Map computeRepositoryIdData; + @Rule + public MojoRule rule = new MojoRule() { + @Override + protected void before() throws Throwable { } - @Before + @Override + protected void after() { } + }; + + @Rule + public TestResources resources = new TestResources(); + + private Map computeRepositoryIdData_github; + private Map computeRepositoryIdData_github_enterprise; + private Map ensureNoTrailingSlash; + + @Before public void setUp() throws Exception { - computeRepositoryIdData = new HashMap(); + computeRepositoryIdData_github = new HashMap(); + + computeRepositoryIdData_github.put("scm:git:https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("scm:git|https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + + computeRepositoryIdData_github.put("scm:git:http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("scm:git|http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + + computeRepositoryIdData_github.put("scm:git:git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("scm:git|git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + + computeRepositoryIdData_github.put("scm:git:https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("scm:git|https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + computeRepositoryIdData_github.put("https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git:https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git|https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("https://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise = new HashMap(); - computeRepositoryIdData.put("scm:git:http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git|http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("http://github.com/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git:https://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git|https://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("https://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git:git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git|git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("git@github.com:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git:http://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git|http://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("http://some-github-enterprise-server.domain/jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git:https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("scm:git|https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); - computeRepositoryIdData.put("https://github.com/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git:git@some-github-enterprise-server.domain:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git|git@some-github-enterprise-server.domain:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("git@some-github-enterprise-server.domain:jutzig/github-release-plugin.git", "jutzig/github-release-plugin"); + + computeRepositoryIdData_github_enterprise.put("scm:git:https://some-github-enterprise-server.domain/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("scm:git|https://some-github-enterprise-server.domain/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + computeRepositoryIdData_github_enterprise.put("https://some-github-enterprise-server.domain/jutzig/github-release-plugin", "jutzig/github-release-plugin"); + + ensureNoTrailingSlash = new HashMap(); + + ensureNoTrailingSlash.put(null, null); + ensureNoTrailingSlash.put("", ""); + ensureNoTrailingSlash.put("no trailing slash", "no trailing slash"); + ensureNoTrailingSlash.put("trailing slash/", "trailing slash"); + ensureNoTrailingSlash.put("no trailing slash trimmed ", "no trailing slash trimmed"); + ensureNoTrailingSlash.put("trimmed trailing slash/ ", "trimmed trailing slash"); } @Test - public void testComputeRepositoryId() throws Exception { - for (String source : computeRepositoryIdData.keySet()) { - String expected = computeRepositoryIdData.get(source); - assertEquals(source, expected, UploadMojo.computeRepositoryId(source)); + public void testComputeRepositoryId_github_com() throws Exception { + + File testProjectDir = this.resources.getBasedir( "github.com" ); + assertNotNull( testProjectDir ); + assertTrue( testProjectDir.exists() ); + + UploadMojo mojo = (UploadMojo) rule.lookupConfiguredMojo( testProjectDir , "release"); + assertNotNull(mojo); + + for (String source : computeRepositoryIdData_github.keySet()) { + String expected = computeRepositoryIdData_github.get(source); + assertEquals(source, expected, mojo.computeRepositoryId(source)); } } + + @Test + public void testComputeRepositoryId_github_enterprise() throws Exception { + + File testProjectDir = this.resources.getBasedir( "github.enterprise" ); + File pom = new File(testProjectDir, "pom.xml"); + assertNotNull( pom ); + assertTrue( pom.exists() ); + + UploadMojo mojo = (UploadMojo) rule.lookupMojo("release", pom); + assertNotNull(mojo); + + for (String source : computeRepositoryIdData_github_enterprise.keySet()) { + String expected = computeRepositoryIdData_github_enterprise.get(source); + assertEquals(source, expected, mojo.computeRepositoryId(source)); + } + } + + @Test + public void testEnsureNoTrailingSlash() throws Exception { + + File testProjectDir = this.resources.getBasedir( "github.com" ); + assertNotNull( testProjectDir ); + assertTrue( testProjectDir.exists() ); + + UploadMojo mojo = (UploadMojo) rule.lookupConfiguredMojo( testProjectDir , "release"); + assertNotNull(mojo); + + for (String str : ensureNoTrailingSlash.keySet()) { + String expected = ensureNoTrailingSlash.get(str); + assertEquals(str, expected, mojo.ensureNoTrailingSlash(str)); + } + + } + } diff --git a/src/test/projects/github.com/pom.xml b/src/test/projects/github.com/pom.xml new file mode 100644 index 0000000..ba304cf --- /dev/null +++ b/src/test/projects/github.com/pom.xml @@ -0,0 +1,41 @@ + + 4.0.0 + github-release-plugin.unit + github-release-plugin + jar + 1.0-SNAPSHOT + test-project + testing POM for github.com tests + + + https://github.com/jutzig/github-release-plugin + scm:git:https://github.com/jutzig/github-release-plugin.git + scm:git:https://github.com/jutzig/github-release-plugin.git + HEAD + + + + + junit + junit + 4.11 + test + + + + + + + github-release-plugin + + github-ncia + Description of your release + 1.0 Final + ${project.version} + + + + + + + diff --git a/src/test/projects/github.enterprise/pom.xml b/src/test/projects/github.enterprise/pom.xml new file mode 100644 index 0000000..c488603 --- /dev/null +++ b/src/test/projects/github.enterprise/pom.xml @@ -0,0 +1,41 @@ + + 4.0.0 + github-release-plugin.unit + github-release-plugin + jar + 1.0-SNAPSHOT + test-project + testing POM for github enterprise tests + + + https://github.com/jutzig/github-release-plugin + scm:git:https://github.com/jutzig/github-release-plugin.git + scm:git:https://github.com/jutzig/github-release-plugin.git + HEAD + + + + + junit + junit + 4.11 + test + + + + + + + github-release-plugin + + github-ncia + Description of your release + v${project.version} + ${project.version} + some-github-enterprise-server.domain + + + + + +