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 [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=TheBusyBiscuit_builds&metric=sqale_rating)](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(() => {