Search code examples
javajenkinsgroovy

How do you reference a function with the same name as a property?


Question

How can I reference this top-level function from within the data class? Or is Java's encapsulation of a class restrictive to the point that you cannot reach beyond the current class?

Code


def String branchName() {
  return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1];
}

public DeployConfig implements IDeployConfig {
  public DeployConfig(IDeployConfig config) {
    this._appName = config.app;
    this._gitUrl = config.gitUrl;
    // ... et cetera
  }
  
  public String getBranchName() {
    return branchName()
  }
}

Background

I'm trying to define a data class that represents our standard Jenkinsfile configuration, in an attempt to make our pipeline more testable, and less "cross your fingers and hope it didn't break anything". Toward that goal, here is a snippet of that implementation.

Now, the property getter I'm trying to write doesn't know the actual branch being built when the object is constructed, because that's derived from the Map<String, String> returned by checkout scm which gets instantiated at runtime. We assign the GIT_BRANCH out to the global environment env.GIT_BRANCH so that it can be referenced elsewhere.

Miscellaneous

To the would-be suggestion of putting the target branch in the Jenkinsfile, that defeats the purpose of the Jenkinsfile being an instruction set for a job with Git configurations assigned, such as a multi-branch job with a shared Jenkinsfile.

Other Code

To give some context about what I mean about the checkout scm command happening after the construction of DeployConfig, the pipeline roughly resembles this:

// ./pipeline-library/vars/deploy.groovy
#!/usr/bin/groovy
def call(Closure body) {
  def config = [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = config
  body()

  environmentVariables(config) // assigns certain keys to global env

  if (env.IS_PROD) {
    deployProd(config)
  }
  else {
    deployNonProd(config)
  }
}

// ./pipeline-library/vars/deployNonProd.groovy
#!/usr/bin/groovy

def call(Map config) {
  // local variable declarations
  
  pipeline {
    agent { 
      label 'some-configuration-name'
    }
    
    environment {
      // shared environment variables
    }

    options {
      // configured options, like timestamps and log rotation
    }
    
    stages {
      stage('Checkout') {
        steps {
          def gitInfo = checkout scm
          env.GIT_BRANCH = gitInfo.GIT_BRANCH
        }
      }
      
      // additional stages
    }
  }
}

Edits

Edit: The idea behind the property that calls the top-level function is a computed property that gets called later in the pipeline, after the checkout scm command has been executed. The DeployConfig would be constructed before the pipeline runs, and so the branch is not known at that time.


Solution

  • So I solved the problem for myself, but it's arguably a less than ideal solution to the problem. Basically, here's what I had to do:

    First, I created a getter getGetBranchName and setter setGetBranchName on the class of type Closure<String> and it had a backing field _getBranchName. I also created a property getBranchName of type String that returned the result of this._getBranchName().

    Second, if the incoming Map has a property branchName, then I set the value this._getBranchName = () -> { return config.branchName } so that I am referencing the getter of an outer object.

    Third, as a final check, I assign the global function signature from Jenkins after constructing the DeployConfig object. That all looks like the below code (Note: ellipses are used to indicate more code unrelated to the specific solution):

    import groovy.json.JsonBuilder
    import groovy.json.JsonSlurper
    
    import com.domain.jenkins.data.DeployConfig
    import com.domain.jenkins.data.GitUrl
    import com.domain.jenkins.exceptions.InterruptException
    import com.domain.jenkins.io.FileSystem
    import com.domain.jenkins.pipeline.PipelineJob
    import com.domain.jenkins.pipeline.PipelineStage
    
    
    class Program {
      static FileSystem fs
      static PipelineJob pipeline
      static Map<String, String> env
      static Map jenkinsfile
      
      static {
        fs = new FileSystem()
        pipeline = new PipelineJob()
        env = [:]
        jenkinsfile = [ ... ]
      }
    
      static String branchName() {
        return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1]
      }
    
      static void main(String[] args) {
        println 'Initialize pipeline'
        pipeline = new PipelineJob()
        println 'Initialize configuration'
        DeployConfig config = new DeployConfig(jenkinsfile)
        println new JsonBuilder(config.toMap()).toPrettyString()
        println 'Assign static method as getBranchName method'
        config.getBranchName = () -> { return branchName() }
        println 'Assign environment variables to global env'
        env << config.environmentVariables
    
        ...
      }
    }
    

    And the DeployConfig class accepts that using the following (Note: most of the related code is not included for brevity's sake):

    package com.domain.jenkins.data
    
    import com.domain.jenkins.interfaces.IDeployConfig
    import com.domain.jenkins.interfaces.IMappable
    import com.domain.jenkins.interfaces.Mappable
    
    class DeployConfig extends Mappable implements IDeployConfig, IMappable {
      private Closure<String> _getBranchName
    
      DeployConfig() {
        this.branchName = 'master'
      }
    
      DeployConfig(Map options) {
        this.branchName = options.branchName
      }
    
      String getBranchName() {
        return this._getBranchName()
      }
      
      void setBranchName(String value) {
        if(this._getBranchName == null) {
          this._getBranchName = () -> { return 'master' }
        }
        
        if(value) {
          this._getBranchName = () -> { return value }
        }
      }
      
      void setGetBranchName(Closure<String> action) {
        this._getBranchName = action
      }
    }