Take action with
triggers
TOMEK SROKA | SOFTWARE DEVELOPER | ATLASSIAN
AUTOMATION
Deployment &
Observability
Authentication &
Authorization
Infrastructure
Connect & 3LO apps
Forge UI
Forge
Developer ExperienceAtlassian Hosting
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Platform Services
Stargate

api.atlassian.com, 3LO, rate limiting
Streamhub

Distributed event bus
ALERT IN OPSGENIE
Acknowledge
On-call must acknowledge within
15 minutes or alert will be escalate
IT SUPPORT
IT SUPPORT
IT Support
APP FEATURES
• Tickets should be automatically assigned to a person
currently on-call.

• If the ticket is about a problem with a service and
severity is critical, then a person on-call should be
alerted.

• When an on-call person acknowledges an alert, ticket
should be commented
ASSIGNING ISSUE
Getting started with Forge
Node.js 12+ NPM
Prerequisites
$ npm install -g @atlassian/forge-cli
$ forge login
$ forge create
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Forge
Application
Manifest
Functions
Lifecycle
app:
id: ari:cloud:ecosystem::app/1234
name: forge-example-app
description: forge example app
modules:
function:
- key: example-function
handler: index.run
trigger:
- key: jira-event-created
function: example-function
events:
- created:issue
Forge
Application
Manifest
Functions
Lifecycle
export async function run(event, context) {
console.log(`Hello from example App!`);
return {
"message": "hello world"
};
}
index.js:
modules:
function:
- key: example-function
handler: index.run
manifest.yml:
Forge
Application
Manifest
Functions
Lifecycle
Functions can be written in anything
that compiles to JavaScript
Executed in constrained
environment
Runtime provides some basic
building blocks
Forge
Application
Manifest
Functions
Lifecycle
dev-1.atlassian.net
dev-2.atlassian.net
my-company.atlassian.net
$ forge install
Forge
Application
Manifest
Functions
Lifecycle
Development Staging Production
Ver 5 Ver 4 Ver 3 stable
pre-prod.atlassian.net
dev-1.atlassian.net
dev-2.atlassian.net
my-company.atlassian.net
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Product triggers
Created issue
Modified issue
Transitioned issue
…
Created paged
Modified page
Commented page
…
Event bus
TRIGGER SERVICE
Event types
CREATED:ISSUE
ASSIGNED:ISSUE
DELETED:ISSUE UPDATED:ISSUE
TRANSITIONED:ISSUE
VIEWED:ISSUE
UNASSIGNED:ISSUE
COMMENTED:COMMENT
VIEWED:PAGE
TRANSITIONED:TASK
PUBLISHED:PAGE
UNASSIGNED:TASK
COMMENTED:COMMENT
LIKED:PAGE
DELETED:TASK
ASSIGNED:TASK
DELETED:PAGEEDITED:PAGE
{
"accountId": "501011:5ba7990e-ae0f-40de-afcc-a92ed2aca3e2",
"activityItem": {
"timestamp": "2019-08-27T09:22:21.303Z",
"eventType": "CREATED",
"object": {
"localResourceId": "120342",
"name": "AUT-1 Onboarding request",
"type": "ISSUE",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/browse/AUT-1",
"iconURL": "https://atlassian.net/secure/viewavatar?avatarId=10310"
Event Format
{
"accountId": "501011:5ba7990e-ae0f-40de-afcc-a92ed2aca3e2",
"activityItem": {
"timestamp": "2019-08-27T09:22:21.303Z",
"eventType": "CREATED",
"object": {
"localResourceId": "120342",
"name": "AUT-1 Onboarding request",
"type": "ISSUE",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/browse/AUT-1",
"iconURL": "https://atlassian.net/secure/viewavatar?avatarId=10310"
Event Format
{
"accountId": "501011:5ba7990e-ae0f-40de-afcc-a92ed2aca3e2",
"activityItem": {
"timestamp": "2019-08-27T09:22:21.303Z",
"eventType": "CREATED",
"object": {
"localResourceId": "120342",
"name": "AUT-1 Onboarding request",
"type": "ISSUE",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/browse/AUT-1",
"iconURL": "https://atlassian.net/secure/viewavatar?avatarId=10310"
},
"containers": [
{
Event Format
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/browse/AUT-1",
"iconURL": "https://atlassian.net/secure/viewavatar?avatarId=10310"
},
"containers": [
{
"localResourceId": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"type": "SITE",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net"
},
{
"localResourceId": "13011",
"name": "Automation Test Project",
Event Format
"type": "SITE",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net"
},
{
"localResourceId": "13011",
"name": "Automation Test Project",
"type": "PROJECT",
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/projects/AUT"
}
],
"contributors": [
{
Event Format
"product": "JIRA_SERVICE_DESK",
"cloudID": "158c8204-ff3b-47c2-adbb-a0906c00ffee",
"url": "https://product-fabric.atlassian.net/projects/AUT"
}
],
"contributors": [
{
"profile": {
"accountId": "501011:5ba7990e-ae0f-40de-afcc-a92ed2aca3e2"
},
"lastAccessedDate": "2019-08-27T09:22:21.303Z",
"count": 1
}
]
}
}
Event Format
Opsgenie - making a request
// Use environment variables to get API TOKEN, never hardcode such
things.
const scheduleId = getEnvVariable("OPSGENIE_SCHEDULE_ID");
const opsGenieApiKey = getEnvVariable("OPSGENIE_API_KEY");
console.log(`Looking for on-call person for schedule ${scheduleId}
`);
const onCallEmail = await fetchOnCall(scheduleId, opsGenieApiKey);
console.log(`Determined on-call person email to be ${onCallEmail}`);
// If we did not get an email from opsgenie then too bad; What can
we do?
if (onCallEmail === null) {
return {};
}
async function fetchOnCall(scheduleId: string, apiKey: string) {
const url = `https://api.opsgenie.com/v2/schedules/${scheduleId}/
on-calls?flat=true`;
let response = await api.fetch(url, {
method: "GET",
headers: {
"Authorization": `GenieKey ${apiKey}`
}
});
if (!response.ok) {
throw `Cannot get opsgenie schedule. Status: $
{response.status}, ${response.statusText}`;
}
const responseBody = await response.json();
[…]
}
Opsgenie - making a request
Jira - making a request
export async function assignJiraIssue(issueId: string, userId: string) {
const url = `/rest/api/3/issue/${issueId}/assignee`;
const body = {
"accountId": userId
};
let response = await api
.asApp()
.requestJira(url, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
[…]
}
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Web triggers
+ OTHERS
https://app.hello.atlassian-dev.net/x0/eyJjd
TRIGGER SERVICE
Request
{
"method": "POST",
"headers": {
"content-type": [
"application/json"
]
},
"body": “{"id":"123"}",
"path": "/x0/eyJjdHg"
}
Response
{
"headers": {
"Content-Type": [
"text/plain"
]
},
"statusCode": 200,
"body": "Hello from web trigger"
}
Example function
export async function run(request) {
return {
headers: {
"Content-Type": ["text/plain"]
},
statusCode: 200,
body: `Hello from web trigger.n ${request.body}`
}
}
Manifest
modules:
function:
- key: func-pr-created
handler: index.onNewPr
webtrigger:
- key: github-pr-created
function: func-pr-created
Generating URL
Opsgenie - webhook
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Triggering self
Commented:issue Event bus
POST /issue/comment
TRIGGER SERVICE
Out of order
processing
DuplicatesDelays
Working with Product Triggers
3 1 2
AUTHENTICATING
WEB TRIGGERS
Agenda
A case for automation
Anatomy of a Forge App
Under the covers
Demo
Product triggers
Web triggers
Thank you!
TOMEK SROKA | SOFTWARE DEVELOPER | ATLASSIAN
Q & A

Take Action with Forge Triggers