From graft
Jenkins Pipeline step implementation — Step/StepExecution pattern, CPS threading model, @Symbol naming, SynchronousNonBlockingStepExecution, durability with onResume(), and Pipeline-compatible API migration. Use this skill when implementing custom Pipeline steps, writing Jenkinsfile-compatible plugin code, or understanding how steps interact with the CPS VM thread. Make sure to use this skill whenever the user mentions Pipeline steps, custom Jenkinsfile steps, CPS threading, Step/StepExecution, or Pipeline credentials — even if they just say "make my plugin work in Pipeline." Triggers on: Pipeline step, StepExecution, workflow-step-api, CpsFlowDefinition, Jenkinsfile, @Symbol, SynchronousNonBlockingStepExecution, Pipeline plugin, Pipeline credentials, CPS thread, Pipeline compatible, workflow-cps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/graft:jenkins-pipelineThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
How to implement custom Pipeline steps that integrate cleanly with Declarative and Scripted
How to implement custom Pipeline steps that integrate cleanly with Declarative and Scripted
Pipeline. The canonical pattern comes from the workflow-step-api-plugin.
Every Pipeline step has two classes:
Step — declares parameters, creates execution:
public class ReadCacheStep extends Step {
private final String name;
@DataBoundConstructor
public ReadCacheStep(String name) {
this.name = name;
}
public String getName() { return name; }
@DataBoundSetter
public void setExcludes(String excludes) { this.excludes = excludes; }
// Prevent snippet generator from emitting excludes: ''
public String getExcludes() { return Util.fixEmpty(excludes); }
@Override
public StepExecution start(StepContext context) throws Exception {
return new Execution(context, this);
}
@Extension
@Symbol("readCache")
public static class DescriptorImpl extends StepDescriptor {
@Override
public String getFunctionName() { return "readCache"; }
@Override
public String getDisplayName() { return "Read Cache"; }
@Override
public Set<? extends Class<?>> getRequiredContext() {
return Set.of(FilePath.class, Run.class, TaskListener.class);
}
}
}
StepExecution — the runtime logic:
private static class Execution extends SynchronousNonBlockingStepExecution<Void> {
private final transient ReadCacheStep step;
Execution(StepContext context, ReadCacheStep step) {
super(context);
this.step = step;
}
@Override
protected Void run() throws Exception {
TaskListener listener = getContext().get(TaskListener.class);
FilePath workspace = getContext().get(FilePath.class);
Run<?, ?> run = getContext().get(Run.class);
listener.getLogger().println("Reading cache: " + step.getName());
// ... implementation ...
return null;
}
@Override
public void onResume() {
// Called after Jenkins restart — restart the operation
// since cache operations are idempotent
super.onResume();
}
}
Choose the right base class:
| Class | Thread Model | When to use |
|---|---|---|
SynchronousStepExecution | Runs on CPS VM thread | Avoid — blocks the entire Pipeline engine |
SynchronousNonBlockingStepExecution | Runs synchronously but off CPS thread | Preferred for I/O-bound steps (network, file ops) |
StepExecution (async) | Non-blocking, callback-based | Long-running or event-driven steps (wait for input, external trigger) |
GeneralNonBlockingStepExecution | Abstract helper for async | Simplifies async patterns |
The CPS VM thread runs all Pipeline Groovy code. If your step blocks this thread
(via SynchronousStepExecution or heavy computation), it blocks ALL Pipeline jobs
on that controller. Always use SynchronousNonBlockingStepExecution for I/O work.
public static class DescriptorImpl extends StepDescriptor {
@Override
public String getFunctionName() { return "myStep"; }
@Override
public Set<? extends Class<?>> getRequiredContext() {
// Declare what context objects the step needs
return Set.of(FilePath.class, Run.class, TaskListener.class);
}
@Override
public boolean takesImplicitBlockArgument() {
// true for steps like withEnv { ... } that wrap a block
return false;
}
}
Required context tells Jenkins what the step needs. Common context types:
Run.class — the buildFilePath.class — workspace (requires agent allocation)TaskListener.class — build logLauncher.class — process execution on agentEnvVars.class — environment variablesFlowNode.class — Pipeline graph nodeSteps that don't need FilePath or Launcher can run without a node {} block.
When Jenkins restarts mid-Pipeline, running steps get onResume() called.
Idempotent operations (cache read/write, file copy): just restart.
Non-idempotent operations (deploy, send notification): check state and decide.
Default behavior: super.onResume() re-runs run().
@Override
public void onResume() {
// Option 1: Restart (safe for idempotent ops)
super.onResume();
// Option 2: Fail gracefully
getContext().onFailure(new AbortException("Interrupted by restart"));
// Option 3: Skip if already done (check external state)
if (alreadyCompleted()) {
getContext().onSuccess(null);
} else {
super.onResume();
}
}
| Legacy (Freestyle only) | Pipeline-compatible |
|---|---|
AbstractBuild | Run<?, ?> |
AbstractProject | Job<?, ?> |
AbstractBuild.getProject() | Run.getParent() |
BuildListener | TaskListener |
getBuiltOn() | FilePath.toComputer() |
TransientProjectActionFactory | TransientActionFactory<Job> |
Trigger<AbstractProject> | Trigger<Job> or Trigger<ParameterizedJob> |
Key rule: If your method signature uses AbstractBuild, it won't work in Pipeline.
Always use Run and Job as the base types.
Without @Symbol: step([$class: 'MyBuilder', name: 'value'])
With @Symbol("myStep"): myStep 'value'
Requires structs plugin dependency. Place @Symbol on the DescriptorImpl:
@Symbol("myStep")
@Extension
public static class DescriptorImpl extends StepDescriptor { ... }
For build steps (Builder/Publisher), @Symbol goes on BuildStepDescriptor:
@Symbol("myBuilder")
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Builder> { ... }
Never use plain String password fields. Use the Credentials Plugin:
credentialsId field (String)<c:select/> in config.jelly for the credential picker// Username + password (most common)
StandardUsernamePasswordCredentials creds = CredentialsProvider.findCredentialById(
credentialsId, StandardUsernamePasswordCredentials.class, run);
// API token / secret text
StringCredentials token = CredentialsProvider.findCredentialById(
credentialsId, StringCredentials.class, run);
String secret = token.getSecret().getPlainText();
// SSH private key
SSHUserPrivateKey sshKey = CredentialsProvider.findCredentialById(
credentialsId, SSHUserPrivateKey.class, run);
// File-based credential (certificates, keystores)
FileCredentials file = CredentialsProvider.findCredentialById(
credentialsId, FileCredentials.class, run);
Common credential types:
| Interface | Use case | Plugin dependency |
|---|---|---|
StandardUsernamePasswordCredentials | Username + password | credentials |
StringCredentials | API tokens, secret strings | plain-credentials |
SSHUserPrivateKey | SSH keys | ssh-credentials |
FileCredentials | Certificates, keystores | plain-credentials |
StandardCertificateCredentials | X.509 certificates | credentials |
Domain scoping — use CredentialsMatchers for filtering:
List<StandardUsernamePasswordCredentials> creds = CredentialsProvider.lookupCredentials(
StandardUsernamePasswordCredentials.class, run.getParent(),
ACL.SYSTEM, URIRequirementBuilder.fromUri(serverUrl).build());
Security: Keep credential usage inside sh steps. Never store secrets in Groovy
variables — they get serialized to program.dat.
script {} blocks — they defeat the purpose of Declarative Pipeline@Symbol so steps work naturally in steps {} blocksUse AbortException for user-facing errors (bad config, missing file):
throw new AbortException("Cache '" + name + "' not found");
Use IllegalStateException for programming errors (should never happen):
if (workspace == null) throw new IllegalStateException("No workspace");
Use getContext().onFailure(exception) for async error reporting.
Best practices observed in well-reviewed production plugins:
ExtensionPoint with backends discovered via @ExtensionSynchronousNonBlockingStepExecution — never blocks CPS threadUtil.fixEmpty() in getters — clean snippet generator output@POST on validation methods — CSRF protection.groovy resources — not inline stringsnpx claudepluginhub aneveux/claude-garden --plugin graftGenerates declarative, scripted Jenkinsfiles and shared libraries for CI/CD pipelines with Docker/K8s agents, parallel stages, approvals, and security scans.
GitHub Actions, GitLab CI, Jenkins; stages, artifacts, caching, and deployment automation.