diff --git a/README.md b/README.md
index 6617d67f7..1fe9391e3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# Maven Builds Server
+# Maven and Gradle Builds Server
This is the repository of the backend for my builds-page.
The page can be found here: https://thebusybiscuit.github.io/builds/
-This kinda serves as a "Continous Integration/Deployment" Service for Maven Projects which utilises static GitHub Pages.
+This kinda serves as a "Continous Integration/Deployment" Service for Maven and Gradle Projects which utilises static GitHub Pages.
# Status
[](https://sonarcloud.io/dashboard?id=TheBusyBiscuit_builds)
@@ -26,10 +26,11 @@ After that it will proceed to step 2.
### 2. Cloning
After we established that our repository is out of date, this program will ```git clone``` said repository.
-It will also locate it's pom.xml file and set the version to "DEV - $id (git $commit)".
+It will also locate it's `pom.xml` or `gradle.properties` file and set the version to "DEV - $id (git $commit)".
### 3. Compiling
This is the easiest step, the program just runs ```mvn clean package -B``` to compile our Maven Project.
+If a Gradle project is detected, the program runs ```gradlew build``` to compile our Gradle Project.
It will also catch the state (Success / Failure).
If you enabled Sonar-Integration for this project, then it will also run a sonar-scan on the repository.
@@ -62,7 +63,7 @@ Note that many of these guidelines are requirements of technical nature.
1. They must be publicly available on GitHub and Open-Source.
2. They must have a valid `LICENSE` file with a permissive Open-Source license (e.g. MIT, Apache or GNU GPL or similar).
-3. They must have a valid `pom.xml` file.
+3. They must have a valid `pom.xml` or `gradle.properties` file.
4. They are not allowed to force auto-updates on people without providing an option to disable it.
### Example
@@ -73,10 +74,12 @@ Note that many of these guidelines are requirements of technical nature.
// Some repositories support the usage of sonar-scanner, custom repositories cannot have this feature though (yet)
"sonar": {
"enabled": false
- },
- // What the builds will be prefixed with. "DEV" would make builds like "CoolAddon - DEV 1 (githash)"
+ },
"options": {
- "prefix": "DEV"
+ // What the builds will be prefixed with. "DEV" would make builds like "CoolAddon - DEV 1 (githash)"
+ "prefix": "DEV",
+ // What type of build tool will be used, must be "maven" or "gradle"
+ "buildTool": "maven"
},
// What your addon supports/depends on. The number key indicates the minium build.
// You can list any text or even links here.
diff --git a/resources/repos.json b/resources/repos.json
index 40e0bd74a..c27d7f3a9 100644
--- a/resources/repos.json
+++ b/resources/repos.json
@@ -1062,7 +1062,6 @@
}
}
},
-
"John000708/SlimeXpansion:master": {
"sonar": {
"enabled": false
diff --git a/resources/schema.json b/resources/schema.json
index 00b7fa7ee..a193ae37f 100644
--- a/resources/schema.json
+++ b/resources/schema.json
@@ -34,7 +34,7 @@
},
"options": {
"type": "object",
- "required": ["prefix"],
+ "required": ["prefix", "buildTool"],
"additionalProperties": false,
"properties": {
@@ -49,6 +49,9 @@
},
"createJar": {
"type": "boolean"
+ },
+ "buildTool": {
+ "type": "string"
}
}
},
diff --git a/src/gradle.js b/src/gradle.js
new file mode 100644
index 000000000..1f73df9ca
--- /dev/null
+++ b/src/gradle.js
@@ -0,0 +1,130 @@
+const process = require('child-process-promise')
+const lodash = require('lodash/lang')
+
+const FileSystem = require('fs')
+const fs = FileSystem.promises
+const path = require('path')
+
+const log = require('../src/logger.js')
+const projects = require('../src/projects.js')
+
+module.exports = {
+ getGradleArguments,
+ setVersion,
+ compile,
+ relocate,
+ isValid
+}
+
+/**
+ * This will return the console line arguments for gradle.compile()
+ *
+ * @return {Array} The needed console line arguments
+ */
+function getGradleArguments () {
+ return ['build']
+}
+
+/**
+ * This method changes the project's version in your gradle.properties file
+ * It also returns a Promise that resolves when it's done.
+ *
+ * @param {Object} job The currently handled Job Object
+ * @param {String} version The Version that shall be set
+ */
+function setVersion (job, version) {
+ return new Promise((resolve, reject) => {
+ if (!isValid(job)) {
+ reject(new Error('Invalid Job'))
+ return
+ }
+ const file = path.resolve(__dirname, `../${job.directory}/files/gradle.properties`)
+ if (!FileSystem.existsSync(file)) {
+ fs.writeFile(file, '\nversion=' + version, 'utf8').then(resolve, reject)
+ } else {
+ fs.readFile(file, 'utf8').then((data) => {
+ const content = data.split('\n')
+ const result = []
+ let line
+ for (line in content) {
+ if (!line.includes('version=')) {
+ result.push(line)
+ }
+ }
+ result.push('\nversion=' + version)
+ fs.writeFile(file, result, 'utf8').then(resolve, reject)
+ }, reject)
+ }
+ })
+}
+
+/**
+ * This method will compile a project using the command
+ * 'gradlew build'
+ *
+ * @param {Object} job The currently handled Job Object
+ * @param {Boolean} logging Whether the internal activity should be logged
+ * @return {Promise} A promise that resolves when this activity finished
+ */
+function compile (job, logging) {
+ return new Promise((resolve, reject) => {
+ if (!isValid(job)) {
+ reject(new Error('Invalid Job'))
+ return
+ }
+
+ log(logging, '-> Granting gradlew +x permissions')
+
+ process.spawn('chmod', ['+x', 'gradlew'], {
+ cwd: path.resolve(__dirname, `../${job.directory}/files`),
+ shell: true
+ })
+
+ log(logging, "-> Executing './gradlew build'")
+
+ const args = getGradleArguments()
+ const compiler = process.spawn('./gradlew', args, {
+ cwd: path.resolve(__dirname, `../${job.directory}/files`),
+ shell: true
+ })
+
+ const logger = (data) => {
+ log(logging, data, true)
+ fs.appendFile(path.resolve(__dirname, `../${job.directory}/${job.repo}-${job.id}.log`), data, 'utf8').catch(err => console.log(err))
+ }
+
+ compiler.childProcess.stdout.on('data', logger)
+ compiler.childProcess.stderr.on('data', logger)
+
+ compiler.then(resolve, reject)
+ })
+}
+
+/**
+ * This method will relocate a project's compiled jar file
+ * to the appropriate directory
+ *
+ * @param {Object} job The currently handled Job Object
+ * @return {Promise} A promise that resolves when this activity finished
+ */
+function relocate (job) {
+ if (!job.success) {
+ return Promise.resolve()
+ }
+ return fs.rename(
+ path.resolve(__dirname, `../${job.directory}/files/build/libs/${job.repo}-${(job.options ? job.options.prefix : 'DEV')} - ${job.id} (git ${job.commit.sha.substr(0, 8)}).jar`),
+ path.resolve(__dirname, `../${job.directory}/${job.repo}-${job.id}.jar`)
+ )
+}
+
+/**
+ * This method will check if a Job is valid.
+ * null / undefined or incomplete Job Objects will fail.
+ *
+ * @param {Object} job The job object to be tested
+ * @return {Boolean} Whether the job is a valid Job
+ */
+function isValid (job) {
+ if (!projects.isValid(job)) return false
+ return lodash.isInteger(job.id)
+}
diff --git a/src/main.js b/src/main.js
index f38c04947..3d1fb7523 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,6 +5,7 @@ const cfg = require('../src/config.js')(path.resolve(__dirname, '../resources/co
// Modules
const projects = require('../src/projects.js')
const maven = require('../src/maven.js')
+const gradle = require('../src/gradle.js')
const github = require('../src/github.js')(cfg.github)
const discord = require('../src/discord.js')(cfg.discord)
const log = require('../src/logger.js')
@@ -149,13 +150,18 @@ function update (job, logging) {
github.clone(job, job.commit.sha, logging).then(() => {
const name = (job.options ? job.options.prefix : 'DEV') + ' - ' + job.id + ' (git ' + job.commit.sha.substr(0, 8) + ')'
- maven.setVersion(job, name, true).then(resolve, reject)
+ log(logging, `-> Building using: ${job.options.buildTool === null ? 'maven' : job.options.buildTool}`)
+ if (!job.options.buildTool || job.options.buildTool === 'maven') {
+ maven.setVersion(job, name, true).then(resolve, reject)
+ } else {
+ gradle.setVersion(job, name).then(resolve, reject)
+ }
}, reject)
})
}
/**
- * This method compiles the project using Maven.
+ * This method compiles the project using Maven or Gradle depending on job.config.buildTool.
* After completing, the job update will have the flag 'success',
* that is either true or false.
*
@@ -175,18 +181,32 @@ function compile (job, logging) {
updateStatus(job, 'Compiling')
return new Promise((resolve) => {
- log(logging, 'Compiling: ' + job.author + '/' + job.repo + ':' + job.branch + ' (' + job.id + ')')
-
- maven.compile(job, cfg, logging)
- .then(() => {
- job.success = true
- resolve()
- })
- .catch((err) => {
- log(logging, err.stack)
- job.success = false
- resolve()
- })
+ if (!job.options.buildTool || job.options.buildTool === 'maven') {
+ log(logging, `Compiling using Maven: ${job.author}/${job.repo}:${job.branch} (${job.id})`)
+
+ maven.compile(job, cfg, logging)
+ .then(() => {
+ job.success = true
+ resolve()
+ })
+ .catch((err) => {
+ log(logging, err.stack)
+ job.success = false
+ resolve()
+ })
+ } else {
+ log(logging, `Compiling using Gradle: ${job.author}/${job.repo}:${job.branch} (${job.id})`)
+ gradle.compile(job, logging)
+ .then(() => {
+ job.success = true
+ resolve()
+ })
+ .catch((err) => {
+ log(logging, err.stack)
+ job.success = false
+ resolve()
+ })
+ }
})
}
@@ -211,12 +231,16 @@ function gatherResources (job, logging) {
return new Promise((resolve, reject) => {
log(logging, 'Gathering Resources: ' + job.author + '/' + job.repo + ':' + job.branch)
-
- Promise.all([
+ const promises = [
github.getLicense(job, logging),
- github.getTags(job, logging),
- maven.relocate(job)
- ]).then((values) => {
+ github.getTags(job, logging)
+ ]
+ if (job.options.buildTool === 'maven') {
+ promises.push(maven.relocate(job))
+ } else {
+ promises.push(gradle.relocate(job))
+ }
+ Promise.all(promises).then((values) => {
const license = values[0]
const tags = values[1]
diff --git a/src/projects.js b/src/projects.js
index ab7c3c8a7..69491b7bc 100644
--- a/src/projects.js
+++ b/src/projects.js
@@ -40,7 +40,6 @@ function getProjects (logging) {
if (json[repo].options) {
job.options = json[repo].options
-
if (json[repo].options.custom_directory) {
job.directory = json[repo].options.custom_directory
}
diff --git a/src/setup.sh b/src/setup.sh
index d5ace91f5..526465bcd 100644
--- a/src/setup.sh
+++ b/src/setup.sh
@@ -1,4 +1,4 @@
git config user.name "TheBusyBot"
git config user.email ${LOGIN_EMAIL}
-git remote set-url origin https://${ACCESS_TOKEN}@github.com/TheBusyBiscuit/builds.git
\ No newline at end of file
+git remote set-url origin https://${ACCESS_TOKEN}@github.com/TheBusyBiscuit/builds.git
diff --git a/test/TestGradle.js b/test/TestGradle.js
new file mode 100644
index 000000000..276a8c315
--- /dev/null
+++ b/test/TestGradle.js
@@ -0,0 +1,93 @@
+const chai = require('chai')
+chai.use(require('chai-as-promised'))
+const { assert } = chai
+const FileSystem = require('fs')
+const path = require('path')
+const fs = FileSystem.promises
+
+const gradle = require('../src/gradle.js')
+const testJobs = require('../test/TestJobs.js')
+const fakeJob = {
+ author: 'TheBusyBiscuit',
+ repo: 'builds',
+ branch: 'master',
+ id: 1,
+ success: false,
+ directory: '.'
+}
+
+describe('Gradle Test', () => {
+
+ it('should do nothing but resolve when relocating a failed Job', () =>
+ assert.isFulfilled(gradle.relocate(fakeJob))
+ )
+
+ describe('gradle.properties Tests', () => {
+ it('should create a gradle.properties file with the right version when there isn\'t one', () => {
+ fs.mkdir(path.resolve(__dirname, '../' + fakeJob.directory + '/files')).then()
+ assert.isFulfilled(gradle.setVersion(fakeJob, '1.1'))
+ fs.readFile(path.resolve(__dirname, '../' + fakeJob.directory + '/files/gradle.properties'), 'utf8').then((data) => assert.equal(data, '\nversion=1.1'))
+ })
+ it('should edit the version of a gradle.properties file', () => {
+ assert.isFulfilled(gradle.setVersion(fakeJob, '1.2'))
+ fs.readFile(path.resolve(__dirname, '../' + fakeJob.directory + '/files/gradle.properties'), 'utf8').then((data) => assert.equal(data, '\nversion=1.2'))
+ })
+ })
+
+ describe('Job Validator', () => {
+ it('should return false for an invalid Job (null)', () => {
+ return assert.isFalse(gradle.isValid(null))
+ })
+
+ it('should return false for an invalid Job (undefined)', () => {
+ return assert.isFalse(gradle.isValid(undefined))
+ })
+
+ it('should return false for an invalid Job (String)', () => {
+ return assert.isFalse(gradle.isValid('This will not work'))
+ })
+
+ it('should return false for an invalid Job (Array)', () => {
+ return assert.isFalse(gradle.isValid([]))
+ })
+
+ it('should return false for an invalid Job (Missing parameter)', () => {
+ return assert.isFalse(gradle.isValid({ repo: 'Nope' }))
+ })
+
+ it('should return false for an invalid Job (Missing parameter)', () => {
+ return assert.isFalse(gradle.isValid({ author: 'Nope' }))
+ })
+
+ it('should return false for an invalid Job (Missing parameter)', () => {
+ return assert.isFalse(gradle.isValid({ branch: 'Nope' }))
+ })
+
+ it('should return false for an invalid Job (parameter of wrong Type)', () => {
+ return assert.isFalse(gradle.isValid({ author: 'Hi', repo: 2, branch: 'master', id: 'lol' }))
+ })
+
+ it('should return false for an invalid Job (parameter of wrong Type)', () => {
+ return assert.isFalse(gradle.isValid({ author: 'Hi', repo: 'Nope', branch: 'master', id: 'lol' }))
+ })
+
+ it('should return true for a valid Job', () => {
+ return assert.isTrue(gradle.isValid({
+ author: 'TheBusyBiscuit',
+ repo: 'builds',
+ branch: 'master',
+ directory: 'TheBusyBiscuit/builds/master',
+ id: 1,
+ success: false
+ }))
+ })
+ })
+
+ describe('Gradle Test: \'compile\'', () => {
+ testJobs(false, (fakeJob) => gradle.compile(fakeJob, true))
+ })
+
+ describe('Gradle Test: \'setVersion\'', () => {
+ testJobs(false, (fakeJob) => gradle.setVersion(fakeJob, '1'))
+ })
+})
diff --git a/test/TestGradleSystem.js b/test/TestGradleSystem.js
new file mode 100644
index 000000000..e802fa6e7
--- /dev/null
+++ b/test/TestGradleSystem.js
@@ -0,0 +1,175 @@
+const system = require('../src/main.js')
+const path = require('path')
+const FileSystem = require('fs')
+const fs = FileSystem.promises
+const projects = require('../src/projects.js')
+
+const chai = require('chai')
+chai.use(require('chai-as-promised'))
+const { assert } = chai
+
+const testJobs = require('../test/TestJobs.js')
+
+// A public sample Gradle project
+var job = {
+ author: 'jitpack',
+ repo: 'gradle-simple',
+ branch: 'master',
+ directory: 'jitpack/gradle-simple/master',
+ options: { buildTool: 'gradle', prefix: 'TEST' }
+}
+
+describe('Full Gradle System Test', function () {
+ this.timeout(60000)
+
+ before(() => {
+ global.status = {
+ task: {},
+ running: true
+ }
+ })
+
+ before(cleanup)
+
+ it('has a valid Config', () => assert.isNotNull(system.getConfig()))
+
+ it('passes stage \'check\' (getLatestCommit & hasUpdate)', () =>
+ system.check(job).then(() => Promise.all([
+ assert.exists(job.id),
+ assert.exists(job.commit),
+ assert.isObject(job.commit),
+ assert.exists(job.commit.sha),
+ assert.exists(job.commit.timestamp),
+ assert.exists(job.commit.date)
+ ]))
+ )
+
+ it('passes stage \'update\' (clone & setVersion)', () =>
+ assert.isFulfilled(system.update(job, true))
+ )
+
+ it('passes stage \'compile\' (compile)', () =>
+ // Writes into settings.gradle since the example repo didn't have one
+ fs.writeFile(path.resolve(__dirname, '../' + job.directory + '/files/settings.gradle'), `rootProject.name = 'gradle-simple'`, 'utf8').then(() =>
+ system.compile(job, true).then(() => Promise.all([
+ assert.exists(job.success),
+ assert.isTrue(job.success),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/files/build/libs/' + job.repo + '-' + (job.options ? job.options.prefix : 'DEV') + ' - ' + job.id + ' (git ' + job.commit.sha.substr(0, 8) + ')' + '.jar')))
+ ]))
+ )
+ )
+
+ it('passes stage \'gatherResources\' (getLicense & getTags & relocate)', () =>
+ system.gatherResources(job, true).then(() => Promise.all([
+ assert.exists(job.license),
+ assert.isObject(job.license),
+ assert.exists(job.tags),
+ assert.isObject(job.tags),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/' + job.repo + '-' + job.id + '.jar')))
+ ]))
+ )
+
+ it('passes stage \'upload\' - first build (addBuild & generateHTML & generateBadge)', async () => {
+ await system.upload(job)
+ return Promise.all([
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/builds.json'))),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/index.html'))),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/badge.svg')))
+ ])
+ })
+
+ it('passes stage \'upload\' - second build (addBuild & generateHTML & generateBadge)', async () => {
+ job.id = 2
+ job.success = false
+ job.tags = {}
+ job.tags['1.0'] = job.commit.sha
+
+ await system.upload(job)
+ return Promise.all([
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/builds.json'))),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/index.html'))),
+ assert.isTrue(FileSystem.existsSync(path.resolve(__dirname, '../' + job.directory + '/badge.svg')))
+ ])
+ })
+
+ it('properly communicates status', () =>
+ assert.strictEqual(global.status.task[job.directory], 'Preparing Upload')
+ )
+
+ it('can handle failed builds', () =>
+ FileSystem.promises.writeFile(path.resolve(__dirname, '../' + job.directory + '/files/src/main/java/Hello.java'), 'This will not compile.', 'utf8').then(() =>
+ system.compile(job, true).then(() => Promise.all([
+ assert.exists(job.success),
+ assert.isFalse(job.success)
+ ]))
+ )
+ )
+
+ describe('Job Validator', () => {
+ describe('Stage \'check\'', () => {
+ testJobs(false, (job) => system.check(job))
+ })
+ describe('Stage \'update\'', () => {
+ testJobs(false, (job) => system.update(job))
+ })
+ describe('Stage \'compile\'', () => {
+ testJobs(false, (job) => system.compile(job))
+ })
+ describe('Stage \'gatherResources\'', () => {
+ testJobs(true, (job) => system.gatherResources(job))
+ })
+ describe('Stage \'upload\'', () => {
+ testJobs(true, (job) => system.upload(job))
+ })
+ describe('Stage \'finish\'', () => {
+ testJobs(true, (job) => system.finish(job))
+ })
+ })
+
+ describe('global.status.running = false', () => {
+ it('will report stage \'start\' as successful', () => {
+ global.status.running = false
+ return assert.isFulfilled(system.start())
+ })
+
+ it('will abort stage \'check\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.check())
+ })
+
+ it('will abort stage \'update\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.update())
+ })
+
+ it('will abort stage \'compile\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.compile())
+ })
+
+ it('will abort stage \'gatherResources\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.gatherResources())
+ })
+
+ it('will abort stage \'upload\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.upload())
+ })
+
+ it('will abort stage \'finish\'', () => {
+ global.status.running = false
+ return assert.isRejected(system.finish())
+ })
+
+ })
+
+ after(cleanup)
+})
+
+function cleanup () {
+ let file = path.resolve(__dirname, '../' + job.author)
+
+ if (!FileSystem.existsSync(file)) return Promise.resolve()
+ else return projects.clearFolder(file)
+}
diff --git a/test/TestSystem.js b/test/TestMavenSystem.js
similarity index 98%
rename from test/TestSystem.js
rename to test/TestMavenSystem.js
index 1f8988e52..0db9cf73b 100644
--- a/test/TestSystem.js
+++ b/test/TestMavenSystem.js
@@ -17,7 +17,9 @@ var job = {
directory: "jitpack/maven-simple/master"
}
-describe("Full System Test", function() {
+describe("Full Maven System Test", function() {
+ job.options = Object;
+ job.options.buildTool = 'maven';
this.timeout(60000);
before(() => {