Skip to main content
In this lesson we build a custom transformer to convert a Jenkins catchError wrapper used in a Code Coverage stage into equivalent GitHub Actions steps. Jenkins’s catchError lets a failing step continue the pipeline while setting a specific build or stage result. When migrating to GitHub Actions we want the workflow to run the same coverage command, continue on error so downstream jobs still run, and preserve HTML artifacts for later inspection. This article walks through the original Jenkins stage, the parsed AST-like representation the importer prints in dry-run, the Ruby transformer implementation, the importer dry-run invocation and output, the generated GitHub Actions job snippet, and the run results.

Jenkinsfile snippet (Code Coverage stage — original)

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: 'Code Coverage'
        ])
    }
}

What the importer sees (AST-like JSON)

Run the importer in dry-run mode to print parsed identifiers and their arguments. In this case the importer detected a catchError node whose child is a sh invocation of the coverage command. Sample parsed representation for catchError (converted to valid JSON for clarity):
{
  "name": "catchError",
  "arguments": [
    {
      "key": "buildResult",
      "value": { "isLiteral": true, "value": "SUCCESS" }
    },
    {
      "key": "message",
      "value": { "isLiteral": true, "value": "Oops! it will be fixed in future releases" }
    },
    {
      "key": "stageResult",
      "value": { "isLiteral": true, "value": "UNSTABLE" }
    }
  ],
  "children": [
    {
      "name": "sh",
      "arguments": [
        {
          "key": "script",
          "value": { "isLiteral": true, "value": "npm run coverage" }
        }
      ]
    }
  ]
}

Approach — desired mapping to GitHub Actions

Goal: mirror Jenkins behavior so coverage failures do not block the pipeline, but HTML reports are still produced and uploaded. Steps:
  • Extract the inner coverage command from item.dig("children", 0, "arguments", 0, "value", "value").
  • Fall back to npm run coverage if the command is not found.
  • Emit two GitHub Actions steps:
    1. Install dependencies: npm install --no-audit.
    2. Run coverage: set continue-on-error: true so the workflow continues even if the command exits non‑zero.
  • Let the existing importer conversion handle publishHTMLactions/upload-artifact@v4 (artifact upload of the coverage HTML).

Transformer implementation (helper-transformer.rb)

The transformer below is added to the importer’s custom transformers so that catchError nodes are converted into a lightweight sequence of GitHub Actions steps.
# Skip transforming certain nodes already handled elsewhere
transform "retry" do |item|
  next nil if item.dig("arguments", 0, "value", "value") == 2
end

transform "junit" do |item|
  # Skip transformation if junit result file is already handled (example)
  next nil if item.dig("arguments", 0, "value", "value") == "test-results.xml"
end

transform "catchError" do |item|
  # If this catchError wraps "npm test" (inside a sh child), skip
  next nil if item.dig("children", 0, "arguments", 0, "value", "value") == "npm test"

  # Extract coverage command from children -> sh -> arguments -> script
  coverage_cmd = item.dig("children", 0, "arguments", 0, "value", "value") || "npm run coverage"

  [
    {
      "name" => "Install dependencies",
      "run" => "npm install --no-audit",
      "shell" => "bash"
    },
    {
      "name" => "Check Code Coverage",
      "continue-on-error" => true,
      "run" => coverage_cmd,
      "shell" => "bash"
    }
  ]
end

Running the importer dry-run

Use the gh actions-importer tool in dry-run mode to test extraction and transformation without writing the final workflows.
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 (abridged) dry-run output showing the parsed catchError and the output file produced:
JSON for identifier: {"name"=>"catchError", "arguments"=>[{"key"=>"buildResult","value"=>{"isLiteral"=>true,"value"=>"SUCCESS"}},{"key"=>"message","value"=>{"isLiteral"=>true,"value"=>"Oops. It will be fixed in future releases"}},{"key"=>"stageResult","value"=>{"isLiteral"=>true,"value"=>"UNSTABLE"}},{"children"=>[{"name"=>"sh","arguments"=>[{"key"=>"script","value"=>{"isLiteral"=>true,"value"=>"npm run coverage"}}]}]}]}
Output file(s):
  tmp/dry-run/ci-pipeline-poll-scm/.github/workflows/ci-pipeline-poll-scm.yml

Resulting GitHub Actions job (excerpt)

The generated Code Coverage job includes checkout, dependency installation, the coverage step with continue-on-error: true, and artifact upload for the HTML report.
jobs:
  Code_Coverage:
    name: Code Coverage
    runs-on:
      - ubuntu-latest
    container:
      image: node:24
    needs: Unit_Testing
    steps:
      - name: checkout
        uses: actions/checkout@v4.1.0
      - name: Install dependencies
        run: npm install --no-audit
        shell: bash
      - name: Check Code Coverage
        continue-on-error: true
        run: npm run coverage
        shell: bash
      - name: Upload Artifacts
        uses: actions/upload-artifact@v4.1.0
        with:
          if-no-files-found: ignore
          name: Code Coverage HTML Report
          path: coverage/lcov-report

Pull request diff (abridged)

The transformer replaced the untranslated catchError block in the PR diff with two explicit steps that install dependencies and run the coverage command with continue-on-error.
@@ -105,20 +105,13 @@ jobs:
   steps:
   - name: checkout
     uses: actions/checkout@v4.1.0
-#  # This item has no matching transformer
-#
-#  - catchError:
-#    - key: buildResult
-#      value:
-#        isLiteral: true
-#        value: SUCCESS
-#    - key: message
-#      value:
-#        isLiteral: true
-#        value: Oops! it will be fixed in future releases
-#    - key: stageResult
-#      value:
-#        isLiteral: true
-#        value: UNSTABLE
+  - name: Install dependencies
+    run: npm install --no-audit
+    shell: bash
+  - name: Check Code Coverage
+    continue-on-error: true
+    run: npm run coverage
+    shell: bash
   - name: Upload Artifacts
     uses: actions/upload-artifact@v4.1.0

What happened when the workflow ran

  • A new workflow run was triggered after merging the PR.
  • The Code Coverage job executed the two transformed steps:
    • The npm install --no-audit step completed.
    • The npm run coverage step failed the coverage threshold (exit code non-zero).
  • Because continue-on-error: true was set, the job continued and downstream jobs also ran.
  • The coverage HTML report was uploaded as an artifact and is available for download from the run.
A screenshot of a GitHub Actions workflow run showing the pipeline summary on the left, a visualization of jobs at the top, and an Annotations panel listing errors (Code Coverage failed and two canceled runs). Below is an Artifacts list with reports like "Code Coverage HTML Report" and "Dependency Check HTML Report."
You can download the artifact (Code Coverage HTML Report) and inspect it locally:
A dark‑theme Windows File Explorer window showing the contents of a "Code Coverage HTML Report.zip" with a list of HTML, CSS, JS and PNG files (e.g., app.js.html, base.css, index.html). The left navigation pane shows drives and folders and the right preview area says "Select a file to preview."

Code coverage failure (abridged)

This is an example of the coverage output that caused the coverage command to exit non-zero:
11 passing (825ms)

ERROR: Coverage for lines (79.06%) does not meet global threshold (90%)
-------------------------------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------------------------------------------
All files | 79.54 | 33.33 | 70 | 79.06 |
app.js | 79.54 | 33.33 | 70 | 79.06 | 23,49-50,58,62-67
-------------------------------------------------------
Error: Process completed with exit code 1.
Using continue-on-error: true in the converted step mirrors Jenkins catchError semantics: it allows the workflow to continue and still upload artifacts even when the coverage command fails. Note that GitHub Actions will mark that step as successful for continuation, so consider also surfacing the failure via annotations, test result upload, or a PR comment if you want to preserve visibility of the failure state.

Mapping summary

Jenkins elementGitHub Actions equivalentNotes
catchError { sh 'npm run coverage' }Two steps: npm install --no-audit + npm run coverage with continue-on-error: trueExtracts inner sh command and wraps in continue-on-error to emulate Jenkins behavior.
publishHTMLactions/upload-artifact@v4Importer handles this conversion automatically and uploads coverage HTML (e.g. coverage/lcov-report).
Example of a JSON-like transformer output is represented in the helper-transformer.rb implementation above.

References

Summary

  • We added a custom transformer that:
    • Detects a catchError node and extracts the inner shell command (defaults to npm run coverage).
    • Emits two GitHub Actions steps: an installation step and a coverage step with continue-on-error: true.
  • The importer still converts publishHTML to actions/upload-artifact automatically, preserving HTML reports as artifacts.
  • This keeps the pipeline behavior consistent: coverage failures do not block downstream jobs while reports remain available for inspection.

Watch Video