Converting a Freestyle project to a Declarative Pipeline

The Declarative Pipeline Migration Assistant plugin was released to the Jenkins open source community and will be available in CloudBees Assurance Program in February 2020.

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, making it faster and less error-prone. 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.

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

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 implements BuilderConverter
    @Override
    public ModelASTStage convert( ConverterRequest request, ConverterResult converterResult, Builder builder )
    {
       return the stage corresponding to the conversion or modify the model
    }

    @Override
    public boolean canConvert( Builder builder )
    {
        return true if your implementation is able to convert the Builder passed as a parameter
    }

Interfaces that define extension of the conversion

Example Build Step conversion

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

@Extension
public class ShellConverter implements BuilderConverter
    ....
    public ModelASTStage convert( ConverterRequest request, ConverterResult converterResult, Builder builder )
    {
        Shell shell = (Shell) builder;
        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 );
        wrapBranch(converterResult, step, branch); (7)

        return stage;
    }
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.

Example Build Wrapper conversion

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

@OptionalExtension(requirePlugins = { "config-file-provider" })
// This was to not have the config-file-provider plugin as a required dependency
// But you can use (as you use your plugin)
@Extension
public class ConfigFileBuildWrapperConverter implements BuildWrapperConverter
    @Override
    public ModelASTStage convert( ConverterRequest request, ConverterResult converterResult, BuildWrapper wrapper )
    {
        ConfigFileBuildWrapper configFileBuildWrapper = (ConfigFileBuildWrapper)wrapper;
        if(configFileBuildWrapper.getManagedFiles() == null || configFileBuildWrapper.getManagedFiles().isEmpty() )
        {
            return null;
        }

        converterResult.addWrappingTreeStep( () -> build( request, configFileBuildWrapper ) ); (1)
        return null;
    }

    private ModelASTTreeStep build(ConverterRequest request, 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:'" ); (3)
        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
3 Convert to groovy code - configFileProvider([configFile(fileId: 'yup', targetLocation: 'myfile.txt')])

Example Publisher conversion

The following example converts the ArtifactArchiver Freestyle post build step using the API. This conversion does not return a stage, but modifies the model to add some build conditions.

@Extension
public class ArtifactArchiverConverter implements PublisherConverter

    public ModelASTStage convert( ConverterRequest request, ConverterResult result, Publisher publisher )
    {
        if (!(publisher instanceof ArtifactArchiver )) {
            return null; (1)
        }
        ArtifactArchiver artifactArchiver = (ArtifactArchiver) publisher;
        ModelASTBuildCondition buildCondition;
        if(artifactArchiver.isOnlyIfSuccessful()) (2)
        {
            buildCondition = ModelASTUtils.buildOrFindBuildCondition( result.getModelASTPipelineDef(), "success" );
        } else {
            buildCondition = ModelASTUtils.buildOrFindBuildCondition( result.getModelASTPipelineDef(), "always" );
        }

        ModelASTBranch branch = buildCondition.getBranch();
        if(branch==null){  (3)
            branch =new ModelASTBranch( this );
            buildCondition.setBranch( branch );
        }

        ModelASTStep archiveArtifacts = buildGenericStep(publisher);  (4)
        branch.getSteps().add( archiveArtifacts );

        return null;
    }
1 Can’t use automatic conversion.
2 Depending on which condition, the artifact needs to be executed.
3 Need to ensure there is a branch, so include a null check.
4 Use the helper method for basic generic publisher. If your Publisher cannot be converted with this, code the step manually.

Example SCM conversion

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

@Extension
public class GitScmConverter implements ScmConverter
    ...
    public void convert( ConverterRequest request, ConverterResult converterResult, SCM scm )
    {
        List<UserRemoteConfig> repoList = ( (GitSCM) scm ).getUserRemoteConfigs();
        if(repoList.isEmpty()){
            return;
        }
        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 ) );
        addStage(converterResult.getModelASTPipelineDef(), stage ); (9)
    }
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 a utility method.

@Extension
public class TimerTriggerConverter implements TriggerConverter
    ...
    @Override
    public void convert( ConverterRequest request, ConverterResult converterResult, TriggerDescriptor triggerDescriptor,
                         Trigger<?> trigger )
    {
        TimerTrigger timerTrigger = (TimerTrigger) trigger;

        String cronValue = timerTrigger.getSpec();
        ModelASTTrigger modelASTTrigger = new ModelASTTrigger( this ); (1)
        modelASTTrigger.setName( "cron" );
        modelASTTrigger.setArgs( Arrays.asList(ModelASTValue.fromConstant( cronValue, this )) );
        ModelASTUtils.addTrigger( converterResult.getModelASTPipelineDef(), modelASTTrigger );  (2)
    }
1 Create the cron option.
2 Add the option to the model.
Copyright © 2010-2020 CloudBees, Inc.Online version published by CloudBees, Inc. under the Creative Commons Attribution-ShareAlike 4.0 license.CloudBees and CloudBees DevOptics are registered trademarks and CloudBees Core, CloudBees Flow, CloudBees Flow Deploy, CloudBees Flow DevOps Insight, CloudBees Flow DevOps Foresight, CloudBees Flow Release, CloudBees Accelerator, CloudBees Accelerator ElectricInsight, CloudBees Accelerator Electric Make, CloudBees CodeShip, CloudBees Jenkins Enterprise, CloudBees Jenkins Platform, CloudBees Jenkins Operations Center, and DEV@cloud are trademarks of CloudBees, Inc. Most CloudBees products are commonly referred to by their short names — Accelerator, Automation Platform, Flow, Deploy, Foresight, Release, Insight, and eMake — throughout various types of CloudBees product-specific documentation. Oracle and Java are registered trademarks of Oracle and/or its affiliates. Jenkins is a registered trademark of the non-profit Software in the Public Interest organization. Used with permission. See here for more info about the Jenkins project. The registered trademark Jenkins® is used pursuant to a sublicense from the Jenkins project and Software in the Public Interest, Inc. Read more at www.cloudbees.com/jenkins/about. Apache, Apache Ant, Apache Maven, Ant and Maven are trademarks of The Apache Software Foundation. Used with permission. No endorsement by The Apache Software Foundation is implied by the use of these marks.Other names may be trademarks of their respective owners. Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this content, and CloudBees was aware of a trademark claim, the designations have been printed in caps or initial caps. While every precaution has been taken in the preparation of this content, the publisher and authors assume no responsibility for errors or omissions, or for damages resulting from the use of the information contained herein.