| name | cross-platform-builds |
| description | Comprehensive guide to building JUCE plugins for macOS, Windows, and Linux with CMake, code signing, notarization, and CI/CD. Use when configuring builds, setting up CI/CD pipelines, troubleshooting cross-platform compilation, implementing code signing, or creating installers for multiple platforms. |
| allowed-tools | Read, Grep, Glob |
Cross-Platform Builds for JUCE Plugins
Comprehensive guide to building JUCE audio plugins across macOS, Windows, and Linux with proper configuration, code signing, and continuous integration.
Overview
JUCE audio plugins must be built for multiple platforms and plugin formats:
- macOS: VST3, AU (Audio Unit), AAX
- Windows: VST3, AAX
- Linux: VST3
Each platform has specific requirements for build tools, code signing, and packaging. This skill covers:
- CMake configuration for all platforms and formats
- Platform-specific build instructions
- Code signing and notarization
- Continuous integration setup
- Reproducible builds
1. CMake Configuration
Root CMakeLists.txt Structure
cmake_minimum_required(VERSION 3.22)
project(MyPlugin VERSION 1.0.0)
# C++17 minimum for JUCE 7+
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Export compile_commands.json for IDEs
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Add JUCE
add_subdirectory(JUCE)
# Plugin formats to build
set(PLUGIN_FORMATS VST3 AU Standalone)
# Add AAX if PACE SDK is available
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
list(APPEND PLUGIN_FORMATS AAX)
juce_set_aax_sdk_path("${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
endif()
# Define the plugin
juce_add_plugin(MyPlugin
COMPANY_NAME "YourCompany"
PLUGIN_MANUFACTURER_CODE Manu # 4-character code
PLUGIN_CODE Plug # 4-character code (unique!)
FORMATS ${PLUGIN_FORMATS}
PRODUCT_NAME "MyPlugin"
# Bundle IDs
BUNDLE_ID com.yourcompany.myplugin
# Plugin characteristics
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
# Copy plugin to system folder after build
COPY_PLUGIN_AFTER_BUILD TRUE
# VST3 category
VST3_CATEGORIES Fx
# AU type (aufx = effect, aumu = instrument)
AU_MAIN_TYPE kAudioUnitType_Effect
)
# Source files
target_sources(MyPlugin PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/DSP/Filter.cpp
Source/DSP/Modulation.cpp
)
# Public compile definitions
target_compile_definitions(MyPlugin PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_DISPLAY_SPLASH_SCREEN=0 # Commercial license only!
)
# Link JUCE modules
target_link_libraries(MyPlugin PRIVATE
juce::juce_audio_utils
juce::juce_dsp
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
)
# Platform-specific settings
if(APPLE)
# macOS deployment target
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version")
# Universal binary (Apple Silicon + Intel)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures")
# Hardened runtime for notarization
target_compile_options(MyPlugin PUBLIC
-Wall -Wextra -Wpedantic
)
elseif(WIN32)
# Static runtime for standalone distribution
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# Windows-specific definitions
target_compile_definitions(MyPlugin PRIVATE
_CRT_SECURE_NO_WARNINGS
)
elseif(UNIX)
# Linux-specific flags
target_compile_options(MyPlugin PRIVATE
-Wall -Wextra
)
# Link against ALSA, JACK, etc.
find_package(PkgConfig REQUIRED)
pkg_check_modules(ALSA REQUIRED alsa)
target_link_libraries(MyPlugin PRIVATE ${ALSA_LIBRARIES})
endif()
# Tests (optional)
option(BUILD_TESTS "Build unit tests" ON)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(Tests)
endif()
Key Configuration Options
Plugin Codes
PLUGIN_MANUFACTURER_CODE Manu # Your unique 4-character manufacturer ID
PLUGIN_CODE Plug # Unique 4-character plugin ID
Important: Register manufacturer code at Steinberg to avoid conflicts.
Bundle Identifiers
BUNDLE_ID com.yourcompany.myplugin # Reverse domain notation
Must be unique and consistent across versions for AU validation.
Plugin Characteristics
IS_SYNTH TRUE # Instrument vs effect
NEEDS_MIDI_INPUT TRUE # Accept MIDI input
NEEDS_MIDI_OUTPUT FALSE # Send MIDI output
IS_MIDI_EFFECT FALSE # MIDI-only processing (no audio)
VST3 Categories
VST3_CATEGORIES Fx # Effect
VST3_CATEGORIES Instrument # Instrument
VST3_CATEGORIES Fx Dynamics # Multiple categories
Available categories: Fx, Instrument, Analyzer, Delay, Distortion, Dynamics, EQ, Filter, Mastering, Modulation, Restoration, Reverb, Spatial, Synth, Tools
AU Types
AU_MAIN_TYPE kAudioUnitType_Effect # Effect
AU_MAIN_TYPE kAudioUnitType_MusicDevice # Instrument
AU_MAIN_TYPE kAudioUnitType_MIDIProcessor # MIDI effect
2. macOS Builds
Prerequisites
Xcode (latest version recommended)
xcode-select --installCMake (3.22+)
brew install cmakeDeveloper ID Certificate (for distribution)
- Enroll in Apple Developer Program ($99/year)
- Create "Developer ID Application" certificate in Xcode
Building
# Configure
cmake -B build-mac -G Xcode \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
# Build all formats
cmake --build build-mac --config Release --parallel
# Or build with Xcode
open build-mac/MyPlugin.xcodeproj
Universal Binaries (Apple Silicon + Intel)
# In CMakeLists.txt
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
Or at build time:
cmake -B build-mac -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
Verify architectures:
lipo -info build-mac/MyPlugin_artefacts/Release/VST3/MyPlugin.vst3/Contents/MacOS/MyPlugin
# Output: Architectures in the fat file: MyPlugin are: x86_64 arm64
Code Signing
Manual Signing
# Sign VST3
codesign --force \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
--options runtime \
--entitlements Resources/Entitlements.plist \
--timestamp \
--deep \
MyPlugin.vst3
# Sign AU
codesign --force \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
--options runtime \
--timestamp \
--deep \
MyPlugin.component
# Verify signature
codesign --verify --deep --strict --verbose=2 MyPlugin.vst3
Entitlements File (Resources/Entitlements.plist)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Allow JIT for DSP optimization -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Allow loading unsigned plugins (for VST3 presets, etc.) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- For networked plugins (optional) -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Automated Signing in CMake
# Add to CMakeLists.txt
if(APPLE AND CMAKE_BUILD_TYPE STREQUAL "Release")
set(CODESIGN_IDENTITY "Developer ID Application: Your Name")
add_custom_command(TARGET MyPlugin POST_BUILD
COMMAND codesign --force
--sign "${CODESIGN_IDENTITY}"
--options runtime
--entitlements "${CMAKE_SOURCE_DIR}/Resources/Entitlements.plist"
--timestamp
$<TARGET_BUNDLE_DIR:MyPlugin>
COMMENT "Code signing ${TARGET}"
)
endif()
Notarization
Required for macOS 10.15+ (Catalina and later).
Setup
- Create app-specific password at appleid.apple.com
- Store credentials in keychain:
xcrun notarytool store-credentials "notary-profile" \ --apple-id "developer@example.com" \ --team-id "TEAM_ID" \ --password "xxxx-xxxx-xxxx-xxxx"
Notarize Plugin
# 1. Create ZIP for notarization
ditto -c -k --keepParent MyPlugin.vst3 MyPlugin-vst3.zip
# 2. Submit to notary service
xcrun notarytool submit MyPlugin-vst3.zip \
--keychain-profile "notary-profile" \
--wait
# 3. If successful, staple the ticket
xcrun stapler staple MyPlugin.vst3
# 4. Verify
spctl -a -vvv -t install MyPlugin.vst3
xcrun stapler validate MyPlugin.vst3
Troubleshooting Notarization
Check submission status:
xcrun notarytool info <submission-id> --keychain-profile "notary-profile"
View detailed log:
xcrun notarytool log <submission-id> --keychain-profile "notary-profile"
Common issues:
- Missing entitlements: Add to Entitlements.plist
- Unsigned nested binaries: Sign all frameworks before parent bundle
- Invalid bundle structure: Verify with
pkgutil --check-signature
AU Validation
# Validate AU (required for App Store distribution)
auval -v aufx Plug Manu
# Output should end with "PASSED"
Fix common AU validation errors:
- "Could not open component": Check bundle ID and AU type
- "Plugin crash": Debug in Xcode, check for exceptions in initialization
- "Latency reporting": Implement
getTailLengthSeconds()correctly
3. Windows Builds
Prerequisites
Visual Studio 2022 (Community, Professional, or Enterprise)
- Install "Desktop development with C++" workload
- Includes Windows 10 SDK
CMake (3.22+)
# Via Chocolatey choco install cmake # Or download from cmake.orgCode Signing Certificate (optional, for distribution)
- EV or standard code signing certificate
- From vendors: DigiCert, Sectigo, GlobalSign
Building
# Configure for Visual Studio 2022
cmake -B build-win -G "Visual Studio 17 2022" -A x64
# Build Release
cmake --build build-win --config Release --parallel
# Or open in Visual Studio
start build-win/MyPlugin.sln
MSVC Runtime Linking
Static Runtime (recommended for plugins):
# Statically link MSVC runtime (no DLL dependencies)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
Dynamic Runtime (smaller binary, requires MSVC redistributable):
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
Code Signing
Manual Signing with signtool
# Sign with PFX file
signtool sign /f certificate.pfx /p <password> `
/tr http://timestamp.digicert.com `
/td sha256 /fd sha256 `
MyPlugin.vst3
# Sign with certificate store
signtool sign /n "Your Company Name" `
/tr http://timestamp.digicert.com `
/td sha256 /fd sha256 `
MyPlugin.vst3
# Verify signature
signtool verify /pa /v MyPlugin.vst3
Automated Signing in CMake
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
find_program(SIGNTOOL_EXECUTABLE signtool
PATHS "C:/Program Files (x86)/Windows Kits/10/bin/*/x64"
)
if(SIGNTOOL_EXECUTABLE)
add_custom_command(TARGET MyPlugin POST_BUILD
COMMAND ${SIGNTOOL_EXECUTABLE} sign
/f "${CMAKE_SOURCE_DIR}/certificate.pfx"
/p "$ENV{CERT_PASSWORD}"
/tr http://timestamp.digicert.com
/td sha256 /fd sha256
$<TARGET_FILE:MyPlugin>
COMMENT "Code signing ${TARGET}"
)
endif()
endif()
Visual Studio Configuration
Optimization Settings
if(MSVC)
# Enable whole program optimization (Release)
target_compile_options(MyPlugin PRIVATE
$<$<CONFIG:Release>:/GL> # Whole program optimization
/MP # Multi-processor compilation
)
target_link_options(MyPlugin PRIVATE
$<$<CONFIG:Release>:/LTCG> # Link-time code generation
)
endif()
Suppress Warnings
target_compile_definitions(MyPlugin PRIVATE
_CRT_SECURE_NO_WARNINGS # Disable CRT security warnings
NOMINMAX # Prevent min/max macros
)
4. Linux Builds
Prerequisites
Ubuntu/Debian:
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
libasound2-dev \
libjack-jackd2-dev \
libfreetype6-dev \
libx11-dev \
libxrandr-dev \
libxinerama-dev \
libxcursor-dev \
libgl1-mesa-dev \
libcurl4-openssl-dev
Fedora/RHEL:
sudo dnf install -y \
gcc-c++ \
cmake \
alsa-lib-devel \
jack-audio-connection-kit-devel \
freetype-devel \
libX11-devel \
libXrandr-devel \
libXinerama-devel \
libXcursor-devel \
mesa-libGL-devel \
libcurl-devel
Building
# Configure
cmake -B build-linux -DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build-linux --config Release --parallel
# Install to system (optional)
sudo cmake --install build-linux
Packaging
Create .tar.gz
tar -czf MyPlugin-1.0.0-Linux-x86_64.tar.gz \
-C build-linux/MyPlugin_artefacts/Release/VST3 \
MyPlugin.vst3
Create .deb Package
# Install packaging tools
sudo apt-get install checkinstall
# Create .deb
sudo checkinstall \
--pkgname=myplugin \
--pkgversion=1.0.0 \
--pkgrelease=1 \
--pkggroup=sound \
--maintainer="you@example.com" \
cmake --install build-linux
5. AAX Format (Pro Tools)
Prerequisites
AAX SDK (requires iLok account)
- Sign up at developer.avid.com
- Download AAX SDK
- Extract to
SDKs/AAX/
PACE Licensing (for distribution)
- Create account at paceap.com
- Use PACE Eden for signing (replaces codesign for AAX)
CMake Configuration
# Set AAX SDK path
juce_set_aax_sdk_path("${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
# Add AAX to plugin formats
set(PLUGIN_FORMATS VST3 AU AAX Standalone)
Building AAX
# macOS
cmake -B build-mac -DAAX_SDK_PATH=SDKs/AAX
cmake --build build-mac --config Release
# Windows
cmake -B build-win -DAAX_SDK_PATH=SDKs/AAX
cmake --build build-win --config Release
AAX Signing with PACE Eden
AAX plugins must be signed with PACE Eden (not regular codesign).
# Sign AAX (macOS/Windows)
wraptool sign \
--account <your-pace-account> \
--password <password> \
--signid <signid> \
--in MyPlugin.aaxplugin \
--out MyPlugin-signed.aaxplugin
# Verify
wraptool verify --verbose MyPlugin-signed.aaxplugin
Note: Keep AAX signing credentials secure. Never commit to version control.
6. Continuous Integration
GitHub Actions Workflow
.github/workflows/build.yml:
name: Build Plugin
on: [push, pull_request]
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
cmake_args: -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
- name: Windows
os: windows-latest
cmake_args: -G "Visual Studio 17 2022" -A x64
- name: Linux
os: ubuntu-latest
cmake_args: ""
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libjack-jackd2-dev \
libfreetype6-dev libx11-dev libxrandr-dev libxinerama-dev \
libxcursor-dev libgl1-mesa-dev
- name: Configure
run: cmake -B build ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release --parallel
- name: Test
run: ctest --test-dir build -C Release --output-on-failure
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.name }}
path: |
build/*_artefacts/Release/VST3/*.vst3
build/*_artefacts/Release/AU/*.component
Secrets for Code Signing
Store signing credentials in GitHub Secrets:
- Go to repository Settings → Secrets → Actions
- Add secrets:
MACOS_CERTIFICATE_BASE64: Base64-encoded .p12 fileMACOS_CERTIFICATE_PASSWORD: Certificate passwordAPPLE_ID: Apple ID for notarizationAPPLE_TEAM_ID: Developer team IDAPPLE_APP_PASSWORD: App-specific passwordWINDOWS_CERTIFICATE_BASE64: Base64-encoded .pfx fileWINDOWS_CERTIFICATE_PASSWORD: Certificate password
Automated Code Signing in CI
macOS:
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
echo $CERTIFICATE_BASE64 | base64 --decode > certificate.p12
security create-keychain -p temp build.keychain
security import certificate.p12 -k build.keychain -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-keychain-settings -lut 21600 build.keychain
security unlock-keychain -p temp build.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain
- name: Sign and Notarize
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
codesign --force --sign "Developer ID Application" --options runtime MyPlugin.vst3
xcrun notarytool submit MyPlugin.vst3.zip --apple-id $APPLE_ID --team-id $APPLE_TEAM_ID --password $APPLE_APP_PASSWORD --wait
xcrun stapler staple MyPlugin.vst3
Windows:
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
[System.Convert]::FromBase64String($env:CERTIFICATE_BASE64) | Set-Content -Path certificate.pfx -Encoding Byte
certutil -importpfx -p $env:CERTIFICATE_PASSWORD certificate.pfx
- name: Sign Binary
run: |
signtool sign /f certificate.pfx /p $env:CERTIFICATE_PASSWORD /tr http://timestamp.digicert.com /td sha256 /fd sha256 MyPlugin.vst3
7. Reproducible Builds
Deterministic Builds
Ensure builds are reproducible across machines:
Pin JUCE version (use git submodule or specific release)
git submodule add https://github.com/juce-framework/JUCE.git cd JUCE && git checkout 7.0.9Lock dependency versions (CMake FetchContent)
FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 # Specific version )Document toolchain versions (README.md)
Build Requirements: - CMake 3.22+ - JUCE 7.0.9 - macOS: Xcode 14.3+ - Windows: Visual Studio 2022 - Linux: GCC 11+ or Clang 14+Disable timestamp embedding
# Remove __DATE__ and __TIME__ macros target_compile_definitions(MyPlugin PRIVATE NO_BUILD_TIMESTAMP=1 )
Build Verification
Generate checksums for reproducibility:
# macOS/Linux
shasum -a 256 MyPlugin.vst3 > checksums.txt
# Windows
certutil -hashfile MyPlugin.vst3 SHA256 >> checksums.txt
8. Troubleshooting
Common Build Errors
"JUCE modules not found"
Solution:
git submodule update --init --recursive
"Symbol not found" (macOS)
Solution:
- Check deployment target matches minimum system requirement
- Verify all symbols are available in target SDK
- Use `nm` to inspect missing symbols:
nm -gU MyPlugin.vst3/Contents/MacOS/MyPlugin | grep <symbol>
"Unresolved external symbol" (Windows)
Solution:
- Ensure all .cpp files are in CMakeLists.txt
- Check library linking order
- Verify static/dynamic runtime consistency (/MT vs /MD)
"Undefined reference" (Linux)
Solution:
- Install missing libraries (libasound2-dev, etc.)
- Add libraries to target_link_libraries()
- Check pkg-config: pkg-config --libs alsa
Plugin Doesn't Load in DAW
macOS:
- Check signing:
codesign --verify --deep --strict MyPlugin.vst3 - Verify notarization:
spctl -a -vvv -t install MyPlugin.vst3 - Check Gatekeeper:
xattr -l MyPlugin.vst3(remove quarantine if needed) - AU validation:
auval -v aufx Plug Manu
Windows:
- Check dependencies: Use Dependency Walker
- Verify signature:
signtool verify /pa MyPlugin.vst3 - Check registry (for VST3):
Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Classes\VST3
Linux:
- Check shared library dependencies:
ldd MyPlugin.vst3 - Verify VST3 path:
~/.vst3/or/usr/lib/vst3/ - Check permissions:
chmod 755 MyPlugin.vst3
9. Best Practices
Version Management
project(MyPlugin VERSION 1.2.3)
# Access in code
target_compile_definitions(MyPlugin PRIVATE
PLUGIN_VERSION="${CMAKE_PROJECT_VERSION}"
)
Conditional Compilation
#if JUCE_MAC
// macOS-specific code
#elif JUCE_WINDOWS
// Windows-specific code
#elif JUCE_LINUX
// Linux-specific code
#endif
#if JUCE_DEBUG
// Debug-only code
#endif
Minimize Plugin Size
- Strip symbols in Release builds
- Enable LTO (link-time optimization)
- Remove unused JUCE modules
- Compress resources (images, fonts)
Cross-Platform File Paths
// Use JUCE File class for portability
juce::File presetFolder = juce::File::getSpecialLocation(
juce::File::userApplicationDataDirectory
).getChildFile("MyPlugin").getChildFile("Presets");
// Not hardcoded paths like:
// "C:\\Users\\...\\Presets" ❌
Summary
Key Takeaways:
- Use CMake for cross-platform builds - single configuration for all platforms
- Code signing is essential for distribution (macOS requires notarization)
- Test on all platforms - behavior can differ (especially AU vs VST3)
- Automate in CI/CD - GitHub Actions, GitLab CI, or Jenkins
- Reproducible builds - pin dependency versions, document toolchain
Platform Checklist:
- macOS: Universal binary (arm64 + x86_64)
- macOS: Code signed with Developer ID
- macOS: Notarized (10.15+ requirement)
- macOS: AU validation passes (
auval) - Windows: Code signed (recommended)
- Windows: Static runtime linked (/MT)
- Linux: Dependencies documented
- All: Tested in major DAWs on each platform
Related Resources:
/release-buildcommand - Automated release workflow- BUILD_GUIDE.md - Detailed build procedures
- RELEASE_CHECKLIST.md - Pre-release validation steps
- @build-engineer - CI/CD and build automation expert