For information on what the Jira Automation plugin is, and how it can help you, please read the first part of my article, Why Atlassian support uses the Jira Automation plugin.

Open for extension: Write your own triggers and actions!

The automation plugin is fully extensible in the sense that you can easily create your own triggers and actions in your plugins. Here’s a small example of creating an action to update a reporter’s profile. Whole source can be found here.

This action is very useful if you need to change some settings in the reporter’s profile. One real-life example: we change the locale settings of the reporter if an issue is created in “Japan-only supported project”, so when they log in Jira, their experience is completely in Japanese.

You can also see the full source of existing actions and triggers on BitBucket.

Set up your pom.xml
To add dependencies for creating your own action/trigger, modify your pom.xml:

[cc lang=”xml” escaped=”true” line_numbers=”0″]

com.atlassian.plugin.automation
automation-api
1.1.4
provided

com.atlassian.soy
soy-template-renderer-api
1.1.9
provided

com.atlassian.templaterenderer
atlassian-template-renderer-api
1.4.0
provided

com.atlassian.plugin.automation
automation-page-objects
1.1.4
test


[/cc]

And add plugin artifacts so atlas-debug will automatically install them for you (this is optional):

[cc lang=”xml” escaped=”true” line_numbers=”0″]

…. com.atlassian.plugin.automation
automation-module
1.1.4 com.atlassian.plugin.automation
jira-automation-spi
1.1.4 com.atlassian.plugin.automation
jira-automation-plugin
1.1.4 [/cc]

Implement your own action
Once this is configured, you can start implementing your Action<Issue> interface:

[cc lang=”xml” escaped=”true” line_numbers=”0″]
/**
* Represents an action that will be executed for all items.
*
* @param the type of the item to apply the action to. Could be Jira issues or Confluence pages for example
*/
public interface Action
{
/**
* Init the action
*
* @param config
*/
void init(ActionConfiguration config);
/**
* Executes this action over the items provided. Will return an error collection with any potential errors.
*
* @param actor the user to execute the action as
* @param items The items to apply this action to
*/
void execute(final String actor, final Iterable items, ErrorCollection errorCollection);
/**
* Returns audit log for this action
*
* @return
*/
AuditString getAuditLog();
/**
* Returns the HTML portion specific to this action needed to configure it. The HTML will be
* embedded in an existing AUI form. Plugin
* developers should not wrap the HTML in a <form/> tag but instead wrap individual form fields in <div
* class=”field-group”/> tags.
*
* @param actionConfiguration The current config options for this action, which may be used to pre-populate form
* fields. If null, default configuration is shown
* @param actor Actor under which this action shall be performed (might be used for access rights check)
* @return the rendered HTML config form for this action
*/
String getConfigurationTemplate(final ActionConfiguration actionConfiguration, final String actor);
/**
* Returns the HTML portion specific to this action needed to view it.
*
* @param actionConfiguration The current config options for this action, which will be used to render the view
* @param actor Actor under which this action shall be performed (might be used for access rights check)
* @return the rendered HTML view for this action
*/
String getViewTemplate(final ActionConfiguration actionConfiguration, final String actor);
/**
* Given a map of form parameters submitted by the user this method should validate the params for this action
*
* @param i18n I18nResolver needed to internationalize error messages
* @param params User submitted parameters for validation
* @param actor Actor under which this action shall be performed (might be used for access rights check)
* @return an error collection containing all validation errors
*/
ErrorCollection validateAddConfiguration(final I18nResolver i18n, final Map<String, List> params, final String actor);
}
[/cc]
For editing the user profile, following Action is implemented:
[cc lang=’java’]

package com.atlassianlabs.tutorial;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.user.UserPropertyManager;
import com.atlassian.plugin.automation.core.Action;
import com.atlassian.plugin.automation.core.action.ActionConfiguration;
import com.atlassian.plugin.automation.core.auditlog.AuditString;
import com.atlassian.plugin.automation.core.auditlog.DefaultAuditString;
import com.atlassian.plugin.automation.util.ErrorCollection;
import com.atlassian.plugin.automation.util.ParameterUtil;
import com.atlassian.sal.api.message.I18nResolver;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.collect.Maps;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import java.util.List;
import java.util.Map;
import static com.atlassian.plugin.automation.util.ParameterUtil.singleValue;
/**
* Implements editing user profile
*/
public class EditUserProfileAction implements Action
{
// These keys are bound to the field names from SOY template
public static final String USER_PROPERTY_FIELD_KEY = “editUserProfileKeyField”;
public static final String USER_PROPERTY_VALUE_KEY = “editUserProfileValueField”;
public static final String USER_PROPERTY_SHOULD_OVERWRITE_KEY = “editUserProfileShouldOverwrite”;
private static final Logger log = Logger.getLogger(EditUserProfileAction.class);
private static final String RESOURCE_KEY = “com.atlassianlabs.tutorial.automation-plugin-tutorial:automation-action-resources”;
private final UserPropertyManager userPropertyManager;
private final SoyTemplateRenderer soyTemplateRenderer;
private String userPropertyKey;
private String userPropertyValue;
private boolean shouldOverwriteProperty;
private int affectedIssues;
public EditUserProfileAction(final UserPropertyManager userPropertyManager,
final SoyTemplateRenderer soyTemplateRenderer)
{
this.userPropertyManager = userPropertyManager;
this.soyTemplateRenderer = soyTemplateRenderer;
}
@Override
public void init(ActionConfiguration config)
{
// This method is used to extract parameters provided when configuring the form for actual execution
userPropertyKey = singleValue(config, USER_PROPERTY_FIELD_KEY);
userPropertyValue = singleValue(config, USER_PROPERTY_VALUE_KEY);
shouldOverwriteProperty = Boolean.parseBoolean(singleValue(config, USER_PROPERTY_SHOULD_OVERWRITE_KEY));
affectedIssues = 0;
}
@Override
public void execute(String actor, Iterable items, ErrorCollection errorCollection)
{
// All issues returned by a trigger will be passed to this method. Use errorCollection to return any errors to the caller
for (Issue issue : items)
{
if (userPropertyManager.getPropertySet(issue.getReporterUser()).isSettable(userPropertyKey))
{
boolean needsUpdate = false;
final String currentValue = userPropertyManager.getPropertySet(issue.getReporterUser()).getString(userPropertyKey);
if (currentValue == null || !userPropertyValue.equals(currentValue))
{
// only update if the value is not yet set (current value == null) or if we should overwrite any value
needsUpdate = (currentValue == null) || shouldOverwriteProperty;
}
if (needsUpdate)
{
userPropertyManager.getPropertySet(issue.getReporterUser()).setString(userPropertyKey, userPropertyValue);
affectedIssues++;
}
}
else
{
errorCollection.addErrorMessage(“Unable to set user property because it is not settable”);
}
}
}
@Override
public AuditString getAuditLog()
{
// This will be added as audit log line
return new DefaultAuditString(String.format(“User property ‘%s’=’%s’ updated for %d issues”, userPropertyKey, userPropertyValue, affectedIssues));
}
@Override
public String getConfigurationTemplate(ActionConfiguration actionConfiguration, String actor)
{
// This method needs to return the rendered HTML fragment which will be used in the UI when configuring the action
try
{
final Map<String, Object> context = Maps.newHashMap();
ParameterUtil.transformParams(context, actionConfiguration);
return soyTemplateRenderer.render(RESOURCE_KEY, “Atlassian.Tutorial.Templates.Automation.editUserProfile”, context);
}
catch (SoyException e)
{
log.error(“Error rendering template”, e);
return “Unable to render configuration form. Consult your server logs or administrator.”;
}
}
@Override
public String getViewTemplate(ActionConfiguration actionConfiguration, String s)
{
// This method needs to return the rendered HTML fragment which will be used in the UI when viewing the action
try
{
final Map<String, Object> context = Maps.newHashMap();
ParameterUtil.transformParams(context, actionConfiguration);
return soyTemplateRenderer.render(RESOURCE_KEY, “Atlassian.Tutorial.Templates.Automation.editUserProfileView”, context);
}
catch (SoyException e)
{
log.error(“Error rendering template”, e);
return “Unable to render configuration form. Consult your server logs or administrator.”;
}
}
@Override
public ErrorCollection validateAddConfiguration(I18nResolver i18n, Map<String, List> params, String actor)
{
// This method will be called when parameters to the action should be validated
final ErrorCollection errors = new ErrorCollection();
if (!params.containsKey(USER_PROPERTY_FIELD_KEY) || StringUtils.isBlank(singleValue(params, USER_PROPERTY_FIELD_KEY)))
{
errors.addError(USER_PROPERTY_FIELD_KEY, i18n.getText(“automation.action.userPropertyField.empty”));
}
if (!params.containsKey(USER_PROPERTY_VALUE_KEY) || StringUtils.isBlank(singleValue(params, USER_PROPERTY_VALUE_KEY)))
{
errors.addError(USER_PROPERTY_VALUE_KEY, i18n.getText(“automation.action.userPropertyField.empty”));
}
return errors;
}
[/cc]

Create the UI for your action
The user interface shown in the Automation admin interface is rendered from in the getConfigurationTemplate() method above. It uses SOY, but basically any HTML fragment returned will do. We use this one for configuring key-value to be edited in the user profile:
[cc lang=”xml” escaped=”true” line_numbers=”0″]
{namespace Atlassian.Tutorial.Templates.Automation}
/**
* @param? editUserProfileKeyField
* @param? editUserProfileValueField
* @param? editUserProfileShouldOverwrite
*/
{template .editUserProfile}


Full key used in Jira to for the user property. Available values are here Only String properties are currently supported

Value of the property to be set.

{/template}
/**
* @param id
* @param editUserProfileKeyField
* @param editUserProfileValueField
* @param? editUserProfileShouldOverwrite
*/
{template .editUserProfileView}


{$editUserProfileKeyField}

{$editUserProfileValueField}

{if $editUserProfileShouldOverwrite}{getText(‘automation.plugin.forms.yes’)}{else}{getText(‘automation.plugin.forms.no’)}{/if}

{/template}
[/cc]

Update atlassian-plugin.xml
The only thing missing to actually see this in action is to add module descriptor to your atlassian-plugin.xml:
[cc lang=”xml” escaped=”true” line_numbers=”0″]

 

${project.description}
${project.version}
images/pluginIcon.pngimages/pluginLogo.png


com.atlassian.soy.soy-template-plugin:soy-deps
com.atlassian.auiplugin:aui-experimental-soy-templates

class=”com.atlassianlabs.tutorial.EditUserProfileAction”
name=”Edit User Profile Action”/>

[/cc]
After this, you can run atlas-run and you should be able to see and configure your new action. Once this is done, you can test it and it should work.

The user interface fragment will be rendered once the new action is selected. This is how it will look:
edit_user
The process of creating a new trigger is very similar to this, just the atlassian-plugin.xml uses

[cc lang=”xml” escaped=”true” line_numbers=”0″]
class=”com.atlassianlabs.tutorial.EditUserProfileAction”
name=”Edit User Profile Action”/>
[/cc]
Adding functional tests
If you want to add functional tests, you need to add automation-page-objects as mentioned above. You can then write your functional test in normal form, just as easy as this:
[cc lang=”xml” escaped=”true” line_numbers=”0″]
package it.com.atlassianlabs.tutorial;
import com.atlassian.jira.rest.api.issue.IssueCreateResponse;
import com.atlassian.jira.tests.TestBase;
import com.atlassian.plugin.automation.page.ActionsForm;
import com.atlassian.plugin.automation.page.AdminPage;
import com.atlassian.plugin.automation.page.TriggerPage;
import com.atlassian.plugin.automation.page.trigger.IssueEventTriggerForm;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TestEditUserProfileAction extends TestBase
{
@Before
public void setup()
{
backdoor().restoreBlankInstance();
jira().quickLoginAsSysadmin();
}
@After
public void teardown()
{
// don’t forget to cleanup all automation rules
jira().quickLoginAsSysadmin();
final AdminPage automationAdmin = jira().goTo(AdminPage.class);
automationAdmin.deleteAllRules();
}
@Test
public void testEditUserPropertiesActionWithOverwrite()
{
AdminPage automationAdmin = jira().goTo(AdminPage.class);
final TriggerPage triggerPage = automationAdmin.addRuleForm().enabled(true).ruleName(“test”).ruleActor(“admin”).next();
triggerPage.selectTrigger(IssueEventTriggerForm.class).setEvents(“Issue Commented”);
ActionsForm actionsForm = triggerPage.next();
actionsForm.setInitialAction(EditUserProfileActionForm.class).propertyKey(“jira.user.timezone”).propertyValue(“Asia/Tokyo”).shouldOverwrite(true);
actionsForm.next().save();
CustomViewProfilePage profilePage = jira().goTo(CustomViewProfilePage.class);
assertFalse(“Profile shouldn’t contain Tokyo”, profilePage.getTimezone().contains(“Tokyo”));
final IssueCreateResponse createResponse = jira().backdoor().issues().createIssue(“HSP”, “test admin issue”);
jira().backdoor().issues().commentIssue(createResponse.key(), “test”);

profilePage = jira().goTo(CustomViewProfilePage.class);
assertTrue(“Profile should contain Tokyo”, profilePage.getTimezone().contains(“Tokyo”));
}
}
[/cc]
The extra class we added here is EditUserProfileActionForm which represents the “HTML” fragment during rule setup:
[cc lang=”xml” escaped=”true” line_numbers=”0″]
package it.com.atlassianlabs.tutorial;
import com.atlassian.pageobjects.elements.CheckboxElement;
import com.atlassian.pageobjects.elements.PageElement;
import com.atlassian.plugin.automation.page.ModuleKey;
import com.atlassian.plugin.automation.page.action.ActionForm;
import org.openqa.selenium.By;
// The only trick here is the usage of the ModuleKey annotation to get the full module key to the tested automation action (value in the Select action selectbox)
@ModuleKey(“com.my.plugin:edit-user-profile-action”)
public class EditUserProfileActionForm extends ActionForm
{
public EditUserProfileActionForm(PageElement container, String moduleKey)
{
super(container, moduleKey);
}
public EditUserProfileActionForm propertyKey(final String propertyKey)
{
setActionParam(“editUserProfileKeyField”, propertyKey);
return this;
}
public EditUserProfileActionForm propertyValue(final String propertyValue)
{
setActionParam(“editUserProfileValueField”, propertyValue);
return this;
}
public EditUserProfileActionForm shouldOverwrite(final boolean shouldOverwrite)
{
final CheckboxElement checkbox = container.find(By.name(“editUserProfileShouldOverwrite”), CheckboxElement.class);
if (shouldOverwrite)
{
checkbox.check();
}
else
{
checkbox.uncheck();
}
return this;
}
}
[/cc]

Extending the Jira Automation plugin