Development

Extending the library without breaking existing envs.

Add a new service

  1. Add the service block to both Compose bundles:

    resources/services/compose-mysql.yml
    resources/services/compose-oracle.yml

    The two files differ only in database-specific wiring (JDBC URL, credentials). Everything else — ports, mounts, healthchecks, env-files — should match.

    my-service:
      build:
        context: ${DATA_FOLDER:-.}
        dockerfile: my-service/my-service.Dockerfile
      container_name: ${ENV_NAME:-ft}_my-service
      env_file: my-service/.env
      ports:
        - "${MY_SERVICE_PORT:-8080}:8080"
      volumes:
        - ${DATA_FOLDER:-.}/my-service/data:/var/lib/my-service
      profiles:                      # optional — makes it opt-in
        - my-service
  2. Create the per-service dir and .env defaults:

    mkdir -p resources/services/my-service
    # resources/services/my-service/.env
    MY_SERVICE_IMAGE=hub.friendly-tech.com/ft/my-service:latest
    MY_SERVICE_PORT=8080

    If your service has DB-specific config, add resources/services/my-service/.env.mysql and .env.oracle as overlays.

  3. Wire global defaults (only for shared / tunable vars) into resources/services/.env.mysql and .env.oracle.

  4. Declare dependencies in resources/schemas/dependencies.yaml:

    services:
      my-service:
        requires:
          - oneOf: [mysql, oracle]
            mandatory: true
          - service: hazelcast
            mandatory: true
          - service: prometheus
            mandatory: false
            autoselect: true

    See Dependency Management for every available flag.

  5. Add a row to the resource estimate map in src/com/qa/config/QAConfig.groovyResources.SERVICE_RESOURCES. Without it the service contributes zero to the RESOURCE_ESTIMATE panel.

  6. (Optional) Update the services table partial at docs/modules/ROOT/partials/services-table.adoc so the catalogue stays accurate.

  7. Smoke-test locally using c.sh:

    cd resources/services
    ./c.sh mysql ./data -f compose-mysql.yml up -d my-service
  8. End-to-end test: run Create-Environment with DATABASE_TYPE=mysql, check that your service appears under SELECTED_SERVICES, deploy it.

Modify pipeline logic

  1. Edit the relevant vars/*Step.groovy or vars/utils.groovy.

  2. Changes are picked up on the next build after syncSharedLibStep has rsynced the workspace to /var/jenkins_home/qa-data/shared-lib/ — Jenkins does not load the library from Git, it loads it from that directory. utils.syncSharedLibrary() is the entry point.

  3. Run the corresponding job and inspect the console output and (on failure) .qa-build.log from the Artifacts panel.

Add a parameter to an existing step

Parameters live in the step’s setupParameters() function. If the parameter’s script body is non-trivial, move the script factory to qaUiScripts.groovy so it can be shared and unit-inspected:

// vars/qaUiScripts.groovy
String myPanelScript(String mode) {
    return '''
        // sandbox-safe body; do NOT call utils.* or qaUiScripts.* here.
        def raw = binding.variables['SELECTED_SERVICES'] ?: ''
        // ...
    '''
}
// vars/someStep.groovy
private void setupParameters() {
    properties([parameters([
        [$class: 'DynamicReferenceParameter',
         name: 'MY_PANEL',
         script: [
           $class: 'GroovyScript',
           script: [classpath: [], sandbox: false,
                    script: qaUiScripts.myPanelScript('create')]
         ],
         referencedParameters: 'SELECTED_SERVICES'
        ]
    ])])
}

Test variable substitution

def binding = [MYSQL_IMAGE: 'mysql:8.0', ENV_NAME: 'test-env']
def result  = utils.substituteVariables(
    'image: ${MYSQL_IMAGE:-mysql:5.7} # ${ENV_NAME}',
    binding)
assert result == 'image: mysql:8.0 # test-env'

Debug Active Choices parameters

Active Choices scripts run on the Jenkins master inside a separate sandbox. When something silently shows the fallback ("Error — Fallback") it is almost always one of:

  • The script hash is not approved (Manage Jenkins → In-process Script Approval). First-time scripts always land here — see approval for details.

  • Triple-quoted String escape trap: /\s+/ inside a '''…​''' body needs to be written as /\\s+/ in the source file (the outer parser strips one level before the sandbox runs).

  • A utils. / qaUiScripts. reference leaked into the returned body. Those are unavailable inside the sandbox.

Logs:

  • Jenkins System Log → filter on org.biouno.unochoice.

  • First build after changing the parameter prints the exception to the build console.

Conventions

  • Groovy filenames under vars/ must match the exposed function name (foo.groovyfoo()).

  • Any method that touches files, YAML, JSON, regex matchers, or heavy closures needs @NonCPS.

  • Before writing YAML-parsed content to JSON, pass it through utils.toBasic() — SnakeYAML’s LazyMap is not serialisable by Jenkins.

  • Inside node(runner) { …​ } blocks use fileExists, not utils.exists (the latter is @NonCPS on master and does not see agent paths).

  • Avoid slashy regex (/\s/) in vars/*.groovy pipeline code — the Jenkins CPS parser rejects them. Embedded Active Choices Strings are exempt because they are not parsed by CPS, but note the triple-quote escape rule above.