FINERACT-1470: Fineract release plugin

This commit is contained in:
Aleksandar Vidakovic
2022-05-04 15:11:53 +02:00
committed by Aleksandar Vidakovic
parent b185937962
commit 40eb547a5c
160 changed files with 4954 additions and 638 deletions
+51
View File
@@ -0,0 +1,51 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
plugins {
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'groovy'
id 'java-gradle-plugin'
id 'groovy-gradle-plugin'
}
apply from: "${projectDir}/src/main/groovy/org.apache.fineract.dependencies.gradle"
description = 'Fineract Gradle Plugin'
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
implementation 'com.sun.mail:jakarta.mail'
implementation 'org.freemarker:freemarker'
implementation 'org.tmatesoft.svnkit:svnkit'
implementation 'org.bouncycastle:bcprov-jdk15on'
implementation 'org.bouncycastle:bcpg-jdk15on'
implementation 'org.eclipse.jgit:org.eclipse.jgit'
implementation 'org.eclipse.jgit:org.eclipse.jgit.ssh.apache'
implementation 'com.vdurmont:semver4j'
implementation 'org.beryx:text-io'
implementation 'commons-io:commons-io'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor'
implementation 'com.squareup.retrofit2:retrofit'
implementation 'com.squareup.retrofit2:converter-jackson'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
@@ -0,0 +1,183 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
apply plugin: 'io.spring.dependency-management'
// Configuration for the dependency management plugin
// https://github.com/spring-gradle-plugins/dependency-management-plugin
dependencyManagement {
imports {
mavenBom 'org.springframework:spring-framework-bom:5.3.19'
mavenBom 'org.springframework.boot:spring-boot-dependencies:2.6.7'
mavenBom 'org.jetbrains.kotlin:kotlin-bom:1.6.21'
mavenBom 'org.junit:junit-bom:5.8.2'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.2.1'
mavenBom 'io.cucumber:cucumber-bom:7.3.3'
mavenBom 'io.netty:netty-bom:4.1.76.Final'
mavenBom 'org.mockito:mockito-bom:4.5.1'
mavenBom 'io.github.swagger2markup:swagger2markup-bom:1.3.4'
}
dependencies {
// We use fixed versions, instead of inheriting them from the Spring BOM, to be able to be on more recent ones.
// We do not use :+ to get the latest available version available on Maven Central, as that could suddenly break things.
// We use the Renovate Bot to automatically propose Pull Requests (PRs) when upgrades for all of these versions are available.
dependency 'org.slf4j:slf4j-api:1.7.36'
dependency 'org.slf4j:slf4j-simple:1.7.36'
dependency 'org.slf4j:jcl-over-slf4j:1.7.36'
dependency 'org.slf4j:jul-to-slf4j:1.7.36'
dependency 'org.slf4j:log4j-over-slf4j:1.7.36'
dependency 'ch.qos.logback:logback-core:1.2.11'
dependency 'ch.qos.logback:logback-classic:1.2.11'
dependency 'org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.10'
dependency 'com.google.guava:guava:31.1-jre'
dependency 'com.google.code.gson:gson:2.9.0'
dependency 'com.google.truth:truth:1.1.3'
dependency 'com.google.truth.extensions:truth-java8-extension:1.1.3'
dependency 'com.google.googlejavaformat:google-java-format:1.15.0'
dependency ('org.apache.commons:commons-email:1.5') {
exclude 'com.sun.mail:javax.mail'
exclude 'javax.activation:activation'
}
dependency 'commons-io:commons-io:2.11.0'
dependency 'com.github.librepdf:openpdf:1.3.27'
dependency ('org.mnode.ical4j:ical4j:3.2.2') {
exclude 'com.sun.mail:javax.mail'
}
dependency 'org.quartz-scheduler:quartz:2.3.2'
dependency 'com.amazonaws:aws-java-sdk-s3:1.12.210'
dependency 'org.ehcache:ehcache:3.10.0'
dependency 'com.github.spullara.mustache.java:compiler:0.9.10'
dependency 'com.jayway.jsonpath:json-path:2.7.0'
dependency 'org.apache.tika:tika-core:2.3.0'
dependency 'org.apache.httpcomponents:httpclient:4.5.13'
dependency 'jakarta.management.j2ee:jakarta.management.j2ee-api:1.1.4'
dependency 'jakarta.jms:jakarta.jms-api:2.0.3'
dependency 'org.glassfish.jaxb:jaxb-runtime:2.3.6' // Swagger needs exactly this version
dependency 'org.apache.bval:org.apache.bval.bundle:2.0.5'
dependency 'joda-time:joda-time:2.10.14'
dependency 'io.github.classgraph:classgraph:4.8.146'
dependency 'org.awaitility:awaitility:4.2.0'
dependency 'com.github.spotbugs:spotbugs-annotations:4.6.0'
dependency 'javax.cache:cache-api:1.1.1'
dependency 'org.mock-server:mockserver-junit-jupiter:5.13.2'
dependency 'org.webjars:webjars-locator-core:0.50'
dependency 'com.icegreen:greenmail-junit5:1.6.8'
dependency 'com.intuit.karate:karate-junit5:1.2.0.RC6'
// fineract client dependencies
dependency "com.squareup.retrofit2:retrofit:2.9.0"
dependency "com.squareup.retrofit2:retrofit-mock:2.9.0"
dependency "com.squareup.retrofit2:adapter-java8:2.9.0"
dependency "com.squareup.retrofit2:adapter-rxjava2:2.9.0"
dependency "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
dependency "com.squareup.retrofit2:adapter-guava:2.9.0"
dependency "com.squareup.retrofit2:converter-wire:2.9.0"
dependency "com.squareup.retrofit2:converter-jackson:2.9.0"
dependency "com.squareup.retrofit2:converter-simplexml:2.9.0"
dependency "com.squareup.retrofit2:converter-jaxb:2.9.0"
dependency "com.squareup.retrofit2:converter-java8:2.9.0"
dependency "com.squareup.retrofit2:converter-scalars:2.9.0"
dependency "com.squareup.retrofit2:converter-gson:2.9.0"
dependency "com.squareup.retrofit2:converter-protobuf:2.9.0"
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.common:1.0.1"
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1"
dependency "org.apache.oltu.oauth2:org.apache.oltu.oauth2.httpclient4:1.0.1"
dependency "com.squareup.okhttp3:okhttp:4.9.3"
dependency "com.squareup.okhttp3:okcurl:4.9.3"
dependency "com.squareup.okhttp3:logging-interceptor:4.9.3"
dependency "com.squareup.okhttp3:okhttp-apache:4.9.3"
dependency "com.squareup.okhttp3:okhttp-android-support:4.9.3"
dependency "com.squareup.okhttp3:okhttp-urlconnection:4.9.3"
dependency "com.squareup.okhttp3:okhttp-sse:4.9.3"
dependency "io.gsonfire:gson-fire:1.8.5"
dependency "com.google.code.findbugs:jsr305:3.0.2"
dependency "commons-codec:commons-codec:1.15"
dependency "org.projectlombok:lombok:1.18.24"
dependency 'org.bouncycastle:bcpkix-jdk15to18:1.70'
dependency 'org.bouncycastle:bcprov-jdk15to18:1.70'
dependency 'org.bouncycastle:bcprov-jdk15on:1.70'
dependency 'org.bouncycastle:bcpg-jdk15on:1.70'
dependency 'org.eclipse.jgit:org.eclipse.jgit:6.1.0.202203080745-r'
dependency 'org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.1.0.202203080745-r'
dependency 'org.tmatesoft.svnkit:svnkit:1.10.5'
dependency 'com.vdurmont:semver4j:3.1.0'
dependency 'org.beryx:text-io:3.4.1'
dependency 'org.springdoc:springdoc-openapi-ui:1.6.8'
dependency 'org.springdoc:springdoc-openapi-webmvc-core:1.6.8'
dependency 'org.springdoc:springdoc-openapi-common:1.6.8'
dependency 'org.springdoc:springdoc-openapi-security:1.6.8'
dependency 'org.springdoc:springdoc-openapi-javadoc:1.6.8'
dependency 'com.google.cloud.sql:mysql-socket-factory-connector-j-8:1.5.0'
dependency ('org.apache.activemq:activemq-broker:5.17.1') {
exclude 'javax.annotation:javax.annotation-api'
}
dependency 'io.swagger:swagger-annotations:1.6.6'
dependency 'io.swagger.core.v3:swagger-annotations:2.2.0'
dependency ('io.swagger.core.v3:swagger-jaxrs2:2.2.0') {
exclude 'jakarta.activation:jakarta.activation-api'
}
dependency ('io.swagger.core.v3:swagger-core:2.2.0') {
exclude 'jakarta.activation:jakarta.activation-api'
}
dependency "jakarta.annotation:jakarta.annotation-api:1.3.5"
dependency 'jakarta.activation:jakarta.activation-api:1.2.2'
dependency ('com.sun.mail:jakarta.mail:1.6.7') {
exclude 'com.sun.activation:jakarta.activation'
}
dependency ('jakarta.xml.bind:jakarta.xml.bind-api:2.3.3') {
exclude 'jakarta.activation:jakarta.activation-api'
}
dependency ('org.liquibase:liquibase-core:4.9.1') {
exclude 'javax.xml.bind:jaxb-api'
}
dependency ('org.dom4j:dom4j:2.1.3') {
exclude 'relaxngDatatype:relaxngDatatype' // already in com.sun.xml.bind:jaxb-osgi:2.3.0.1
// FINERACT-940 && FINERACT-966 https://github.com/spotbugs/spotbugs/issues/1128
exclude 'xpp3:xpp3'
exclude 'pull-parser:pull-parser'
}
dependency 'org.owasp.esapi:esapi:2.4.0.0'
dependencySet(group: 'org.apache.poi', version: '5.2.2') {
entry 'poi'
entry 'poi-ooxml'
entry 'poi-ooxml-schemas'
}
dependencySet(group: 'io.rest-assured', version: '4.5.1') {
entry 'rest-assured'
entry 'json-path'
entry 'xml-path'
}
}
}
@@ -0,0 +1,263 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
apply plugin: 'org.apache.fineract.gradle'
apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle"
fineract {
config {
username = "${findProperty('fineract.config.username')}"
password = "${findProperty('fineract.config.password')}"
doc {
url = 'git@github.com:apache/fineract-site.git'
directory = "${System.getProperty("java.io.tmpdir")}/fineract-site"
branch = "asf-site"
}
git {
dir = "${projectDir.absolutePath}/.git"
sections = [
[
section: "user",
name: "name",
value: "${findProperty('fineract.config.name')}",
],
[
section: "user",
name: "email",
value: "${findProperty('fineract.config.email')}",
],
[
section: "user",
name: "signingkey",
value: "${findProperty('fineract.config.gnupg.keyName')}",
],
[
section: "commit",
name: "gpgsign",
value: "true",
],
]
}
template {
templateDir = "${projectDir}/buildSrc/src/main/resources"
}
gpg {
keyName = "${findProperty('fineract.config.gnupg.keyName')}"
publicKeyring = "${findProperty('fineract.config.gnupg.publicKeyring')}"
secretKeyring = "${findProperty('fineract.config.gnupg.secretKeyring')}"
password = "${findProperty('fineract.config.gnupg.password')}"
}
smtp {
host = 'smtp.gmail.com'
username = "${findProperty('fineract.config.smtp.username')}"
password = "${findProperty('fineract.config.smtp.password')}"
tls = true
ssl = true
}
subversion {
username = "${findProperty('fineract.config.username')}"
password = "${findProperty('fineract.config.password')}"
revision = 'HEAD'
}
jira {
url = 'https://issues.apache.org/jira/rest/api/2/'
username = "${findProperty('fineract.config.username')}"
password = "${findProperty('fineract.config.password')}"
}
confluence {
url = 'https://cwiki.apache.org/confluence/rest/api/'
username = "${findProperty('fineract.config.username')}"
password = "${findProperty('fineract.config.password')}"
}
}
steps = [
test: [
order: 0,
description: 'Test',
email: [
from: "${findProperty('fineract.config.email')}",
to: "${findProperty('fineract.config.email')}",
mime: 'text/plain',
subject: "[FINERACT] [TEST] 📣 🏁 🗳️ 🚀 📈 🔀 🌠 Propose new release ${version}",
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step01.headsup.message.ftl"
]
],
subversion: [
url: 'https://dist.apache.org/repos/dist/dev/fineract',
directory: "${System.getProperty("java.io.tmpdir")}"
],
jira: [
query: "project in (\'FINERACT\') and fixVersion = ${findProperty('fineract.release.version')} and status in (CLOSED, RESOLVED)"
],
template: [
templateFile: "${projectDir}/buildSrc/src/main/resources/jira/changelog.txt.ftl"
],
gpg: [
file: "${projectDir}/buildSrc/build/libs/buildSrc.jar"
]
],
step1: [
order: 1,
description: 'Send heads-up email to dev mailing list',
email: [
from: "${findProperty('fineract.config.email')}",
name: "${findProperty('fineract.config.name')}",
to: "${findProperty('fineract.config.email')}",
// to: 'dev@fineract.apache.org',
mime: 'text/plain',
subjectTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step01.headsup.subject.ftl"
],
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step01.headsup.message.ftl"
]
]
],
step2: [
order: 2,
description: 'Clean up JIRA',
],
step3: [
order: 3,
description: 'Create a release branch',
email: [
from: "${findProperty('fineract.config.email')}",
name: "${findProperty('fineract.config.name')}",
to: "${findProperty('fineract.config.email')}",
// to: 'dev@fineract.apache.org',
mime: 'text/plain',
subjectTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step03.branch.subject.ftl"
],
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step03.branch.message.ftl"
]
]
],
step4: [
order: 4,
description: 'Freeze JIRA version',
],
step5: [
order: 5,
description: 'Create release tag',
git: [
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/git/release.step05.tag.message.ftl"
]
]
],
step6: [
order: 6,
description: 'Create a distribution',
],
step7: [
order: 7,
description: 'Sign the distribution artifacts',
gpg: [
file: "${rootDir}/fineract-war/build/distributions/apache-fineract-binary-${project.version}.tar.gz"
]
],
step8: [
order: 8,
description: 'Upload distribution artifacts to staging environment',
subversion: [
url: 'https://dist.apache.org/repos/dist/dev/fineract',
directory: "${System.getProperty("java.io.tmpdir")}/fineract-dist-dev"
],
],
step9: [
order: 9,
description: 'Verify distribution in staging environment',
],
step10: [
order: 10,
description: 'Start voting on the dev mailing list',
email: [
from: "${findProperty('fineract.config.email')}",
name: "${findProperty('fineract.config.name')}",
to: "${findProperty('fineract.config.email')}",
// to: 'dev@fineract.apache.org',
mime: 'text/plain',
subjectTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step10.vote.subject.ftl"
],
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step10.vote.message.ftl"
]
]
],
step11: [
order: 11,
description: 'Conclude the vote and announce results',
email: [
from: "${findProperty('fineract.config.email')}",
name: "${findProperty('fineract.config.name')}",
to: "${findProperty('fineract.config.email')}",
// to: 'dev@fineract.apache.org',
mime: 'text/plain',
subjectTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step11.vote.subject.ftl"
],
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step11.vote.message.ftl"
]
]
],
step12: [
order: 12,
description: 'Upload distribution artifacts to release environment',
subversion: [
url: 'https://dist.apache.org/repos/dist/release/fineract',
directory: "${System.getProperty("java.io.tmpdir")}/fineract-dist-release"
],
],
step13: [
order: 13,
description: 'Close release branch and merge to \'develop\'',
],
step14: [
order: 14,
description: 'Update website (not yet automated)',
],
step15: [
order: 15,
description: 'Send email to announcement mailing list',
jira: [
projectId: "12319420",
query: "project in (\'FINERACT\') and fixVersion = ${findProperty('fineract.release.version')} and status in (CLOSED, RESOLVED, WONTFIX)"
],
email: [
from: "${findProperty('fineract.config.email')}",
name: "${findProperty('fineract.config.name')}",
to: "${findProperty('fineract.config.email')}",
// to: 'dev@fineract.apache.org',
mime: 'text/plain',
subjectTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step15.announce.subject.ftl"
],
messageTemplate: [
templateFile: "${projectDir}/buildSrc/src/main/resources/email/release.step15.announce.message.ftl"
]
]
]
]
}
@@ -0,0 +1,581 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle
import groovy.json.JsonSlurper
import org.apache.commons.io.FileUtils
import org.apache.commons.io.filefilter.NotFileFilter
import org.apache.commons.io.filefilter.PrefixFileFilter
import org.apache.fineract.gradle.service.*
import org.beryx.textio.TextIO
import org.beryx.textio.TextIoFactory
import org.beryx.textio.TextTerminal
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class FineractPlugin implements Plugin<Project> {
private static final Logger log = LoggerFactory.getLogger(FineractPlugin.class)
private JiraService jiraService
private ConfluenceService confluenceService
private SubversionService subversionService
private EmailService emailService
private GpgService gpgService
private TemplateService templateService
private GitService gitService
private Map<String, ?> context;
static {
System.setProperty("org.beryx.textio.TextTerminal", "org.beryx.textio.system.SystemTextTerminal")
}
void apply(Project project) {
def extension = project.extensions.create("fineract", FineractPluginExtension, project)
project.fineract.extensions.create("config", FineractPluginExtension.FineractPluginConfig)
project.fineract.config.extensions.create("doc", FineractPluginExtension.FineractPluginConfigDoc)
project.fineract.config.extensions.create("template", FineractPluginExtension.FineractPluginConfigTemplate)
project.fineract.config.extensions.create("smtp", FineractPluginExtension.FineractPluginConfigSmtp)
project.fineract.config.extensions.create("subversion", FineractPluginExtension.FineractPluginConfigSubversion)
project.fineract.config.extensions.create("jira", FineractPluginExtension.FineractPluginConfigJira)
project.fineract.config.extensions.create("confluence", FineractPluginExtension.FineractPluginConfigConfluence)
project.fineract.config.extensions.create("gpg", FineractPluginExtension.FineractPluginConfigGpg)
project.fineract.config.extensions.create("git", FineractPluginExtension.FineractPluginConfigGit)
project.afterEvaluate {
this.templateService = new TemplateService(extension.config.template)
this.jiraService = new JiraService(extension.config.jira)
this.confluenceService = new ConfluenceService(extension.config.confluence)
this.subversionService = new SubversionService(extension.config.subversion)
this.emailService = new EmailService(extension.config.smtp)
this.gpgService = new GpgService(extension.config.gpg)
this.gitService = new GitService(extension.config.git, extension.config.gpg)
this.context = context(project)
}
project.tasks.register("fineractDocPublish") {
dependsOn(":fineract-doc:doc")
doLast {
log.warn("Fineract Publish Doc...")
def url = extension.config.doc.url?:"git@github.com:apache/fineract-site.git";
def branch = extension.config.doc.branch?:"asf-site"
def directory = extension.config.doc.directory?:System.getProperty("java.io.tmpdir")+"/fineract-site"
def d = new File(directory)
if(d.exists()) {
gitService.pull(directory)
} else {
gitService.clone(url, branch, directory)
gitService.config(directory, extension.config.git.sections)
}
def source = new File("fineract-doc/build/docs/html/en");
def target = new File(directory + "/docs/current")
if(!target.name) {
target.mkdirs()
}
FileUtils.copyDirectory(source, target, new NotFileFilter(new PrefixFileFilter(".asciidoctor")), true)
gitService.commit(directory)
}
}
project.tasks.register("fineractReleaseTest") {
doFirst {
// emojis:
// https://gist.github.com/parmentf/035de27d6ed1dce0b36a
// https://emojipedia.org/
// https://unicode.org/emoji/charts/full-emoji-list.html
FineractPluginExtension.FineractPluginStep step = step(extension, "test")
// TODO:
// - jira:
// - move tickets to new version
// jira query
FineractPluginExtension.FineractPluginJiraParams result = jiraService.search(step.jira)
log.warn(">>>>>>>>>>>>>>>> ISSUE: ${result.result[0]?.id} - ${result.result[0]?.key} - ${result.result[0]?.fields?['summary']} - ${result.result[0]?.fields?['fixVersions']}")
/*
// confluence create
def content = new ConfluenceService.ConfluenceContent(title: "1.7.0 - Apache Fineract",
space: new ConfluenceService.ConfluenceSpace(key: "FINERACT"),
ancestors: [new ConfluenceService.ConfluenceParent(id: "75974324")],
body: new ConfluenceService.ConfluenceBody(storage: new ConfluenceService.ConfluenceStorage(value: "<h1>Release Artifacts</h1>")))
confluenceService.createContent(content)
// confluence get
ConfluenceService.ConfluenceContent content = confluenceService.getContent("211881930")
log.warn(">>>>>>>>>>>>>>>>>>>>> CONFLUENCE: ${content.id} - ${content.title} - ${content.body?.storage?.value}")
// confluence update
content.body?.storage?.value = "<h1>Release Artifacts</h1><p><ac:structured-macro ac:macro-id=\"ac1cb3cb-84d4-45aa-820b-05cc42d39dd9\" ac:name=\"jira\" ac:schema-version=\"1\"><ac:parameter ac:name=\"server\">ASF JIRA</ac:parameter><ac:parameter ac:name=\"columnIds\">issuekey,summary,issuetype,created,updated,duedate,assignee,reporter,customfield_12311032,customfield_12311037,customfield_12311022,customfield_12311027,priority,status,resolution</ac:parameter><ac:parameter ac:name=\"columns\">key,summary,type,created,updated,due,assignee,reporter,Priority,Priority,Priority,Priority,priority,status,resolution</ac:parameter><ac:parameter ac:name=\"maximumIssues\">50</ac:parameter><ac:parameter ac:name=\"jqlQuery\">project = FINERACT AND fixVersion = 1.7.0 AND status not in (Open)</ac:parameter><ac:parameter ac:name=\"serverId\">5aa69414-a9e9-3523-82ec-879b028fb15b</ac:parameter></ac:structured-macro></p><p><br/></p>"
content.space = new ConfluenceService.ConfluenceSpace(key: "FINERACT")
content.status = null
content.version = new ConfluenceService.ConfluenceVersion(number: 3)
// confluence update
content = confluenceService.updateContent(content.id, content)
log.warn(">>>>>>>>>>>>>>>>>>>>> CONFLUENCE: ${content.id} - ${content.title} - ${content.body?.storage?.value}")
// confluence delete
ConfluenceService.ConfluenceResponse response = confluenceService.deleteContent("211881930")
log.warn(">>>>>>>>>>>>>>>>>>>>> CONFLUENCE: ${response?.statusCode} - ${response?.message}")
// jira versions
def versions = jiraService.getProjectVersions("12319420")
versions.findAll {
log.warn(">>>> VERSION: ${it.id} - ${it.name} - ${it.description}")
}
def version = jiraService.getVersion("12351431")
log.warn(">>>> !!! VERSION: ${version.id} - ${version.name} - ${version.description} - ${version.archived}")
version.archived = !version.archived
jiraService.updateVersion(version)
version = jiraService.getVersion("12351431")
log.warn(">>>> === VERSION: ${version.id} - ${version.name} - ${version.description} - ${version.archived}")
// jira projects
def projects = jiraService.getProjects()
projects.findAll {
log.warn(">>>>>>>>>>>> PROJECT: ${it.id} - ${it.key} - ${it.name}")
}
// jira info
Map<String, Object> info = jiraService.serverInfo()
log.warn(">>>>>>>>>>>>>>>> INFO: ${info}")
// jira query
FineractPluginExtension.FineractPluginJiraParams result = jiraService.search(step.jira)
log.warn(">>>>>>>>>>>>>>>> INFO: ${result.result.size()}")
// format with template
def tmp = templateService.render(step.template, [issues: result.result])
log.warn(tmp.output)
*/
}
}
// TODO: step for creating release Jira ticket missing!
// step 1
project.tasks.register("fineractReleaseStep1") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step1")
String version = project.properties?['fineract.release.version']
String issue = project.properties?['fineract.release.issue']
String date = project.properties?['fineract.release.date']
if(!version || !issue || !date) {
TextIO textIO = TextIoFactory.getTextIO()
if(!version) {
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
if(!issue) {
issue = textIO.newStringInputReader()
.withMaxLength(4)
.withPattern("\\d+")
.read("Jira Issue");
}
if(!date) {
date = textIO.newStringInputReader()
.withMaxLength(10)
.withPattern("\\d\\d\\d\\d-\\d\\d-\\d\\d")
.read("Date");
}
TextTerminal terminal = textIO.getTextTerminal()
terminal.printf("\nResult: %s - FINERACT-%s - %s", version, issue, date)
}
// TODO: input validation, see FINERACT-1610
this.context?.project?['fineract.release.version'] = version
this.context?.project?['fineract.release.issue'] = issue
this.context?.project?['fineract.release.date'] = date
if(step.email) {
emailService.send( processEmailParams(step.email, this.context) )
}
}
}
// step 2
project.tasks.register("fineractReleaseStep2") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step2")
// TODO: implement this, see FINERACT-1611
printInstructions(project, "step2")
}
}
// step 3
project.tasks.register("fineractReleaseStep3") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step3")
String version = project.properties?['fineract.release.version']
String date = project.properties?['fineract.release.date']
if(!version || !date) {
TextIO textIO = TextIoFactory.getTextIO()
if(!version) {
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
if(!date) {
date = textIO.newStringInputReader()
.withMaxLength(10)
.withPattern("\\d\\d\\d\\d-\\d\\d-\\d\\d")
.read("Date");
}
TextTerminal terminal = textIO.getTextTerminal()
terminal.printf("\nResult: %s - %s", version, date)
}
// TODO: input validation, see FINERACT-1610
// TODO: create release branch, see FINERACT-1611
this.context?.project?['fineract.release.version'] = version
this.context?.project?['fineract.release.date'] = date
if(step.email) {
emailService.send( processEmailParams(step.email, this.context) )
}
}
}
// step 4
project.tasks.register("fineractReleaseStep4") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step4")
// TODO: implement this, see FINERACT-1611
printInstructions(project, "step4")
}
}
// step 5
project.tasks.register("fineractReleaseStep5") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step5")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO()
if (!version) {
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
}
this.context?.project?['fineract.release.version'] = version
def params = processGitParams(step.git, this.context)
params.tag = params.tag?:version
gitService.createTag(params.tag, params.message)
}
}
// step 6
project.tasks.register("fineractReleaseStep6") {
dependsOn(":fineract-war:binaryDistTar")
}
// step 7
project.tasks.register("fineractReleaseStep7") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step7")
gpgService.sign(step.gpg)
def md5 = gpgService.md5(step.gpg)
def md5File = new File("${step.gpg.file}.md5")
md5File.write md5
def sha512 = gpgService.sha512(step.gpg)
def sha512File = new File("${step.gpg.file}.sha512")
sha512File.write sha512
}
}
// step 8
project.tasks.register("fineractReleaseStep8") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step8")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO()
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
// TODO: input validation, see FINERACT-1610
subversionService.checkout(step.subversion)
def directory = step.subversion.directory?:System.getProperty("java.io.tmpdir") + "/fineract-dist-dev"
def source = new File("fineract-war/build/distributions")
def target = new File("${directory}/${version}")
FileUtils.copyDirectory(source, target, true)
subversionService.commit(step.subversion)
}
}
// step 9
project.tasks.register("fineractReleaseStep9") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step9")
// TODO: implement this, see FINERACT-1611
printInstructions(project, "step9")
}
}
// step 10
project.tasks.register("fineractReleaseStep10") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step10")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO();
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
// TODO: input validation, see FINERACT-1610
this.context?.project?['fineract.release.version'] = version
if(step.email) {
emailService.send( processEmailParams(step.email, this.context) )
}
}
}
// step 11
project.tasks.register("fineractReleaseStep11") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step11")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO()
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
// TODO: input validation, see FINERACT-1610
this.context?.project?['fineract.release.version'] = version
def jsonSlurper = new JsonSlurper()
def data = jsonSlurper.parse(new File("buildSrc/src/main/resources/vote/result.${version}.json"))
this.context?.project?['fineract.vote'] = data
if(step.email) {
emailService.send( processEmailParams(step.email, this.context) )
}
}
}
// step 12
project.tasks.register("fineractReleaseStep12") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step12")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO()
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
// TODO: input validation, see FINERACT-1610
subversionService.checkout(step.subversion)
def directory = step.subversion.directory?:System.getProperty("java.io.tmpdir") + "/fineract-dist-release"
def source = new File("fineract-war/build/distributions")
def target = new File("${directory}/${version}")
FileUtils.copyDirectory(source, target, true)
subversionService.commit(step.subversion)
}
}
// step 13
project.tasks.register("fineractReleaseStep13") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step13")
// TODO: implement this, see FINERACT-1611
printInstructions(project, "step13")
}
}
// step 14
project.tasks.register("fineractReleaseStep14") {
doFirst {
FineractPluginExtension.FineractPluginStep step = step(extension, "step14")
// TODO: implement this, see FINERACT-1611
printInstructions(project, "step14")
}
}
// step 15
project.tasks.register("fineractReleaseStep15") {
doFirst {
log.warn("Release step 15: send email to announcement mailing list")
FineractPluginExtension.FineractPluginStep step = step(extension, "step15")
String version = project.properties?['fineract.release.version']
if(!version) {
TextIO textIO = TextIoFactory.getTextIO()
version = textIO.newStringInputReader()
.withPattern("\\d+.\\d+.\\d+")
.read("Release Version");
}
// TODO: input validation, see FINERACT-1610
FineractPluginExtension.FineractPluginJiraParams issues = jiraService.search(step.jira)
def versions = jiraService.getProjectVersions(step.jira.projectId)
def filteredVersions = versions.findAll {
log.warn(">>>> VERSION: ${it.id} - ${it.name} - ${it.description}")
it.name == version
}
this.context?.project?['fineract.release.version'] = version
this.context?.project?['fineract.release.issues'] = issues.result
this.context?.project?['fineract.release.projectId'] = step.jira.projectId
this.context?.project?['fineract.release.versionId'] = filteredVersions[0]?.id
if(step.email) {
emailService.send( processEmailParams(step.email, this.context) )
}
}
}
}
private FineractPluginExtension.FineractPluginStep step(FineractPluginExtension extension, String id) {
FineractPluginExtension.FineractPluginStep step = extension.steps[id]
if(step) {
log.warn("Fineract release step ${step.order}: ${step.description}.")
} else {
throw new RuntimeException("Could not find any parameters for step with ID '${id}'")
}
return step
}
private FineractPluginExtension.FineractPluginEmailParams processEmailParams(FineractPluginExtension.FineractPluginEmailParams params, Object data) {
if(params.subjectTemplate) {
def result = templateService.render(params.subjectTemplate, data)
params.subject = result.output
}
if(params.messageTemplate) {
def result = templateService.render(params.messageTemplate, data)
params.message = result.output
}
return params
}
private FineractPluginExtension.FineractPluginGitParams processGitParams(FineractPluginExtension.FineractPluginGitParams params, Object data) {
if(params.messageTemplate) {
def result = templateService.render(params.messageTemplate, data)
params.message = result.output
}
return params
}
private Map<String, ?> context(Project project) {
return Map.of("project", project.getProperties().findAll { it.key != "password"})
}
private void printInstructions(Project project, String step) {
String version = project.properties?['fineract.release.version']?:"0.0.0"
this.context?.project?['fineract.release.version'] = version
FineractPluginExtension.FineractPluginTemplateParams result = templateService.render(new FineractPluginExtension.FineractPluginTemplateParams(templateFile: new File("buildSrc/src/main/resources/instructions/${step}.txt.ftl")), this.context)
log.warn(result.output)
}
}
@@ -0,0 +1,164 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle
import org.apache.fineract.gradle.service.JiraService
import org.gradle.api.Project
class FineractPluginExtension {
Project project
Map<String, FineractPluginStep> steps = [:]
FineractPluginExtension(Project project) {
this.project = project
}
static class FineractPluginConfig {
String username
String password
}
static class FineractPluginConfigDoc {
String url
String directory
String branch
}
static class FineractPluginConfigJira {
String url
String username
String password
String project
}
static class FineractPluginConfigConfluence {
String url
String username
String password
String project
}
static class FineractPluginConfigGpg {
String keyName
String publicKeyring
String secretKeyring
String password
}
static class FineractPluginConfigTemplate {
String templateDir
}
static class FineractPluginConfigSmtp {
String host
int port
String username
String password
boolean tls
boolean ssl
}
static class FineractPluginConfigGit {
String username
String password
String dir
boolean dryRun
List<FineractPluginConfigGitSection> sections
}
static class FineractPluginConfigGitSection {
String section
String subsection
String name
String value
}
static class FineractPluginConfigSubversion {
String username
String password
String revision
}
static class FineractPluginStep {
int order
String description
FineractPluginEmailParams email
FineractPluginJiraParams jira
FineractPluginTemplateParams template
FineractPluginConfluenceParams confluence
FineractPluginGitParams git
FineractPluginSubversionParams subversion
FineractPluginGpgParams gpg
}
static class FineractPluginEmailParams {
String from
String name
String to
String cc
String bcc
String mime
String subject
FineractPluginTemplateParams subjectTemplate
String message
FineractPluginTemplateParams messageTemplate
}
static class FineractPluginJiraParams {
String command
String projectId
String fields = "*all"
String query
List<JiraService.JiraIssue> result = new ArrayList<>()
int pageOffset = 0
int pageSize= 50
int total = 1000
List<String> includes = ["summary", "status", "assignee", "fixVersions"]
}
static class FineractPluginConfluenceParams {
String title
String content
Integer ancestor
}
static class FineractPluginGitParams {
String tag
String message
FineractPluginTemplateParams messageTemplate
}
static class FineractPluginSubversionParams {
String url
String command
String revision = "HEAD"
String directory
}
static class FineractPluginTemplateParams {
String template
String templateFile
String output
String outputFile
}
static class FineractPluginGpgParams {
String file
}
}
@@ -0,0 +1,209 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import okhttp3.ConnectionPool
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import org.apache.fineract.gradle.FineractPluginExtension.FineractPluginConfigConfluence
import org.jetbrains.annotations.NotNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.*
import java.util.concurrent.TimeUnit
class ConfluenceService {
private static final Logger log = LoggerFactory.getLogger(ConfluenceService.class)
private ConfluenceApi api
ConfluenceService(FineractPluginConfigConfluence config) {
def credentials = Credentials.basic(config.username, config.password)
final ConnectionPool connectionPool = new ConnectionPool(5, 60, TimeUnit.SECONDS);
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(log::debug)
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder okClient = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.addInterceptor(loggingInterceptor)
.addInterceptor(new Interceptor() {
@Override
Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
def request = chain.request()
request = request.newBuilder().header("Authorization", credentials).build()
return chain.proceed(request)
}
})
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(config.url)
.client(okClient.build())
.addConverterFactory(JacksonConverterFactory.create(mapper))
Retrofit retrofit = builder.build()
this.api = retrofit.create(ConfluenceApi.class)
}
ConfluenceContent getContent(String id) {
this.api.getContentById(id).execute().body()
}
ConfluenceContent createContent(ConfluenceContent content) {
this.api.postContent(content).execute().body()
}
ConfluenceContent updateContent(String id, ConfluenceContent content) {
this.api.putContent(id, content).execute().body()
}
ConfluenceResponse deleteContent(String id) {
this.api.deleteContentById(id).execute().body()
}
static class ConfluenceContent {
String id
String type
String title
String status
String self
List<ConfluenceParent> ancestors
ConfluenceSpace space
ConfluenceBody body
ConfluenceVersion version
ConfluenceHistory history
}
static class ConfluenceContentResult {
List<ConfluenceContent> results
private int start
private int limit
private int size
}
static class ConfluenceParent {
String id
String type
}
static class ConfluenceVersion {
ConfluenceCreatedBy by
Date when
String message
Integer number
}
static class ConfluenceCreatedBy {
String username
String displayName
}
static class ConfluenceHistory {
boolean latest
ConfluenceCreatedBy createdBy
ConfluenceVersion nextVersion
ConfluenceVersion previousVersion
ConfluenceVersion lastUpdated
}
static class ConfluenceSpace {
String key
String name
}
static class ConfluenceSpaceResult {
List<ConfluenceSpace> results
}
static class ConfluenceBody {
ConfluenceStorage storage
}
static class ConfluenceStorage {
String value
String representation
}
static class ConfluenceResponse {
int statusCode
String message
Map<String, ?> data
}
static interface ConfluenceApi {
@GET("content")
Call<ConfluenceContentResult> getContentResults();
@GET("content")
Call<ConfluenceContentResult> getContentBySpaceKeyAndTitle(final @Query("key") String key, final @Query("title") String title);
@GET("content/{id}/child/{type}")
Call<ConfluenceContentResult> getChildren(final @Path("id") String parentId, final @Path("type") String type, final @QueryMap Map<String, String> params);
@GET("content/{id}?expand=body.storage")
Call<ConfluenceContent> getContentById(final @Path("id") String id);
@POST("contentbody/convert/{to}")
Call<ConfluenceStorage> postContentConversion(final @Body ConfluenceStorage storage, final @Path("to") String convertToFormat);
@POST("content")
Call<ConfluenceContent> postContent(final @Body ConfluenceContent content);
@PUT("content/{id}")
Call<ConfluenceContent> putContent(final @Path("id") String id, final @Body ConfluenceContent content);
@POST("content")
void postContentWithCallback(final @Body ConfluenceContent content, final Callback<ConfluenceContent> callback);
@DELETE("content/{id}")
Call<ConfluenceResponse> deleteContentById(final @Path("id") String id);
@GET("space")
Call<ConfluenceSpaceResult> getSpaces();
@POST("space")
Call<ConfluenceSpace> createSpace(final @Body ConfluenceSpace space);
@POST("space/_private")
Call<ConfluenceSpace> createPrivateSpace(final @Body ConfluenceSpace space);
@GET("space/{spaceKey}/content/page")
Call<ConfluenceContentResult> getAllSpaceContent(final @Path("spaceKey") String spaceKey, final @QueryMap Map<String, String> params);
@GET("space/{spaceKey}/content/{type}")
Call<ConfluenceContentResult> getRootContentBySpaceKey(final @Path("spaceKey") String spaceKey, final @Path("type") String contentType);
}
}
// curl -u aleks:xxx -X POST -H 'Content-Type: application/json' -d '{"type":"page","title":"1.7.0 - Apache Fineract","space":{"key":"FINERACT"},"ancestors":[{"id":75974324}],"body":{"storage":{"value":"<p>This is <br/> a new page</p>","representation": "storage"}}}' https://cwiki.apache.org/confluence/content/ | python -mjson.tool
@@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import org.apache.fineract.gradle.FineractPluginExtension
import org.apache.fineract.gradle.FineractPluginExtension.FineractPluginEmailParams
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.mail.*
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
class EmailService {
private static final Logger log = LoggerFactory.getLogger(EmailService.class)
String username
String password
Properties properties
EmailService(FineractPluginExtension.FineractPluginConfigSmtp config) {
this.username = config.username
this.password = config.password
this.properties = new Properties()
this.properties.put("mail.smtp.host", config.host)
this.properties.put("mail.smtp.auth", "true")
this.properties.put("mail.smtp.starttls.enable", config.tls.toString())
if(config.ssl) {
this.properties.put("mail.smtp.port", "465")
this.properties.put("mail.smtp.socketFactory.port", "465");
this.properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
} else {
this.properties.put("mail.smtp.port", "587")
}
}
void send(FineractPluginEmailParams params) {
log.warn("Sending email: ${params.from} -> ${params.to} - ${params.subject}")
Session session = Session.getInstance(this.properties, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(getUsername(), getPassword())
}
})
try {
Message msg = new MimeMessage(session)
msg.setFrom(new InternetAddress(params.from, params.name))
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(params.to))
if(params.cc) {
msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(params.cc))
}
if(params.bcc) {
msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(params.bcc))
}
msg.setSubject(params.subject)
msg.setText(params.message);
Transport.send(msg);
log.info("Done")
} catch (MessagingException e) {
log.error(e.toString(), e)
}
}
}
@@ -0,0 +1,136 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import org.apache.fineract.gradle.FineractPluginExtension
import org.eclipse.jgit.annotations.NonNull
import org.eclipse.jgit.annotations.Nullable
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.CanceledException
import org.eclipse.jgit.lib.CommitBuilder
import org.eclipse.jgit.lib.GpgSigner
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.StoredConfig
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.RefSpec
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class GitService {
private static final Logger log = LoggerFactory.getLogger(GitService.class)
private Git git
private boolean dryRun
GitService(FineractPluginExtension.FineractPluginConfigGit config, FineractPluginExtension.FineractPluginConfigGpg gpgConfig) {
GpgSigner.setDefault(new GpgSigner() {
@Override
void sign(@NonNull CommitBuilder commit, @Nullable String gpgSigningKey, @NonNull PersonIdent committer, CredentialsProvider credentialsProvider) throws CanceledException {
log.warn("------------------------ KEY: ${gpgSigningKey} IDENT: ${committer}")
}
@Override
boolean canLocateSigningKey(@Nullable String gpgSigningKey, @NonNull PersonIdent committer, CredentialsProvider credentialsProvider) throws CanceledException {
log.warn("------------------------ KEY: ${gpgSigningKey} IDENT: ${committer}")
return false
}
});
Repository repository = new FileRepositoryBuilder()
.readEnvironment()
.setGitDir(new File(config.dir))
.findGitDir()
.build();
this.git = new Git(repository)
this.dryRun = config.dryRun
}
void clone(String url, String branch, String directory) {
def ref = "refs/heads/" + branch
Git.cloneRepository()
.setURI(url)
.setDirectory(new File(directory))
.setBranchesToClone(Arrays.asList(ref))
.setBranch(ref)
.call();
}
void pull(String directory) {
Git git = Git.open(new File(directory));
git.pull().call()
}
void commit(String directory) {
Git git = Git.open(new File(directory));
def status = git.status().call()
if(!status.isClean()) {
git.add().addFilepattern(".").call()
git.commit().setSign(true).setMessage("chore: Publish current docs").call()
}
def result = git.push().call()
}
void config(String directory, List<FineractPluginExtension.FineractPluginConfigGitSection> sections) {
Git git = Git.open(new File(directory));
StoredConfig gitConfig = git.getRepository().getConfig();
sections.forEach(section -> {
gitConfig.setString(section.section, section.subsection, section.name, section.value);
})
// gitConfig.setString("branch", "master", "merge", "refs/heads/master");
gitConfig.save();
}
void createTag(String name, String message) {
def ref = git.tag()
.setName(name)
.setMessage(message)
.setAnnotated(true)
.setForceUpdate(true)
.call()
try {
git.push().setPushTags().setForce(true).setDryRun(dryRun).call()
log.warn("Tag created: ${name} (${ref.name})")
} catch(Exception e) {
log.error(e.toString(), e)
}
}
// TODO: explicitly provide the directory
void removeTag(String name) {
git.tagDelete().setTags(name).call()
RefSpec refSpec = new RefSpec(":refs/tags/${name}");
git.push().setRefSpecs(refSpec).setRemote("origin").call()
log.warn("Tag deleted: ${name}")
}
}
@@ -0,0 +1,148 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import org.apache.fineract.gradle.FineractPluginExtension
import org.apache.fineract.gradle.FineractPluginExtension.FineractPluginConfigGpg
import org.bouncycastle.bcpg.ArmoredOutputStream
import org.bouncycastle.bcpg.BCPGOutputStream
import org.bouncycastle.gpg.keybox.BlobType
import org.bouncycastle.gpg.keybox.PublicKeyRingBlob
import org.bouncycastle.gpg.keybox.bc.BcKeyBox
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.GeneralSecurityException
import java.security.MessageDigest
import java.security.Security
class GpgService {
private static final Logger log = LoggerFactory.getLogger(GpgService.class)
private PGPPrivateKey privateKey;
private PGPPublicKey publicKey;
static {
Security.addProvider(new BouncyCastleProvider());
}
GpgService(FineractPluginConfigGpg config) {
// TODO: provide implementation for *.gpg key rings
def keyBox = config.publicKeyring.endsWith(".kbx") ? new BcKeyBox(new FileInputStream(config.publicKeyring)) : null
if(keyBox) {
for(def keyBlob : keyBox.getKeyBlobs()) {
switch (keyBlob.type) {
case BlobType.X509_BLOB:
break
case BlobType.OPEN_PGP_BLOB:
def publicKeyRing = (keyBlob as PublicKeyRingBlob).getPGPPublicKeyRing();
def iterator = publicKeyRing.getPublicKeys();
while (publicKey == null && iterator.hasNext()) {
def k = iterator.next();
if (k.isEncryptionKey()) {
def keyName = Long.toHexString(k.keyID).toUpperCase()
if(config.keyName.substring(config.keyName.length()-keyName.length()) == keyName) {
publicKey = k;
log.warn("Found: ${publicKey.getUserIDs().next()}")
break
} else {
log.warn("Key mismatch: ${config.keyName} <-> ${keyName}")
}
}
}
break
}
}
PGPSecretKeyRingCollection secretRingCollection = new PGPSecretKeyRingCollection(new FileInputStream(config.secretKeyring), new BcKeyFingerprintCalculator())
def secretKey = secretRingCollection.getSecretKey(publicKey.keyID)
if(secretKey!=null) {
this.privateKey = secretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build(config.password.toCharArray()))
}
} else {
log.warn("Could not open the public key ring.")
}
}
void sign(FineractPluginExtension.FineractPluginGpgParams params) {
InputStream is = new FileInputStream(params.file)
BCPGOutputStream os = new BCPGOutputStream(new ArmoredOutputStream(new FileOutputStream(params.file + ".asc")))
sign(is).encode(os)
os.close();
}
PGPSignature sign(InputStream is) throws IOException, PGPException, GeneralSecurityException {
PGPSignatureGenerator generator = new PGPSignatureGenerator(new BcPGPContentSignerBuilder(publicKey.getAlgorithm(), PGPUtil.SHA1));
generator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) >= 0) {
generator.update(buf, 0, len);
}
return generator.generate();
}
String md5(FineractPluginExtension.FineractPluginGpgParams params) {
return calc(new FileInputStream(params.file), MessageDigest.getInstance("MD5", BouncyCastleProvider.PROVIDER_NAME))
}
String sha512(FineractPluginExtension.FineractPluginGpgParams params) {
return calc(new FileInputStream(params.file), MessageDigest.getInstance("SHA-512", BouncyCastleProvider.PROVIDER_NAME))
}
private static String calc(InputStream is, MessageDigest digest) {
String output
int read
byte[] buffer = new byte[8192]
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read)
}
byte[] hash = digest.digest()
BigInteger bigInt = new BigInteger(1, hash)
output = bigInt.toString(16)
while ( output.length() < 32 ) {
output = "0"+output
}
} catch (Exception e) {
log.error(e.toString(), e)
return null
}
return output
}
}
@@ -0,0 +1,324 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import org.apache.fineract.gradle.FineractPluginExtension
import org.jetbrains.annotations.NotNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.*
import java.util.concurrent.TimeUnit
class JiraService {
private static final Logger log = LoggerFactory.getLogger(JiraService.class)
private JiraApi api
JiraService(FineractPluginExtension.FineractPluginConfigJira config) {
def credentials = Credentials.basic(config.username, config.password)
final ConnectionPool connectionPool = new ConnectionPool(5, 60, TimeUnit.SECONDS);
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(log::debug)
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder okClient = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.addInterceptor(loggingInterceptor)
.addInterceptor(new Interceptor() {
@Override
Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
def request = chain.request()
request = request.newBuilder().header("Authorization", credentials).build()
return chain.proceed(request)
}
})
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(config.url)
.client(okClient.build())
.addConverterFactory(JacksonConverterFactory.create(mapper))
Retrofit retrofit = builder.build()
this.api = retrofit.create(JiraApi.class)
}
Map<String, Object> serverInfo() {
return this.api.serverInfo.execute().body()
}
FineractPluginExtension.FineractPluginJiraParams search(FineractPluginExtension.FineractPluginJiraParams params) {
def query = [:]
query['jql'] = params.query
query['startAt'] = params.pageOffset?:0
query['maxResults'] = params.pageSize?:50
// query['expand'] = [
// "names",
// "schema",
// "operations"
// ]
// query['fields'] = params.fields?:"*all"
query['fields'] = params.includes?:[
"summary",
"status",
"assignee",
"fixVersions"
]
// params.total = params.total?:1000
params.total = 1000
def result = this.api.searchIssues(query).execute().body()
params.result.addAll(result?.issues as List)
if(result?.issues.size()>=params.pageSize && params.result.size() < params.total) {
params.pageOffset += params.pageSize
search(params);
}
return params
}
List getProjects() {
return this.api.getProjects()?.execute()?.body()
}
List getProjectVersions(String projectId) {
return this.api.getProjectVersions(projectId)?.execute()?.body()
}
JiraVersion getVersion(String id) {
return this.api.getVersion(id)?.execute()?.body()
}
void updateVersion(JiraVersion version) {
this.api.updateVersion(version.id, version).execute()
}
static class JiraVersion {
String self
String id
String description
String name
boolean archived
boolean released
String releaseDate
String userReleaseDate
String project
Integer projectId
}
static class JiraSearchResult {
String expand
Integer startAt
Integer maxResults
Integer total
List<JiraIssue> issues
List<String> warningMessages
Map<String, Object> names
Map<String, Object> schema
}
static class JiraIssue {
String expand
String self
String id
String key
Map<String, Object> renderedFields
Map<String, Object> properties
Map<String, Object> names
Map<String, Object> schema
Map<String, Object> fields
}
static class JiraIssueUpdateRequest {
Map<String, Map<String, Object>> historyMetadata
Map<String, List<Map<String, Object>>> update
Map<String, Object> fields
Map<String, List<Map<String, Object>>> properties
}
static interface JiraApi {
// Server Info
@GET("serverInfo")
Call<Map<String, Object>> getServerInfo()
// Components
@GET("component/{id}")
Call<Object> getComponent(@Path("id") String id)
@POST("component")
Call<Object> createComponent(@Body Object component)
@PUT("component/{id}")
Call<Void> updateComponent(@Path("id") String id, @Body Object component)
@GET("component/{id}/relatedIssueCounts")
Call<Object> getComponentIssueCount(@Path("id") String id)
// Issue
@GET("issue/{issueIdOrKey}")
Call<Object> getIssue(@Path("issueIdOrKey") String issueIdOrKey)
@POST("issue")
Call<Object> createIssue(@Body Object issue)
@PUT("issue/{issueIdOrKey}")
Call<Object> updateIssue(@Path("issueIdOrKey") String issueIdOrKey, @Body Object issue, @QueryMap Map<String, String> queryMap)
@POST("issue/bulk")
Call<Object> createIssues(@Body Object issues)
@PUT("issue/{issueIdOrKey}/assignee")
Call<Void> assignIssue(@Path("issueIdOrKey") String issueIdOrKey, @Body Object user)
@GET("issue/{issueIdOrKey}/comment")
Call<Object> getComments(@Path("issueIdOrKey") String issueIdOrKey)
@POST("issue/{issueIdOrKey}/comment")
Call<Object> addComment(@Path("issueIdOrKey") String issueIdOrKey, @Body Object comment)
@PUT("issue/{issueIdOrKey}/comment/{id}")
Call<Object> updateComment(@Path("issueIdOrKey") String issueIdOrKey, @Path("id") String id, @Body Object comment)
@GET("issue/{issueIdOrKey}/comment/{id}")
Call<Object> getComment(@Path("issueIdOrKey") String issueIdOrKey, @Path("id") String id)
@POST("issue/{issueIdOrKey}/notify")
Call<Void> notifyIssue(@Path("issueIdOrKey") String issueIdOrKey, @Body Object notify)
@GET("issue/{issueIdOrKey}/transitions?expand=transitions.fields")
Call<Object> getTransitions(@Path("issueIdOrKey") String issueIdOrKey)
@POST("issue/{issueIdOrKey}/transitions")
Call<Void> transitionIssue(@Path("issueIdOrKey") String issueIdOrKey, @Body Object issue)
@GET("issue/{issueIdOrKey}/watchers")
Call<Object> getIssueWatches(@Path("issueIdOrKey") String issueIdOrKey)
@POST("issue/{issueIdOrKey}/watchers")
Call<Void> addIssueWatcher(@Path("issueIdOrKey") String issueIdOrKey, @Body String user)
// Remote Issue Links
@GET("issue/{issueIdOrKey}/remotelink")
Call<Object> getIssueRemoteLinks(@Path("issueIdOrKey") String issueIdOrKey, @Query("globalId") String globalId)
@GET("issue/{issueIdOrKey}/remotelink/{linkId}")
Call<Object> getIssueRemoteLink(@Path("issueIdOrKey") String issueIdOrKey, @Path("linkId") String linkId)
@POST("issue/{issueIdOrKey}/remotelink")
Call<Object> createIssueRemoteLink(@Path("issueIdOrKey") String issueIdOrKey, @Body Object issueLink)
@POST("issue/{issueIdOrKey}/remotelink/{linkId}")
Call<Object> updateIssueRemoteLink(@Path("issueIdOrKey") String issueIdOrKey, @Path("linkId") String linkId, @Body Object issueLink)
@DELETE("issue/{issueIdOrKey}/remotelink")
Call<Object> deleteIssueRemoteLinks(@Path("issueIdOrKey") String issueIdOrKey, @Query("globalId") String globalId)
@DELETE("issue/{issueIdOrKey}/remotelink/{linkId}")
Call<Object> deleteIssueRemoteLink(@Path("issueIdOrKey") String issueIdOrKey, @Path("linkId") String linkId)
// Issue Links
@POST("issueLink")
Call<Void> createIssueLink(@Body Object issueLink)
@GET("issueLink/{linkId}")
Call<Object> getIssueLink(@Path("linkId") String linkId)
@DELETE("issueLink/{linkId}")
Call<Object> deleteIssueLink(@Path("linkId") String linkId)
// Issue Link Types
@GET("issueLinkType")
Call<Object> getIssueLinkTypes()
// Project
@GET("project?expand=lead,description")
Call<List> getProjects()
@GET("project/{projectIdOrKey}")
Call<Object> getProject(@Path("projectIdOrKey") String projectId)
@GET("project/{projectIdOrKey}/statuses")
Call<Object> getProjectStatuses(@Path("projectIdOrKey") String projectId)
@GET("project/{projectIdOrKey}/components")
Call<Object> getProjectComponents(@Path("projectIdOrKey") String projectId)
@GET("project/{projectIdOrKey}/versions")
Call<List<JiraVersion>> getProjectVersions(@Path("projectIdOrKey") String projectId)
// Search
@POST("search")
Call<JiraSearchResult> searchIssues(@Body Map<String, Object> search)
// Version
@POST("version")
Call<JiraVersion> createVersion(@Body JiraVersion version)
@GET("version/{id}")
Call<JiraVersion> getVersion(@Path("id") String id)
@PUT("version/{id}")
Call<Void> updateVersion(@Path("id") String id, @Body JiraVersion version)
// Fields
@GET("field")
Call<Object> getFields()
@POST("field")
Call<Object> createField(@Body Object field)
// Users
@GET("user/search")
Call<Object> userSearch(@Query("username") String userName, @Query("startAt") int startAt, @Query("maxResults") int maxResults)
@GET("user/assignable/search")
Call<Object> assignableUserSearch(@Query("username") String userName, @Query("project") String project, @Query("issueKey") String issueKey, @Query("startAt") int startAt, @Query("maxResults") int maxResults)
@Multipart
@retrofit2.http.Headers("X-Atlassian-Token: no-check")
@POST("issue/{issueIdOrKey}/attachments")
Call<Object> uploadAttachment(@Path("issueIdOrKey") String issueIdOrKey, @Part MultipartBody.Part file)
@GET("attachment/{attachmentId}")
Call<Object> getAttachment(@Path("attachmentId") String attachmentId)
@DELETE("attachment/{attachmentId}")
Call<Object> deleteAttachment(@Path("attachmentId") String attachmentId)
@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrl(@Url String fileUrl)
}
}
@@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import org.apache.fineract.gradle.FineractPluginExtension
import org.gradle.api.GradleException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.tmatesoft.svn.core.SVNCancelException
import org.tmatesoft.svn.core.SVNDepth
import org.tmatesoft.svn.core.SVNException
import org.tmatesoft.svn.core.SVNURL
import org.tmatesoft.svn.core.wc.*
class SubversionService {
private static final Logger log = LoggerFactory.getLogger(SubversionService.class)
private SVNUpdateClient client;
private String revision;
SubversionService(FineractPluginExtension.FineractPluginConfigSubversion config) {
ISVNOptions options = SVNWCUtil.createDefaultOptions(true)
SVNClientManager clientManager = SVNClientManager.newInstance(options, SVNWCUtil.createDefaultAuthenticationManager(config.username, config.password))
this.client = clientManager.updateClient;
this.client.ignoreExternals = false
this.client.eventHandler = new ISVNEventHandler() {
@Override
void handleEvent(SVNEvent event, double progress) throws SVNException {
println ">>> PROGRESS: ${progress>0?:'-'}"
}
@Override
void checkCancelled() throws SVNCancelException {
}
}
this.revision = config.revision
}
void checkout(FineractPluginExtension.FineractPluginSubversionParams params) {
SVNURL svnUrl = SVNURL.parseURIEncoded(params.url)
SVNRevision svnRevision = this.revision == null ? SVNRevision.HEAD : SVNRevision.parse(this.revision)
if (svnRevision == SVNRevision.UNDEFINED) {
throw new GradleException("Invalid SVN revision: " + this.revision)
}
this.client.doCheckout(svnUrl, new File(params.directory), SVNRevision.HEAD, svnRevision, SVNDepth.INFINITY, false)
}
void commit(FineractPluginExtension.FineractPluginSubversionParams params) {
// TODO: implement this
SVNURL svnUrl = SVNURL.parseURIEncoded(params.url)
}
}
@@ -0,0 +1,84 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.gradle.service
import freemarker.core.Environment
import freemarker.template.Configuration
import freemarker.template.Template
import freemarker.template.TemplateException
import freemarker.template.TemplateExceptionHandler
import org.apache.fineract.gradle.FineractPluginExtension
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class TemplateService {
private static final Logger log = LoggerFactory.getLogger(TemplateService.class)
private Configuration config
TemplateService(FineractPluginExtension.FineractPluginConfigTemplate config) {
def dir = new File(config.templateDir);
this.config = new Configuration(Configuration.VERSION_2_3_31);
this.config.setDirectoryForTemplateLoading(dir)
this.config.setDefaultEncoding("UTF-8");
this.config.setLogTemplateExceptions(false);
this.config.setWrapUncheckedExceptions(true);
this.config.setFallbackOnNullLoopVariable(false);
this.config.setTemplateExceptionHandler(new TemplateExceptionHandler() {
@Override
void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException {
if (!env.isInAttemptBlock()) {
PrintWriter pw = (out instanceof PrintWriter) ? (PrintWriter) out : new PrintWriter(out);
pw.print("FreeMarker template error\n");
te.printStackTrace(pw, false, true, false);
pw.flush(); // To commit the HTTP response
}
}}
)
}
FineractPluginExtension.FineractPluginTemplateParams render(FineractPluginExtension.FineractPluginTemplateParams params, Object data) {
Template template = null;
if(params.templateFile) {
template = new Template("template", new FileReader(new File(params.templateFile)), this.config)
}
if(params.template) {
template = new Template("template", new StringReader(params.template), this.config)
}
if(template) {
if(params.outputFile) {
def output = new File(params.outputFile)
output.createNewFile()
template.process(data, new FileWriter(output, false))
} else {
def output = new StringWriter()
template.process(data, output)
params.output = output.toString()
}
}
return params
}
}
@@ -0,0 +1,20 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
implementation-class=org.apache.fineract.gradle.FineractPlugin
@@ -0,0 +1,34 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<h1>Release Artifacts</h1>
<p>
<ac:structured-macro ac:macro-id="ac1cb3cb-84d4-45aa-820b-05cc42d39dd9" ac:name="jira" ac:schema-version="1">
<ac:parameter ac:name="server">ASF JIRA</ac:parameter>
<ac:parameter ac:name="columnIds">issuekey,summary,issuetype,created,updated,duedate,assignee,reporter,customfield_12311032,customfield_12311037,customfield_12311022,customfield_12311027,priority,status,resolution</ac:parameter>
<ac:parameter ac:name="columns">key,summary,type,created,updated,due,assignee,reporter,Priority,Priority,Priority,Priority,priority,status,resolution</ac:parameter>
<ac:parameter ac:name="maximumIssues">50</ac:parameter>
<ac:parameter ac:name="jqlQuery">project = FINERACT AND fixVersion = ${project['fineract.release.version']} AND status not in (Open) </ac:parameter>
<ac:parameter ac:name="serverId">5aa69414-a9e9-3523-82ec-879b028fb15b</ac:parameter>
</ac:structured-macro>
</p>
<p>
<br/>
</p>
@@ -0,0 +1,39 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Hello everyone,
... based on our "How to Release Apache Fineract" process documented at https://cwiki.apache.org/confluence/x/DRwIB:
I will create a ${project['fineract.release.version']} branch off develop in our git repository at https://github.com/apache/fineract on ${project['fineract.release.date']}.
The release tracking umbrella issue for tracking all activity in JIRA is FINERACT-${project['fineract.release.issue']!'0000'} (https://issues.apache.org/jira/browse/FINERACT-${project['fineract.release.issue']!'0000'}) for this Fineract ${project['fineract.release.version']}.
If you have any work in progress that you would like to see included in this release, please add "blocking" links to the release JIRA issue.
I am the release manager for this release.
Cheers,
${project['fineract.config.name']}
🎉 Powered by Fineract Release Plugin 🎊
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[FINERACT] [PROPOSAL] 📦 New release ${project['fineract.release.version']}
@@ -0,0 +1,35 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Hello everyone,
... as previously announced, I've just created the release branch for our upcoming ${project['fineract.release.version']} release.
You can continue working and merging PRs to the develop branch for future releases, as always.
The DRAFT release notes are on https://cwiki.apache.org/confluence/display/FINERACT/${project['fineract.release.version']}+-+Apache+Fineract. Does anyone see anything missing?
Does anyone have any last minutes changes they would like to see cherry-picked to branch ${project['fineract.release.version']}, or are we good go and actually cut the release based on this branch as it is?
I'll initiate the final stage of actually creating the release on ${project['fineract.release.date']} if nobody objects.
Cheers,
${project['fineract.config.name']}
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[FINERACT] [ANNOUNCE] 🔀 ${project['fineract.release.version']} release branch
@@ -0,0 +1,43 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Hello everyone,
... we have created Apache Fineract ${project['fineract.release.version']} release, with the artifacts below up for a vote.
It fixes the following issues: https://cwiki.apache.org/confluence/display/FINERACT/${project['fineract.release.version']}+-+Apache+Fineract
Source & Binary files : https://dist.apache.org/repos/dist/dev/fineract/${project['fineract.release.version']}/
Tag to be voted on (rc#): https://gitbox.apache.org/repos/asf?p=fineract.git;a=commit;h=refs/heads/${project['fineract.release.version']}
Fineract's KEYS containing the PGP key we used to sign the release: https://dist.apache.org/repos/dist/dev/fineract/KEYS
Note that this release contains source and binary artifacts.
This vote will be open for 72 hours:
[ ] +1 approve
[ ] +0 no opinion
[ ] -1 disapprove (and reason why)
Cheers,
${project['fineract.config.name']}
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[FINERACT] [VOTE] 🗳️ ${project['fineract.release.version']} for release
@@ -0,0 +1,89 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<#if (project['fineract.vote'].approve.binding?size + project['fineract.vote'].approve.nonBinding?size > project['fineract.vote'].disapprove.binding?size + project['fineract.vote'].disapprove.nonBinding?size)>
Voting is now closed and has passed with the following tally,
Binding +1s: ${project['fineract.vote'].approve.binding?size}
Non binding +1s: ${project['fineract.vote'].approve.nonBinding?size}
<#else>
Voting is now closed and has not passed with the following tally,
Binding +1s: ${project['fineract.vote'].approve.binding?size}
Non binding +1s: ${project['fineract.vote'].approve.nonBinding?size}
Binding -1s: ${project['fineract.vote'].disapprove.binding?size}
Non binding -1s: ${project['fineract.vote'].disapprove.nonBinding?size}
</#if>
Here are the detailed results:
<#list project['fineract.vote'].approve.binding>
Binding +1s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#list project['fineract.vote'].approve.nonBinding>
Non binding +1s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#list project['fineract.vote'].disapprove.binding>
Binding -1s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#list project['fineract.vote'].disapprove.nonBinding>
Non binding -1s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#list project['fineract.vote'].noOpinion.binding>
Binding +0s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#list project['fineract.vote'].noOpinion.nonBinding>
Non binding +0s:
<#items as item>
- ${item.name} (${item.email})
</#items>
</#list>
<#if (project['fineract.vote'].approve.binding?size + project['fineract.vote'].approve.nonBinding?size > project['fineract.vote'].disapprove.binding?size + project['fineract.vote'].disapprove.nonBinding?size)>
Thanks to everyone who voted! I'll now continue with the rest of the release process.
<#else>
Thanks to everyone who voted! Looks like we have to repeat the vote.
</#if>
${project['fineract.config.name']}
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[FINERACT] [VOTE] [RESULT] 🧾️ ${project['fineract.release.version']} for release
@@ -0,0 +1,43 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
The Apache Fineract project is pleased to announce
the release of Apache Fineract ${project['fineract.release.version']}.
The release is available for download from
https://fineract.apache.org/#downloads
Fineract provides a reliable, robust, and affordable solution for entrepreneurs,
financial institutions, and service providers to offer financial services to the
world’s 2 billion underbanked and unbanked. Fineract is aimed at innovative mobile
and cloud-based solutions, and enables digital transaction accounts for all.
This release addressed ${project['fineract.release.issues']?size} issues.
Readme: https://github.com/apache/fineract/blob/${project['fineract.release.version']}/README.md
Release page: https://cwiki.apache.org/confluence/display/FINERACT/${project['fineract.release.version']}+-+Apache+Fineract
List of fixed issues:
https://issues.apache.org/jira/secure/ReleaseNote.jspa?version=${project['fineract.release.versionId']}&styleName=Html&projectId=${project['fineract.release.projectId']}
For more information on Apache Fineract please visit
project home page: https://fineract.apache.org
The Apache Fineract Team
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[ANNOUNCE] Apache Fineract ${project['fineract.release.version']} Release
@@ -0,0 +1,21 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Fineract ${project['fineract.release.version']} release
@@ -0,0 +1,38 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[INSTRUCTIONS:START]
As discussed in https://issues.apache.org/jira/browse/FINERACT-1154, now that everything is final, please do the following to remove the release branch (and just keep the tag),
and make sure that everything on the release tag is merged to develop and that e.g. git describe works:
>> git checkout develop
>> git branch -D ${project['fineract.release.version']}
>> git push origin :${project['fineract.release.version']}
>> git checkout develop
>> git checkout -b merge-${project['fineract.release.version']}
>> git merge -s recursive -Xignore-all-space ${project['fineract.release.version']}
>> git commit
>> git push <USER>
>> hub pull-request
[INSTRUCTIONS:END]
@@ -0,0 +1,28 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[INSTRUCTIONS:START]
Finally update the https://fineract.apache.org website with the latest release details.
The website's HTML source code is available at https://github.com/apache/fineract-site.
[INSTRUCTIONS:END]
@@ -0,0 +1,39 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[INSTRUCTIONS:START]
Before a release is done, make sure that any issues that are fixed have their fix version setup correctly.
"project = FINERACT and resolution = fixed and fixVersion is empty"
Move all unresolved JIRA issues which have this release as Fix Version to the next release
"project = FINERACT and fixVersion = ${project['fineract.release.version']} and status not in ( Resolved, Done, Accepted, Closed )"
You can also run the following query to make sure that the issues fixed for the to-be-released version look accurate:
"project = FINERACT and fixVersion = ${project['fineract.release.version']}"
Finally, check out the output of the JIRA release note tool to see which tickets are included in the release, in order to do a sanity check.
[INSTRUCTIONS:END]
@@ -0,0 +1,29 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[INSTRUCTIONS:START]
You first need to close the release in JIRA so that the about to be released version cannot be used as "fixVersion" for new bugs anymore.
Go to JIRA "Administer project" page and follow "Versions" in left menu. Table with list of all releases should appear, click on additional menu on the right of your release and choose "Release" option.
Submit release date and you're done.
[INSTRUCTIONS:END]
@@ -0,0 +1,38 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
[INSTRUCTIONS:START]
Following are the typical things we need to verify before voting on a release candidate. And the release manager should verify them too before calling out a vote.
Make sure release artifacts are hosted at https://dist.apache.org/repos/dist/dev/fineract
* Release candidates should be in format apache-fineract-${project['fineract.release.version']}-binary.tar.gz
* Verify signatures and hashes. You may have to import the public key of the release manager to verify the signatures. (`gpg --recv-key <key id>`)
* Git tag matches the released bits (diff -rf)
* Can compile successfully from source
* Verify DISCLAIMER, NOTICE and LICENSE (year etc)
* All files have correct headers (Rat check should be clean - gradlew rat)
* No jar files in the source artifacts
* Integration tests should work
[INSTRUCTIONS:END]
@@ -0,0 +1,33 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
= Changelog
== Breaking Changes
<#list issues?filter(i -> i.fields.labels?seq_contains("BreakingChange")) as issue>
* [[${issue.key}]]${issue.key} - ${issue.fields.summary}
</#list>
== Issues
<#list issues as issue>
* [[${issue.key}]]${issue.key} - ${issue.fields.summary}
</#list>
@@ -0,0 +1,26 @@
<#--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
Changelog
=========
<#list issues as issue>
* ${issue.key} - ${issue.fields.summary}
</#list>
@@ -0,0 +1,22 @@
{
"approve": {
"binding": [
{
"name": "Some Body",
"email": "bob@apache.org"
}
]
},
"disapprove": {
"binding": [
],
"nonBinding": [
]
},
"noOpinion": {
"binding": [
],
"nonBinding": [
]
}
}