diff --git a/electron-app/icons/icon.icns b/electron-app/icons/icon.icns index b8bcc71d..213fb036 100644 Binary files a/electron-app/icons/icon.icns and b/electron-app/icons/icon.icns differ diff --git a/electron-app/icons/icon.ico b/electron-app/icons/icon.ico index e13b4442..a8bc336d 100644 Binary files a/electron-app/icons/icon.ico and b/electron-app/icons/icon.ico differ diff --git a/electron-app/icons/icon.png b/electron-app/icons/icon.png new file mode 100644 index 00000000..b61db3ff Binary files /dev/null and b/electron-app/icons/icon.png differ diff --git a/jenkins/Docker/Dockerfile b/jenkins/Docker/Dockerfile new file mode 100644 index 00000000..2376305d --- /dev/null +++ b/jenkins/Docker/Dockerfile @@ -0,0 +1,6 @@ +FROM electronuserland/builder:wine-03.18 + +RUN mkdir /hostHome +RUN apt-get update && apt-get install -y libusb-1.0 nasm graphicsmagick autoconf automake libtool python-pip +RUN pip install awscli --upgrade --user +ENV PATH "$PATH:/root/.local/bin" diff --git a/jenkins/Jenkinsfile b/jenkins/Jenkinsfile new file mode 100644 index 00000000..fad1505b --- /dev/null +++ b/jenkins/Jenkinsfile @@ -0,0 +1,26 @@ +pipeline { + agent { + dockerfile { + filename 'Dockerfile' + dir 'jenkins/Docker' + args '--env ETH_SIGNING_KEY=$ETH_SIGNING_KEY --env S3_BUCKET_NAME=$S3_BUCKET_NAME' + } + } + stages { + stage('Build') { + environment { + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES = 1 + } + steps { + sh 'rm -rf node_modules' + sh 'npm install' + sh 'npm run jenkins:build:linux' + } + } + stage('Upload') { + steps { + sh 'npm run jenkins:upload' + } + } + } +} \ No newline at end of file diff --git a/jenkins/constants.js b/jenkins/constants.js new file mode 100644 index 00000000..8758cc06 --- /dev/null +++ b/jenkins/constants.js @@ -0,0 +1,33 @@ +const VERSION = require('../package.json').version; +const GIT_COMMIT = process.env.GIT_COMMIT || 'commit-not-set'; +const GIT_COMMIT_SHORT = GIT_COMMIT.substring(0, 7); +const JENKINS_BUILD_ID = process.env.BUILD_ID; +const LINUX_FILES = [`MyCrypto-${VERSION}-i386.AppImage`, `MyCrypto-${VERSION}-x86_64.AppImage`]; +const WINDOWS_FILES = [`MyCrypto Setup ${VERSION}.exe`, `MyCrypto Setup ${VERSION}.exe.blockmap`]; +const OSX_FILES = []; +const FLAVOR = (() => { + const { platform } = process; + + if (platform === 'linux') { + return 'linux-windows'; + } else if (platform === 'darwin') { + return 'mac'; + } else { + throw new Error('Unsupported platform.'); + } +})(); +const S3_BUCKET = process.env.S3_BUCKET_NAME; +const ETH_SIGNING_KEY = process.env.ETH_SIGNING_KEY; + +module.exports = { + VERSION, + GIT_COMMIT, + GIT_COMMIT_SHORT, + JENKINS_BUILD_ID, + LINUX_FILES, + WINDOWS_FILES, + OSX_FILES, + FLAVOR, + S3_BUCKET, + ETH_SIGNING_KEY +}; diff --git a/jenkins/lib.js b/jenkins/lib.js new file mode 100644 index 00000000..ff5d144c --- /dev/null +++ b/jenkins/lib.js @@ -0,0 +1,112 @@ +const path = require('path'); +const { createHash } = require('crypto'); +const { readFileSync } = require('fs'); +const { spawn } = require('child_process'); + +const { hashPersonalMessage, ecsign, toBuffer, addHexPrefix } = require('ethereumjs-util'); + +const genCommitFilename = (name, version, commit, buildId) => { + const split = name.split(version); + return `${split[0]}${version}-${commit}-${buildId}${split[1]}`; +}; + +const genFileList = (linux, windows, osx) => { + const { platform } = process; + if (platform === 'linux') { + return [...linux, ...windows]; + } else if (platform === 'darwin') { + return [...osx]; + } else { + throw new Error('Unrecognized host platform.'); + } +}; + +const genSha512 = filePath => { + const hash = createHash('sha512'); + const data = readFileSync(filePath); + hash.update(data); + return hash.digest('hex'); +}; + +const runChildProcess = cmd => + new Promise((resolve, reject) => { + const child = spawn('sh', ['-c', cmd]); + + child.stdout.on('data', data => { + process.stdout.write(data); + }); + + child.stderr.on('data', data => { + process.stderr.write(data); + }); + + child.on('close', code => { + if (code !== 0) { + return reject(`Child process exited with code: ${code}`); + } + resolve(); + }); + }); + +const uploadToS3 = (localFilePath, s3FilePath) => + runChildProcess(`aws s3 cp "${localFilePath}" "${s3FilePath}"`); + +const genS3Url = (filename, commit, bucket) => `s3://${bucket}/${commit}/${filename}`; + +const genManifestFile = manifest => + manifest.map(info => ({ + Filename: info.commitFilename, + SHA512: info.fileHash + })); + +const genManifestFilename = (flavor, version, commit, buildId) => + `manifest.${flavor}.v${version}.${commit}.${buildId}.json`; + +const genSignatureFile = (manifestHash, pKeyString) => { + const pKeyBuffer = Buffer.from(pKeyString, 'hex'); + return signMessageWithPrivKeyV2(pKeyBuffer, manifestHash); +}; + +const genSignatureFilename = (flavor, version, commit, buildId) => + `manifest.${flavor}.v${version}.${commit}.${buildId}.signature`; + +const genManifest = (fileList, version, jenkinsBuildId, gitCommit, gitCommitShort, s3Bucket) => + fileList.map(filename => { + const fullPath = path.resolve('dist/electron-builds/', filename); + const commitFilename = genCommitFilename(filename, version, gitCommitShort, jenkinsBuildId); + + return { + fullPath, + filename, + commitFilename, + fileHash: genSha512(fullPath), + s3Url: genS3Url(commitFilename, gitCommit, s3Bucket) + }; + }); + +function signMessageWithPrivKeyV2(privKey, msg) { + const hash = hashPersonalMessage(toBuffer(msg)); + const signed = ecsign(hash, privKey); + const combined = Buffer.concat([ + Buffer.from(signed.r), + Buffer.from(signed.s), + Buffer.from([signed.v]) + ]); + const combinedHex = combined.toString('hex'); + + return addHexPrefix(combinedHex); +} + +module.exports = { + genCommitFilename, + genManifestFile, + genFileList, + genSha512, + genS3Url, + uploadToS3, + signMessageWithPrivKeyV2, + genManifestFilename, + genManifest, + genSignatureFile, + genSignatureFilename +}; diff --git a/jenkins/upload.js b/jenkins/upload.js new file mode 100644 index 00000000..163a1c25 --- /dev/null +++ b/jenkins/upload.js @@ -0,0 +1,68 @@ +const path = require('path'); +const { writeFileSync } = require('fs'); + +const { + VERSION, + FLAVOR, + GIT_COMMIT, + GIT_COMMIT_SHORT, + LINUX_FILES, + WINDOWS_FILES, + OSX_FILES, + S3_BUCKET, + JENKINS_BUILD_ID, + ETH_SIGNING_KEY +} = require('./constants'); + +const { + genSha512, + genFileList, + genCommitFilename, + genS3Url, + genManifest, + genManifestFile, + genManifestFilename, + genSignatureFile, + genSignatureFilename, + uploadToS3 +} = require('./lib'); + +const fileList = genFileList(WINDOWS_FILES, LINUX_FILES, OSX_FILES); + +const manifest = genManifest( + fileList, + VERSION, + JENKINS_BUILD_ID, + GIT_COMMIT, + GIT_COMMIT_SHORT, + S3_BUCKET +); + +const manifestFile = genManifestFile(manifest); +const manifestFilename = genManifestFilename(FLAVOR, VERSION, GIT_COMMIT_SHORT, JENKINS_BUILD_ID); +const manifestFilePath = path.resolve(`./${manifestFilename}`); +const manifestS3Url = genS3Url(manifestFilename, GIT_COMMIT, S3_BUCKET); + +// write manifest file +writeFileSync(manifestFilename, JSON.stringify(manifestFile, null, 2), 'utf8'); + +const manifestHash = genSha512(manifestFilePath); + +const signatureFile = genSignatureFile(manifestHash, ETH_SIGNING_KEY); +const signatureFilename = genSignatureFilename(FLAVOR, VERSION, GIT_COMMIT_SHORT, JENKINS_BUILD_ID); +const signatureFilePath = path.resolve(`./${signatureFilename}`); +const signatureS3Url = genS3Url(signatureFilename, GIT_COMMIT, S3_BUCKET); + +// write signature file +writeFileSync(signatureFilePath, signatureFile, 'utf8'); + +// upload all the things to S3 +(async () => { + for (let fileInfo of manifest) { + const { fullPath, s3Url } = fileInfo; + await uploadToS3(fullPath, s3Url); + } + + await uploadToS3(manifestFilePath, manifestS3Url); + await uploadToS3(signatureFilePath, signatureS3Url); +})(); diff --git a/package.json b/package.json index 0be4baf3..51c34811 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,9 @@ "build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js", "build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js", "prebuild:electron": "check-node-version --package", + "jenkins:build:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=JENKINS_LINUX node webpack_config/buildElectron.js", + "jenkins:build:mac": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=JENKINS_MAC node webpack_config/buildElectron.js", + "jenkins:upload": "node jenkins/upload", "test:coverage": "jest --config=jest_config/jest.config.json --coverage", "test": "jest --config=jest_config/jest.config.json", "test:unit": "jest --config=jest_config/jest.config.json --coverage", diff --git a/webpack_config/buildElectron.js b/webpack_config/buildElectron.js index 73bc0641..b9ab4aae 100644 --- a/webpack_config/buildElectron.js +++ b/webpack_config/buildElectron.js @@ -6,7 +6,15 @@ const builder = require('electron-builder'); const config = require('./config'); function shouldBuildOs(os) { - return !process.env.ELECTRON_OS || process.env.ELECTRON_OS === os; + const { ELECTRON_OS } = process.env; + + if (ELECTRON_OS === 'JENKINS_LINUX') { + return os === 'linux' || os === 'windows'; + } else if (ELECTRON_OS === 'JENKINS_MAC') { + return os === 'mac'; + } else { + return !process.env.ELECTRON_OS || process.env.ELECTRON_OS === os; + } } async function build() { @@ -36,7 +44,7 @@ async function build() { productName: 'MyCrypto', directories: { app: jsBuildDir, - output: electronBuildsDir, + output: electronBuildsDir }, mac: { category: 'public.app-category.finance', @@ -49,14 +57,11 @@ async function build() { }, linux: { category: 'Finance', + icon: path.join(config.path.electron, 'icons/icon.png'), compression }, - publish: { - provider: 'github', - owner: 'MyCryptoHQ', - repo: 'MyCrypto', - vPrefixedTagName: false - }, + // IMPORTANT: Prevents from auto publishing to GitHub in CI environments + publish: null, // IMPORTANT: Prevents extending configs in node_modules extends: null }