# MonorepoUtils

The MonorepoUtils class provides utility methods for handling common monorepo functionality in Jenkins pipelines.

## Usage

### Creating an instance of MonorepoUtils

First add a method to your Jenkinsfile to retrieve an instance of MonorepoUtils.

If you want to automatically load all projects relative to a specific base path, specify the `projectBasePath` parameter. 
The `projectsToBuild` parameter can be used to override which projects should be built, e.g. if the pipeline is run manually with user input.
The `excludedProjects` parameter can be used to exclude specific projects under the base path.

```groovy
def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        excludedProjects: ['example'],
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES ? params.LAMBDA_FUNCTION_NAMES.split(',') : null
    )
}
```

You can also load projects explicitly by providing `projectConfig`, which is a mapping of project paths to arbitrary project-specific configuration 
that you want to provide to your pipeline stages.

```groovy
def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectConfig: [
            'lambda/projectA': [someConfig: 'someValueA'],
            'fargate/projectB': [someConfig: 'someValueB']
        ],
        projectsToBuild: params.PROJECT_NAMES ? params.PROJECT_NAMES.split(',') : null
    )
}
```

`projectConfig` can be provided in addition to `projectBasePath`, either to provide additional configuration for projects loaded via `projectBasePath` 
or to add additional projects outside of the base path.

```groovy
def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES ? params.LAMBDA_FUNCTION_NAMES.split(',') : null,
        projectConfig: [
            'projectA': [someConfig: 'someValueA'], // Provide config for the project at 'lambda/projectA' loaded via projectBasePath
            'fargate/projectB': [someConfig: 'someValueB'] // Add additional project outside of base path
        ],
    )
}
```

If using `projectConfig` to provide additional configuration for projects loaded via `projectBasePath`, then by default the keys
are expected to be relative to `projectBasePath`. Set `useRelativePaths` to `false` to use full paths.

```groovy
def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES ? params.LAMBDA_FUNCTION_NAMES.split(',') : null,
        useRelativePaths: false,
        projectConfig: [
            'lambda/projectA': [someConfig: 'someValueA'], // Provide config for the project at 'lambda/projectA' loaded via projectBasePath
            'fargate/projectB': [someConfig: 'someValueB'] // Add additional project outside of base path
        ],
    )
}
```

If your monorepo has shared/common modules, you can specify these and the projects that depend on them via the `projectDependencies` parameter.
If the common modules have changed, then any dependent projects will also be included.
`projectDependencies` is a map where the keys are paths to the dependency, and the values are a list of regex patterns matching the full paths to the dependent projects.

```groovy
def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES ? params.LAMBDA_FUNCTION_NAMES.split(',') : null,
        projectDependencies: [
            'common': [/^lambda\/.*$/]
        ]
    )
}
```

### Executing steps for modified projects in parallel

Once you have an instance of MonorepoUtils, you can use the `withModifiedProjects` method to execute steps for modified projects in parallel.

If `projectsToBuild` was set to a non-null value when creating the MonorepoUtils instance, the steps will be executed for those projects only (typically for enabling manual builds of specific projects).
Otherwise the steps will be executed for all projects that have been modified in the current pull request, or since the last successful build if running on the main branch.

```groovy
pipeline {
    // Don't allocate an agent for the pipeline as agents will be allocated for each parallel stage
    agent none
    
    stages {
        stage('Stage 1') {
            steps {
                withModifiedProjects { project ->
                    // The steps below will be executed for each modified project in parallel.
                    // Each set of parallel steps will run on a separate Jenkins executor.
                    // Code is not checked out by default
                    echo "Building project ${project}"
                }
            }
        }
        stage('Stage 2') {
            steps {
                withModifiedProjects(checkout: true) { project ->
                    // The steps below will be executed for each modified project in parallel.
                    // Each set of parallel steps will run on a separate Jenkins executor.
                    // Repository code will be checked out.
                    echo "Building project ${project}"
                }
            }
        }
        stage('Stage 3') {
            steps {
                withModifiedProjects(allocateAgent: false) { project ->
                    // The steps below will be executed for each modified project in parallel.
                    // The steps will not be allocated a separate Jenkins executor.
                    // Use this option for lightweight steps that do not require an executor, and in particular if you are calling a downstream job.
                    build job: 'some-downstream-job'
                }
            }
        }
        stage('Stage 4') {
            steps {
                withModifiedProjects(agentLabel: 'can_reach_orchard_aws_dev') { project ->
                    // The steps below will be executed for each modified project in parallel.
                    // The steps will be allocated a Jenkins executor with the specified label.
                    // Code is not checked out by default. Use checkout: true to check out repository code. 
                    echo "Building project ${project}"
                }
            }
        }
    }
}

// Wrap MonorepoUtils.withModifiedProjects to keep pipeline stages tidy (otherwise we have to wrap every usage in a "script" block)
def withModifiedProjects(Map args = [:], Closure steps) {
    getMonorepoUtils().withModifiedProjects(args, steps)
}

def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES ? params.LAMBDA_FUNCTION_NAMES.split(',') : null
    )
}
```

The steps passed to `withModifiedProjects` should be a closure with either one or two parameters. 
The first parameter is the path of the modified project (if using `projectBasePath`, this will be relative to the base path, unless `useRelativePaths` is set to `false`).
The second parameter (optional) is the project-specific configuration provided via `projectConfig` when creating the MonorepoUtils instance.

Example with two parameters:

```groovy
pipeline {
    // Don't allocate an agent for the pipeline as agents will be allocated for each parallel stage
    agent none
    
    stages {
        // Prints:
        // Building project projectA. Config: [someConfig:someValueA]
        // Building project projectB. Config: [someConfig:someValueB]
        stage('Stage') {
            steps {
                withModifiedProjects { project, config ->
                    echo "Building project ${project}. Config: ${config}"
                }
            }
        }
    }
}

// Wrap MonorepoUtils.withModifiedProjects to keep pipeline stages tidy (otherwise we have to wrap every usage in a "script" block)
def withModifiedProjects(Map args = [:], Closure steps) {
    getMonorepoUtils().withModifiedProjects(args, steps)
}

def getMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectConfig: [
            'projectA': [someConfig: 'someValueA'],
            'projectB': [someConfig: 'someValueB']
        ],
        projectsToBuild: params.PROJECT_NAMES ? params.PROJECT_NAMES.split(',') : null
    )
}
```
