Converting a Freestyle project to a Declarative Pipeline

Maintaining Freestyle jobs in Jenkins is cumbersome. Declarative Pipelines provide a more modern, recommended approach. However, attempting to convert Freestyle jobs to Declarative Pipelines manually is time-consuming and error-prone. Using the Declarative Pipeline Migration Assistant plugin streamlines this process. The Declarative Pipeline Migration Assistant uses a best-effort approach during the conversion: supported configurations in Freestyle projects are automatically converted, and placeholder stages are created for plugins that are not yet supported.

Benefits of the Declarative Pipeline Migration Assistant include:

  • Avoid writing your pipeline from scratch if migrating from legacy job types

  • Reduce the learning curve to start writing Declarative Pipelines

  • Manage your pipelines easier, compared to freestyle projects

  • Lessen the overhead involved in manually maintaining freestyle jobs to execute properly

  • Solve the problem of running scripts on the master with shared libraries that can jeopardize the stability of the master.

The process of converting a Freestyle project to a Declarative Pipeline involves the following steps:

Supported versions

The Declarative Pipeline Migration Assistant plugin is available on the following versions and platforms:

  • CloudBees CI on modern cloud platforms version 2.222.1.1 (and higher)

  • CloudBees CI on traditional platforms version 2.222.1.1 (and higher)

  • CloudBees Jenkins Platform - Client Master version 2.222.1.1 (and higher)

  • CloudBees Jenkins Distribution version 2.222.1.1 (and higher)

  • CloudBees Jenkins Enterprise - Managed Master version 2.222.1.1 (and higher)

Generating a Jenkinsfile from a Freestyle project

Pipeline or Multibranch Pipeline projects are based on a centralized configuration file, called a Jenkinsfile. A Jenkinsfile can be created in the GUI or in a text editor. The file is stored either with the project code or in a separate repository, for instance a software configuration management (SCM) tool like Git. Using an SCM to store the file provides a centralized location for the configuration file, allows for code review, and creates an audit trail for the Pipeline. The Declarative Pipeline Migration Assistant uses details from a Freestyle project to create a starting Jenkinsfile.

The Declarative Pipeline Migration Assistant is available only in the Jenkins UI, and is not available from the Jenkins CLI.
For more information about pipelines, see Defining Pipeline.

Prerequisites

  • The Declarative Pipeline Migration Assistant plugin

  • The Pipeline plugins

To generate a Jenkinsfile from a Freestyle project:

  1. Navigate to the Freestyle project.

  2. Select To Declarative in the left navigation menu from the Freestyle project’s page.

    Select To Declarative in left navigation menu

Once the conversion is complete, a Jenkinsfile will be provided for review.

Generated starting Jenkinsfile

The Declarative Pipeline Migration Assistant plugin currently supports a limited number of plugins. See table below for a list of the plugins currently supported.

If the conversion lists a warning for plugins it was unable to convert:

  • The plugin is not Pipeline compatible. You can check the plugin’s documentation to see if it is compatible with Pipeline. If the plugin is not compatible with Pipeline, use a shell step as a replacement.

  • The plugin is Pipeline compatible and appears in the Snippet Generator. (See table below below for a list of compatible plugins.) Use the Snippet Generator to create the correct syntax.

  • The plugin is Pipeline compatible and does not appear in the Snippet Generator. (See table below for a list of plugins currently supported.) You should reference the Pipeline documentation for more information on implementing the plugin, see Customizing a Jenkinsfile for more information.

For reusability, you can write extensions for the Declarative Pipeline Migration Assistant; see Extending the Declarative Pipeline Migration Assistant plugin.

Supported plugins

Type Step

scm

git

step

shell step

step

batch step

step

Maven build step

build wrapper

Config File Provider plugin

build wrapper

secret (convert to credentials binding)

build wrapper

Build timeout plugin

job property

Lockable Resources plugin

job property

build parameters

job property

build discarder configuration

build trigger

upstream projects trigger

build trigger

SCM pooling

build trigger

timer trigger

build environment

provide configuration files

build environment

use secret text(s) or file(s)

post build action

Junit plugin

post build action

HTML Publisher plugin

post build action

trigger another project

post build action

mail notification

post build action

Do not fail build if archiving returns nothing

post build action

archive artifacts only if build is successful

post build action

fingerprint all archived projects

Customizing a Jenkinsfile

After generating a Jenkinsfile from a Freestyle project, either copy-paste the Jenkinsfile to a text file or download the provided text file and open it in a text editor. Then review the Jenkinsfile and edit as needed for the new Pipeline project to accomplish the same tasks the Freestyle project accomplished.

Generated starting Jenkinsfile

For more information about editing a Jenkinsfile see:

As a general reference, see the Pipeline syntax reference guide.

Creating a Pipeline project in Jenkins and adding a Jenkinsfile

After generating a Jenkinsfile from a Freestyle project and editing the Jenkinsfile, the next step is to add the Jenkinsfile to a Pipeline or Multibranch Pipeline project as the configuration file. First, create the Pipeline project in Jenkins. Afterwards, add the Jenkinsfile to the project by copying and pasting it in the Pipeline editor or by storing it in an SCM like GitHub, and connecting the repository to the Pipeline project.

To create a Pipeline project and add the Jenkinsfile:

Extending the Declarative Pipeline Migration Assistant plugin

The Declarative Pipeline Migration Assistant plugin currently supports a limited number of plugins. See the table above for a list of supported plugins.

If you want to add support for a specific plugin that is not currently supported, the process includes adding the converter API dependency and creating the extension.

Adding the converter API dependency

The following code snippet illustrates how to add the converter API dependency:

    <dependency>
      <groupId>org.jenkins-ci.plugins.to-declarative</groupId>
      <artifactId>declarative-pipeline-migration-assistant-api</artifactId>
      <version></version>
    </dependency>

Creating the extension

The following code snippet illustrates how to create the extension:

```
@Extension
public class ShellConverter extends SingleTypedConverter<Shell>
    @Override
    public boolean convert( ConverterRequest request, ConverterResult result, Object target )
    {
      Shell shell = (Shell) target;
      /* Do some work to generate a new stage . . . */
      ModelASTStage stage = new ModelASTStage( this );
      // use a utility method to add the stage to the pipeline model
      ModelASTUtils.addStage( request.getModelASTPipelineDef(), stage);

      // true for success, false for failure
      return true;
    }
```

Example conversions

The abstract class SingleTypedConverter<T> defines the extension of the conversion.

Example Build Step conversion

The following example converts a Shell script Freestyle step using the API:

```
@Extension
public class ShellConverter extends SingleTypedConverter<Shell>
{
  @Override
  public boolean convert (ConerterRequest request, ConverterRequest result, Object target )
    {
        Shell shell = (Shell) target;
        ModelASTStage stage = new ModelASTStage( this );
        int stageNumber = request.getAndIncrement( SHELL_NUMBER_KEY );  (1)
        stage.setName( "Shell script " + stageNumber );
        ModelASTBranch branch = new ModelASTBranch( this );  (2)
        stage.setBranches( Arrays.asList( branch ) );  (3)
        ModelASTStep step = new ModelASTStep( this );  (4)
        step.setName( "sh" );  (5)
        ModelASTSingleArgument singleArgument = new ModelASTSingleArgument( this );
        singleArgument.setValue( ModelASTValue.fromConstant( shell.getCommand(), this ) ); (6)
        step.setArgs( singleArgument );
        ModelASTUtils.wrapBranch(result, step, branch); (7)
        ModelASTUtils.addStage(result.getModelASTPipelineDef(), stage); (8)
        return true;
    }
}
```
1 Names need to be unique for this; use a counter internal to the current conversion.
2 Create a branch of the pipeline.
3 Add it to the returned stage.
4 This is the step doing the job.
5 This is the used pipeline function.
6 Add the argument(s) coming from the Freestyle Build Step configuration.
7 Use helper methods as you may have some wrappers around steps such as credential, timeout, configfile, etc.
8 Use a utility method to add the stage to the pipeline model.

Example Build Wrapper conversion

The following example converts the Config File Freestyle wrapper build using the API. This conversion uses a helper method to add a wrapper around all future build step conversions.

```
// This was to not have the config-file-provider plugin as a required dependency
// But you can use (as you use your plugin)
@OptionalExtension(requirePlugins = { "config-file-provider" })
public class ConfigFileBuildWrapperConverter extends SingleTypedConverter<ConfigFileBuildWrapper>
{
    private Logger LOGGER = LoggerFactory.getLogger( ConfigFileBuildWrapperConverter.class );

    @Override
    public boolean convert(ConverterRequest request, ConverterResult result, Object target)
    {
        ConfigFileBuildWrapper configFileBuildWrapper = (ConfigFileBuildWrapper) target;
        if(configFileBuildWrapper.getManagedFiles() == null || configFileBuildWrapper.getManagedFiles().isEmpty() )
        {
            return true;
        }

        result.addWrappingTreeStep( () -> build( configFileBuildWrapper) ); (1)
        return true;
    }

    private ModelASTTreeStep build(ConfigFileBuildWrapper configFileBuildWrapper) {
        ModelASTTreeStep configFileProvider = new ModelASTTreeStep( this );

        configFileProvider.setName( "configFileProvider" );
        ModelASTSingleArgument singleArgument = new ModelASTSingleArgument( null );
        configFileProvider.setArgs( singleArgument );

        ManagedFile managedFile = configFileBuildWrapper.getManagedFiles().get( 0 ); (2)
        StringBuilder gstring = new StringBuilder( "[configFile(fileId:'" );
        gstring.append( managedFile.getFileId());
        gstring.append( "', targetLocation: '" );
        gstring.append( managedFile.getTargetLocation() );
        gstring.append( "')]" );
        singleArgument.setValue( ModelASTValue.fromGString( gstring.toString(), this ) );


        return configFileProvider;
    }
}
```
1 Return a lambda which will be called to wrap build step branches conversions; see Example Build Step conversion.
2 Only the 1st one

Example Publisher conversion

The following example converts the ArtifactArchiver Freestyle post build step using the API. This conversion modifies the model to add some build conditions.

```
@Extension
public class ArtifactArchiverConverter extends SingleTypedConverter<ArtifactArchiver> {
    @Override
    public boolean convert(ConverterRequest request, ConverterResult result, Object target) {
        ArtifactArchiver artifactArchiver = (ArtifactArchiver) target;
        ModelASTBuildCondition buildCondition;
        if(artifactArchiver.isOnlyIfSuccessful()) { (1)
            buildCondition = ModelASTUtils.buildOrFindBuildCondition( result.getModelASTPipelineDef(), "success" );
        } else {
            buildCondition = ModelASTUtils.buildOrFindBuildCondition( result.getModelASTPipelineDef(), "always" );
        }
        ModelASTStep archiveArtifacts = ModelASTUtils.buildGenericStep(artifactArchiver, this);
        ModelASTUtils.addStep(buildCondition, archiveArtifacts); (2)
        return true;
    }
}
```
1 Depending on which condition, the artifact needs to be executed.
2 We add the step to the build condition.

Example SCM conversion

The following example converts the Git SCM Freestyle stage using the API. This conversion adds a stage to the Pipeline model.

```
@OptionalExtension(requirePlugins = { "git"})
public class GitScmConverter extends SingleTypedConverter<GitSCM>
{
  @Override
  public boolean convert(ConverterRequest request, ConverterResult result, Object target)
    {
        List<UserRemoteConfig> repoList = ( (GitSCM) target ).getUserRemoteConfigs();
        if(repoList.isEmpty()){
            return true;
        }
        ModelASTStage stage = new ModelASTStage( this ); (1)
        stage.setName( "Checkout Scm" );
        List<ModelASTStep> steps = new ArrayList<>(); (2)
        // a step will be created per remote repository
        for( UserRemoteConfig userRemoteConfig : repoList) (3)
        {
            ModelASTStep git = new ModelASTStep( null ); (4)
            git.setName( "git" );

            Map<ModelASTKey, ModelASTValue> args = new HashMap<>();
            {
                ModelASTKey url = new ModelASTKey( this ); (5)
                url.setKey( "url" );
                ModelASTValue urlValue = ModelASTValue.fromConstant( userRemoteConfig.getUrl(), this );
                args.put( url, urlValue );
            } (6)

            ...

            ModelASTNamedArgumentList stepArgs = new ModelASTNamedArgumentList( null);  (7)
            stepArgs.setArguments( args );
            git.setArgs( stepArgs );
            steps.add( git );
        }

        ModelASTBranch branch = new ModelASTBranch( this ); (8)
        branch.setSteps(steps);
        stage.setBranches( Arrays.asList( branch ) );
        ModelASTUtils.addStage(result.getModelASTPipelineDef(), stage ); (9)
        return true;
    }
}
```
1 Create the new stage.
2 This is what will be generated as step - git url: "", branch: '',changelog: '', credentialsId: '', pool: ''
3 A step will be created per remote repository.
4 Create the Git step.
5 Add parameters - url.
6 Add more parameters in the original code.
7 Configure args of the step.
8 Create a branch for the stage.
9 Use a utility method to add the stage to the Pipeline model.

Example Build Trigger conversion

The following example converts the cron trigger using the API. This conversion modifies the pipeline mode to add a trigger property via an utility method.

```
@Extension
public class TimerTriggerConverter extends SingleTypedConverter<TimerTrigger>
{
    @Override
    public boolean convert(ConverterRequest request, ConverterResult result, Object target)
    {
        TimerTrigger timerTrigger = (TimerTrigger) target;

        String cronValue = timerTrigger.getSpec();
        ModelASTTrigger modelASTTrigger = new ModelASTTrigger( this ); (1)
        modelASTTrigger.setName( "cron" );
        modelASTTrigger.setArgs( Arrays.asList(ModelASTValue.fromConstant( cronValue, this )) );

        ModelASTUtils.addTrigger( result.getModelASTPipelineDef(), modelASTTrigger );  (2)
        return true;
    }
}
```
1 Create the cron option.
2 Add the option to the model.