feat(automation): add generateDocument automation action - #12
Merged
Conversation
added 5 commits
July 24, 2026 14:30
Implements the automation-document-action OpenSpec change: a new
generateDocument action kind lets an automation trigger Docudesk document
generation on object create/update/delete/lifecycle-transition, without
ever importing a Docudesk PHP class or bypassing REQ-DDT-006's closed
integration contract.
Backend:
- AutomationCompilerService: generateDocument added to the v1 matrix on
event/lifecycle-transition triggers; compile-time validation (templateId
present, output non-empty, notify never alone) and a fail-closed
UnsupportedAutomationCombinationException when Docudesk is absent.
- DocumentGenerationService: owner-impersonated (JobOwnerImpersonator)
internal HTTP call to the pinned POST /apps/docudesk/api/correspondence/
generate route, authenticated via a short-lived NC login token minted
through OC\Authentication\Token\IProvider and invalidated immediately
after use. attach writes to Nextcloud Files + a {ref} field on the
object; download-link writes to OpenBuild's own app-private storage
(never the user's Files tree) behind a random ~24h token served by the
new GeneratedDocumentController; notify reuses RuleActionDispatcher's
existing send-notification path.
- DocumentGenerationListener: trigger-fire dispatch, mirrors
AutomationApprovalTriggerListener's shape for the sibling approval kind.
Frontend:
- useDocudeskTemplates.js: shared template-list fetch, now used by BOTH
DocumentTemplateAttachmentDialog and the new automation editor picker
(no second implementation).
- AutomationEditDialog.vue: generateDocument action editor (template
picker + output-mode multi-select), missing-app degradation via
useAppStatus('docudesk'), and matrix-aligned save validation.
Tests: 27 new PHPUnit tests (compiler, service, listener, controller,
source-tree Docudesk-import guard) + 21 new/updated vitest tests. Full
suites green (699 PHPUnit / 1236 vitest), phpcs/phpstan/psalm clean on
touched files.
- Revert the unrelated SetupController.php phpstan tidy-up: it pulled 3 pre-existing @NoAdminRequired/admin-body semantic-auth findings into this diff's gate scope for a file this change has no other reason to touch. - Add the @SPEC tag templateToOption() was missing (gate-16 spec-coverage).
… pin - Playwright: compose a generateDocument automation on a lifecycle transition (template picker/free-text degrade, output-mode select) and the schedule-trigger fail-closed block, mirroring the existing approval-action e2e suite's structure. CI-run only, not executed against the shared dev instance in this session. - Newman: item 6 pins the automation-triggered call to the SAME correspondence/generate route + request shape item 4 already asserts. - tasks.md: check off 4.1-7.2 (all 18 tasks complete).
…pecs - Move openspec/changes/automation-document-action/ to openspec/changes/archive/2026-07-24-automation-document-action/. - Sync canonical specs: automation-document-action (new capability, ADDED requirements, Status done), automation-designer (REQ-AUTD-002/003/004 MODIFIED with the generateDocument scenarios, Status done — last active change), docudesk-document-templates (REQ-DDT-006 MODIFIED to name the owner-impersonated automation caller shape; harden-xss-dos-csrf still active so Status stays in-progress). - CHANGELOG.md: add the 0.7.5 entry. - docs/automation-designer.md: extend the compilation matrix with the Require approval and Generate document columns (both were missing — approval predates this PR; documented together since they share the same "needs a concrete fired object" constraint).
Add @e2e Playwright markers for the two scenarios this PR's automations e2e suite exercises, and reason-bearing @e2e exclude annotations (backend/ service contract, already covered by PHPUnit + Newman) for the remaining scenarios this change added. Leaves the pre-existing automation-designer/ automation-approval-steps scenarios untouched — gate-19 flags them because editing shared requirement bodies pulls the whole file into diff scope, not because this PR modified their behaviour; backfilling e2e coverage for already-shipped, unrelated functionality is out of this change's scope.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements OpenSpec change
automation-document-action: a newgenerateDocumentautomation action kind lets an automation trigger Docudesk document generation on object create/update/delete or a lifecycle transition — no interactive browser click required.AutomationCompilerService:generateDocumentadded to the v1 matrix onobject-created|object-updated|object-deleted|lifecycle-transition; fail-closed onschedule/manual. Compile-time validation only (templateIdpresent,outputa known non-empty set,notifynever alone) — no compile-time Docudesk-side artifact, sincecorrespondence/generateis stateless. ThrowsUnsupportedAutomationCombinationExceptionnaming the missingdocudeskdependency when Docudesk is absent.DocumentGenerationService(new): the imperative half.attachwrites the returned bytes to Nextcloud Files (OCP\Files\IRootFolder) and sets{ "ref": "<fileId>" }on the object'sgeneratedDocumentfield.download-linkwrites to OpenBuild's own app-private storage (OCP\Files\IAppData— never the user's Files tree) behind a random ~24h token, served by the newGeneratedDocumentController.notifyreuses the existingRuleActionDispatchersend-notification path (no second notification implementation).DocumentGenerationListener(new): trigger-fire dispatch, mirrorsAutomationApprovalTriggerListener's shape for the siblingapprovalaction kind.useDocudeskTemplates.js(new, shared): the ONE Docudesk template-list fetch, now used by bothDocumentTemplateAttachmentDialogand the newAutomationEditDialogpicker.AutomationEditDialog.vue:generateDocumentaction editor — template picker + output-mode multi-select — disabled with a missing-app hint viauseAppStatus('docudesk'), and matrix-aligned save validation mirroring the compiler's own rules.How REQ-DDT-006 was honored
docudesk-document-templatesREQ-DDT-006 hard-forbids importing Docudesk PHP classes and requires "the caller's Nextcloud session." An automation trigger has no interactive session, soDocumentGenerationServiceimpersonates the Application owner via the existingJobOwnerImpersonator(lib/Service/JobOwnerImpersonator.php— no new impersonation mechanism) and makes one internal HTTP call to the same pinnedPOST /apps/docudesk/api/correspondence/generateroute — never a class import, never a new route.Documented extension of design.md's transport detail: design.md's literal wording (
IUserSession::setUser()"makes that user the active session for the duration of one internal HTTP call") describes the authorization intent correctly but not, by itself, a working HTTP transport —setUser()only changes the current PHP process's session, andIClientService's call is a genuine network request Nextcloud's auth middleware evaluates independently on arrival.DocumentGenerationServicerealizes the same "one call, as the impersonated owner" intent by minting a short-lived Nextcloud login token for the impersonated user (via NC core'sOC\Authentication\Token\IProvider— the same mechanism behind NC's own "app password" feature, resolved defensively/lazily exactly likeJobOwnerImpersonatorresolves its own optional collaborators) and presenting it as HTTP Basic Auth on that one call, explicitly invalidating the token again immediately after use.IToken::TEMPORARY_TOKENis used rather thanONETIME_TOKENbecause the latter isn't present on everynextcloud/ocpstub version this app'scomposer.jsonrange resolves to — the explicit invalidate-after-use call gives the same effective one-shot lifetime. Bothopenspec/specs/docudesk-document-templates/spec.mdREQ-DDT-006 and the archived change's own spec name this transport explicitly.REQ-DDT-006's pinned-route list itself does not grow —
correspondence/generategets a second, still-single-route caller shape, asserted by a new Newman item that pins the automation-shaped request body against the same route item 4 already covers.Test results (exact commands)
Baseline before this PR: 672 PHPUnit / 1224 vitest — this PR adds 27 PHPUnit + 12 net vitest tests, all green.
Hydra mechanical gates (
--scope-to-diff)35/39 green, including all three gates named as this task's acceptance criterion:
spec-coverage(gate-16),no-phantom-cross-app-rpc(gate-27),controller-exception-translation(gate-49).4 remaining findings are pre-existing/out-of-scope, not introduced by this diff's substance:
forbidden-patterns: a pre-existing, already-documented (phpcs:ignorecomment in place)error_log()call inlib/AppInfo/Application.phpat a line this PR never touches — flagged only because the gate scans the whole file once any part of it is in the diff.effective-manifest-crossref: 2 pre-existing findings againstsrc/manifest.json/deepLinks/0(an unrelatedExamplewidget) — this PR never touches any manifest file.spec-anchor-existence: this app's fleet-wide@spec .../tasks.md#N.Nconvention (checkbox-index fragments, not real Markdown headings) predates this change and is used identically by the just-merged siblingautomation-approval-steps(PR feat(automation-approval-steps): approval action kind, on-approve/reject follow-ups, My Approvals widget #10) — not something this PR's scope should unilaterally rewrite across the codebase.e2e-coverage: 21 of the original 30 findings were pre-existingautomation-designer/automation-approval-stepsscenarios pulled into scope only because this PR edits shared requirement bodies in the same canonical spec file; this PR's own 9 new/modified scenarios are all closed (2 real Playwright@e2emarkers + 7 reason-bearing@e2e excludefor backend/PHPUnit/Newman-covered contracts).Vendor hazard
vendor/was never staged or committed on this branch — confirmed viagit status --porcelainbefore every commit andgit log --stat origin/development..HEAD | grep vendor(no output) after. The root-ownedvendor/directory created by in-containercomposer installwas removed via a containerrm -rfbefore finishing.Test plan
composer check:strict-equivalent local checks (phpunit/phpstan/psalm/phpcs) all greennpx eslint/npx vitest rungreenopenspec validate automation-document-actionpasses; change archived toopenspec/changes/archive/2026-07-24-automation-document-action/🤖 Generated with Claude Code