Architecture - BredaUniversityGames/JenkinsLib GitHub Wiki

Architecture

Module Organization

Each tool has its own vars/ script with implementation classes in src/com/buas/:

Script Methods Purpose
perforce.groovy perforce.sync() Perforce/Helix Core
git.groovy git.sync() Git/GitHub
ue5.groovy ue5.build(), ue5.test() Unreal Engine 5
vs.groovy vs.build(), vs.test() Visual Studio / MSBuild
cmake.groovy cmake.build(), cmake.test(), cmake.pack(), cmake.workflow() CMake / CTest / CPack
steam.groovy steam.deploy() Steam deployment
itch.groovy itch.deploy() itch.io deployment
gdrive.groovy gdrive.deploy() Google Drive deployment
epic.groovy epic.deploy() Epic Games Store deployment
swarm.groovy swarm.review() Helix Swarm code reviews
sentry.groovy sentry.upload() Sentry debug symbols
discord.groovy discord.alert() Discord notifications
stages.groovy stages { ... } Pipeline orchestrator
matrix.groovy matrix(axes) { ... } Multi-axis build combinations
only.groovy only(condition) { ... } Conditional stage filter

Utilities:

Script Purpose
log.groovy Logging (log(), log.warning(), log.error())
utilWin.groovy Windows path helpers
utilZip.groovy Zip/unzip operations
utilPython.groovy Python script runner

How It Works

Registration Phase

When you write:

stages {
    perforce.sync()
    ue5.build()
    steam.deploy()
    discord.alert()
}

Each method (e.g., steam.deploy()) creates an implementation class and registers a module with the orchestrator:

def deploy(Map overrides = [:]) {
    def impl = new com.buas.deploy.Steam(this)
    ModuleRegistry.register(
        category: 'deploy',
        name: 'Steam',
        params: impl.pipelineParams(overrides),
        execute: { params, ctx -> impl.execute(params, ctx) },
        hasCleanup: false
    )
}

Execution Phase

stages.groovy collects all registered modules, then executes them in order:

  1. Parameter collection — all module parameters are merged and registered with Jenkins
  2. Pre-matrix stages — modules registered before the matrix run by category order
  3. Matrix stages (if present) — each axis combination runs inner modules sequentially, with overridden params and isolated context per combination
  4. Post-matrix stages — modules registered after the matrix run by category order
  5. Deploy stages run in parallel — within each phase, multiple deploy targets execute simultaneously
  6. Notifications run in the finally block (always execute, even on failure)
  7. Cleanup runs for modules that registered a cleanup closure

When no matrix() block is used, the pipeline runs identically to a single-phase execution (all modules are "pre-matrix").

Module Scoping (push/pop)

matrix() and only() use ModuleRegistry.push() / pop() to capture modules registered inside their closures. push() places a sentinel marker on the registry; pop() returns all modules added after the most recent sentinel. This allows nested scoping — an only() inside a matrix() works correctly because each push/pop pair operates on its own sentinel.

Context Passing

A ctx map is passed through all stages, allowing modules to share data:

perforce.sync()  → sets ctx.revision, ctx.changelist, ctx.vcsType
ue5.build()      → sets ctx.buildConfig, ctx.buildPlatform, ctx.engineRoot
cmake.build()    → sets ctx.buildEngine, ctx.cmakeBuildPreset (or ctx.buildConfig, ctx.buildPlatform, ctx.cmakeBuildDir)
ue5.test()       → sets ctx.testResults
steam.deploy()   → reads ctx.outputDir, ctx.buildPlatform
discord.alert()  → reads ctx.revision, ctx.buildConfig, ctx.testResults

In a matrix build, each combination gets a cloned ctx with its own outputDir (${WORKSPACE}\Output\<value1>\<value2>\...). Changes to ctx inside one combination don't affect other combinations.

Design Principles

  1. Stateless modules — no module-level mutable state. All config passed via Map parameters.
  2. Pluggable modules — add new implementations by creating a class in src/com/buas/ and a vars/ script.
  3. Parallel deployments — independent deploy stages run simultaneously.
  4. Secure credentials — all secrets use Jenkins withCredentials, never raw parameters.
  5. Closure-based dispatch — modules register execute/cleanup/notify closures.

src/ Class Pattern

Each implementation class follows this pattern:

package com.buas.deploy

class Steam implements Serializable {
    def steps  // pipeline context for Jenkins DSL steps

    Steam(steps) {
        this.steps = steps
    }

    def pipelineParams(Map overrides = [:]) {
        return [
            steps.string(name: 'STEAM_APP_ID', defaultValue: overrides.STEAM_APP_ID ?: '',
                   description: 'Steam App ID'),
            // ...
        ]
    }

    def execute(Map params, Map ctx) {
        deploy(appId: params.STEAM_APP_ID, /* ... */)
    }

    def deploy(Map config) {
        steps.bat(label: "Deploy to Steam", script: "...")
    }
}

Jenkins DSL calls (bat, withCredentials, writeFile, etc.) are accessed via steps.bat(...), steps.withCredentials(...), etc.

Adding a New Module

Example: Adding a Slack Notification Channel

  1. Create the implementation class at src/com/buas/alert/Slack.groovy:

    package com.buas.alert
    
    class Slack implements Serializable {
        def steps
    
        Slack(steps) { this.steps = steps }
    
        def pipelineParams(Map overrides = [:]) {
            return [
                steps.string(name: 'SLACK_WEBHOOK', defaultValue: overrides.SLACK_WEBHOOK ?: '',
                       description: 'Slack webhook URL')
            ]
        }
    
        def executeNotify(String status, Map params, Map ctx) {
            if (!params.SLACK_WEBHOOK) return
            // Send Slack notification...
        }
    }
  2. Create a vars/slack.groovy script:

    def alert(Map overrides = [:]) {
        def impl = new com.buas.alert.Slack(this)
        ModuleRegistry.register(
            category: 'alert',
            name: 'Slack',
            params: impl.pipelineParams(overrides),
            alert: { status, params, ctx -> impl.executeNotify(status, params, ctx) },
            hasCleanup: false
        )
    }
  3. Use it in a Jenkinsfile:

    stages {
        perforce.sync()
        ue5.build()
        discord.alert()
        slack.alert()
    }

Groups System (Swarm/Discord Integration)

The swarm.review() and discord.alert() modules use a JSON groups format for managing team participants and Discord mentions:

{
  "groups": [
    {
      "name": "TeamLead",
      "discordID": "123456789",
      "swarmID": ["swarmuser1"],
      "type": "user"
    },
    {
      "name": "Developers",
      "discordID": "987654321",
      "swarmID": ["dev1", "dev2"],
      "type": "role"
    }
  ]
}

Types control Discord mention format:

Type Discord Format
user <@ID>
role <@&ID>
channel <#ID>
⚠️ **GitHub.com Fallback** ⚠️