> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Demo Custom Transformer Docker Build

> Describes a custom transformer that converts Jenkins sh docker build commands into GitHub Actions steps, replacing hard-coded values with repository variables and outlining registry authentication handling.

Let's create a custom transformer that recognizes and converts the embedded Docker build command inside Jenkins `sh` steps into a proper GitHub Actions step.

<Frame>
  <img src="https://mintcdn.com/kodekloud-c4ac6d9a/iRlNGmU-DLlt2Ghu/images/Migrating-Jenkins-Pipelines-to-GitHub-Actions/Automate-Migration-From-Jenkins-to-GitHub-Actions/Demo-Custom-Transformer-Docker-Build/custom-transformer-docker-build-kodekloud.jpg?fit=max&auto=format&n=iRlNGmU-DLlt2Ghu&q=85&s=21a2b646d351a90960c299f4efde11d9" alt="A blue-green gradient slide with the centered title &#x22;Custom Transformer Docker Build.&#x22; A small &#x22;© Copyright KodeKloud&#x22; appears in the bottom-left corner." width="1920" height="1080" data-path="images/Migrating-Jenkins-Pipelines-to-GitHub-Actions/Automate-Migration-From-Jenkins-to-GitHub-Actions/Demo-Custom-Transformer-Docker-Build/custom-transformer-docker-build-kodekloud.jpg" />
</Frame>

Overview

* Source: Jenkins pipeline builds and pushes a Docker image, then scans it with Trivy.
* Goal: Convert the `docker build` command embedded in `sh` into a native GitHub Actions step and replace hard-coded values with repository/workflow variables.

Relevant Jenkinsfile stages (cleaned and complete)

```groovy theme={null}
stage('Code Coverage') {
    agent {
        docker {
            image 'node:24'
            args '-u root:root'
        }
    }
    steps {
        catchError(buildResult: 'SUCCESS', message: 'Oops! it will be fixed in future releases', stageResult: 'UNSTABLE') {
            sh 'npm run coverage'
        }
        publishHTML([
            allowMissing: true,
            alwaysLinkToLastBuild: true,
            keepAll: true,
            reportDir: 'coverage/lcov-report',
            reportFiles: 'index.html',
            reportName: 'Coverage Report'
        ])
    }
}

stage('Build Publish Image') {
    steps {
        sh 'docker build -t siddharth67/solar-system:$GIT_COMMIT .'
        withDockerRegistry(credentialsId: 'docker-hub-credentials', url: "") {
            sh 'docker push siddharth67/solar-system:$GIT_COMMIT'
        }
    }
}

stage('Trivy Vulnerability Scanner') {
    steps {
        sh '''trivy image siddharth67/solar-system:$GIT_COMMIT --severity CRITICAL --exit-code 1 --quiet --format json -o trivy-image-CRITICAL-results.json'''
        sh '''trivy convert --format template --template "@/usr/local/share/trivy/templates/html.tpl" --output trivy-image-CRITICAL-results.html trivy-image-CRITICAL-results.json'''
        publishHTML([
            allowMissing: true,
            alwaysLinkToLastBuild: true,
            keepAll: true,
            reportDir: './',
            reportFiles: 'trivy-image-CRITICAL-results.html',
            reportName: 'Trivy Report'
        ])
    }
}
```

Problem seen in the dry-run conversion

* The `docker build` was inside an `sh` step and was not converted into a proper GitHub Actions `run` step.
* The `withDockerRegistry` block had no matching transformer, so it was left commented out in the generated workflow.

Example of the dry-run output for the Build Publish Image job (trimmed):

```yaml theme={null}
if-no-files-found: ignore
name: Code Coverage HTML Report
path: coverage/lcov-report

Build_Publish_Image:
  name: Build Publish Image
  runs-on:
    - ubuntu-latest
  needs: Code_Coverage
  steps:
    - name: checkout
      uses: actions/checkout@v4.1.0

# This item has no matching transformer
# - withDockerRegistry:
#   - key: credentialsId
#     value:
#       isLiteral: true
#       value: docker-hub-credentials
#   - key: url
#     value:
#       isLiteral: true
Trivy_Vulnerability_Scanner:
  name: Trivy Vulnerability Scanner
  runs-on:
    - ubuntu-latest
  needs: Build_Publish_Image
```

Identifying which `sh` steps contain Docker commands

* We ran a dry-run with a helper transformer that prints each `sh` identifier to locate the embedded Docker command.

Command used:

```bash theme={null}
gh actions-importer dry-run jenkins \
  --source-url http://139.84.149.83:8080/job/ci-pipeline-poll-scm/ \
  --output-dir tmp/dry-run \
  --custom-transformers helper-transformer.rb
```

Example `sh` identifier JSON for the Docker build:

```json theme={null}
{
  "name": "sh",
  "arguments": [
    {
      "key": "script",
      "value": {
        "isLiteral": true,
        "value": "docker build -t siddharth67/solar-system:$GIT_COMMIT ."
      }
    }
  ]
}
```

This confirmed the `docker build` command appears in the `sh` identifier and is transformable.

Custom `sh` transformer: extract and convert the `docker build`

* We added a transformer that targets `sh` nodes and only transforms steps that match the specific `docker build` invocation for this repository/image.
* The transformer substitutes hard-coded values with GitHub Actions variables, e.g. `siddharth67` → `vars.DOCKERHUB_USERNAME`, `solar-system` → `vars.IMAGE_NAME`, and `$GIT_COMMIT` → `github.sha`.

Add this Ruby transformer (example name: `ss-pipeline-transformer.rb`):

```ruby theme={null}
transform "sh" do |item|
  # Safely extract the shell script from the 'sh' identifier
  script = item.dig("arguments", 0, "value", "value").to_s

  if script.include?("docker build") && script.include?("siddharth67/solar-system")
    # Replace hard-coded values with GitHub Actions variables
    converted_script = script
      .gsub('siddharth67', '${{ vars.DOCKERHUB_USERNAME }}')
      .gsub('solar-system', '${{ vars.IMAGE_NAME }}')
      .gsub('$GIT_COMMIT', '${{ github.sha }}')

    # Return a GitHub Actions step
    {
      "name" => "Build Docker image",
      "run" => converted_script,
      "shell" => "bash"
    }

  else
    # Skip transformation for certain known commands already handled elsewhere
    next nil if item.dig("arguments", 0, "value", "value") == "npm test"
  end
end
```

Notes on the transformer

* Uses `dig` to safely navigate nested JSON and extract the script string.
* Matches only the intended `docker build` for this repository to avoid false positives.
* Substitutions convert Jenkins-specific/ hard-coded values to GitHub Actions expressions: use `vars.DOCKERHUB_USERNAME`, `vars.IMAGE_NAME`, and `github.sha`.
* The transformer returns a hash that becomes a GitHub Actions step with `name`, `run`, and `shell`.

Re-running the dry-run (with this transformer)

```bash theme={null}
gh actions-importer dry-run jenkins \
  --source-url http://139.84.149.83:8080/job/ci-pipeline-poll-scm/ \
  --output-dir tmp/dry-run \
  --custom-transformers ss-pipeline-transformer.rb
```

Result: The Build Publish Image job now contains a step named "Build Docker image" with the `docker build` command converted and variables substituted (no hard-coded credentials or commit tags remain).

Best-practice mapping (quick reference)

| Jenkins construct                                             |                                                                              GitHub Actions equivalent | Notes / Example                                                            |
| ------------------------------------------------------------- | -----------------------------------------------------------------------------------------------------: | -------------------------------------------------------------------------- |
| `sh 'docker build -t owner/repo:$GIT_COMMIT .'`               |       `run: docker build -t ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.IMAGE_NAME }}:${{ github.sha }} .` | Replace `owner`, `repo`, and Jenkins variables with `vars` / `github.sha`. |
| `withDockerRegistry(credentialsId: 'docker-hub-credentials')` | `uses: docker/login-action@v2` or `` `run: echo ${{ secrets.DOCKERHUB_TOKEN }} \| docker login ...` `` | Requires mapping Jenkins credentials to GitHub Secrets.                    |
| `publishHTML([...])`                                          |                                         Upload artifact or use a GH Action that publishes HTML reports | Use `actions/upload-artifact` or a static-site publish workflow.           |

<Callout icon="lightbulb" color="#1CB2FE">
  Define the repository or organization variables used by the transformer (for example, `vars.DOCKERHUB_USERNAME` and `vars.IMAGE_NAME`) in your GitHub repository or organization settings so the workflow can access them at runtime. For more, see [Using variables in GitHub Actions](https://docs.github.com/en/actions/using-workflows/variables).
</Callout>

Warning about credentials and registry authentication

<Callout icon="warning" color="#FF6B6B">
  Do not store credentials directly in workflows. Map Jenkins `credentialsId` to GitHub Secrets (e.g. `secrets.DOCKERHUB_USERNAME` and `secrets.DOCKERHUB_TOKEN`) or use `docker/login-action` with secure inputs. You will need a separate transformer for `withDockerRegistry` to perform this mapping correctly.
</Callout>

What remains to be done

* Transform `withDockerRegistry` blocks:
  * Create a transformer that recognizes `withDockerRegistry` and emits a GitHub Actions step to authenticate to the container registry (for example, `docker/login-action`), mapping Jenkins credentials to `secrets.*`.
* Ensure Trivy/report publishing:
  * If Trivy steps are in `sh`, they can be transformed similarly. Add steps to upload artifacts or publish HTML reports (e.g., `actions/upload-artifact`).
* Validate end-to-end:
  * Run the importer in dry-run first, then with real output, and test the generated workflow in a branch.

References

* Jenkinsfile syntax: [https://www.jenkins.io/doc/book/pipeline/jenkinsfile/](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/)
* GitHub Actions expressions and variables: [https://docs.github.com/en/actions/learn-github-actions/expressions](https://docs.github.com/en/actions/learn-github-actions/expressions)
* docker/login-action: [https://github.com/docker/login-action](https://github.com/docker/login-action)
* Trivy: [https://aquasecurity.github.io/trivy/latest/](https://aquasecurity.github.io/trivy/latest/)

That's it — we converted the embedded `docker build` in `sh` into a proper GitHub Actions `run` step and replaced hard-coded values with workflow variables. Next step: implement a transformer to convert `withDockerRegistry` into secure GitHub Actions authentication and finalize the push flow.

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/migrating-jenkins-pipelines-to-github-actions/module/3b5e500f-482a-4860-9f2c-d5f9fbc95159/lesson/f84d4317-bb2f-4d8e-8b18-ce4eec765ecf" />
</CardGroup>
