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; // NEWprivate 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-endpointactionVerb: CREATEdescription: Start, archive, or delete approval(s)config:resource: app-resourcesinputs:issueId:title: Issue IDtype: stringrequired: trueprojectId:title: Project IDtype: stringrequired: trueconfig:title: Configurationtype: stringrequired: true
Add to endpoint: list:
key: automation-endpointremote: backend-remoteroute: { 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 → hostvar jira = jiraApiAccessProvider.get(context.getCloudId());var siteUrl = jira.getServerInfo().getBaseUrl();var host = hosts.findBySiteUrl(addon.getKey(), siteUrl);// 2. Parse config JSON → TriggerConfigurationvar config = om.readValue(payload.config(), TriggerConfiguration.class);// 3. Get userId from FIT principalvar userId = context.getUserId();// 4. Add job with TriggerSource.AUTOMATIONvar 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
-
Part A (refactoring) — rename classes, add TriggerSource, add AUTOMATION to InstanceErrorType
-
Part B (backend) — manifest, security filter, AutomationController
-
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
-
Java compilation after refactoring — no errors
-
Existing TriggerConfigurationTest passes
-
Deploy to dev — workflow post functions still work unchanged
-
Add automation action to a Jira Automation rule → config UI renders with definition picker + action radio group
-
Configure START action with a definition → save rule → trigger → approval starts on the issue
-
Test ARCHIVE and DELETE actions similarly
-
Instance errors from automation triggers show type AUTOMATION