A Jenkins plugin providing:
-
EphemeralCredentialsProvider— aCredentialsProviderthat resolves credentials only for Pipeline builds that put something into it themselves. Nothing it holds is ever written to disk — a plain in-memory map, cleared as soon as the owning build finishes or Jenkins restarts.WarningUse of this plugin does not magically guarantee the safety of your secrets, just that they would not be present on your system throughout its lifetime and readable with sufficient access from the running JVM or stored configuration files (or backups thereof).
The ephemeral credentials, if collected, are stored in instances of classes provided by other plugins and later used by standard Jenkins code paths, which may leave traces on the build systems. For example, SSH keys or certificates may be saved into temporary files for tools like
gitorssh-agentto use them. Simple echoes of passwords and usernames may get scrubbed from displayed build logs and replaced by asterisks (as implemented by respective plugins, see e.g. thewithCredentials()step), but at least temporarily are present in raw log file snippets on the server. The collection overall should not get serialized to disk when a Jenkins JVM saves state (configuration, CPS machinery).In-flight data passing through the
input()step may get serialized by its action class, so special care must be paid to which input field type one would request there: some backing classes involve encoding aSecretwith the Jenkins instance key, orSecretByteswith a random key unique to each JVM process instance, for storage in memory right away.For more details, please see the "What can get saved, when and where" chapter below.
-
withEphemeralCredentials(…) { … }— a global pipeline step (no@Libraryimport needed) that, for each declared credential ID missing from every other store, pauses oninputto collect it interactively, caches it in the provider above, and then runs its body — so an unmodified nestedwithCredentials/checkout/etc. inside that body just finds the value.
|
Note
|
Technical note
This exists as a real plugin (not a JSL shared-library class), because shared-library It is currently pipeline-only. Everything below — the If someone finds a way to reliably discover the |
Assume you develop a database-backed project, and your testing and automation pipelines involve clients which somehow access that database. In your daily practice you could juggle different deployments of such a project with very different requirements about usability vs. security of the credentials:
-
a local instance on your development workstation: no real data to secure, a persisted credential is convenient to be quickly available so you can iterate without overheads;
-
a farm of instances in your CI: to avoid confusion, you may want unique stored credentials (even if predictable or loaded from some Kubernetes secrets) just to rule out situations where your client instances might access the "wrong" database (e.g. due to failed re-deployments) and so corrupt unrelated test runs for somebody else;
-
customer production instances: to minimize the attack surface, you do not want the credentials saved anywhere anytime, and always entered when some maintenance/patching/whatever job runs (thus presumed called by humans who know what they are doing, when and where… and shifting the responsibility about security from automation suppliers to a customer operations team).
And yet you want to use exactly the same pipelines (or at least shared libraries) for all Jenkins deployments that deal with instances of this project. For a local instance, you would just persist standard credentials in your local Jenkins configuration (manually, JCasC, etc.); for the CI testing farm you could have a custom job that prepares the testbed and runs the tests, so it can also put() the unique value of same-named ephemeral credential into the store associated with this Run; for the customer instance you do not pre-define the credential anywhere at all and so require it to be input() for every Run.
Either way, your jobs call the same custom Jenkins Shared Library routine which would ask local providers for a credential named like DB_ADMIN, and then access the database instance using its value resolved either globally or for the current run by the standard step like withCredentials.
call() {
withEphemeralCredentials([
ephemeralUsernamePassword(id: 'DB_ADMIN',
description: 'DB admin login for this deployment')
]) {
withCredentials([
usernamePassword(credentialsId: 'DB_ADMIN',
usernameVariable: 'U', passwordVariable: 'P')
]) {
sh './run-migration --user "$U" --password "$P"'
}
}
}
Here is a single-pipeline variant to illustrate the call stack with the Ephemeral Credentials plugin wrapping its consumers:
pipeline {
agent any
stages {
stage('deploy') {
steps {
withEphemeralCredentials([
ephemeralUsernamePassword(id: 'DB_ADMIN', description: 'DB admin login for this deploy'),
ephemeralSecretText(id: 'NEXUS_TOKEN', description: 'Nexus API token')
]) {
withCredentials([usernamePassword(credentialsId: 'DB_ADMIN', usernameVariable: 'U', passwordVariable: 'P')]) {
sh './run-migration --user "$U" --password "$P"'
}
// an unmodified checkout(credentialsId: 'NEXUS_TOKEN', ...) here should also just work
}
}
}
}
}
If DB_ADMIN and NEXUS_TOKEN values already resolve via a "real" credentials store (global/folder/job-scoped), nothing is prompted — like other call paths, the withEphemeralCredentials step checks the common entry point, CredentialsProvider.findCredentialById() method, first.
Only a genuinely missing ID triggers input, guarded by a lock keyed on <runId>-<credentialId> so parallel branches or repeated calls within the same run (unless Jenkins gets restarted and the job goes on) don’t prompt twice.
The withEphemeralCredentials step does not require its body to do anything — the for loop that resolves/prompts/caches each declared ID runs regardless of what the body contains, and an empty closure is perfectly valid:
pipeline {
agent any
stages {
stage('collect ephemeral_credentials') {
steps {
// Nothing here needs these IDs yet - this just forces them
// to be resolved (and, if missing, prompted for) while
// whoever started the build is still around to answer.
withEphemeralCredentials([
ephemeralUsernamePassword(id: 'DB_ADMIN', description: 'DB admin login for this deploy'),
ephemeralSecretText(id: 'NEXUS_TOKEN', description: 'Nexus API token')
]) { /* empty */ }
}
}
stage('build') {
steps {
sh 'make all' // takes an hour; nobody's watching by the time it's done
}
}
stage('deploy') {
steps {
// Unmodified - DB_ADMIN/NEXUS_TOKEN are already cached from
// the first stage, so nothing pauses here even though this
// stage runs long after the human who started the build
// walked away.
withCredentials([usernamePassword(credentialsId: 'DB_ADMIN', usernameVariable: 'U', passwordVariable: 'P')]) {
sh 'run-migration --user "$U" --password "$P"'
}
}
}
}
}
This is the practical way to avoid a pipeline silently blocking on input deep inside a long build, minutes or hours after whoever triggered it has stopped watching. Here we move the (possibly interactive) credential resolution into a cheap, early, empty-bodied call, and let everything downstream — wrapped or not — just find the value already cached for the rest of the run.
It also allows modernization of your pipelines to use this plugin by addressing it in one place, instead of wrapping each code path that might use a credential. You would have to know the necessary credential names and types by then, though (in some pipelines this in itself might be a site- or codepath-specific configuration element, e.g. juggling database users vs. admins for the generally same call of a database client).
Caveat worth knowing: the cache is the in-memory JVM singleton described below: it does not survive a controller restart, but a paused Pipeline build does (that’s the whole point of Pipeline’s durability model). If Jenkins restarts between the early warm-up stage and the later stage that actually uses the credential, the build resumes correctly, but the cache is gone: a later withEphemeralCredentials call for the same ID prompts again (safe, just an extra interruption), while a later unmodified call relying solely on the earlier warm-up (no withEphemeralCredentials at that point) simply won’t find the credential at all, since nothing at that later point would re-trigger the interactive path. While such situations are rare, they can be real — weigh it against how long the gap between warm-up and use is, and how likely a restart is in that window. Some Jenkins deployments deliberately disable durability in favor of performance, and to make reasonably valid assumptions about the state of external "systems under test" (e.g. firmware being re-flashed, etc.)
The @Extension-registered CredentialsProvider. Holds a Map<runExternalizableId, Map<credentialsId, Credentials>> in memory with disabled serialization, so that sensitive credentials can be contained.
Its getCredentialsInItemGroup(…) (the method Jenkins' generic credentials lookup calls) resolves which build is asking from the calling thread’s own CPS execution context (CpsThread.current(), anchored to the whole build’s CpsFlowExecution rather than to any transient per-node-block Executor, so it stays correct across parallel branches and sequential stage/agent changes).
|
Note
|
That resolution only works when the caller is itself CPS-interpreted Pipeline code — this plugin’s own This can be fixed by a new API proposed for |
Handles onFinalized/onDeleted cleanup. This is the authoritative cleanup path, not a finally block in the pipeline script, since a hard-killed build can skip the latter.
withEphemeralCredentials step (WithEphemeralCredentials.groovy + WithEphemeralCredentialsGlobalVariable)
Registered via the GlobalVariable extension point, the same mechanism that makes env/params/currentBuild available without an import. The actual logic ships as a .groovy resource file (not precompiled Java/Groovy) and is parsed on demand through the calling script’s own GroovyClassLoader — the same technique DockerDSL in docker-workflow-plugin uses for its Docker.groovy for example — so it goes through the same CPS transformation as a shared-library script, which is what makes its calls to lock and input steps provided by the pipeline script context safe.
Use of the lock step allows to limit the input of a previously missing credential to one of possibly many parallel or agent-bound stages that would want it: the first one to get the lock would ask for it, and others would find it already cached when their turn comes.
The user may cancel the input step, causing the credential to remain unknown but not interrupting the pipeline immediately in any other way.
Two things worth knowing before touching this file:
-
The calling build’s own sandbox status applies to this code too. Since it’s compiled through the calling script’s classloader, a sandboxed Jenkinsfile makes this code sandboxed as well — every non-step Java call it makes (even into this plugin’s own classes) needs to be explicitly approved for sandboxed use, or it’s rejected with
RejectedAccessException, the same waycredentials-bindinganddocker-workflow-pluginapprove their own DSL glue. This plugin does that approval with@org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelistedannotation directly on the relevant methods (all on this plugin’s own classes, inWithEphemeralCredentialsSupport,EphemeralCredentialSpecandEphemeralCredentialsAccessor) rather than a separate static allowlist file, so the approved surface stays visible right next to the code it approves. -
Never hold a live
Runacross alock/inputpause. This class sits in the script’s binding for the whole build, so CPS’s own program-state serialization walks it every time the pipeline pauses, andWorkflowRunisn’t Java-serializable. The class itself only ever holds a plainexternalizableIdString (used to name the per-credentiallockresource); the actualRunis resolved fresh, on demand, byWithEphemeralCredentialsSupportfrom the calling thread’s own CPS context each time it’s needed, never held in a field or a variable that could still be live across a pause.
All five credential types the standard "Add Credentials" UI offers are wrapped, each its own GlobalVariable returning a precompiled groovy.lang.Closure that builds the matching EphemeralCredentialSpec subclass:
| Factory | Spec class | Materializes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Every EphemeralCredentialSpec carries:
-
an
id, -
a
descriptionpassed straight through toinputstep’smessage, -
the
inputParameterDefinitionitems to collect, and -
the logic to turn the answers into a
Credentialsobject.
Since these factories never invoke a pipeline step themselves, a plain precompiled Closure is fine for them — no CPS transformation, no sandbox approval needed (unlike WithEphemeralCredentials.groovy itself).
Secret file and certificate collect their content as pasted base64 text, not a real file upload. The input step’s FileParameterDefinition writes the uploaded file to disk as part of its own normal handling — in direct tension with this plugin’s "never persisted" design — so both specs ask for base64-encoded text instead (the file’s raw bytes / the PKCS#12 keystore’s raw bytes, base64-encoded) and decode it entirely in memory via SecretBytes.fromBytes(…). A pragmatic simplification, not a faithful reproduction of the web UI’s upload widget.
|
Warning
|
Every field that carries actual secret material — For The |
The ephemeralSSHUserPrivateKey has a soft dependency on the "SSH Credentials" plugin (https://plugins.jenkins.io/ssh-credentials/) — unlike the other four types, which only need plugins this one already requires unconditionally. It’s declared <optional>true</optional> in pom.xml, so an installation that never uses this specific credential type doesn’t need SSH Credentials installed at all — the manifest records it as resolution:=optional. Constructor of EphemeralSSHUserPrivateKey deliberately touches BasicSSHUserPrivateKey.class (a class literal is enough to force resolution) so a missing plugin fails immediately, with a clear message, at the point the pipeline author declares the spec — not deep inside materialize() after a human has already answered an input prompt for nothing.
The privateKey is collected via PasswordParameterDefinition and reconstructed server-side, not pasted as multi-line text. A private key is exactly the kind of secret a naive implementation would put in a TextParameterDefinition (it’s genuinely multi-line PEM), but doing so means the raw key text is written to the build’s own workflow/*.xml in cleartext every time (see the analysis section below) — so instead:
-
The field is a
PasswordParameterDefinition. Per the HTML Standard’s value-sanitization algorithm, a single-line<input>(which is whatpassword-type renders as) has any\r/\ncharacters stripped from its value — by the browser, unconditionally, whether the text was typed or pasted. A multi-line PEM key pasted into this field arrives atmaterialize()with its-----BEGIN …-----/base64 body/-----END …-----all run together with no line breaks at all. -
EphemeralSSHUserPrivateKey.reconstructPem(…)matches the flattened string against(-----BEGIN .?-----)(.*)(-----END .?-----)and reassembles it with real newlines only immediately around the BEGIN/END marker lines — the base64 body itself is written back as one unbroken line, no re-wrapping at any particular column width needed. -
This is not a guess: verified against
ssh-keygen -y,openssl pkey/genpkey, andssh-add(loading the reconstructed key into a realssh-agentand using it tossh-keygen -Y signa test payload). All three tools parsed/used a reconstructed, single-unbroken-line-body key identically to the original traditionally-wrapped one — for both the newerOPENSSH PRIVATE KEYformat and the older PKCS#8PRIVATE KEYPEM format. PEM’s traditional 64/70-column wrapping is a legacy generator convention, not something parsers require.
The same technique generalizes to any other multi-line secret format with a recognizable fixed structure (a header/footer, or any other unambiguous anchor) — worth knowing if you’re writing a custom EphemeralCredentialSpec for something similarly shaped.
Adding another credential type (e.g. for GitLab or GitHub API tokens, other SSH and certificate types) means one more EphemeralCredentialSpec subclass plus one more tiny factory GlobalVariable, following the same shape.
EphemeralCredentialSpec.inputParameters()/materialize() never invoke a pipeline step: they are pure data/logic building a ParameterDefinition list and a Credentials object — so a new spec subclass has none of the CPS/sandbox constraints WithEphemeralCredentials.groovy itself has.
Three places a new type can live:
-
This plugin: add the subclass + factory
GlobalVariablehere, cut a new release. Right choice for something broadly useful, like the five standard types above. -
A separate plugin: depend on
ephemeral-credentials-provider, define your own subclass +GlobalVariable. Only needed for the factory/DSL-registration half, sinceGlobalVariableis a genuineExtensionPointrequiring@Extensiondiscovery at Jenkins startup, which only a real installed plugin gets. -
JSL — no new plugin at all. The spec subclass can be an ordinary JSL
src/class extending this plugin’spublic abstract class EphemeralCredentialSpec(its constructor isprotected, so any subclass anywhere can callsuper(id, description)). Many JSL’s already reference classes from other installed plugins directly today (e.g.CredentialNotFoundExceptionfromcredentials-binding), this is the same pattern. The factory doesn’t needGlobalVariableeither: a plainvars/ephemeralXyz.groovywithdef call(Map args) { new EphemeralXyzSpec(args.id, args.description) }becomes callable exactly the wayusernamePassword(…)reads, because JSLvars/files are the native mechanism of Jenkins for contributing global callables — no explicit@Extension, no plugin install, just a file in the library.
So a plugin release is only strictly required for the GlobalVariable registration step — anything that’s pure data (every implementation of an EphemeralCredentialSpec so far) can live in a JSL instead, with zero coordination against this plugin’s release cycle.
This plugin’s own design goal is that nothing it handles is ever persisted to disk. That guarantee is easy to state and easy to accidentally break when adding a new credential type, because most of the actual risk lives outside this plugin’s own code — in how Jenkins core and pipeline-input-step treat the input step’s answers, and in how CPS persists a paused pipeline’s state. Both are documented in detail below.
1. The input step parameter type controls whether the answer is encrypted on disk — unconditionally, every time
InputStepExecution.proceed(…) — the method that runs the moment a human submits an input form — always does this, regardless of what the pipeline does with the result afterwards:
getNode().addAction(new InputSubmittedAction(approverId, params));
InputSubmittedAction implements PersistentAction: it is written to the build’s own workflow/*.xml action records on disk as part of that same call, before your pipeline script ever sees the answer. There is no way for a withEphemeralCredentials-style caller to opt out of this — it happens inside pipeline-input-step itself.
Whether the value inside that persisted XML is encrypted or plaintext depends entirely on the ParameterValue type behind whichever ParameterDefinition you asked for it with:
ParameterDefinition |
ParameterValue.getValue() returns |
Persisted as |
|---|---|---|
|
|
Encrypted — |
|
plain |
Cleartext, verbatim, forever — in every build’s own XML on disk, for as long as that build record exists. |
This is why every field carrying secret material in this plugin’s own five credential types is a PasswordParameterDefinition, with no exception — including contentBase64/keystoreBase64/privateKey (see "Credential type factories" above).
If you write a custom EphemeralCredentialSpec, this is the single most important rule: never collect secret material through anything but PasswordParameterDefinition. A field that merely identifies something you do not require to be protected (such as a key identifier, in many cases a username or a filename) is fine as a StringParameterDefinition — only fields carrying the actual secret values need this.
A PasswordParameterDefinition renders as a single-line HTML <input>; the HTML Standard’s value-sanitization algorithm for single-line text controls strips any \r/\n characters from the submitted value, browser-side, unconditionally. If your secret is naturally multi-line (a PEM key, a multi-line config block, …), you cannot just swap the parameter type and expect the line breaks to survive — see EphemeralSSHUserPrivateKey above for a worked, empirically-verified example of collecting a flattened value and reconstructing it server-side around a recognizable fixed structure. The same idea generalizes to any format with an unambiguous anchor to reconstruct around.
Also avoid FileParameterDefinition entirely: beside writing the uploaded file to the controller’s disk as part of its own normal handling (the reason EphemeralSecretFile/EphemeralCertificate use pasted base64 text instead), pipeline-input-step has disabled it by default since SECURITY-2705 and may remove it outright in a future release.
2. CPS’s own program-state persistence (program.dat) is a second, independent path — mind what’s CPS-reachable
Separately from (1), any value held in a field or a named local variable of CPS-transformed Groovy (a .groovy file loaded through the classloader of a CpsScript instance — such as this plugin’s own WithEphemeralCredentials.groovy, a shared-library vars/src script, or the Jenkinsfile itself) is walked by CPS’s continuation-serialization machinery every time the pipeline pauses on a step, and can end up written to the build’s program.dat on disk as part of that checkpoint.
Known EphemeralCredentialSpec implementations are typically safe here, but only because this plugin’s own five ship as plain, precompiled Java — never CPS-transformed, so none of their internals are ever CPS-continuation-reachable in the first place. That is not a property every EphemeralCredentialSpec has intrinsically: for example, one defined as a JSL src/ class (per "Extending with more types" above) is itself ordinary CPS-transformed Groovy, exactly like a Jenkinsfile or a vars/ script — see the @NonCPS note below.
The WithEphemeralCredentials.groovy script itself only takes one, narrower precaution given that: it never assigns input step’s return value to a named local variable which survives until a next potential CPS "pause and save" checkpoint (such as a new step) — the value flows directly from script.input(…) into materialize(…)/put(…) as part of one statement, rather than sitting in a def raw = … which would remain part of the enclosing method’s CPS continuation for the rest of that method’s execution (not necessarily just for as long as it’s lexically "in scope" the way a plain JVM stack frame would). Nothing here forces a mid-computation checkpoint in the first place — CPS’s persistence to program.dat is tied to step invocations, not plain statement execution, and no further step call happens between input returning and the credential being cached.
@NonCPS looks like a stronger guarantee here, but does not work for this purpose — worth knowing before you reach for it yourself. Wrapping the "materialize-and-cache" logic in a @NonCPS (com.cloudbees.groovy.cps.NonCPS) method looks like it should give an even harder guarantee (a @NonCPS method has no CPS continuation at all for the interpreter to ever walk). It does, right up until that method needs to call a custom EphemeralCredentialSpec implementation class instance’s materialize() method — if that spec is a JSL src/ class, its materialize() code is CPS-transformed, and a @NonCPS method cannot call into CPS-transformed code at all: every such call fails with CpsCallableInvocation logging the infamous "expected to call X but wound up catching Y" mismatch error (see the https://jenkins.io/redirect/pipeline-cps-method-mismatches/ page), which would break the entire "extend via JSL, no plugin release needed" story this plugin offers.
If you’re writing your own glue code around a custom spec and are tempted to reach for @NonCPS for extra assurance, this is exactly the trap to check for first — it is only safe if you can guarantee that every EphemeralCredentialSpec your glue might ever call is plain, non-CPS-transformed Java, which a plugin generally can’t assume about specs it doesn’t own.
3. The materialized credential lives in memory for the whole build, not just the moment it’s collected
The EphemeralCredentialsProvider class' cache is what makes withEphemeralCredentials useful for pre-warming (see "Pre-warming" above) — but it also means the credential’s plaintext (recoverable on demand, e.g. via Secret.getPlainText(), since that’s exactly what lets withCredentials inject it later) stays live and heap-reachable for the build’s entire remaining duration, not just for one withCredentials block’s usual, much shorter window. A JVM heap dump taken any time in that window would expose it. This isn’t unique to a custom credential type — it’s inherent to this plugin’s whole caching design, and to how Jenkins credentials work in general whenever they’re actively bound (nothing in the JVM/Java security model lets an ordinary application defend live, in-use plaintext against a memory dump of the very process that legitimately holds the decryption key) — but it does mean the exposure window is larger than usual. Worth weighing for anything unusually sensitive, regardless of which credential type is involved.
-
You might want a different step implementation that asks for the credential if missing, and discards from the credential store it after calling the passed
Closure. This plugin does not currently provide such code, but it should be a trivial change from what it does.
One thing a custom spec can do, cheaply: if materialize() decodes its own byte[] (base64 content, a keystore, …) before wrapping it in SecretBytes or similar, overwrite that array (Arrays.fill(bytes, (byte) 0)) immediately after use, in a finally block, rather than waiting on Java GC — see sources of EphemeralSecretFile and EphemeralCertificate for the code pattern. Note that java String objects can’t be scrubbed this way (they’re immutable, and Java gives no reliable way to zero their backing memory), which is one more reason to prefer decoding into a byte[] object that you control over building up a secret based on String instances, where the format allows it.
Obvious, but worth stating for extension authors as an explicit rule rather than something to infer: nothing in this plugin ever calls echo step or logs an input answer or a materialized secret, in the console log or anywhere else, including in exception messages. A custom spec’s own materialize()/glue code should hold to the same rule — in particular, don’t include the secret value itself in a thrown exception’s message, since that message can end up in the build log or server log.
WithEphemeralCredentialsTest runs real declarative pipelines against an embedded Jenkins (JenkinsRule, JUnit 5 @WithJenkins):
-
alreadyRegisteredCredentialIsNeverPrompted: an ID already resolvable viaSystemCredentialsProvideris used as-is; noinputever appears. -
missingCredentialPromptsAndReusesCacheOnSecondCall: a genuinely missing ID pauses oninput(submitted programmatically viaInputStepExecution.proceed(Map)), the submitted value reaches an unmodified nestedwithCredentials, and a secondwithEphemeralCredentialscall for the same ID in the same run reuses the cache instead of pausing again — as does a third, completely unwrappedwithCredentialscall later in the same run, confirming the cache is genuinely visible to code that never callswithEphemeralCredentialsat all. -
decliningInputMovesOnWithoutTheCredential: declining theinputprompt lets the pipeline continue without the credential rather than aborting the build; a later request for the same ID prompts again instead of reusing anything (nothing was cached for a declined answer). -
sshKeySecretFileAndCertificateMaterializeCorrectly: the SSH key, secret file, and certificate credential types, resolved in onewithEphemeralCredentialsblock (three sequentialinputpauses), each consumed through its standardwithCredentialsbinding (sshUserPrivateKey/file/certificate) inside the same pipeline run — not by queryingEphemeralCredentialsProviderafter the build finishes, sinceEphemeralCredentialsRunListenerclears the cache for a run as soon as it’s done, so nothing would be left to find by then. -
managementStepsAndMapStyleAccessWork: theephemeralCredentialsPut/Find/Has/Forgetsteps and theephemeralCredentialsmap-like variable, exercised without ever pausing oninput(credentials pre-populated programmatically), consumed through the standardwithCredentialsbinding to confirm real integration with the ordinary credential lookup path.
The ssh-credentials plugin is present on this project’s own test classpath (it is a normal, if optional, compile dependency of this plugin: optional only affects whether it propagates to consumers of this artifact, not whether it’s available here), so the "plugin genuinely absent" code path for ephemeralSSHUserPrivateKey isn’t exercised by an automated test — that guarantee rests on the well-understood JVM behavior described above (a class literal inside a method is resolved when that method runs, not when the enclosing class loads), rather than on a harness rigged to remove an already-declared dependency.
WithEphemeralCredentialsCustomLibraryTest illustrates and confirms that a completely custom credential type can be added without touching this plugin at all — by defining an EphemeralCredentialSpec subclass and its factory function in an ordinary (trusted, global) shared library instead, per the "Extending with more types" section above. The library content lives as genuine .groovy resource files under src/test/resources/io/jenkins/plugins/ephemeral_credentials/{vars,src}/ (not embedded Java string literals), copied into a throwaway local git repo (GitSampleRepoRule) and loaded via GlobalLibraries/SCMSourceRetriever/ GitSCMSource — the same mechanism a real Jenkins controller uses for a globally-configured library, and the exact pattern the shared-library plugin’s own test suite (GlobalLibrariesTest) uses to test this feature. The example library defines vars/myCorpApiToken.groovy (an independently-named factory, parallel to this plugin’s own ephemeralSecretText) and src/com/example/jsl/MyCorpApiTokenSpec.groovy (an independently-named spec subclass reusing StringCredentialsImpl — the same type EphemeralSecretText already wraps — purely to prove the extension mechanism, not to add a genuinely new credential type).
This test uses ordinary JUnit 5 (@WithJenkins, @Test), the same as the rest of this project’s suite — this project’s enforcer configuration bans any org.junit.* (JUnit 4) import in test code outright (a RestrictImports rule, check-junit-imports execution — see the Build section). GitSampleRepoRule — the standard way to test SCM-loaded shared libraries, and a classic JUnit 4 @Rule with no JUnit 5 form of its own — is driven directly from @BeforeEach/@AfterEach instead of through a rule mechanism.
Worth knowing if this test needs touching again: @WithJenkins annotation’s JenkinsExtension boots a JenkinsRule instance by calling rule.before() directly, never apply(Statement, Description) — and the timeout field of a JenkinsRule is only ever enforced inside an apply(…) call, so it has no effect for a @WithJenkins-style test. Since this test’s heavier plugin set (git, scm-api, pipeline-groovy-lib, and their own transitive dependencies) makes plugin discovery noticeably slower than other tests here, its own @Test method carries an explicit @Timeout(600) instead — which bounds the test method’s own invocation, but not the JenkinsRule boot time itself (the parameter resolution that happens before the method runs), so overall test time is effectively bounded only by whatever timeout Surefire itself applies.
One more thing worth watching: workflow-cps logs (but does not yet enforce) a GroovySourceFileAllowlist check against loading WithEphemeralCredentials.groovy "without sandbox protection" the way WithEphemeralCredentialsGlobalVariable does (getResourceAsStream(…)
parseClass(…), described above). The build still succeeds today, but a future workflow-cps release could plausibly tighten that check from "warn" to "enforce" — worth re-checking if this plugin’s workflow-cps dependency is ever bumped.
Run with mvn test.
Standard Jenkins plugin Maven build, via the org.jenkins-ci.plugins:plugin parent POM:
mvn package
Produces target/ephemeral-credentials-provider.hpi.
mvn verify additionally requires a clean spotbugs:check and spotless:check (both bound to the verify phase). SpotBugs comes from the parent POM unconditionally; Spotless is already fully configured there too (Palantir Java Format for .java, sortPom for pom.xml) but ships off by default behind spotless.check.skip — like others once the codebase got a standard format, this project overrides that property to false. Please fix any formatting issues in new contributions with mvn spotless:apply.
The lockable-resources and pipeline-input-step plugins are declared as real (non-test) dependencies, even though no Java code here imports their classes — WithEphemeralCredentials.groovy calls their lock/input steps by name, and declaring them here is what makes maven-hpi-plugin record them as required plugin dependencies in the manifest, so a Jenkins controller refuses to load this plugin without them installed.
Note on API surface: credentials-plugin does not declare getCredentials(…) as abstract — it’s kept only as a deprecated compatibility shim. The real extension point subclasses must override is getCredentialsInItemGroup(Class, ItemGroup, org.springframework.security .core.Authentication, List<DomainRequirement>), which is what this plugin does.
Generally, run mvn dependency:tree -Dincludes=<groupId>:<artifactId> yourself to re-verify any of this later, since it does drift as the common BOM gets updated.