Gradle Plugin的开发及发布

1,542 阅读16分钟

前言

经过前面部署Jenkins自动化打包任务,以及桌面端APK工具的开发,基本已经把国内项目组所面临的问题解决了。但是呢,Jenkins这一套着实是繁琐,而且对海外项目组支持也不友好。所以我又决定开发一个gradle插件来作为一个轻量级的打包发布方案,该方案可以同时兼顾公司海内外的所有项目。整体目标大概就是如下:

  1. 依赖assembleXxxRelease等Task构建新任务;
  2. 将打包生成的APK文件上传到FTP服务器;
  3. 根据服务器上APK文件的地址生成二维码;
  4. 发送通知消息到钉钉群组;

该插件现已开源,GitHub地址在:ApkPublisherGradlePlugin

插件的开发

gradle插件的开发又涉及多种方式,我们这里在不同的阶段分别使用不同方式进行演示。 首先使用AS新建GradleSample5项目。

学习阶段(build.gradle脚本)

在这个阶段我们只需学习如何创建任务(Task),并且了解如何依赖任务即可。

首先创建任务“activeTask1”,我们需要在app模块下的build.gradle文件中编写如下代码:

tasks.register("activeTask1") {
    doLast {
        println(">>>>> Greeting from activeTask1")
    }
}

点击 Sync Now 后就可以在右侧gradle面板中看到我们新创建的任务了。 Snipaste_2022-11-22_11-41-50.png 双击该任务即可执行,并打印出日志信息:

11:42:39: Executing 'activeTask1'...


> Task :app:activeTask1
>>>>> Greeting from activeTask1

BUILD SUCCESSFUL in 284ms
1 actionable task: 1 executed
11:42:40: Execution finished 'activeTask1'.

接下来,我们需要创建一个activeTask2,该任务需要依赖activeTask1,也就是说执行task2的时候要先执行task1,那么关键就是dependsOn()

tasks.register("activeTask2") {

    dependsOn("activeTask1")

    doLast {
        println(">>>>> Greeting from activeTask2")
    }
}

Snipaste_2022-11-22_11-45-44.png

那么这时候执行activeTask2的话,输出信息则如下所示:

11:46:41: Executing 'activeTask2'...


> Task :app:activeTask1
>>>>> Greeting from activeTask1

> Task :app:activeTask2
>>>>> Greeting from activeTask2

BUILD SUCCESSFUL in 180ms
2 actionable tasks: 2 executed
11:46:41: Execution finished 'activeTask2'.

所以这就是我们后续依赖assemble相关任务所需要学习的一点前置内容了。

开发阶段(buildSrc)

buildSrc是Gradle项目的一个特殊目录,我们先看下官网给的整体介绍。

Complex build logic is usually a good candidate for being encapsulated either as custom task or binary plugin. Custom task and plugin implementations should not live in the build script. It is very convenient to use buildSrc for that purpose as long as the code does not need to be shared among multiple, independent projects. The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code. buildSrc uses the same source code conventions applicable to Java and Groovy projects. It also provides direct access to the Gradle API. Additional dependencies can be declared in a dedicated build.gradle under buildSrc.

整体大致意思呢就是,gradle发现buildSrc目录后,会自动编译和测试其中的代码,会优先于其他的脚本。这对我们开发插件来说就非常的便利了,如果我们开发一个插件必须先发布然后再从项目中依赖然后再执行的话,效率就大打折扣了。

我们在项目的根目录下创建一个buildSrc的目录,然后创建一个build.gradle的文件放入该目录下,build.gradle文件内容如下所示: Snipaste_2022-11-22_17-57-37.png

点击Sync Now后可以看到,buildSrc目录样式变了,说明AS成功识别了该目录。 Snipaste_2022-11-22_17-59-25.png

接下来我们来开发示例插件了,这里我们使用Java而不使用Groovy。因为其一我对Groovy不熟悉,其二Groovy太灵活了,我虽然能写出来相关的代码,可以我无法查看到相关的类提示等,这就导致我好奇某个类、某个变量到底是怎么来的。所以我选择了Java(Kotlin当然也不错),这样我可以明确知道相关类型,也方便查看源码。

Plugin

buildSrc目录建好后我们创建src目录,然后建立插件类MyPlugin并实现Plugin接口如下: Snipaste_2022-11-22_21-19-33.png 如果这个时候你跟我出现一样的情况,例如上面的红色报错,而且方法中连一个简单的String类都无法导入,那么说明AS的JDK配置出现了问题。请先查看下文疑难杂症篇章,解决后再进行插件的开发。

我们实现apply方法后在其中打印相关信息:

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {

    @java.lang.Override
    public void apply(Project project) {
        System.out.println(">>>>> Greeting from buildSrc MyPlugin");
    }

}

然后在buildSrc的build.gradle中声明该插件:

plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        pluginDemo {
            id = "my-plugin"
            implementationClass = 'com.vsloong.myplugin.MyPlugin'
        }
    }
}

在app模块下的build.gradle中添加该插件的依赖:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'

    //添加插件的依赖
    id 'my-plugin'
}

Sync后即可看到相关打印信息:

...... Task :buildSrc:validatePlugins UP-TO-DATE
Task :buildSrc:check UP-TO-DATE
Task :buildSrc:build

Configure project :app
>>>>> Greeting from buildSrc MyPlugin

Task :prepareKotlinBuildScriptModel UP-TO-DATE

BUILD SUCCESSFUL in 13s

这表明我们的插件已经成功运行起来了,接下来需要注册我们自己的Task了。

Task

创建MyTask类继承自DefaultTask,这其中有两点需要注意: 1、在构造函数中添加分组(分组名自行定义),便于我们找到该任务; 2、添加一个doTaskAction()(方法名自行定义)的方法打印相关信息,注意该方法需要使用 @TaskAction 注解;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

public class MyTask extends DefaultTask {

    public MyTask() {
        setGroup("myTaskGroup");
    }

    @TaskAction
    public void doTaskAction() {
        System.out.println(">>>>> Greeting from buildSrc MyTask");
    }
}

然后将MyTask注册到任务列表中,在MyPlugin的apply()方法中注册我们的任务,任务名是myTask1:

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {

    @java.lang.Override
    public void apply(Project project) {
        System.out.println(">>>>> Greeting from buildSrc MyPlugin");

        project.getTasks().register("myTask1", MyTask.class);
    }

}

Sync项目后即可在右侧gradle面板中查看到我们创建的任务组以及任务: Snipaste_2022-11-23_19-58-25.png 双击myTask1任务即可在控制台查看到相关输出信息:

......
Task :app:myTask1
>>>>> Greeting from buildSrc MyTask

BUILD SUCCESSFUL in 2s

经过上述的一系列步骤之后我们对Plugin以及Task有了一个大致的了解了,接下来我们回归到开始时候的任务,首先我们需要创建自己的任务并依赖于项目中的assembleRelease等任务。

这里我们可能需要了解下Gradle的生命周期,见官网 Build Lifecycle,具体不再详述。这就好比我们在Activity的生命周期中收到onCreate、onResume等回调一样,在Gradle的生命周期中也有相应的回调,比如我们要用到的afterEvaluate(Action<? super Project> var1),在项目评估完后即可收到该回调,收到该回调后我们就可以获取到项目中的所有任务,并插入自己的任务。

接下来我们先改造下MyTask,因为我们的任务需要依赖于匹配到的assembleXxxRelease任务,所以我们设置一个dependsOnTaskName变量,并且通过构造函数传递进来,然后调用dependsOn()方法进行依赖。

注意:构造函数中添加参数后就需要使用@Inject进行注解:

public class MyTask extends DefaultTask {

    private String dependsOnTaskName;

    @Inject
    public MyTask(String dependsOnTaskName) {
        setGroup("myTaskGroup");
        this.dependsOnTaskName = dependsOnTaskName;
        dependsOn(dependsOnTaskName);
    }

    @TaskAction
    public void doTaskAction() {
        System.out.println(">>>>> Greeting from buildSrc MyTask");
    }
}

然后在MyPlugin的apply()方法中,添加gradle项目评估完成的回调,在回调中获取所有匹配到的assembleXxxRelease任务,然后创建publishXxxRelease任务,同时记得传递需要依赖的任务名给MyTask。

public class MyPlugin implements Plugin<Project> {

    @java.lang.Override
    public void apply(Project project) {
        System.out.println(">>>>> Greeting from buildSrc MyPlugin");

        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {
                for (Task task : project.getTasks()) {
                    String taskName = task.getName();
                    if (taskName.startsWith("assemble") && taskName.endsWith("Release")) {
                        String myTaskName = taskName.replace("assemble", "publish");
                        System.out.println(">>>>> 匹配到任务 = " + myTaskName);

                        project.getTasks().register(myTaskName, MyTask.class, taskName);
                    }
                }
            }
        });
    }
}

此时,Sync项目后可以在右侧gradle面板中看到我们创建的任务了: Snipaste_2022-11-24_11-22-19.png 双击publishRelease任务可以从输出日志中看到,先执行了assembleRelease任务后再执行的publishRelease任务:

......
Task :app:packageRelease
......
Task :app:assembleRelease

Task :app:publishRelease
>>>>> Greeting from buildSrc MyTask

BUILD SUCCESSFUL in 1m 24s
49 actionable tasks: 43 executed, 6 up-to-date
11:25:03: Execution finished 'publishRelease'.

上面是未配置项目Flavor的情况,如果项目中有多个Flavor的情况呢?比如我们在app模块下的build.gradle中添加如下Flavor:

android {
    defaultConfig {
        //添加 产品、环境 两个维度
        flavorDimensions "product", "environment"
    }

    productFlavors {
        //产品1
        app1 {
            dimension "product"
        }
    	//产品2
        app2 {
            dimension "product"
        }
    	//开发环境
        develop {
            dimension "environment"
        }
    	//生产环境
        product {
            dimension "environment"
        }
    }
}    

Sync项目后即可在右侧看到我们创建的任务组中多了新增的这些风味: Snipaste_2022-11-24_11-34-26.png

Extension

要实现文章开头的业务,我们还需要配置FTP地址,端口号,钉钉机器token人等信息,这就需要使用到Extension了。新建MyPluginExtension类:

public class MyPluginExtension {
    public String host;
    public String port;
}

然后在MyPlugin类中创建相关Extension的信息:

public class MyPlugin implements Plugin<Project> {

    @java.lang.Override
    public void apply(Project project) {
        project.getExtensions().create("myExtensionInfo", MyPluginExtension.class);

        //....
    }
}

读取的时候可以在需要的地方使用getExtension()方法读取,比如在MyTask类中:

public class MyTask extends DefaultTask {

    @TaskAction
    public void doTaskAction() {
        MyPluginExtension extension = getProject().getExtensions().getByType(MyPluginExtension.class);

        System.out.println(">>>>> extension.host = " + extension.host);
        System.out.println(">>>>> extension.port = " + extension.port);
    }
}

此时执行相关任务后输出结果肯定为null:

Task :app:publishApp1ProductRelease
>>>>> extension.host = null
>>>>> extension.port = null

BUILD SUCCESSFUL in 1m 7s

在app模块下的build.gradle文件中最外层,添加我们插件所需的配置信息如下:

myExtensionInfo {
    host = "192.168.1.1"
    port = 21
}

然后再执行相关任务就能拿到配置信息了:

Task :app:publishApp1ProductRelease
>>>>> extension.host = 192.168.1.1
>>>>> extension.port = 21

BUILD SUCCESSFUL in 3s

OK,至此我们已经讲解完了和我们的目标相关的Plugin和Task内容了,剩下的关于二维码、FTP以及DingTalk SDK的使用请查看源码吧。

发布阶段(独立module)

经过上面两个阶段的内容我们以及开发完毕一个简陋的插件了,接下来我们先尝试发布到本地文件夹,然后再尝试发布到MavenCentral中。

如果你是初次写插件,我建议你再新建一个工程,然后创建一个新的module将该工程中的buildSrc中的内容全部拷贝到新module中,这样还容易处理一点,不会受上述阶段应用插件和配置插件信息的影响。

这里呢,我是将buildSrc目录直接改名为了 module-plugin-my,然后在setting.gradle文件中包含该模块:

include ':module-plugin-my'

同时需要将app模块下的build.gradle做一下修改,注掉了自己的插件id,注掉了插件的配置信息:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'

//    id 'my-plugin'
}

//myExtensionInfo {
//    host = "192.168.1.1"
//    port = 21
//}

OK,接下来准备发布了。

发布到本地

先在module-plugin-my模块下的build.gradle中添加maven-publish插件,然后配置发布内容:

plugins {
    id 'java-gradle-plugin'

    id 'maven-publish'
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId = "io.github.vsLoong"
            artifactId = "my-plugin"
            version = "0.0.1"

            from components.java
        }
    }

    repositories {
        maven {
            //发布到该工程下的localRepo文件夹下
            url = "../localRepo"
        }
    }
}

gradlePlugin {
    plugins {
        apkPublisher {
            id = "my-plugin"
            implementationClass = 'com.vsloong.myplugin.MyPlugin'
        }
    }
}

脚本内容如上所示,Sync工程后,可以在右侧gradle面板中找到publish任务组了: Snipaste_2022-11-24_14-31-43.png

双击publish任务,构建完毕后即可在工程目录下查看到生成的依赖仓库: Snipaste_2022-11-24_14-36-50.png

那么如何应用该仓库这里有得好好说说了,7.0后的配置有些变化,但是使用之前的方式的话还是没问题的,这里就演示了通用的写法,在项目的build.gradle文件开头添加如下信息:

buildscript {
    repositories {
        maven {
            url("./localRepo")
        }
    }

    dependencies {
        classpath "io.github.vsLoong:my-plugin:0.0.1"
    }
}

然后在app模块下的build.gradle文件中应用该插件,并添加插件配置信息,Sync项目后即可成功看到我们自定义的Tasks成功显示在gradle面板了:

plugins {
	//......

    id 'my-plugin'
}

myExtensionInfo {
    host = "192.168.1.1"
    port = 21
}

发布到MavenCentral

准备sonatype账号

MavenCentral是由sonatype运营的,所以首先我们需要创建一个sonatype的账号,网上文章非常多了,这里就提一点,现在填写groupId的时候已经不支持com.github开头的命名了,建议使用io.github.yourgroupid。

创建pgp证书

账号注册好并拥有上传权限后,我们还需要的一个东西就是签名证书。类似我们的apk文件需要签名后才能发布到应用市场一样,我们的插件也需要一个签名。创建签名证书的话需要使用GPG4,我是Windows电脑所以使用了win工具,官网地址是:gpg4win.org 。我下载的版本是Gpg4win-4.0.4,页面如下所示。 Snipaste_2022-11-24_20-33-39.png

点击 文件-> New OpenPGP Key Pair... ,填写自己的名称和邮箱地址: Snipaste_2022-11-24_15-53-12.png

填写完后记得点击高级设置,将密钥类型设置为RSA,4096比特,OK后进入创建证书的页面,填写证书密码,这里的密码需要记住后续会用到: Snipaste_2022-11-24_16-04-56.png

创建好证书后,回到主页面右键该证书,然后导出公钥备份私钥Snipaste_2022-11-24_16-01-18.png

导出和备份的时候也请务必手动更改其文件的后缀名为pgp: Snipaste_2022-11-24_16-07-10.png

最后右键你创建好的证书,然后选择“在服务器上发布...”,发布成功后过段时间,在主面板中点击“在服务器上查找”,能找到自己名称的证书的话,则表示这一步已经成功了。 Snipaste_2022-11-24_16-10-57.png

配置sonatype和证书

经过上两步的操作后我们有了sonatype的账号密码以及仓库的上传权限,还有了已经发布到服务器的证书。上传插件到仓库需要这些信息,但这些信息又是不能暴漏到项目中的,否则这些信息被别人拿到岂不是可以随意上传内容到你的仓库了,所以这里的话我们需要将这些信息配置到本地。首先查看你的Gradle user home文件夹在哪里,例如我这里更改到的是D盘下AndroidGradle目录下: Snipaste_2022-11-24_16-24-19.png

那么我们就将备份的私钥文件拷贝到该目录,然后创建gradle.properties文件并输入如下内容:

# pgp信息
signing.keyId=证书密钥ID的后8位
signing.password=证书的密码
signing.secretKeyRingFile=D\:\\AndroidGradle\\你的私钥文件.pgp

# sonatype账号、密码
ossrhUsername=sonatype账号
ossrhPassword=sonatype密码

发布Snapshot版本

上述的pgp证书及sonatype信息在本地设置好后,编辑module-plugin-my模块下的build.gradle文件(请仔细按照如下脚本内容配置,避免出错):

plugins {
    id 'java-gradle-plugin'

    //发布使用
    id 'maven-publish'
    id 'signing'
}

//发布所需的相关内容
def MAVEN_GROUP_ID = "io.github.vsLoong"
def MAVEN_ARTIFACT_ID = "my-plugin"

def MAVEN_ARTIFACT_VERSION = "0.0.1-SNAPSHOT"
def REPOSITORY_URL_SNAPSHOT = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
def REPOSITORY_URL_RELEASE = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"

gradlePlugin {
    plugins {
        apkPublisher {
            id = MAVEN_ARTIFACT_ID
            implementationClass = 'com.vsloong.myplugin.MyPlugin'
        }
    }
}

// 将源码打包
task publishSourcesJar(type: Jar) {
    classifier = 'sources'
    exclude "**/R.class"  //排除`R.class`
    exclude "**/BuildConfig.class"  //排除`BuildConfig.class`
}

// mavenCentral要求必须有Javadoc的信息
task publishJavadocsJar(type: Jar, dependsOn: publishSourcesJar) {
    archiveClassifier.set('javadoc')
}

afterEvaluate {
    publishing {
        publications {
            maven(MavenPublication) {
                groupId = MAVEN_GROUP_ID
                artifactId = MAVEN_ARTIFACT_ID
                version = MAVEN_ARTIFACT_VERSION

                from components.java

                artifact publishSourcesJar
                artifact publishJavadocsJar

                pom {
                    name = 'Apk publisher gradle plugin'
                    description = 'A gradle plugin that helps upload APK files to FTP servers and generate QR code images with download links.'
                    url = 'https://github.com/vsLoong/ApkPublisherGradlePlugin'
                    licenses {
                        license {
                            name = 'The Apache License, Version 2.0'
                            url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                        }
                    }
                    developers {
                        developer {
                            id = 'vsLoong'
                            name = 'vsLoong'
                            email = '你的邮箱地址'
                        }
                    }
                    scm {
                        connection = 'https://github.com/vsLoong/ApkPublisherGradlePlugin.git'
                        developerConnection = 'https://github.com/vsLoong/ApkPublisherGradlePlugin.git'
                        url = 'https://github.com/vsLoong/ApkPublisherGradlePlugin'
                    }
                }
            }
        }

        repositories {
            maven {
                allowInsecureProtocol true
                url = MAVEN_ARTIFACT_VERSION.endsWith('SNAPSHOT') ? REPOSITORY_URL_SNAPSHOT : REPOSITORY_URL_RELEASE
                credentials {
                    username = ossrhUsername
                    password = ossrhPassword
                }
            }
        }
    }
}

// 证书及签名信息
signing {
    sign publishing.publications
}

配置完毕Sync项目后在右侧双击 publishMavenPublicationToMavenRepository(这个名字取决于上述脚本中的相关命名),然后即可成功上传到MavenCentral了: Snipaste_2022-11-24_16-39-26.png

请注意脚本中的一处版本判断的代码,如果版本号后缀是SNAPSHOT,那么会发布到Snapshots仓库,否则会发布到Releases仓库。发布到Snapshots仓库的话没有那么严格,可以先发布到该仓库进行尝试。
发布到Snapshots仓库成功后可以登录 s01.oss.sonatype.org/#welcome 进行查看,此时在项目中也可以依赖到该插件的SNAPSHOT版本。

发布Release版本

发布到Releases仓库的话,你的插件会先放在 暂存库 中,我们需要点击左侧的 Build Promotion -> Staging Repositories,然后选中你发布的插件操作Close以及Release步骤。Close这一步会检测你发布上来内容的各种规范等,如果你在Close的阶段成功了,那么恭喜你少走了很多弯路,我所踩的坑都已经罗列在下文疑难杂症中了,同时上文脚本也基本是最完善的脚本了。

下面是终于close成功的截图,就差点击Release按钮就可以发布了。Release后再耐心等等就可以从Maven Central中搜索到你的插件了,搜索地址在 search.maven.org/image.png

疑难杂症

JDK路径

也有可能你建立Android工程的时候AS已经提示过你了,例如下图这种情况。 Snipaste_2022-11-22_21-26-34.png 但是离谱的情况就是你按照提示select a JDK 处理完后,即便是选择了自己安装的JDK11的路径,但是AS仍旧不生效。 image.png

这时我们可以查看下 .idea/misc.xml文件,相关配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
    <component name="ExternalStorageConfigurationManager" enabled="true" />
    <component name="ProjectRootManager" default="true" languageLevel="JDK_11"
        project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK" version="2">
        <output url="file://$PROJECT_DIR$/build/classes" />
    </component>
    <component name="ProjectType">
        <option name="id" value="Android" />
    </component>
</project>

project-jdk-name字段的值是Android Studio default JDK,项目左侧的依赖中依旧是Android Studio default JDK。 image.png 所以如果我们使用上面自行安装的JDK11版本的话,直接手动把 project-jdk-name="Android Studio default JDK" 修改为 project-jdk-name="11",然后重启Android Studio即可。

如果这时候你的AS还是无法识别这种情况,那么你就用IntelliJ IDEA打开这个项目,然后IDEA也会提示你JDK有问题,按照提示配置一遍应该也可以了。如果还有其他情况我也束手无策了,我就在这个破问题上卡了一天。

发布到仓库

Received status code 403 from server: Forbidden

Execution failed for task ':apk-publisher-gradle-plugin:publishPluginMavenPublicationToMavenRepository'.
> Failed to publish publication 'pluginMaven' to repository 'maven'
> Could not PUT 'https://s01.oss.sonatype.org/content/repositories/snapshots/ApkPublisherGradlePlugin/apk-publisher-gradle-plugin/unspecified/apk-publisher-gradle-plugin-unspecified.jar'. Received status code 403 from server: Forbidden

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

解决方案添加sign,或者服务器上还未查询到签名。

It may not be a PGP secret key ring

Execution failed for task ':apk-publisher-gradle-plugin:signMavenPublication'.
> Unable to read secret key from file: E:\WorkSpace\ApkPublisherGradlePlugin\vsLoong_0x000000_SECRET.pgp (it may not be a PGP secret key ring)

请务必将导出时候默认的asc后缀,修改为pgp后缀。如果是导出了asc格式的文件后,又手动修改文件名后缀为pgp同样不可以,也会报错如上。 Snipaste_2022-11-24_16-07-10.png

Unknown public key algorithm encountered

请务必在创建时选择高级设置,将密钥类型更改为 RSA。 Snipaste_2022-11-24_16-04-56.png

CLOSE步骤

如果你在编写发布脚本的时候未按照上文脚本添加sourcesJar,javadocJar,以及必须的POM信息,那么在发布Release版本操作close步骤时可能会遇到如下错误,请认真按照上述发布脚本进行检查。 image.png

Event: Failed: Javadoc Validation

typeId javadoc-staging failureMessage Missing: no javadoc jar found in folder '/io/github/vsLoong/apk-publisher-gradle-plugin/1.0.0'

Event: Failed: POM Validation

typeId pom-staging failureMessage Invalid POM: /io/github/vsLoong/apk-publisher-gradle-plugin/1.0.0/apk-publisher-gradle-plugin-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing

Event: Failed: Sources Validation

typeId sources-staging failureMessage Missing: no sources jar found in folder '/io/github/vsLoong/apk-publisher-gradle-plugin/1.0.0'

结尾

该插件已开源,GitHub地址在:ApkPublisherGradlePlugin 。 最后,祝大家一路披荆斩棘,都能顺利发布好玩的gradle插件吧。

参考文章