diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..e2e2729228 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,179 @@ +name: Release Nightly + +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: + inputs: + tag_name: + description: 'Tag name for release' + required: false + default: nightly + push: + tags: ['v[0-9]+.[0-9]+.[0-9]+*'] + pull_request: + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + tagname: + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.tag.outputs.tag }} + steps: + - if: github.event_name == 'workflow_dispatch' + run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV + + - if: github.event_name == 'schedule' + run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV + + - if: github.event_name == 'push' + run: | + TAG_NAME=${{ github.ref }} + echo "TAG_NAME=${TAG_NAME#refs/tags/}" >> $GITHUB_ENV + + - if: github.event_name == 'pull_request' + run: echo 'TAG_NAME=debug' >> $GITHUB_ENV + + - id: vars + shell: bash + run: echo "sha_short=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - if: env.TAG_NAME == 'nightly' + run: echo 'TAG_NAME=nightly-${{ steps.vars.outputs.sha_short }}' >> $GITHUB_ENV + + - id: tag + run: echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT + + windows: + runs-on: windows-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + + steps: + - uses: actions/checkout@v4 + + - name: Fetch dependencies + run: npm i + + - name: Build packages + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + + - name: Build app + run: | + npm run build:electron -- win + ls ./packages/bruno-electron/out/ + + - uses: actions/upload-artifact@v4 + with: + name: build-windows + path: | + ./packages/bruno-electron/out/bruno_* + retention-days: 1 + + linux: + runs-on: ubuntu-latest + container: node:20.11 + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + DEBIAN_FRONTEND: noninteractive + + steps: + - uses: actions/checkout@v4 + + - name: Fetch dependencies + run: npm i + + - name: Build packages + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + + - name: Build app + run: | + npm run build:electron -- linux + npm run build:electron -- deb + + - uses: actions/upload-artifact@v4 + with: + name: build-linux + path: | + ./packages/bruno-electron/out/bruno_* + retention-days: 1 + + macos: + runs-on: macos-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + + steps: + - uses: actions/checkout@v4 + + - name: Fetch dependencies + run: | + npm i + + - name: Build packages + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + + - name: Build app + run: | + npm run build:electron -- mac + + - uses: actions/upload-artifact@v4 + with: + name: build-macos + path: | + ./packages/bruno-electron/out/bruno_* + retention-days: 1 + if-no-files-found: error + + publish: + if: github.event_name != 'pull_request' + needs: [linux, windows, macos] + runs-on: ubuntu-latest + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + # Must perform checkout first, since it deletes the target directory + # before running, and would therefore delete the downloaded artifacts + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + + - if: github.event_name == 'workflow_dispatch' + run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV + + - if: github.event_name == 'schedule' + run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV + + - if: github.event_name == 'push' + run: | + TAG_NAME=${{ github.ref }} + echo "TAG_NAME=${TAG_NAME#refs/tags/}" >> $GITHUB_ENV + + - run: | + (echo 'SUBJECT=Bruno development build'; + echo 'PRERELEASE=--prerelease') >> $GITHUB_ENV + gh release delete nightly --yes || true + git push origin :nightly || true + + - name: Publish release + env: + DEBUG: api + run: | + gh release create $TAG_NAME $PRERELEASE --title "$TAG_NAME" --target $GITHUB_SHA build-linux/* build-windows/* build-macos/* diff --git a/packages/bruno-electron/electron-builder-config.js b/packages/bruno-electron/electron-builder-config.js index 1b75e4d13d..a695226a80 100644 --- a/packages/bruno-electron/electron-builder-config.js +++ b/packages/bruno-electron/electron-builder-config.js @@ -1,8 +1,14 @@ require('dotenv').config({ path: process.env.DOTENV_PATH }); +// TAG_NAME is set inside the CI-Pipeline +const isNightly = process.env.RELEASE_TAG_NAME !== undefined; +// Add Nightly-Suffix to the version for nightly builds +const artifactName = + '${name}_${version}' + (isNightly ? `-${process.env.RELEASE_TAG_NAME}` : '') + '_${arch}_${os}.${ext}'; + const config = { - appId: 'com.usebruno.app', - productName: 'Bruno', + appId: `com.usebruno${isNightly ? '-nightly' : ''}.app`, + productName: 'Bruno' + (isNightly ? ' Nightly' : ''), electronVersion: '21.1.1', directories: { buildResources: 'resources', @@ -11,7 +17,7 @@ const config = { files: ['**/*'], afterSign: 'notarize.js', mac: { - artifactName: '${name}_${version}_${arch}_${os}.${ext}', + artifactName, category: 'public.app-category.developer-tools', target: [ { @@ -30,15 +36,15 @@ const config = { entitlementsInherit: 'resources/entitlements.mac.plist' }, linux: { - artifactName: '${name}_${version}_${arch}_linux.${ext}', + artifactName, icon: 'resources/icons/png', target: ['AppImage', 'deb', 'snap', 'rpm'] }, win: { - artifactName: '${name}_${version}_${arch}_win.${ext}', + artifactName, icon: 'resources/icons/png', - certificateFile: `${process.env.WIN_CERT_FILEPATH}`, - certificatePassword: `${process.env.WIN_CERT_PASSWORD}` + certificateFile: process.env.WIN_CERT_FILEPATH ? `${process.env.WIN_CERT_FILEPATH}` : undefined, + certificatePassword: process.env.WIN_CERT_PASSWORD ? `${process.env.WIN_CERT_PASSWORD}` : undefined } }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 3ad2736d0a..8ad80a9691 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -6,6 +6,10 @@ "private": true, "main": "src/index.js", "author": "Anoop M D (https://helloanoop.com/)", + "repository": { + "type": "git", + "url": "https://github.com/usebruno/bruno.git" + }, "scripts": { "clean": "rimraf dist", "dev": "electron .", diff --git a/scripts/build-electron.js b/scripts/build-electron.js index 9825c3a09b..95c4991ef4 100644 --- a/scripts/build-electron.js +++ b/scripts/build-electron.js @@ -1,19 +1,25 @@ const os = require('os'); const fs = require('fs-extra'); -const util = require('util'); -const spawn = util.promisify(require('child_process').spawn); +const spawn = require('child_process').spawn; + +function log(...args) { + console.log('-> ', ...args); +} +function error(...args) { + console.log('!> ', ...args); +} async function deleteFileIfExists(filePath) { try { const exists = await fs.pathExists(filePath); if (exists) { await fs.remove(filePath); - console.log(`${filePath} has been successfully deleted.`); + log(`${filePath} has been successfully deleted.`); } else { - console.log(`${filePath} does not exist.`); + log(`${filePath} does not exist.`); } } catch (err) { - console.error(`Error while checking the existence of ${filePath}: ${err}`); + error(`Error while checking the existence of ${filePath}: ${err}`); } } @@ -22,12 +28,12 @@ async function copyFolderIfExists(srcPath, destPath) { const exists = await fs.pathExists(srcPath); if (exists) { await fs.copy(srcPath, destPath); - console.log(`${srcPath} has been successfully copied.`); + log(`${srcPath} has been successfully copied.`); } else { - console.log(`${srcPath} was not copied as it does not exist.`); + log(`${srcPath} was not copied as it does not exist.`); } } catch (err) { - console.error(`Error while checking the existence of ${srcPath}: ${err}`); + error(`Error while checking the existence of ${srcPath}: ${err}`); } } @@ -38,17 +44,22 @@ async function removeSourceMapFiles(directory) { if (file.endsWith('.map')) { const filePath = path.join(directory, file); await fs.remove(filePath); - console.log(`${filePath} has been successfully deleted.`); + log(`${filePath} has been successfully deleted.`); } } } catch (error) { - console.error(`Error while deleting .map files: ${error}`); + error(`Error while deleting .map files: ${error}`); } } -async function execCommandWithOutput(command) { +/** + * @param {String} command + * @param {String[]} args + * @returns {Promise} + */ +async function execCommandWithOutput(command, args) { return new Promise(async (resolve, reject) => { - const childProcess = await spawn(command, { + const childProcess = spawn(command, args, { stdio: 'inherit', shell: true }); @@ -65,51 +76,50 @@ async function execCommandWithOutput(command) { }); } -async function main() { - try { - // Remove out directory - await deleteFileIfExists('packages/bruno-electron/out'); - - // Remove web directory - await deleteFileIfExists('packages/bruno-electron/web'); - - // Create a new web directory - await fs.ensureDir('packages/bruno-electron/web'); - console.log('The directory has been created successfully!'); - - // Copy build - await copyFolderIfExists('packages/bruno-app/out', 'packages/bruno-electron/web'); - - // Change paths in next - const files = await fs.readdir('packages/bruno-electron/web'); - for (const file of files) { - if (file.endsWith('.html')) { - let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8'); - content = content.replace(/\/_next\//g, '_next/'); - await fs.writeFile(`packages/bruno-electron/web/${file}`, content); - } - } - - // Remove sourcemaps - await removeSourceMapFiles('packages/bruno-electron/web'); - - // Run npm dist command - console.log('Building the Electron distribution'); - - // Determine the OS and set the appropriate argument - let osArg; +/** + * @param {String[]} args + * @returns {Promise} + */ +async function main(args) { + let target = args[args.length - 1]; + if (!target) { + // Auto detect target if (os.platform() === 'win32') { - osArg = 'win'; + target = 'win'; } else if (os.platform() === 'darwin') { - osArg = 'mac'; + target = 'mac'; } else { - osArg = 'linux'; + target = 'linux'; } + log('Target automatically set to ', target); + } - await execCommandWithOutput(`npm run dist:${osArg} --workspace=packages/bruno-electron`); - } catch (error) { - console.error('An error occurred:', error); + log('Clean up old build artifacts'); + await deleteFileIfExists('packages/bruno-electron/web'); + await deleteFileIfExists('packages/bruno-app/out'); + + log('Building web'); + await execCommandWithOutput('npm', ['run', 'build:web']); + await fs.ensureDir('packages/bruno-electron/web'); + await copyFolderIfExists('packages/bruno-app/out', 'packages/bruno-electron/web'); + + // Change paths in next + const files = await fs.readdir('packages/bruno-electron/web'); + for (const file of files) { + if (file.endsWith('.html')) { + let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8'); + content = content.replace(/\/_next\//g, '_next/'); + await fs.writeFile(`packages/bruno-electron/web/${file}`, content); + } } + + // Run npm dist command + log(`Building the Electron app for target: ${target}`); + await execCommandWithOutput('npm', ['run', `dist:${target}`, '--workspace=packages/bruno-electron']); + log('Build complete'); + return 0; } -main(); +main(process.argv) + .then((code) => process.exit(code)) + .catch((e) => console.error('An error occurred during build', e) && process.exit(1));