When working with Jenkins shared libraries, handling YAML files is a common requirement. However, you might encounter serialization issues when trying to parse or manipulate YAML content. In this post, we’ll explore how to create a robust YAML utility class using the @NonCPS annotation to avoid these common pitfalls.

The Problem Link to heading

Jenkins Pipeline runs in a distributed environment where objects need to be serialized and deserialized. When working with YAML files, you might encounter java.io.NotSerializableException errors, especially when using libraries like SnakeYAML. This happens because some objects created during YAML parsing aren’t serializable by default.

The Solution Link to heading

We can solve this by creating a utility class with methods annotated with @NonCPS. The @NonCPS annotation tells Jenkins that the method should be executed in a non-CPS (Continuation Passing Style) context, which means it won’t be serialized.

Here’s a complete implementation of a YAML utility class:

package com.organization

@Grab(group='org.yaml', module='snakeyaml', version='1.29')
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.DumperOptions

class YamlUtils implements Serializable {

    def steps

    YamlUtils(steps) {
        this.steps = steps
    }

    Map<String, Object> loadYamlFromFile(String yamlFile) {
        String yamlContent = steps.readFile(file: yamlFile)
        return loadYamlFromString(yamlContent)
    }

    /**
     * Loads YAML content from a string and returns it as a Map
     * @param yamlContent The YAML content as a string
     * @return Map containing the parsed YAML data
     * Reference: https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-controllers/pipeline-the-pipeline-even-if-successful-ends-with-java-io-notserializableexception
     */
    @NonCPS
    Map<String, Object> loadYamlFromString(String yamlContent) {
        Yaml yaml = new Yaml()
        Map data = yaml.load(yamlContent)
        return data
    }

    /**
     * Saves YAML data to a file
     * @param yamlFile The path to the file where YAML should be saved
     * @param data Map containing the data to be saved as YAML
     */
    void saveYamlToFile(String yamlFile, Map<String, Object> data) {
        String yamlContent = dumpYamlToString(data)
        steps.writeFile(file: yamlFile, text: yamlContent)
    }

    /**
     * Converts a Map to YAML string format
     * @param data Map containing the data to be converted to YAML
     * @return String containing the YAML representation of the data
     */
    @NonCPS
    String dumpYamlToString(Map<String, Object> data) {
        Yaml yaml = new Yaml()
        DumperOptions options = new DumperOptions()
        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK)
        options.setPrettyFlow(true)
        return yaml.dump(data)
    }
}

Key Components Explained Link to heading

  1. @Grab Annotation: This automatically downloads the SnakeYAML dependency when the shared library is loaded.

  2. Serializable Interface: The class implements Serializable to ensure it can be properly serialized in the Jenkins environment.

  3. @NonCPS Annotation: Applied to methods that perform YAML operations to prevent serialization issues.

  4. DumperOptions: Used to control the output format of the YAML, ensuring a clean, readable structure.

Usage Example Link to heading

Here’s how you can use this utility class in your Jenkins pipeline:

@Library('your-shared-library') _

def yamlUtils = new YamlUtils(this)

// Load YAML from a file
def config = yamlUtils.loadYamlFromFile('config.yaml')

// Modify the configuration
config.someKey = 'new value'

// Save back to YAML
def newYaml = yamlUtils.dumpYamlToString(config)

Conclusion Link to heading

By using the @NonCPS annotation and following these patterns, you can create robust YAML handling utilities in your Jenkins shared libraries. This approach helps avoid serialization issues while maintaining clean, maintainable code.

Remember to test your implementation thoroughly, especially when dealing with complex YAML structures or large files. The @NonCPS annotation is a powerful tool, but it should be used judiciously and only when necessary.