How to Build a Cross-product Forge App for Jira and Confluence

Reading Time: 7 minutes

When it comes to creating automations across Atlassian products, there's more than one way to peel an orange (as the animal-friendly saying goes). For most use cases, Automation for Jira and Confluence provide a quick, no-code solution, making them the first option to investigate.

But, there are some use cases that require a custom app. Recently I came across one example: creating a Confluence page when a new Jira issue is created, and embedding the Jira issue in the body of the page. Automation could get you 90% of the way there, but it doesn't support creating a Confluence page with content in the body.

That's where Forge can be a useful alternative for those who are willing to roll up their sleeves and write a little bit of code. Forge is an app development platform built into Atlassian Cloud. It allows you to write code that runs in response to events such as Jira issue transitions, Confluence page views and even external events. If you've ever used Atlassian APIs to do some custom scripting, Forge is similar, but it gives you a development and hosting environment that's already set up and ready to connect with Atlassian products.

In this quick tutorial, we'll demo how you can use Forge to execute actions across both Jira and Confluence using product triggers and Atlassian REST APIs. You can take this basic concept and extend it further, automating all kinds of use cases where you want to listen for a product event and then call an API in response.

🏎️ Already familiar with Forge? Skip straight to the project code.

Getting started with Forge

If it's your first time using Forge, take a moment to set up your development environment and install dependencies. Follow the Getting Started guide and then meet us back here.

Useful pages to bookmark:

What we'll build

This tutorial is a great first Forge project because it illustrates a few key concepts without getting too complex. In fact, the app we'll build doesn't even have a user interface.

The app subscribes to the Jira issue created event. When an issue is created, the app calls the Confluence REST API and creates a new page. The app configures the new page so it contains a macro that embeds the Jira issue. To make it easy to see which issue the page is linked to, we'll also include the Jira issue key in the page title.

One very nice Forge feature is the ability to create these types of interactions that work across multiple products. But, there are a few important things to know about this type of app:

  1. Cross product apps must be installed in all of the products they interact with. The app we'll create in this tutorial needs to be installed in Jira and Confluence tenants that belong to the same site.
  2. Cross product apps are not supported by Atlassian Marketplace. This limitation is due to the need to install in multiple products. If you intend to commercialize your multi-product app, see the workaround listed here.

Step one: generate the app template

When you begin a new Forge app, Forge walks you through a few questions about what you're building and creates a template containing your app's file structure. This provides a skeleton for your app, ready for you to start building.

To create your app template:

  1. Navigate to a folder where you want to create your app
  2. In your terminal, run the command forge create
  3. Enter a name for your app, e.g. new-issue-automation (but you can call it anything you like)
  4. Using your arrow keys, select the Triggers and Validators category
  5. Then, select product-trigger from the template options. Forge will begin creating a new folder containing your app template files
  6. Navigate to the new folder using the command cd new-issue-automation (or whatever you've named your app). From here, you can run commands to tunnel, install, or deploy your app.

Step two: Update your app's permissions

API scopes define your app's level of access to different types of product actions and data. The app we're building needs the ability to read data in Jira, which will allow it to request information about an issue as it's created. The app also needs write access to Confluence, which will allow it to create a new page.

To add API scopes to your app:

  1. Open the new-issue-automation folder in a text editor
  2. Open the manifest.yml file
  3. Add the following to the top or bottom of the file, then save:
permissions:
  scopes:
    - read:jira-work
    - write:page:confluence

Before you close the manifest.yml file, there's one more thing to note. Because we selected the product-trigger template during setup, Forge pre-populated the trigger module in the manifest. Notice the event we've subscribed to is avi:jira:created:issue, which sends a notification to the app when an issue is created. You can subscribe to additional events by listing them here in the manifest.

Step three: Add the app logic

Index.jsx is another file automatically created for you when you begin a new Forge project. This is the default JavaScript file, where we'll write our functions. You'll notice the file is already populated with a boilerplate function that's triggered by the subscribed issue create event.

Install the @forge/api package

Before we go further, we need to install the @forge/api package. This package provides methods for authenticating and calling Atlassian product REST APIs.

  1. Open your terminal
  2. From your project folder, run the command: npm install @forge/api --save
  3. At the top of the index.jsx file, add the following line to import the package:
    import api, { route } from "@forge/api";

Edit the Jira trigger

Next, we'll create the function that runs when an issue subscribe event is detected. The purpose of this function is to extract the issue key from the event payload and pass it into another function, which creates a Confluence page. When we call the addPage function, we also pass in our Confluence space Id and the ID of the existing page we want to nest the new page under.

Below the import api statement, change the boilerplate export function so it matches this:

export async function onIssueCreated(event, context) {
  console.log('Issue created:');
  console.log(` * event: ${JSON.stringify(event, null, 2)}`);
  console.log(` * context: ${JSON.stringify(context, null, 2)}`);

  // Specify which page the new page should be created under. Visit the page and copy the ID
  // from the URL. For example, the ID of the following page is 1496317953
  // https://mytenant.atlassian.net/wiki/spaces/TEST/pages/1496317953/My+page+name

  const parentId = 122322945;
  // Now that you have the parent page ID, you can determine the spaceId by entering the
  // following URL in a browser and inspecting the response.
  // https://mytenant.atlassian.net/wiki/api/v2/pages/{parentId}
  // For the above example, it would be:
  // https://mytenant.atlassian.net/wiki/api/v2/pages/1496317953
  const spaceId = 98306;
  await addPage(event.issue.key, parentId, spaceId);
}

Let's step through what's happening here. The onIssueCreated function is invoked when the subscribed event happens. Then, it calls second function called addPage() which we'll create next. The event payload contains contextual information about the event that occurred. We'll grab the issue key from that object and also go ahead and print the whole payload to the terminal with a console.log() statement.

Create the addPage() function

Finally, we need to call the Confluence REST API to create the new page. Below the export function, add the following:

async function addPage(issueKey, parentId, spaceId) {
  // To get the server ID:
  //   1. create a Confluence page and insert a Jira issue macro.
  //   2. Enter the following URL in your browser, substituting the pageId value:
  //        https://forgery.atlassian.net/wiki/api/v2/pages/{pageId}?body-format=storage
  //   3. Search for "serverId" and retrieve the value after it.
  const jiraServerId = '44cec7fd-c7a6-3972-8f8e-a26e5b95551d';
  const columnNames = `key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution`;

  // Build the Issue Macro
  const issueMacro =
    `<ac:structured-macro
      ac:name=\"jira\"
      ac:schema-version=\"1\"
      ac:local-id=\"\"
      ac:macro-id=\"\">
      <ac:parameter ac:name=\"server\">System JIRA</ac:parameter>
      <ac:parameter ac:name=\"columns\">${columnNames}</ac:parameter>
      <ac:parameter ac:name=\"maximumIssues\">20</ac:parameter>
      <ac:parameter ac:name=\"jqlQuery\">
        issueKey = &quot;${issueKey}&quot; 
      </ac:parameter>
      <ac:parameter ac:name=\"serverId\">${jiraServerId}</ac:parameter>
    </ac:structured-macro>`;
  // Build the API request body
  const body = {
    title: `New page created for ${issueKey}`,
    type: "page",
    parentId: parentId,
    spaceId: spaceId,
    body: {
      storage: {
        value: `<p>${issueMacro}</p>`,
        representation: "storage"
      }
    }
  };
  console.log(` * body: ${JSON.stringify(body, null, 2)}`);
  // Send the request to the Confluence REST API
  let response = await api.asApp().requestConfluence(route`/wiki/api/v2/pages`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });
  console.log(`Response: ${response.status} ${response.statusText}`);
  console.log(await response.json());
}

As you can see, the addPage() function takes in the issue key as an argument, along with the space ID and parent page ID. Then, we define an issueMacro variable that contains the structured data for the macro body. This is then passed into the body variable, which forms the JSON request body we'll send to the Confluence REST API. We supply a title, type, space, and body.storage, which creates the macro. Notice the issue key is populated in the title and macro using string interpolation.

💡Tip: The macro body is written in a format called Confluence Storage Format. The easiest way to construct the proper syntax for a macro body is to create a new Confluence Cloud page manually and insert the type of macro you want to create, then publish the page. Next, you can use an API client like Postman to do a GET request for the page. This will show you what the data looks like, properly formatted. You can do a Ctrl+F in the response to find your server ID (see altherate method described in the code comments above). If you're an admin, you can also go to View Storage Source from the … menu to view the data.

When you create a new page macro via API, the local ID and macro ID can both be omitted from the request body. However, you do need to supply the server ID, which will be unique to your installation.

Step four: Update the function handler

In your manifest file, function is a general-purpose module that defines the behavior of your app. The handler property tells Forge the location of the function responsible for handling the invocation.

Edit the function section of your manifest file so it points to the onIssueCreated function:

function:
    - key: main
      handler: index.onIssueCreated

Step five: Deploy and install

Forge provides three built-in development environments: development, staging, and production. While testing, the app should be deployed in the development environment and installed in a test site, and later deployed in the production environment and installed in the target site. If you don't specify which environment you want to deploy to, Forge CLI will deploy to the development environment by default. After deployment, you can install the app on a site where you have admin permissions. Since our app is a cross platform app, we'll need to install it into two different tenants: Jira and Confluence.

  1. In your terminal, run the command: forge deploy
  2. Run the command: forge install
  3. Select Jira as the product the app uses
  4. Enter the name of your Atlassian site: your-domain.atlassian.net
  5. Enter y to confirm. Because the app manifest contains scopes for multiple products, you'll then be prompted to install in Confluence.
  6. Copy the suggested command and input it to your terminal:
    forge install -p Confluence -s your-domain.atlassian.net -e development
  7. Enter y to confirm. You're done!

Step six: Test the app

While you're in the development environment, the Forge tunnel gives you a fast way test your app. The Forge tunnel runs your app on your local computer, so you can see how your app behaves while you're building it. You can make changes in your text editor and see how those changes render in real time, without having to redeploy.

  1. Start the tunnel by running the command: forge tunnel
  2. Once the tunnel is running, open Jira. Create a new issue.
  3. Navigate to Confluence and check the space you specified in the API request. You should see a new page with the issue key in the title and the issue embedded in the body.
  4. In the terminal, you'll see a printout of two objects. One is the payload from the issue created event. The other is the API response for the newly created Confluence page.

Going further

This app is a simple example, but there are lots of ways you can adapt it. You might add an if statement, to only create a page when the Jira issue matches a certain type or project. Or, you could execute a different action or subscribe to a different product event trigger – there are many possibilities.

You can find the full project code on GitHub.

Check out the developer docs for inspiration and code examples. And if you get stuck, head to the Atlassian Developer Community. Happy building!