Active Choices — Patterns and Pitfalls

Active Choices is the Jenkins plugin that powers every reactive parameter on the Create / Update / Deploy / Manage jobs. This page collects the patterns that actually work in this codebase and the traps we have stepped on.

Parameter types we use

Plugin class When to use it

CascadeChoiceParameter + PT_SINGLE_SELECT, PT_RADIO, PT_CHECKBOX

Source parameters that drive other parameters via referencedParameters. Example: DATABASE_TYPE (radio), SELECTED_SERVICES (checkbox), OWNERS_LIST (checkbox, commit e48fa6d).

DynamicReferenceParameter + ET_FORMATTED_HTML

Display-only panels derived from others. Example: DEP_GRAPH, SERVICE_VARIABLES, FINAL_PREVIEW, OWNERS_PREVIEW.

Hidden DynamicReferenceParameter named _UI_STATE

Cross-panel state channel, injected by qaUiScripts.sharedCssJs().

DynamicReferenceParameter cannot serve as the source of another parameter cascade. If a downstream parameter lists a DRP in referencedParameters, it will not fire on changes. Use CascadeChoiceParameter (with PT_RADIO / PT_SINGLE_SELECT / PT_CHECKBOX) as the cascade source and strip :selected / :disabled markers in consumers.

Selection markers

PT_CHECKBOX values arrive with :selected / :disabled suffixes per entry. Consumers must strip them before comparing:

def selected = (binding.variables['SELECTED_SERVICES'] ?: '')
    .toString()
    .split(',')
    .collect { it.trim().replaceAll(/:selected$|:disabled$/, '') }
    .findAll { it }

Trailing-comma trap (DRP)

DynamicReferenceParameter serialises its chosen value with a trailing comma, e.g. "manage-owners,". Literal comparisons fail silently. Always strip:

def raw   = (binding.variables['ACTION'] ?: '').toString()
def clean = raw.replaceAll(/^[\s,]+|[\s,]+$/, '')
if (clean == 'manage-owners') { ... }

Inside a triple-quoted qaUiScripts body the regex is written as /^|[\\s,]$/ (see escaping contract).

Sandbox approval (sandbox: false)

Our scripts run with sandbox: false because they use enough of the Jenkins API that the restricted sandbox rejects them. That puts every new script body through In-process Script Approval:

  • First build after a script change may silently render the fallback ("Error").

  • Go to Manage Jenkins → In-process Script Approval (direct link), approve the pending entry, reload the form.

Symptoms that usually mean "not approved" rather than "bug in the script":

  • Only the fallback HTML shows up, with no message in the build console.

  • Any value of referencedParameters still shows the fallback.

  • Jenkins System Log → filter on org.biouno.unochoice or ScriptApproval shows the rejection.

Check the approval list before debugging script logic.

Forcing pending approvals to surface

Jenkins only registers a script hash for approval after the job body actually runs setupParameters() and submits the script. If you changed a vars/*.groovy or qaUiScripts.groovy script body but the approval page shows nothing pending, the job has not re-registered the new hash yet. Force it:

  1. Open the job config, e.g. QA-Environments → <job-name> → Configure — for the Deploy job that is QA-Environments → Deploy-Environment → Configure (config link).

  2. Untick This project is parameterized and Save. This lets you trigger the pipeline directly instead of landing on the parameter form (which would just render the unapproved fallback).

  3. Click Build Now. The build may fail — that is expected; all we need is for call()setupParameters() to run and submit the new script bodies.

  4. Reload In-process Script Approval and check whether new entries appeared. Approve them.

  5. Re-tick This project is parameterized, Save, and run the job normally — the reactive parameters now render instead of the fallback.

If step 4 still shows nothing to approve, the script hash is already approved (an identical body was approved before) and the fallback has a different cause — go back to the quiet-failure checklist.

Referenced parameters + reactivity

Every reactive parameter lists its dependencies in referencedParameters as a comma-separated String:

[$class: 'DynamicReferenceParameter',
 name: 'SERVICE_VARIABLES',
 script: [
   $class: 'GroovyScript',
   script: [classpath: [], sandbox: false,
            script: qaUiScripts.serviceVariablesScript('create')]
 ],
 referencedParameters: 'DATABASE_TYPE,SELECTED_SERVICES,_UI_STATE',
 choiceType: 'ET_FORMATTED_HTML']

Rules:

  • referencedParameters must be a String, not a List.

  • Listed parameters must be declared before the dependent one in the parameters([…​]) list.

  • Active Choices looks up values via binding.variables[paramName] inside the body. Never reach for params.* — that binding does not exist in the parameter sandbox.

Dep graph and gating UI

Several panels are "gated": until the env name (Update / Manage / Deploy) or a required selection is in place, the panel shows a short "pick a value first" banner instead of its main body. This is centralised in qaUiScripts.envGateHtml(body) and envGateChoices(body):

return qaUiScripts.envGateChoices("""
    // main body — only reached when ENV_NAME is bound
""")

Consumers can rely on the gate wrapper; they do not need to repeat the env-name check themselves.

When something quietly goes wrong

A checklist that has saved us more than once:

  1. Is the script approved? (Manage Jenkins → In-process Script Approval)

  2. Did the backslash count halve when the String went through the outer parser? (Use the escaping contract table.)

  3. Are all consumers stripping :selected / :disabled from checkbox values and trailing commas from DRP values?

  4. Is the upstream parameter a CascadeChoiceParameter (not a DRP)? If the source is a DRP, consumers will never see the change.

  5. Are all referenced parameters declared before the dependent one?

  6. If the body contains GString interpolation: the value was bound at build time, not at render time. Rebuild the job to rebind.