Add approval path actions in Jira automation

Description

Introduce native approval path actions in Jira automation to replace the current webhook-based integration.

Add support for key actions: starting, deleting, and archiving approvals directly.

Plan: Jira Automation Actions for Approval Path

Context

The app currently has jira:jiraWorkflowPostFunctions handling START/ARCHIVE/DELETE approval operations during workflow transitions. The goal is to expose the same operations as Jira Automation actions (action + automation:actionProvider with backend-remote) and unify the processing classes (WorkflowJobWorker, WorkflowJobExecutor, ApprovalWorkflowService) to serve both workflow and automation triggers.

Key facts about Forge Automation Action:

  • POST body contains only declared inputs — no automatic issue context

  • FIT JWT contains cloudId, principal (userId), but NOT issueId/projectId

  • Issue context must be passed via smart values in inputs ({{issue.id}}, {{project.id}})

  • No automation execution ID — deduplication via hostId+refId+definitionId+userId (existing filter skips null executionId)

  • endpoint (Forge Remote) works identically to function for payload structure


Part A: Refactoring — Unify naming between workflow and automation

All classes are in a single package ovh.atlasinc.ap.jira.action. No external references outside this package (except WorkflowController).

A1. Rename classes
┌──────────────────────────┬───────────────────────────┬─────────────────────────────────────────┐
│ Old name │ New name │ File │
├──────────────────────────┼───────────────────────────┼─────────────────────────────────────────┤
│ WorkflowJob │ ApprovalActionJob │ action/ApprovalActionJob.java │
├──────────────────────────┼───────────────────────────┼─────────────────────────────────────────┤
│ WorkflowJobWorker │ ApprovalActionJobWorker │ action/ApprovalActionJobWorker.java │
├──────────────────────────┼───────────────────────────┼─────────────────────────────────────────┤
│ WorkflowJobExecutor │ ApprovalActionJobExecutor │ action/ApprovalActionJobExecutor.java │
├──────────────────────────┼───────────────────────────┼─────────────────────────────────────────┤
│ ApprovalWorkflowService │ ApprovalActionService │ action/ApprovalActionService.java │
├──────────────────────────┼───────────────────────────┼─────────────────────────────────────────┤
│ WorkflowApprovalPathUser │ SystemApprovalPathUser │ action/SystemApprovalPathUser.java │
└──────────────────────────┴───────────────────────────┴─────────────────────────────────────────┘
WorkflowController keeps its name — it is specifically the Connect workflow controller.
TriggerConfiguration keeps its name — it is already generic.

A2. Add TriggerSource enum

New file: workflow/TriggerSource.java

public enum TriggerSource {     WORKFLOW,     AUTOMATION } 

A3. Add TriggerSource to ApprovalActionJob

public abstract sealed class ApprovalActionJob permits
StartApprovalJob, ArchiveApprovalJob, DeleteApprovalJob {

 private TriggerConfigurationType type; private TriggerSource source;  // NEW private int hostId; @Nullable private String currentUserId; @Nullable private EvaluatorPayload evaluator; private String issueId; // ...

}

All job subclass constructors get a new TriggerSource source parameter, passed to super(...).

A4. Update ApprovalActionJobWorker (ex-WorkflowJobWorker)

  • Log messages: "Workflow {} operation..." → "{} {} operation..." using t.getSource()

  • InstanceErrorType selection: t.getSource() == AUTOMATION ? AUTOMATION : WORKFLOW

  • SystemApprovalPathUser: display name based on source:

    • WORKFLOW → "Workflow (post function)"

    • AUTOMATION → "Automation (action)"

  • Job queue name "workflow" → stays unchanged (backward compat with in-flight jobs)

A5. Add AUTOMATION to InstanceErrorType

File: instanceerror/InstanceErrorType.java

 public enum InstanceErrorType {     JIRA_EXPRESSION,     WORKFLOW,     AUTOMATION,     NOTIFICATION; }

A6. Update ApprovalActionService (ex-ApprovalWorkflowService)

  • Change method visibility from package-private to public (required by new AutomationController in ovh.atlasinc.ap.forge package)

  • Methods startApproval(), archive(), delete() — no logic changes

A7. Update WorkflowController

  • Update imports to new class names

  • new StartApprovalJob(...) → add TriggerSource.WORKFLOW parameter

  • Same for ArchiveApprovalJob, DeleteApprovalJob

A8. Update test TriggerConfigurationTest

  • Update imports to new class names

Files to modify (refactoring):

┌────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐
│ File │ Change │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/WorkflowJob.java │ Rename → ApprovalActionJob.java, add TriggerSource source field │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/StartApprovalJob.java │ Update extends, add TriggerSource to constructor │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/ArchiveApprovalJob.java │ Same as above │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/DeleteApprovalJob.java │ Same as above │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/WorkflowJobWorker.java │ Rename → ApprovalActionJobWorker.java, dynamic logs/error types │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/WorkflowJobExecutor.java │ Rename → ApprovalActionJobExecutor.java, update imports │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/ApprovalWorkflowService.java │ Rename → ApprovalActionService.java, make methods public │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/WorkflowApprovalPathUser.java │ Rename → SystemApprovalPathUser.java, source-based display name │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ action/WorkflowController.java │ Update imports, add TriggerSource.WORKFLOW │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ instanceerror/InstanceErrorType.java │ Add AUTOMATION │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ test/.../TriggerConfigurationTest.java │ Update imports │
└────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘


Part B: New modules — Automation Action

B1. Manifest — template.yml

File: approval-path/src/main/frontend/jira/manifest/template.yml

Add to modules: section:

  automation:actionProvider:     - key: approval-path-automation-provider<< appPrefix >>       name: Approval Path<< labelsSuffix >>       actions:         - approval-path-automation-action<< appPrefix >>    action:     - key: approval-path-automation-action<< appPrefix >>       name: Approval Path actions<< labelsSuffix >>       endpoint: automation-endpoint       actionVerb: CREATE       description: Start, archive, or delete approval(s)       config:         resource: app-resources       inputs:         issueId:           title: Issue ID           type: string           required: true         projectId:           title: Project ID           type: string           required: true         config:           title: Configuration           type: string           required: true

Add to endpoint: list:

key: automation-endpoint    remote: backend-remote    route: { path: /forge/automation/triggered }    auth: { appSystemToken: { enabled: true } }

issueId and projectId — pre-filled with smart values {{issue.id}}, {{project.id}} in the config UI.
config — JSON string containing TriggerConfiguration (type, definitionId, evaluator).

B2. Backend — Security filter chain

File: config/ForgeConfiguration.java

Add SecurityFilterChain bean for /forge/automation/** — same pattern as existing secureForgeEventHandlers:

  • FIT JWT authentication via fitAuthManager

  • CSRF disabled (server-to-server)

  • AtlassianApiTokensExtractorRequestFilter for token extraction

B3. Backend — AutomationController

New file: forge/AutomationController.java

@RestController
@RequestMapping("/forge/automation")
public class AutomationController {

 private final ObjectMapper om; private final ApprovalActionJobWorker worker; private final Addon addon; private final Hosts hosts; private final JiraApiAccessProvider jiraApiAccessProvider;  @CsrfHandler.Skip @PostMapping("/triggered") public ResponseEntity<Void> handleTriggered(     VerifiedAtlassianContext context,     @RequestBody AutomationActionPayload payload ) {     // 1. Resolve host: cloudId → siteUrl → host     var jira = jiraApiAccessProvider.get(context.getCloudId());     var siteUrl = jira.getServerInfo().getBaseUrl();     var host = hosts.findBySiteUrl(addon.getKey(), siteUrl);      // 2. Parse config JSON → TriggerConfiguration     var config = om.readValue(payload.config(), TriggerConfiguration.class);      // 3. Get userId from FIT principal     var userId = context.getUserId();      // 4. Add job with TriggerSource.AUTOMATION     var evaluator = config.payload() != null ? config.payload().evaluator() : null;     switch (config.type()) {         case START -> worker.add(new StartApprovalJob(             host.id(), userId, evaluator,             payload.issueId(), payload.projectId(),             config.payload().definitionId(), null,             TriggerSource.AUTOMATION         ));         case ARCHIVE -> worker.add(new ArchiveApprovalJob(             host.id(), userId, evaluator,             payload.issueId(), TriggerSource.AUTOMATION         ));         case DELETE -> worker.add(new DeleteApprovalJob(                 host.id(), userId, evaluator,                 payload.issueId(), TriggerSource.AUTOMATION             ));         }         return ResponseEntity.noContent().build();     } }

New file: forge/AutomationActionPayload.java

 public record AutomationActionPayload(     String issueId,     String projectId,     String config  // JSON-serialized TriggerConfiguration ) {}

B4. Backend — Resolve host from FIT/VerifiedAtlassianContext

Reuse existing pattern from AtlassianForgeEventsRecipientController:

  • context.getCloudId() → jiraApiAccessProvider.get(cloudId).getServerInfo().getBaseUrl() → hosts.findBySiteUrl()

  • userId from FIT principal claim


Part C: Frontend — Config UI (React)

C1. New hook useAutomationActionConfig.ts

New file: src/automation/config/useAutomationActionConfig.ts

Based on useWorkflowPostfun.ts with key differences:

  • Read config: @forge/bridge → view.getContext() → context.extension.config

  • Save config: view.submit({ issueId: '{{issue.id}}', projectId: '{{project.id}}', config: JSON.stringify(triggerConfig) })

  • Smart values {{issue.id}} and {{project.id}} — always submitted as constant string values (hidden inputs resolved by Automation at runtime)

  • No ref pattern — submit directly from current state (no useRef closures needed)

  • Auto-submit on every form change via view.submit() (Forge automation expects this pattern)

  • Reuses: api.backend.fetchWorkflowDefinitions(), useIssueFieldSearch(), WorkflowConfigType, UserEvaluatorCallerOrigin from workflow/constants.ts

C2. New component AutomationActionConfig.tsx

New file: src/automation/config/AutomationActionConfig.tsx

Same UI as WorkflowApprovalPostfun.tsx edit mode (radio group, definition select, evaluator). Differences:

  • No view mode (automation config has no separate view URL)

  • view.submit() on every change (Forge automation pattern)

  • Reuses shared components: SpaceInfoMessage, LoadingSpinner, Atlaskit Select/RadioGroup/Form

C3. Router integration

File: src/workflow/navigation/constants.ts

 export const AUTOMATION_ROUTES = {     ACTION_CONFIG: '/automation/action/config', } as const;

File: src/modules/navigation/router.config.tsx

Add route alongside workflow routes (~line 289):

 {     path: AUTOMATION_ROUTES.ACTION_CONFIG,     element: <AutomationActionConfig /> },

File: src/modules/navigation/navigation.utils.ts

Update resolveForgeInitialPath() — detect automation action config context from view.getContext() (check context.extension.type or context.moduleKey).


Implementation order

  1. Part A (refactoring) — rename classes, add TriggerSource, add AUTOMATION to InstanceErrorType

  2. Part B (backend) — manifest, security filter, AutomationController

  3. Part C (frontend) — hook, component, router integration

Part A must come first — Parts B and C depend on the new class names.

New files to create
┌────────────────────────────────────────────────────┬───────────────────────────────┐
│ File │ Purpose │
├────────────────────────────────────────────────────┼───────────────────────────────┤
│ workflow/TriggerSource.java │ Enum: WORKFLOW / AUTOMATION │
├────────────────────────────────────────────────────┼───────────────────────────────┤
│ forge/AutomationActionPayload.java │ Request body record │
├────────────────────────────────────────────────────┼───────────────────────────────┤
│ forge/AutomationController.java │ Forge Remote endpoint handler │
├────────────────────────────────────────────────────┼───────────────────────────────┤
│ src/automation/config/useAutomationActionConfig.ts │ Config hook (Forge bridge) │
├────────────────────────────────────────────────────┼───────────────────────────────┤
│ src/automation/config/AutomationActionConfig.tsx │ Config React component │
└────────────────────────────────────────────────────┴───────────────────────────────┘
Verification

  1. Java compilation after refactoring — no errors

  2. Existing TriggerConfigurationTest passes

  3. Deploy to dev — workflow post functions still work unchanged

  4. Add automation action to a Jira Automation rule → config UI renders with definition picker + action radio group

  5. Configure START action with a definition → save rule → trigger → approval starts on the issue

  6. Test ARCHIVE and DELETE actions similarly

  7. Instance errors from automation triggers show type AUTOMATION