Dagger modules and Go libraries behind Basis' CI/CD pipelines.
As of version 3.0 the repository is configuration-driven: a consumer project no longer writes a Dagger module of its own, it just declares its targets in ci/pipeline.toml and includes the CI template. The orchestrator module does the rest.
- Overview
- The orchestrator module
- The
ci/pipeline.tomlschema - The anti-drift guarantee
- Projects with a custom build
- Maven reactor mode
- 3.0 breaking changes
- Tagging scheme
- ci.skip
- GitLab integration
- Go libraries
- Per-technology modules
| Component | Role |
|---|---|
|
The generic Dagger module. Reads |
|
Git and registry utilities (change detection, bump/commit, |
|
Per-build-technology Dagger modules. Called by the orchestrator, not by the project. |
|
Go library: the configuration schema ( |
|
Go library: GitLab client for commit statuses and merge request notes. |
orchestrator is the generic Dagger module introduced in 3.0. It replaces the per-project Dagger module that existed up to 2.x: all behaviour comes from the consumer project’s declarative file, and the targets declared there become simultaneously the build strategies, the quality checks and the image map used by check-images/promote.
A project needs exactly two files.
-
ci/pipeline.toml— the repository’s targets (see Theci/pipeline.tomlschema). -
.gitlab-ci.yml— five lines including the shared template:
include:
- project: 'basis/iac/ci-templates'
ref: v1.7.0
file: 'templates/dagger-orchestrator.gitlab-ci.yml'The template defines the check, build, pre-prod and promote stages, and calls the orchestrator functions with the GitLab variables. Two variables can be overridden in the project’s own variables: block:
-
DAGGER_MODULE— defaults to the orchestrator version pinned by the template. Projects with a custom build point it at"."(a local module, see Projects with a custom build). -
PIPELINE_CONFIG— defaults toci/pipeline.toml.
The module constructor takes --source (the repository root) and --config-path:
dagger call -m github.com/BasisTI/daggerverse/orchestrator@3.6.0 \
--source . --config-path ci/pipeline.toml \
validateLoads and validates the declarative file and returns a textual report with the resolved targets (defaults applied), the derived image map and any warnings. It makes no network calls — it is the configuration lint job, triggered on merge requests that touch ci/pipeline.toml.
The warnings cover what is valid in the schema but the generic orchestrator cannot execute: type = "custom" targets.
Prints the SonarQube project key of every target with sonar = true, one per line. Also makes no network calls. It is the input for provisioning the projects on the server before the first merge request — see sonar-project-key: the analysis identity, decoupled from the target name.
Runs tests and static analysis on the changed targets that declare sonar = true. It takes --base-branch, --commit-sha, --sonar-host, --sonar-token, the optional --stop-on-first-fail, the optional --gitlab-host / --gitlab-token / --gitlab-project-id / --gitlab-ref for commit statuses, the optional --merge-request-id / --merge-request-source-branch / --merge-request-target-branch, and the optional --sonar-branch / --all-targets.
Nothing is published, but the build still carries the project’s real version — the one declared in the version file, from the last publish. Forcing a constant there would pin SonarQube’s PREVIOUS_VERSION new-code anchor to that constant, and the whole codebase would read as new code forever.
Fails with an explanatory message if the configuration has custom targets.
--merge-request-* and --sonar-branch select where the analysis lands on the server. They are mutually exclusive: passing both is an error, not a precedence rule, because each one sends the result to a different place and picking one silently would report the right analysis in the wrong location.
Pull Request analysis, when the merge request triple is present: new code is the diff against the base branch instead of the project’s new-code period, and SonarQube decorates the merge request in GitLab. All three are required — with any of them missing it falls back to the third mode below.
Branch analysis, with --sonar-branch <name>: emits -Dsonar.branch.name, so the server keeps that branch’s history and Overall Code. This is what a project dashboard reads from; a project whose branches were never analysed shows "no analysis" in the project list, and SonarQube hides the project menu bar — Project Settings included — even though permissions are fine.
Neither: the analysis is written to the project’s main branch, whatever git branch was actually built. Correct when the main branch is in fact what is being analysed, silently wrong otherwise, so check-quality prints a warning when it happens.
Skips change detection and analyses every target with sonar = true, in alphabetical order.
Outside a merge request there is no natural base for the diff, so change detection returns an empty list: the run prints Nenhuma mudança detectada, analyses nothing and still exits green. A full branch sweep must pass this flag; an incremental analysis after a merge should not, since the targets that changed are exactly the ones worth re-analysing.
Detects the targets changed between --base-branch and the current commit, builds and publishes each image to --registry, applies the sha-<commit> tag and returns the list of published images (one per line).
If --git-remote-url is given, it bumps the version files of the published targets and commits back to --git-branch (defaults to develop). It also fails if there are custom targets.
With the GitLab parameters set, the published images are posted as a comment on the merge request the commit came from — no need to open the job and hunt for the refs in the log. See Merge request notes.
publish-all plus the inputs of the branch analysis. It takes the same parameters, does the same work (build, publish, sha-<commit> tag, version bump) and returns a directory instead of a string:
published.txt the published images, one per line (what publish-all returns)
<target>/ one per Maven target with `sonar = true` that was built:
sonar-reuse-manifest.json commit, target and an inventory of what follows
<module>/target/... the target/ of the module and of every child module
It exists so that the develop branch analysis does not run the same mvn clean verify the publish has just run. The directory is meant to be exported (… publish-all-with-reports … export --path .sonar-reuse) and handed to check-quality-from-reports.
What is exported. Inside every target/ of the target’s module tree — the aggregator and each child — only what the Sonar scanner reads, recognised by its content, wherever the build wrote it:
-
*.class(the bytecode:sonar.java.binariesandsonar.java.test.binaries); -
XML whose root element is
testsuiteortestsuites(test reports, wherever the pom sent surefire/failsafe); -
XML whose root is
reportand that declares the JaCoCo DOCTYPE, andjacoco*.exec(coverage); -
.java/.ktundergenerated-sourcesandgenerated-test-sources.
Nothing else travels. Surefire and JaCoCo can write anywhere (reportsDirectory, outputDirectory) and the pom can point Sonar there, so selecting by path would drop those reports and the analysis would lose tests and coverage with a green gate. Selecting by content keeps them without taking arbitrary configuration along.
Why by content and not by exclusion. The first version excluded extensions (.properties, .yml, .env, keys…), and the review showed it leaks: anything the pom writes under target/ is a candidate, and a list of what can hold a secret is never complete — generated-resources/auth.xml, .env.production, token.json and settings.xml got through. By content, an auth.xml has root auth and stays out whatever its name or directory; an XML named TEST-.xml or jacoco*.xml with another root stays out too. What the scanner does not read does not leave the build.
Residual risk, accepted. The XML test reports are read by the scanner, so their content (system-out, system-err, JVM properties) still travels. The text also reaches the job log, but masking applied to the GitLab trace does not sanitise artifact files. The artifact expires in a day. A project whose tests print secrets must stop printing them, or opt out with SONAR_REUSE_BUILD: "false" in the template. Also: reports of other tools (SpotBugs, Checkstyle, PMD) that a pom points Sonar at are not carried, so a project that feeds them to Sonar loses them in the reuse path; such a project should opt out.
Checksums. sonar-reuse.sha256 lists the sha256 of every exported file. The analysis re-checks it, so a report that arrives truncated, empty, swapped or missing falls back to the full path with the reason in the log (the manifest counts alone would not see an emptied report that kept its name).
The manifest. Written at publish time and validated by the analysis (see below): schema, target, commit SHA and, per module (path relative to the target’s module, . for the module itself), the counts of compiled classes, test classes, test reports (TEST-*.xml), JaCoCo XML and JaCoCo exec files. Counting, and not mere presence, is what separates a project with no tests (zeros on both sides) from reports lost in transport (counts that no longer match).
Trust boundary. The manifest and the checksum file travel in the artifact itself, so they do not authenticate where the artifact came from: whoever can rewrite the artifact can rewrite commitSha and the hashes together and pass the same set off as another commit. The guarantee is narrower and sufficient for the pipeline: the artifact comes from the publish-develop job of the same pipeline (needs with artifacts: true), so the commit it declares is the commit it was built from; the manifest and the checksums then catch what actually goes wrong — another revision’s reports, partial transport, truncated files. Per-file hashes are worth their cost for that (a few seconds, one extra container run) and are already in; they would not be worth more than that, because defending against a forger needs a signature with a key the analysis job trusts and the publish job holds, which this setup does not have. Do not feed the function a directory from an untrusted source.
It is a separate function, not a flag of publish-all, because the return type differs. publish-all is unchanged, so a project pinned to an older CI template keeps working.
The branch-analysis counterpart of check-quality: for each changed target with sonar = true, if --reports (the directory publish-all-with-reports returned) has the target and the target passes the checks below, it runs only sonar:sonar over it — no clean verify, no tests. The test counts and coverage Sonar records are the ones from the publish build.
It takes --base-branch, --commit-sha, --sonar-host, --sonar-token, --sonar-branch (required: this is a branch analysis), the optional --reports, --stop-on-first-fail and --nvd-api-key. The quality gate is still awaited (sonar.qualitygate.wait=true).
Checks, per target, before reusing. Any failure sends that target to the full path (the same as check-quality) and logs the reason; the analysis never runs on stale or partial data.
-
The manifest exists, has the known schema and is for this target.
-
The manifest commit is
--commit-sha. Reports of another revision would hide the current code’s failures. -
The inventory counted again over what arrived equals the manifest’s, module by module. A lost
surefire-reportsor JaCoCo file, a lost child module or an unknown extra module is a mismatch. -
The build is coherent: no module has test classes and zero test reports, and something was compiled.
-
Every exported file matches its sha256 (
sonar-reuse.sha256), and no XML report is empty.
Other targets — npm and uv, or Maven the publish did not build — also take the full path, and the log says which targets took which path and why. Without --reports every target takes the full path. Skipping them instead would leave the branch silently unanalysed.
Why it needs no compilation: the sonar-maven-plugin goal resolves the module’s dependencies itself, and takes sonar.java.binaries / sonar.java.test.binaries from the restored target/classes and target/test-classes of each module. The JUnit reports and the JaCoCo XML come back at the same relative paths, so the pom’s properties keep applying. In a Maven reactor the sibling modules must be in ~/.m2; a -DskipTests install stage guarantees that, and is cheap when the build cache is on the same engine.
It is not for merge requests, where check-quality stays a single job.
Accepted on 2026-10-05 after triage against the real environment; none is fixed on purpose.
-
An XML comment that mentions
testsuiteexports the file. The root element is found by its first start tag in the first 1 KB, and a comment that quotes the tag can make a non-report XML look like one.publish-developandsonar-branch-developrun in the same pipeline, in a closed environment, and a secret passing between those two jobs is not a risk worth chasing. The content-based selection andexpire_in: 1 daystay. -
A JaCoCo report with a long prologue is not exported. The filter reads the first 1 KB to find the JaCoCo DOCTYPE.
jacoco-maven-pluginwrites no comment before it, so it only happens with a hand-made XML, which none of our projects has. Coverage would be missing for that target. -
Reports of other tools are not carried. Checkstyle, PMD and SpotBugs reports,
sonar.externalIssuesReportPathsand the like are not exported, and neither islcov. None of our consumers' poms hands those to Sonar (Ábaco, the one that hadlcov, no longer runs Sonar). A project that needs them setsSONAR_REUSE_BUILD: "false"in the CI template.
The checksum verification is not a known limit: it fails closed. If a tool it needs (sha256sum, find, grep, head, sed) is missing from the Maven image, or it does not reach its final marker, the result is an explicit reason and the target runs the full check.
Checks that every image derived from the configuration already exists in the registry, before promotion. Runs on merge requests targeting main, using --build-branch (defaults to origin/develop) to locate the commit the images were built from.
Custom targets are not skipped here: their images live in the registry and need to be checked like any other.
Promotes the images derived from the configuration from --src-registry to --dst-registry (which, when empty, is --src-registry itself). Like check-images, it covers custom targets' images too.
Every promotion walks all of the project’s images, not only the ones that changed — that is how an old image keeps its production- tag present. But the copy only happens when it changes something: before copying, the source tag’s digest is compared with the production tag’s, and if they match the image is skipped with ⏭️ Já promovida. Services untouched for months stop generating registry traffic on every merge to main, and the log shows at a glance what actually went into the promotion.
The version comes from the source image’s org.opencontainers.image.version label, read by the engine with --registry-user/--registry-password on --src-registry. The crane login does not cover that read: without the explicit credential the engine fell back to whatever it had (a cached resolution, the host’s login), and the same image that resolved in the morning failed in the afternoon with no basic auth credentials.
Implemented in pipeline/config. Parsing is strict: any unknown field is an error.
| Field | Type | Description |
|---|---|---|
|
int |
Required, currently always |
|
table |
Repository metadata. |
|
table |
Per-build-type defaults. |
|
table |
One block per buildable artifact. At least one is required. |
| Field | Type | Description |
|---|---|---|
|
string |
Required. The registry group: |
| Block | Field | Type | Description |
|---|---|---|---|
|
|
string |
Base image for Maven builds. |
|
|
bool |
Binds a Docker daemon to the build (Testcontainers). |
|
|
string |
Version of |
|
|
string |
Build image (e.g. |
|
|
string |
Runtime image (e.g. |
|
|
string |
Build image (e.g. |
|
|
string |
Runtime image (e.g. |
| Field | Type | Description |
|---|---|---|
|
string |
Required. One of |
|
string |
Change-detection path. Defaults to the target name. |
|
string |
Directory mounted into the build. Defaults to the effective |
|
string |
Image name in the registry, without group and without tag. Defaults to the target name. Effective names must be unique across targets. |
|
string |
Version file bumped by |
|
string |
Required as |
|
bool |
Marks |
|
list of string |
Additional paths that also trigger this target’s build/check (e.g. a shared library, the root |
|
bool |
The target takes part in |
|
string |
Project key in SonarQube. Defaults to the target name. Only accepted alongside |
|
string |
The build system that analyses the target’s code, when it differs from how the image is built. One of |
The key is the permanent identity of the project on the SonarQube server: renaming it orphans the
entire analysis history. The target name, on the other hand, is a local choice of the repository —
and on a shared server, names picked locally (frontend, snf, ponto) collide across projects.
sonar-project-key is what lets a repository call its target frontend and still analyse under
contavinculada-frontend.
Omitting it keeps the key equal to the target name, so no existing pipeline.toml changes
behaviour. It is orthogonal to image: the two identifiers name the same target in different
systems, and both default to the target name.
orchestrator sonar-project-keys lists the keys of the targets with sonar = true, one per line.
It exists for provisioning: on an empty SonarQube the first analysis of a project is always a
merge-request analysis, and the branch plugin validates the base branch before the server
auto-provisions the project — the analysis dies with No branch exists in Sonarqube with the name
main and the project is never created. The projects have to exist before the first merge request;
this function is the source of that list so it is not duplicated outside pipeline.toml.
type answers "how is the image built"; quality-type answers "which build system tests and analyses the code". For the vast majority of targets they are the same thing, and quality-type is omitted.
They diverge when a project needs its own Dockerfile for the runtime — virtual display, headless browser, native drivers — while the code is still a plain uv/Maven/npm project with tests and static analysis. Without quality-type such a target would have to choose between publishing the right image and having its quality measured; with it, publish-all builds from the Dockerfile and check-quality runs through the declared build system.
That is the case of the licitacao scrapers, which run a browser at runtime:
[targets.scrapers_mte]
type = "dockerfile" # the image comes from the scraper's Dockerfile
path = "scrapers/mte"
sonar = true
quality-type = "uv" # the code is Python: tests and Sonar through the uv moduleThe quality build system’s fields (run-subdir, uv-build-image, customizations, maven-image, …) apply normally on the target and are what check-quality consumes — and validate shows them in a separate quality (<type>) block.
Two validation rules keep the field honest:
-
quality-typewithoutsonar = trueis an error: it would be an inert field, misleading whoever reads the file; -
sonar = trueon adockerfiletarget withoutquality-typeis a validation error, with a message naming exactly the missing field — not a late failure when the check runs.
| Field | Type | Description |
|---|---|---|
|
string |
Overrides |
|
bool |
Overrides |
|
string |
Overrides |
|
bool |
Multi-module build: mounts the root and builds with |
|
string |
Path of the module inside the reactor. Required when |
|
list of string |
Extra options passed to |
| Field | Type | Description |
|---|---|---|
|
string |
Overrides |
|
string |
Overrides |
|
string |
Execution subdirectory inside the build (e.g. |
|
list of string |
uv module build customizations: |
| Field | Type | Description |
|---|---|---|
|
string |
Path to the Dockerfile, relative to the effective |
type = "npm" has no fields of its own: its images come exclusively from [defaults.npm].
Four targets — three Maven and one Angular frontend — all with a directory name matching the target name, which makes path and image unnecessary:
schema-version = 1
[project]
group = "contavinculada"
[defaults.maven]
image = "maven:3.9.11-eclipse-temurin-21"
use-docker = false
[defaults.npm]
build-image = "node:22-alpine"
run-image = "nginx:alpine"
[targets.contavinculada]
type = "maven"
sonar = true
[targets.integracaosgo]
type = "maven"
sonar = true
[targets.snf]
type = "maven"
sonar = true
[targets.frontend]
type = "npm"
sonar = trueTwo modules of the same reactor. Both mount the root (source-path = "."), share the root pom.xml as their version file (root-version-file = true) and are rebuilt when the shared contract or the root pom changes:
schema-version = 1
[project]
group = "triagem"
[defaults.maven]
image = "maven:3.9.11-eclipse-temurin-25"
use-docker = true
[targets.triagem-core]
type = "maven"
source-path = "."
reactor = true
module = "triagem-core"
version-file = "pom.xml"
root-version-file = true
extra-trigger-paths = ["triagem-contracts", "pom.xml"]
sonar = true
[targets.triagem-ingestao]
type = "maven"
source-path = "."
reactor = true
module = "triagem-ingestao"
version-file = "pom.xml"
root-version-file = true
extra-trigger-paths = ["triagem-contracts", "pom.xml"]
sonar = trueMore real-world examples live in pipeline/config/testdata/.
This is the schema’s central rule.
The image map consumed by check-images and promote is not declared: it is derived from the same targets that describe the build, through Config.ProjectImages(), which returns "<group>/<effective image>" → "<effective path>" for every target.
Before 3.0, the list of images to check and promote was maintained by hand, in parallel with the project orchestrator’s build targets. Adding a new service and forgetting the second list was enough for the promotion to silently skip an image — or to point at one that no longer existed.
With a single source of truth, divergence is impossible: the set of what gets built and the set of what gets promoted are literally the same collection of targets. type = "custom" targets enter the map normally — the generic orchestrator does not build them, but their images live in the registry and must be checked and promoted like the rest.
Some projects have builds that none of the generic types covers. They keep a Dagger module of their own, but they do not duplicate the configuration: that module loads the same ci/pipeline.toml through pipeline/config and merges the hand-written custom targets with the generic ones derived from the file.
In the schema, those targets are marked type = "custom":
[targets.beneficios]
type = "maven"
path = "apps/beneficios"
sonar = true
[targets.rh-dp]
type = "custom"
[targets.lightdash-content]
type = "custom"
extra-trigger-paths = ["rh-dp/dbtrh"]The generic orchestrator does not know how to build those targets and fails with a clear message in publish-all and check-quality, stating that the project must point DAGGER_MODULE at its local module. validate only emits a warning, and check-images/promote keep working through the generic module, including for those targets' images.
A multi-module reactor is the case where a module depends on sibling modules under the same root pom.xml. Building the module’s directory in isolation does not work: the sibling dependencies are not on disk.
With reactor = true the maven module (ReactorMode option):
-
mounts the reactor root instead of the module directory;
-
builds with
-pl <module> -am, so Maven builds the target module and everything it needs; -
injects
-Drevision=<version>into every invocation instead of runningversions:set.
versions:set is avoided on purpose: rewriting the POMs would fight flatten-maven-plugin and break resolution of the sibling dependencies -am has just built.
-
The root
pom.xmldeclares<version>${revision}</version>and a<revision>property holding a local development value:<version>${revision}</version> <properties> <revision>0.0.1-SNAPSHOT</revision> </properties>
-
flatten-maven-plugin configured, so installed/published artifacts carry the resolved version and not the literal
${revision}. -
maven-install-plugin active on
verify. This is mandatory: the Jib stage runs without-am, that is, without rebuilding the sibling modules — they must already be in the local repository, installed by the build-and-test stage.
Nobody writes that number by hand. The <revision> property value in the file is not used by the pipeline: every Maven invocation receives -Drevision=<CalVer>, and a command-line property beats the POM’s. Whatever is committed there only matters to someone running mvn on their own machine without passing -Drevision — which is why the initial value should be a development SNAPSHOT, not a CalVer.
After publishing the images, publish-all writes the CalVer back to the file and commits: when the pom’s <version> is ${revision}, orchestrator-utils edits the root pom’s <revision> property rather than the <version> element. That is why reactor targets declare version-file = "pom.xml" together with root-version-file = true.
In other words, the number you find in the repository is the trace of the last successful pipeline, not an entry someone has to maintain.
-
ParentPomandFullBuildModuleswere removed. Multi-module is nowReactorMode(reactor = truein the schema), which builds with-pl <module> -amfrom the root. See Maven reactor mode. -
The
buildImagedefault is no longer the-alpinevariant, it is nowmaven:3.9.11-eclipse-temurin-21. Alpine is musl-based and the node binary frontend-maven-plugin downloads is linked against glibc, so frontend builds died on it.
-
EntryPointInfowas removed fromBuildTarget. The execution subdirectory of Python targets became therun-subdirfield of the declarative configuration, forwarded to theuvmodule. -
BuildStrategyandQualityStrategyno longer carry opaque configuration fields: the strategies close over the resolved target.
The repository runs two tagging schemes in parallel, because it holds two kinds of artifact.
| Artifact | Tag format | Example |
|---|---|---|
Dagger modules ( |
Repository-wide tag, no prefix |
|
Go libraries ( |
Directory-prefixed tag, as Go modules require |
|
In practice: DAGGER_MODULE references …daggerverse/orchestrator@3.0.0, while go.mod files reference github.com/BasisTI/daggerverse/pipeline v0.9.1. The two numberings evolve independently — a 3.0.0 release of the modules can coexist with pipeline/v0.9.1.
publish-all bumps the version files and commits back to the branch. That commit must not trigger a new pipeline, on pain of an infinite loop.
Suppression is done exclusively through the push option, in CommitAndPush (orchestrator-utils):
git push -o ci.skip <remote> HEAD:<branch>The commit message is just Bump versão para <version>, without the [skip ci] prefix. The prefix was removed on purpose: [skip ci] in the message is honoured by GitLab on merge request pipelines too, so a merge request containing the bump commit would have its pipeline blocked — check-quality included. The push option, by contrast, only affects the push that produced it.
orchestrator-utils recognises both formats when looking for the build commit (--grep=^\[skip ci\] and --grep=^Bump versão para `, with `--invert-grep), so check-images and promote skip bump commits when locating the build branch.
The gitlabci/ directory is a standalone Go library (github.com/BasisTI/daggerverse/gitlabci).
gitlabci.Client reports each stage’s progress as a commit status, visible in the merge request/commit UI. It calls POST /api/v4/projects/:id/statuses/:sha to move each stage from running to success or failed.
client := &gitlabci.Client{
BaseURL: "https://gitlab.example.com",
Token: "glpat-...",
ProjectID: "12345",
Ref: "develop",
}
client.SetCommitStatus(sha, gitlabci.StateRunning, "my-app: Build", "")
// ... run the stage ...
client.SetCommitStatus(sha, gitlabci.StateSuccess, "my-app: Build", "")Names follow the "{project}: {stage}" pattern, for example portal-web: Install Dependencies or api-gateway: Docker Build and Push.
Ref is load-bearing. Without it GitLab attaches each status to the most recent pipeline for that SHA. If a second pipeline is born on the same commit mid-build — a push to develop that is also a merge request head, say — the terminal status lands on the new pipeline and the original one stays running forever, with nobody left to close it.
The orchestrator builds the client from --gitlab-host, --gitlab-token, --gitlab-project-id and --gitlab-ref. When any of them is missing, the modules behave exactly the same, simply without reporting statuses.
The same client posts reports as merge request comments. Each report carries an invisible HTML marker, which is how a rerun edits the existing note instead of stacking a new one — the same way SonarQube does it. One marker per subject, so reports from different sources coexist on the same merge request:
| Marker | Written by |
|---|---|
|
|
|
Quality gates that report their own verdict, for projects whose checks do not go through SonarQube. |
Client.ReportPublishedImages(commitSha, published, version) is the entry point for the first one. The publish job runs on a push to the branch, already past the merge, so there is no CI_MERGE_REQUEST_IID in the environment — the merge request is discovered from the commit through GET /repository/commits/:sha/merge_requests, which matches both the merge commit and the squash commit. A direct push with no merge request simply has nowhere to comment, and the job says so.
Failures here are reported and swallowed. The report is a convenience, and bringing down a publish that already succeeded because a comment did not go through would trade a small problem for a large one.
Generic, type-safe orchestration. It uses generics to abstract over the Dagger SDK types, letting orchestrators on different SDK versions share the same logic.
-
BuildStrategy[Dir, Secret]/QualityStrategy[Dir, Secret]— the signatures any module implements to plug into the orchestration. -
DaggerOps[Dir, Secret]— callbacks isolating the SDK-specific operations (change detection, directory access, image tagging). -
PublishAll— detects the changed projects, runs their build strategies in sequence, aggregates the published images and tags them withsha-<commit>. -
CheckQuality— runs tests and static analysis on the changed projects, with optional fail-fast. The orchestrator’scheck-quality-from-reportsreuses the build ofpublish-all-with-reportsinstead of running the tests again; the library is unchanged, the reuse lives in the per-target quality strategy.
Reading and validation of ci/pipeline.toml, and the anti-drift derivation of the image map (ProjectImages, ProjectImagesJSON). Consumed both by the generic orchestrator and by the projects' custom modules.
BuildOptions(host, token, projectKey, waitForQualityGate, extra) assembles the -Dsonar.* options in canonical order. The maven, npm and uv modules delegate to it; each keeps only its own edges (reading the *dagger.Secret and handling a nil config).
These modules are called by the orchestrator; a project does not normally invoke them directly.
Build and test (clean verify), Sonar analysis and image publishing through the Jib plugin. Supports Maven Wrapper, local repository caching, an optional Docker daemon for Testcontainers, reactor mode and commit status reporting.
Outside reactor mode the version is applied with versions:set; in reactor mode, with -Drevision. When no version is given the pom is left untouched — that is the quality-check case, which publishes nothing.
The scanner is invoked by full coordinates — org.sonarsource.scanner.maven:sonar-maven-plugin:<version>:sonar — rather than through the sonar:sonar prefix. The prefix only resolves when the project declares the plugin, since it is not in Maven’s default pluginGroups, and that implicit dependency broke projects not generated by JHipster.
The version is pinned (sonar-plugin-version, with a default in the maven module) for two reasons: a new SonarSource release must not break every pipeline overnight without a commit anywhere, and the scanner has to stay compatible with the server, which upgrades on its own schedule. As a consequence, the <version> declared in the pom no longer influences which scanner runs — whoever needs a different one declares it in ci/pipeline.toml.
Node.js/Angular projects. Runs npm ci, npm run build, optional SonarQube analysis, and publishes dist/ inside an nginx container. Supports version override by patching package.json, and commit status reporting.
Python projects using uv. Two-stage dependency sync (uv sync), optional SonarQube analysis and publishing of a slim Python image. Supports run-subdir and build customizations for dlt and dbt projects.
Like maven and npm, the module is path-aware: module-path says where the project sits inside the tree it was handed. The build receives the project directly and leaves it empty; the quality check receives the whole repository and fills it in, so that the .git directory reaches the scanner. Without SCM data SonarQube cannot tell which lines a merge request changed, marks the entire project as new code, and fails the quality gate on the target’s historical debt — which is what happened to every uv target until this was fixed.
For the same reason the scanner analyses the source tree rather than the build output: the build output is /app after uv sync, with .venv inside it. The build still runs before the analysis, so a broken uv sync fails the quality job.
Git and registry utilities used by the orchestrator: change detection between branches, commit SHA lookup, image tagging with sha-<commit>, bumping and committing version files, image existence checks and promotion between registries.
All Git operations run inside an alpine/git container; image operations use crane.
security-report-index takes the directory produced by security-check (<target>/exit-code, <target>//dependency-check-report.{html,json}, <target>//dependency-check-junit.xml, optional FAILED) and returns it with the empty reports (HTML, JSON and JUnit) removed — the parent/aggregator pom that -am pulls into the reactor reports 0 dependencies — plus an index.html (one row per report: target, module, dependencies, vulnerable dependencies, CVEs by severity, target status, link) a summary.json with the same data, and a self-contained shields-style badge.svg (CVE | 40 críticas · 102 altas in red when there is a critical, orange for highs only, yellow for medium/low only, sem achados in green, falhou in grey when a target failed and no report was produced). security-check asks Dependency-Check for HTML,JSON,JUNIT: the JUnit report feeds the pipeline Tests tab in GitLab CE, which has no security dashboard and, for a scheduled scan, no merge request either. It lives here rather than in orchestrator so that a project-local orchestrator (DAGGER_MODULE: ".") implementing the same security-check contract can call dag.OrchestratorUtils().SecurityReportIndex(dir) instead of duplicating it. Severity is normalised to CRITICAL/HIGH/MEDIUM/LOW (moderate counts as MEDIUM); anything else is counted as OTHER in summary.json.