Skip to content

819: Start the building block version linked to the case - #879

Open
maarten-ritense wants to merge 5 commits into
next-minorfrom
bugfix/819-actions-respect-building-block-versions
Open

819: Start the building block version linked to the case#879
maarten-ritense wants to merge 5 commits into
next-minorfrom
bugfix/819-actions-respect-building-block-versions

Conversation

@maarten-ritense

@maarten-ritense maarten-ritense commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Describe the changes

Link to the related Github issue: generiekzaakafhandelcomponent/gzac-issues#819

A building block linked to a case as an action ran the newest version of that building block instead of the version that is linked. After creating a draft version and changing its process, starting the action still ran the draft, which failed as soon as that version wrote to a document field the linked version does not have.

Everything up to the submit was already correct: StartableBuildingBlockItemProvider returns the version-specific process definition id, and the start form is fetched by that id. The version was lost inside the submit. DefaultFormSubmissionService resolved the right versioned OperatonProcessDefinition and then forwarded only processDefinition.key. Resolving a key in OperatonProcessService.startProcess first tries the blueprint version tag — which for a case document is CD:<case>:<v> and can never match a building block process tagged BB:<key>:<v> — and then fell back to maxVersionOf(byNotLinkedToCaseDefinition()). That filter excludes CD: and DETACHED: but not BB:, so it selected the highest engine version of the key: the draft.

The engine call is already by id (formService.submitStartForm(processDefinition.getId(), ...)); only the resolution from a key was lossy. So the versioned process definition id is now carried end to end for process-link starts, and key-based resolution was made blueprint-correct and fail-fast.

Specify the code branch location: bugfix/819-actions-respect-building-block-versionsnext-minor

Relevant comments:

Why carry the id and not the blueprint. A case can link x:0.0.1 and x:0.0.2 at the same time — CaseDefinitionBuildingBlockLinkRepository.findAllByCaseDefinitionId returns both as separate startable items — so document plus key genuinely cannot disambiguate which one the user pressed. That intent exists only on the process link. Carrying the id also removes a findOne multiplicity risk (the version-tag specs never filter on tenant) and makes the two authorization checks agree: DefaultFormSubmissionService.requirePermission already checked the process link's definition, while startProcess checked whatever the key resolved to. Today those are different definitions.

⚠ Behaviour change: a building-block process can no longer be started by key alone. The key-only fallback now also excludes BB:-tagged definitions, so instead of silently running whichever version was deployed last, the caller gets No process definition found with key: '...' and blueprintId: 'null'. Version tags deployed for this key: BB:bezwaar:1.0.0, BB:bezwaar:1.0.1. Callers that pass only a key are ProcessResource.java:375, StartProcessCommandHandler.kt:32, ProcessDocumentsService.kt:76,104 and PortaalTaakEventListener.kt:227. None of them intends a building block — they start global/unlinked processes — and for those nothing changes (see below). Failing loudly was chosen over guessing so this class of bug cannot recur silently. This is described in the release notes. I judged it non-breaking in the SemVer sense (no API removed or changed incompatibly, and the previous behaviour for this input was undefined rather than specified), but it is a behavioural change and worth a second opinion — happy to move it if you read it differently.

@JsonIgnore on the new processDefinitionId is load-bearing, not cosmetic. ProcessDocumentResource binds NewDocumentAndStartProcessRequest and ModifyDocumentAndStartProcessRequest from client JSON with @Valid @RequestBody, and FAIL_ON_UNKNOWN_PROPERTIES is disabled, so a bindable field would silently bind and let a caller bypass key+blueprint resolution entirely: reaching a DETACHED: definition, a deliberately suspended draft (the start path activates it for the duration of the start), or a definition belonging to a different case than the document being modified. Three tests in ProcessDocumentResourceTest pin this, including that the field never appears on the wire.

startProcessById needs its own DETACHED: guard. The key-based path implicitly filtered detached definitions out; starting by id does not. Redeploying a draft's BPMN renames the previous definition to DETACHED:... (OperatonProcessService.java:605) without removing its process links, so a browser tab held open across a redeploy can still submit a processLinkId pointing at one. BuildingBlockDefinitionId.fromProcessVersionTag returns null for a DETACHED: tag, which would produce exactly the instance-less process this PR is fixing. It now throws a message telling the user to reload the case.

caseDefinitionId()asBlueprintId(). document.definitionId().caseDefinitionId() returns null for a building block document (BlueprintOwner.asCaseDefinitionId() only answers for BlueprintType.CASE), and byBlueprintId(null) degrades to "latest version of this key". The new DocumentDefinition.Id.asBlueprintId() default returns case or building block. For a case document this is byte-identical to before — a case document always carries both key and version tag, so no "latest" resolution was ever involved — and it fixes standalone building block start forms plus the startProcessByProcessDefinitionKey BPMN helper when called from inside a building block. That second one matters: without it the fail-fast guard above would have broken those calls.

Unlinked / "system" processes are unaffected, and that is pinned by a test. They have no version tag, so byNotLinkedToBuildingBlock() is satisfied and the subquery still yields the latest version of the key. CopyProcessLinkOnProcessDeploymentListener copies process links onto every new deployment, and every path that hands out a processDefinitionId for an unlinked process returns the latest version (byActive().and(byLatestVersion()), getUnlinkedDeployedDefinitions() takes the max per key), so the id now carried resolves to the same definition the old key lookup picked. UnlinkedProcessStartIntTest asserts both halves. The one difference: submitting a processLinkId from a non-latest version of a system process (a tab held open across a redeploy) now starts the version that link — and the form that was filled in — belongs to, instead of the latest.

The regression test was verified to actually catch the bug. With the fix temporarily reverted, BuildingBlockStartableItemVersionIT reports expected: building-block-process:1:... but was: building-block-process:2:... — the draft instead of the linked version, which is issue 819 exactly. It deploys the second version directly rather than through BuildingBlockManagementService.createDraft, because going through createDraft made it depend on state that the non-transactional integration tests in that module commit: it passed in isolation and failed in the full suite.

Found along the way, filed separately as #840: a form flow configured as the start form of a building block's main process throws a NullPointerException before the form even opens, because FormFlowProcessLinkActivityHandler dereferences a ProcessDefinitionCaseDefinition that building block processes do not have. Fixing it needs the same id-threading as this PR plus that null fix, so it is out of scope here. URLProcessLinkService.kt:77 has the same dereference on the path where documentDefinitionName is null.

Not addressed on purpose: the suspend/activate/re-suspend dance in startProcessInstance is globally visible for the duration of a start. It was moved verbatim into the extracted method; changing its semantics does not belong in a bugfix.

Breaking changes

  • The contribution only contains changes that are not breaking.

All additions are additive: OperatonProcessService.startProcessById, withProcessDefinitionId on three request DTOs, and a DocumentDefinition.Id.asBlueprintId() default method. No signature was removed or changed incompatibly, and the JSON contract of the request DTOs is unchanged. See the flagged behavioural change under Relevant comments — a building-block-owned process can no longer be started by process definition key alone. Previously that input had no defined outcome (it returned an arbitrary version), which is why I read it as a bug fix rather than a breaking change.

Documentation

  • Release notes have been written for these changes.

New features or changes that have been introduced have been documented.

  • Yes

Tests

Unit tests have been added that cover these changes

  • Yes

OperatonProcessServiceTest (6 new: start-by-id uses the exact id and never resolves by key, not-found, DETACHED:, suspended activate → start → re-suspend including on failure, UNDEFINED_BUSINESS_KEY → null — this class had no startProcess coverage at all before), DefaultFormSubmissionServiceTest (3 new, including a building-block variant), OperatonProcessJsonSchemaDocumentServiceTest (2 new: start-by-id routing, building-block blueprint derivation), ProcessDocumentResourceTest (3 new, the binding/serialisation guards).

Integration tests have been added that cover these changes

  • Yes

BuildingBlockStartableItemVersionIT (new — the bug itself, the fail-fast path, and a standalone building block start), UnlinkedProcessStartIntTest (new — system processes are not regressed), and two cases added to OperatonProcessDefinitionSpecificationHelperIntTest for the composed unlinked specification.

Ran green: :backend:{core,process-document,form,process-link-url,form-view-model,case,building-block}:test, :backend:zgw:{portaaltaak,verzoek,zaken-api}:test, and integrationTestingPostgresql for building-block, core, process-document, form, form-view-model, process-link-url, zgw:verzoek and zgw:portaaltaak. Checkstyle passes with no new warnings. The zgw:verzoek suite matters because the key-only fallback exists for the Verzoek plugin.

Describe the testing steps

  • With a building block that has two versions — 0.0.1 final and linked to a case as an action, 0.0.2 a draft whose main process writes to a document field that does not exist in 0.0.1 — starting the action from a case runs 0.0.1 and completes without an error. See provided 'Hello World' building block exports below.
  • The started process instance's process definition id is the 0.0.1 one (BB:<key>:0.0.1), and a BuildingBlockInstance is created for 0.0.1 with the case document as its case_document_id
  • Changing 0.0.2 further has no effect on starting the action
  • A regular (non-building-block) supporting process still starts from the case actions menu
  • A process on /processes that is not linked to a case definition still starts, and starts its latest version
  • Creating a new case from the case list still works (start form → new document → process started)
  • Completing a user task through its form still works

2 versions of the 'Hello World' building block which can be used for testing:

Security

The Secure by Design principle has been applied to these changes

  • Yes

The new processDefinitionId is deliberately not bindable from client JSON (see Relevant comments); allowing it would let a caller choose which process definition to start for a document, bypassing the key and blueprint resolution that couples the two. The value is only ever set server-side from the process link the request refers to. The DETACHED: guard closes a second path to starting a definition that is no longer reachable through normal resolution.

Added or changed REST API endpoints have authentication and authorization in place

  • Yes
  • Not applicable

No endpoints were added or changed. Two existing ones (/api/v1/process-document/operation/{new,modify}-document-and-start-process) have request DTOs that gained a field, which is why that field is @JsonIgnore and why ProcessDocumentResourceTest now asserts a client-supplied processDefinitionId is not bound and never serialised.

Valtimo access control checks have been implemented

  • Yes

The existing RelatedEntityAuthorizationRequest(OperatonExecution, CREATE, OperatonProcessDefinition, <id>) check is kept on both start paths, and is now evaluated against the definition that is actually started. Before this change the form submission authorized the process link's definition while the start authorized whichever definition the key resolved to — potentially a different one. Permission rules that key off versionTag will therefore see the linked version instead of the newest; that is the intended outcome. ProcessAuthorizationService in form-view-model duplicates this resolution and was aligned for the same reason.

Dependencies

Newly added dependencies do not introduce known vulnerabilities/CVE's and are in line with the Valtimo license

  • Yes
  • Not applicable

No dependencies were added or changed.

An action that starts a building block ran the newest version of that building
block instead of the version linked to the case, failing when that version
wrote to fields the linked version does not have.

The process link carries the exact, versioned process definition id, but form
submission forwarded only the process definition key. Resolving a key then fell
back to "highest engine version not linked to a case definition", which does not
exclude building-block-owned definitions, so a newer draft version won. Carry
the versioned id through to the engine for process-link starts, and derive a
document's blueprint as case or building block - caseDefinitionId() is null for
building block documents, which is what made the lookup degrade to "latest".
Resolving by key alone now refuses building-block definitions instead of
guessing a version.

processDefinitionId on the start-process requests is @JsonIgnore on purpose:
ProcessDocumentResource binds two of them from client JSON with
FAIL_ON_UNKNOWN_PROPERTIES disabled, so a bindable field would let a caller
bypass key and blueprint resolution and reach a suspended draft or another
case's definition.
@maarten-ritense
maarten-ritense requested review from a team as code owners August 5, 2026 08:46
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aea31b81-49eb-4e30-b754-99307bc9433b

📥 Commits

Reviewing files that changed from the base of the PR and between e616999 and 68d6d6f.

📒 Files selected for processing (4)
  • backend/core/src/main/java/com/ritense/valtimo/service/OperatonProcessService.java
  • backend/core/src/main/kotlin/com/ritense/valtimo/operaton/repository/OperatonProcessDefinitionSpecificationHelper.kt
  • backend/core/src/test/kotlin/com/ritense/valtimo/operaton/repository/OperatonProcessDefinitionSpecificationHelperIntTest.kt
  • backend/form-view-model/src/main/kotlin/com/ritense/formviewmodel/service/ProcessAuthorizationService.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/form-view-model/src/main/kotlin/com/ritense/formviewmodel/service/ProcessAuthorizationService.kt
  • backend/core/src/main/java/com/ritense/valtimo/service/OperatonProcessService.java
  • backend/core/src/test/kotlin/com/ritense/valtimo/operaton/repository/OperatonProcessDefinitionSpecificationHelperIntTest.kt

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Case actions now run the exact building block version linked to the case.
    • Process starts can target a specific process definition version.
    • Unlinked processes automatically use the latest available version when no specific version is provided.
  • Bug Fixes

    • Prevented ambiguous process-key-only starts from selecting an unintended building block version.
    • Improved process-version resolution for forms, document actions, and process links.
  • Documentation

    • Added release notes for version 13.41.0.

Walkthrough

Process startup now supports exact process-definition IDs and blueprint-aware resolution. Unlinked process starts select the latest definition that is not linked to a case or building block. Form, URL, and document services propagate resolved definition IDs through internal start requests. JSON binding excludes client-supplied IDs. Tests and BPMN fixtures cover linked, building block, unlinked, exact-version, and failure scenarios.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch bugfix/819-actions-respect-building-block-versions
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/819-actions-respect-building-block-versions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

13.40.0 is being released, so the entry moves to the next unreleased minor.
Adds the 13.41.0 directory and registers it in SUMMARY.md so GitBook renders it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d70da6c0-31fd-4e60-bec6-184961cf3d99

📥 Commits

Reviewing files that changed from the base of the PR and between b5e93af and ef49ab3.

📒 Files selected for processing (2)
  • documentation/SUMMARY.md
  • documentation/release-notes/13.x.x/13.41.0/README.md

Comment thread documentation/release-notes/13.x.x/13.41.0/README.md Outdated
@Klaas-Ritense Klaas-Ritense added the test env Create a remote test environment for this PR in the Ritense product development cluster label Aug 6, 2026
@valtimo-platform

valtimo-platform Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Will create a test environment. This comment will be updated once it is available. This usually takes a few minutes.
Closing or merging this PR will automatically delete the test environment. Pushing commits to this PR will update the test environment.
Progress:

  • Created test environment
  • Waiting for frontend and backend images with tag "testEnv.pr879_commit.<commit sha>_contents.<contents sha>_build.<build number>". Check the GitHub Actions logs for progress on the build / image tagging.
  • Starting test environment
  • Waiting for environment to run

@Klaas-Ritense Klaas-Ritense removed the test env Create a remote test environment for this PR in the Ritense product development cluster label Aug 6, 2026
@Klaas-Ritense Klaas-Ritense added the test env Create a remote test environment for this PR in the Ritense product development cluster label Aug 6, 2026
@valtimo-platform

valtimo-platform Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Will create a test environment. This comment will be updated once it is available. This usually takes a few minutes.
Closing or merging this PR will automatically delete the test environment. Pushing commits to this PR will update the test environment.
Progress:

  • Created test environment
  • Images tagged testEnv.pr879_commit.20504f35a0_contents.5dd7a7edab_build.644 (frontend) / testEnv.pr879_commit.9b2daea467_contents.4007ae234c_build.1952 (backend) available
  • Started test environment
  • Test environment is running at https://pr879.product-development.test.k8s.ritense.com

Test environment metadata:

  • URL: https://pr879.product-development.test.k8s.ritense.com
  • Commit: 9b2daea
  • Frontend contents: 5dd7a7edab
  • Backend contents: 4007ae234c
  • Frontend image: ghcr.io/valtimo-platform/valtimo/gzac-frontend:testEnv.pr879_commit.20504f35a0_contents.5dd7a7edab_build.644
  • Backend image: ghcr.io/valtimo-platform/valtimo/gzac-backend:testEnv.pr879_commit.9b2daea467_contents.4007ae234c_build.1952

Observability:

@Klaas-Ritense Klaas-Ritense added feedback and removed test env Create a remote test environment for this PR in the Ritense product development cluster labels Aug 7, 2026
The specification that resolves a process definition from its key alone
was written out three times: in OperatonProcessService, inlined in
ProcessAuthorizationService and mirrored in the helper's integration
test. Move it to OperatonProcessDefinitionSpecificationHelper so all
three share one definition and the test covers the real specification.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants