Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
84dd6ba
Add updateLock pipeline step for resource management
mPokornyETM Mar 31, 2026
02a6d66
Remove @since TODO - not applicable for plugins
mPokornyETM Mar 31, 2026
cbbe8bf
Add updateLock step documentation to README
mPokornyETM Mar 31, 2026
869ecba
Add 'reason' parameter to lock() step
mPokornyETM Mar 31, 2026
04d83b8
Merge origin/master into feature/lock-step-reason
mPokornyETM Apr 1, 2026
e93075b
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 1, 2026
8cfea03
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 2, 2026
e556bea
feat: Add reason parameter to reserve button
mPokornyETM Apr 2, 2026
d43ab84
fix: consolidate i18n templates to avoid duplicate IDs
mPokornyETM Apr 2, 2026
900823c
feat: Enhance reservation logging and add authentication check
mPokornyETM Apr 3, 2026
810cecc
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 3, 2026
3db60c5
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 3, 2026
23a2269
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 3, 2026
cc51f46
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 4, 2026
6e43667
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 6, 2026
cb65282
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 10, 2026
f998463
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 10, 2026
698fcdd
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 13, 2026
23ee40d
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 13, 2026
6811bde
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 16, 2026
c72368f
refactor: improve logging format for resource reservation and user au…
mPokornyETM Apr 16, 2026
e33314a
fix: include reason in toString(), steal action, and fix spotless for…
mPokornyETM Apr 16, 2026
0370732
Merge branch 'feature/lock-step-reason' of https://github.com/jenkins…
mPokornyETM Apr 16, 2026
6c23950
Merge branch 'master' into feature/lock-step-reason
mPokornyETM Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ echo 'Finish'

```

#### Lock with a reason

You can specify a reason why the resource is being locked. This is displayed
in the lockable resources UI while the resource is locked:

```groovy
lock(resource: 'staging-server', reason: 'Running integration tests') {
echo 'Deploying to staging'
}
```

The reason helps other users understand why a resource is unavailable.

Example for declarative pipeline:

```groovy
Expand Down Expand Up @@ -184,6 +197,46 @@ lock(resource: 'some_resource', skipIfLocked: true) {
}
```

#### Update resource properties

The `updateLock` step allows pipelines to dynamically manage lockable resources without using the Jenkins UI.

**Create a new resource:**

```groovy
updateLock(resource: 'my-resource', createResource: true, setLabels: 'env-test team-a')
```

**Modify labels on an existing resource:**

```groovy
// Replace all labels
updateLock(resource: 'my-resource', setLabels: 'new-label1 new-label2')

// Add labels (keeps existing)
updateLock(resource: 'my-resource', addLabels: 'additional-label')

// Remove specific labels
updateLock(resource: 'my-resource', removeLabels: 'old-label')

// Add and remove in one step
updateLock(resource: 'my-resource', addLabels: 'new', removeLabels: 'old')
```

**Set a note on a resource:**

```groovy
updateLock(resource: 'my-resource', setNote: 'Updated by build #${BUILD_NUMBER}')
```

**Delete a resource:**

```groovy
updateLock(resource: 'my-resource', deleteResource: true)
```

> **Note:** Resources cannot be deleted while locked, queued, or reserved.

Detailed documentation can be found as part of the
[Pipeline Steps](https://jenkins.io/doc/pipeline/steps/lockable-resources/)
documentation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class LockStep extends Step implements Serializable {
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
public String label = null;

/** The reason why this resource is being locked, displayed in the UI while locked. */
@CheckForNull
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
public String reason = null;

@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
public int quantity = 0;

Expand Down Expand Up @@ -103,6 +108,13 @@ public void setLabel(String label) {
}
}

@DataBoundSetter
public void setReason(String reason) {
if (reason != null && !reason.trim().isEmpty()) {
this.reason = reason.trim();
}
}

@DataBoundSetter
public void setVariable(String variable) {
if (variable != null && !variable.trim().isEmpty()) {
Expand Down Expand Up @@ -223,7 +235,7 @@ public void validate(boolean allowEmptyOrNullValues) {
public List<LockStepResource> getResources() {
List<LockStepResource> resources = new ArrayList<>();
if (resource != null || label != null) {
resources.add(new LockStepResource(resource, label, quantity));
resources.add(new LockStepResource(resource, label, quantity, reason));
}

if (extra != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public boolean start() throws Exception {
return false;
}

if (!lrm.lock(available, run)) {
if (!lrm.lock(available, run, step.reason)) {
// this here is very defensive code, and you will probably never hit it. (hopefully)
LOGGER.warning("Internal program error: Can not lock resources: " + available);
onLockFailed(logger, resourceHolderList);
Expand Down Expand Up @@ -156,7 +156,8 @@ private void onLockFailed(PrintStream logger, List<LockableResourcesStruct> reso
step.toString(),
step.variable,
step.inversePrecedence,
step.priority);
step.priority,
step.reason);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ public class LockStepResource extends AbstractDescribableImpl<LockStepResource>
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
public int quantity = 0;

/** The reason why this resource is being locked, displayed in the UI while locked. */
@CheckForNull
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.")
public String reason = null;

LockStepResource(@Nullable String resource, @Nullable String label, int quantity) {
this(resource, label, quantity, null);
}

LockStepResource(@Nullable String resource, @Nullable String label, int quantity, @Nullable String reason) {
this.resource = Util.fixEmptyAndTrim(resource);
this.label = Util.fixEmptyAndTrim(label);
this.quantity = quantity;
this.reason = Util.fixEmptyAndTrim(reason);
}

@DataBoundConstructor
Expand All @@ -56,24 +66,37 @@ public void setQuantity(int quantity) {
this.quantity = quantity;
}

@DataBoundSetter
public void setReason(String reason) {
this.reason = Util.fixEmptyAndTrim(reason);
}

@Override
public String toString() {
return toString(resource, label, quantity);
return toString(resource, label, quantity, reason);
}

public static String toString(String resource, String label, int quantity) {
return toString(resource, label, quantity, null);
}

public static String toString(String resource, String label, int quantity, String reason) {
// a label takes always priority
StringBuilder sb = new StringBuilder();
if (label != null) {
sb.append("Label: ").append(label);
if (quantity > 0) {
return "Label: " + label + ", Quantity: " + quantity;
sb.append(", Quantity: ").append(quantity);
}
return "Label: " + label;
} else if (resource != null) {
sb.append("Resource: ").append(resource);
} else {
return "[no resource/label specified - probably a bug]";
}
// make sure there is an actual resource specified
if (resource != null) {
return "Resource: " + resource;
if (reason != null && !reason.isEmpty()) {
sb.append(", Reason: ").append(reason);
}
return "[no resource/label specified - probably a bug]";
return sb.toString();
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public class LockableResource extends AbstractDescribableImpl<LockableResource>
private Date reservedTimestamp = null;
private String note = "";

/**
* The reason why this resource is currently locked. Set via the lock() step's reason parameter.
* Cleared when the resource is unlocked.
*/
private String lockReason = "";

/**
* Track that a currently reserved resource was originally reserved for someone else, or locked
* for some other job, and explicitly taken away - e.g. for SUT post-mortem while a test job runs.
Expand Down Expand Up @@ -258,6 +264,26 @@ public void setNote(@Nullable String note) {
this.note = Util.fixNull(note);
}

/**
* Returns the reason why this resource is currently locked.
*
* @return The lock reason, or empty string if not set.
*/
@Exported
public String getLockReason() {
return this.lockReason;
}

/**
* Sets the reason why this resource is being locked. This is typically set via the lock() step's
* reason parameter and cleared when the resource is unlocked.
*
* @param lockReason The reason for locking, or null to clear.
*/
public void setLockReason(@Nullable String lockReason) {
this.lockReason = Util.fixNull(lockReason);
}

@DataBoundSetter
public void setEphemeral(boolean ephemeral) {
this.ephemeral = ephemeral;
Expand Down Expand Up @@ -425,6 +451,7 @@ private boolean evaluateScript(@NonNull SecureGroovyScript script, @CheckForNull
binding.setVariable("resourceDescription", description);
binding.setVariable("resourceLabels", this.getLabelsAsList());
binding.setVariable("resourceNote", note);
binding.setVariable("resourceLockReason", lockReason);
try {
Object result = script.evaluate(Jenkins.get().getPluginManager().uberClassLoader, binding, null);
if (LOGGER.isLoggable(Level.FINE)) {
Expand Down Expand Up @@ -667,6 +694,7 @@ public void reset() {
this.unReserve();
this.unqueue();
this.setBuild(null);
this.setLockReason(null);
invalidateCaches();
}

Expand All @@ -681,6 +709,7 @@ public void copyUnconfigurableProperties(final LockableResource sourceResource)
setReservedTimestamp(sourceResource.getReservedTimestamp());
setNote(sourceResource.getNote());
setReservedBy(sourceResource.getReservedBy());
setLockReason(sourceResource.getLockReason());
}
}

Expand All @@ -692,6 +721,7 @@ public void resetUnconfigurableProperties() {
setReservedBy(null);
setReservedTimestamp(null);
setNote("");
setLockReason("");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,21 @@ public boolean lock(
// ---------------------------------------------------------------------------
/** Try to lock the resource and return true if locked. */
public boolean lock(List<LockableResource> resourcesToLock, Run<?, ?> build) {
return lock(resourcesToLock, build, (String) null);
}

// ---------------------------------------------------------------------------
/**
* Try to lock the resource and return true if locked.
*
* @param resourcesToLock The resources to lock.
* @param build The build that is locking the resources.
* @param reason The reason why the resources are being locked (displayed in UI).
* @return true if locked successfully.
*/
public boolean lock(List<LockableResource> resourcesToLock, Run<?, ?> build, @Nullable String reason) {

LOGGER.fine("lock it: " + resourcesToLock + " for build " + build);
LOGGER.fine("lock it: " + resourcesToLock + " for build " + build + " with reason: " + reason);

if (build == null) {
LOGGER.warning("lock() will fails, because the build does not exits. " + resourcesToLock);
Expand All @@ -654,6 +667,9 @@ public boolean lock(List<LockableResource> resourcesToLock, Run<?, ?> build) {
for (LockableResource r : resourcesToLock) {
r.unqueue();
r.setBuild(build);
if (reason != null && !reason.isEmpty()) {
r.setLockReason(reason);
}
}

LockedResourcesBuildAction.findAndInitAction(build).addUsedResources(getResourcesNames(resourcesToLock));
Expand Down Expand Up @@ -683,6 +699,7 @@ private void freeResources(List<LockableResource> unlockResources, Run<?, ?> bui

resource.unqueue();
resource.setBuild(null);
resource.setLockReason(null);
uncacheIfFreeing(resource, true, false);

if (resource.isEphemeral()) {
Expand Down Expand Up @@ -768,7 +785,7 @@ private boolean proceedNextContext() {
LOGGER.warning("Skip this context, as the build cannot be retrieved");
return true;
}
boolean locked = this.lock(requiredResourceForNextContext, build);
boolean locked = this.lock(requiredResourceForNextContext, build, nextContext.getReason());
if (!locked) {
// defensive line, shall never happen
LOGGER.warning("Can not lock resources: " + requiredResourceForNextContext);
Expand Down Expand Up @@ -1360,6 +1377,22 @@ public void queueContext(
String variableName,
boolean inversePrecedence,
int priority) {
queueContext(context, requiredResources, resourceDescription, variableName, inversePrecedence, priority, null);
}

/*
* Adds the given context and the required resources to the queue if
* this context is not yet queued.
*/
@Restricted(NoExternalUse.class)
public void queueContext(
StepContext context,
List<LockableResourcesStruct> requiredResources,
String resourceDescription,
String variableName,
boolean inversePrecedence,
int priority,
String reason) {
synchronized (syncResources) {
for (QueuedContextStruct entry : this.queuedContexts) {
if (entry.getContext() == context) {
Expand All @@ -1369,8 +1402,8 @@ public void queueContext(
}

int queueIndex = 0;
QueuedContextStruct newQueueItem =
new QueuedContextStruct(context, requiredResources, resourceDescription, variableName, priority);
QueuedContextStruct newQueueItem = new QueuedContextStruct(
context, requiredResources, resourceDescription, variableName, priority, reason);

if (!inversePrecedence || priority != 0) {
queueIndex = this.queuedContexts.size() - 1;
Expand Down
Loading
Loading