properties([ buildDiscarder(logRotator(numToKeepStr: '10')) ]) def bunBinDir() { if (isUnix()) { return "${env.HOME}/.bun/bin" } return "${env.USERPROFILE}/.bun/bin" } def withBun(Closure body) { def sep = isUnix() ? ':' : ';' withEnv(["PATH=${bunBinDir()}${sep}${env.PATH}"]) { body() } } def checkBun() { if (isUnix()) { sh 'bun -v' } else { bat 'bun -v' } } def writeBuildMetadata() { if (isUnix()) { sh ''' bun --eval "await Bun.write('src/buildInfo.json', JSON.stringify({ buildNumber: process.env.BUILD_NUMBER || 'dev' }, null, 2) + '\\n')" ''' } else { bat ''' bun --eval "await Bun.write('src/buildInfo.json', JSON.stringify({ buildNumber: process.env.BUILD_NUMBER || 'dev' }, null, 2) + '\\n')" ''' } } def runBuild(buildCommand) { def releaseBaseUrl = "https://dist.farmcontrol.app/jenkins/farmcontrol/farmcontrol-ui" withEnv([ "VITE_BUILD_NUMBER=${env.BUILD_NUMBER ?: 'dev'}", 'NODE_ENV=production', "ELECTROBUN_RELEASE_BASE_URL=${releaseBaseUrl}", ]) { if (isUnix()) { sh buildCommand } else { bat buildCommand } } } def deploy() { node('ubuntu') { try { stage('Checkout (Ubuntu)') { checkout scm } stage('Setup Node.js (Ubuntu)') { nodejs(nodeJSInstallationName: 'Node23') { sh 'node -v' sh 'pnpm -v' } } stage('Install Dependencies (Ubuntu)') { nodejs(nodeJSInstallationName: 'Node23') { sh 'pnpm install --frozen-lockfile --production=false' } } stage('Build (Ubuntu)') { nodejs(nodeJSInstallationName: 'Node23') { sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare" } } stage('Deploy (Ubuntu)') { nodejs(nodeJSInstallationName: 'Node23') { withCredentials([string(credentialsId: 'cloudflare-api-token', variable: 'CLOUDFLARE_API_TOKEN')]) { sh 'pnpm wrangler pages deploy build --branch main' } } } } finally { cleanWs() } } } def prepareMacBuildWorkspace() { if (isUnix()) { sh ''' df -h . for vol in /Volumes/Farm\\ Control*; do if [ -d "$vol" ]; then hdiutil detach "$vol" -force || true fi done rm -rf build/stable-macos-*/.dmg-staging build/stable-macos-*/.finalize-dmg-staging 2>/dev/null || true ''' } } def buildOnLabel(label, buildCommand, bundleCef = false) { def cefLabel = bundleCef ? 'cef' : 'native' return { node(label) { try { stage("Checkout (${label}/${cefLabel})") { checkout scm } withBun { stage("Check Bun (${label}/${cefLabel})") { checkBun() } if (label.startsWith('macos')) { stage("Prepare macOS workspace (${label}/${cefLabel})") { prepareMacBuildWorkspace() } } stage("Install Dependencies (${label}/${cefLabel})") { if (isUnix()) { sh 'bun install --frozen-lockfile' } else { bat 'bun install --frozen-lockfile' } } stage("Write Build Metadata (${label}/${cefLabel})") { writeBuildMetadata() } stage("Build (${label}/${cefLabel})") { withEnv(["ELECTROBUN_BUNDLE_CEF=${bundleCef ? 'true' : 'false'}"]) { runBuild(buildCommand) } } } stage("Archive Artifacts (${label}/${cefLabel})") { archiveArtifacts artifacts: 'app_dist/farmcontrol-*', fingerprint: true } } finally { cleanWs() } } } } def buildMacOnLabel(label, targetArch, bundleCef = false) { return buildOnLabel(label, "ELECTROBUN_TARGET_ARCH=${targetArch} bun run build:app:mac", bundleCef) } def setBuildNameFromPackageVersion() { node('ubuntu') { stage('Set Build Name') { checkout scm withBun { checkBun() def version = sh( script: "bun --eval \"console.log(JSON.parse(await Bun.file('package.json').text()).version)\"", returnStdout: true ).trim() def buildName = "v${version}-b${env.BUILD_NUMBER}" currentBuild.displayName = buildName echo "Build name set to: ${buildName}" } } } } try { setBuildNameFromPackageVersion() parallel( 'Windows Build': buildOnLabel('windows', 'bun run build:app', false), 'Windows CEF Build': buildOnLabel('windows', 'bun run build:app', true), 'MacOS x64 Build': buildMacOnLabel('macos', 'x64', false), 'MacOS x64 CEF Build': buildMacOnLabel('macos', 'x64', true), 'MacOS arm64 Build': buildMacOnLabel('macos', 'arm64', false), 'MacOS arm64 CEF Build': buildMacOnLabel('macos', 'arm64', true), 'Ubuntu Deploy': { deploy() } ) echo 'All parallel stages completed successfully!' } catch (Exception e) { echo "Pipeline failed: ${e.message}" throw e }