One Atlassian Connect Add-on both for Jira and Confluence

Hi, community,

is it possible to have one Atlassian Connect Add-on both for Jira and Confluence.
Is it possible to have several atlassian-connect.json descriptors there? If yes, should they be registered separately in Atlassian Marketplace?

Thanks!

1 Like

Yes - your backend can do whatever you want.

As far as the descriptor and marketplace it gets complicated. If you’re using JIRA or Confluence specific modules - you have to have separate descriptors. If you’re doing a paid descriptor (or if you’re thinking that you’ll ever have it be paid) you’ll need unique app keys in the marketplace. If you need unique app keys you’ll need unique descriptors.

3 Likes

Thank you, daniel!

Do Atlassian Connect frameworks (atlassian-connect-spring-boot in particular) support having multiple atlassian-connect.json descriptors in one add-on project?

Thanks.

I haven’t used Atlassian-Connect-spring-boot but on the Atlassian Connect Express side on the house - all it does is to add some variable escaping. There is nothing stopping you from serving up a static version (or creating your own variable based version).

2 Likes

If you are using the Nodejs ACE framework, it only supports one atlassian-connect.json file (and you cannot start your app if this file is missing).

Workaround:

Include all Confluence and Jira modules of your app to atlassian-connect.json.

atlassian-connect.json is now invalid because, for example, if you install your app to Confluence, it will not let you install, because it sees Jira modules in atlassian-connect.json as invalid.

So, simply serve atlassian-connect.json by yourself. For Confluence, filter out Jira modules, and vice versa.

Example code:

const CONF_MODULES = [...];
const JIRA_MODULES = [...];

const ORIG_DESC_PATH = '/atlassian-connect.json';
const CONF_DESC_PATH = '/conf/atlassian-connect.json';
const JIRA_DESC_PATH = '/jira/atlassian-connect.json';

export default function(app, addon) {
  // Override the handler added by ACE
  app.get(ORIG_DESC_PATH, (req, res) => {
    const confDescUrl = getConfDescAbsUrl(addon.config);
    const jiraDescUrl = getJiraDescAbsUrl(addon.config);
    res.status(404).send(`Use ${confDescUrl} or ${jiraDescUrl} instead`);
  });

  app.get(CONF_DESC_PATH, (req, res) => {
    const desc = getDescriptor(addon.config, CONF_MODULES);
    res.json(desc);
  });

  app.get(JIRA_DESC_PATH, (req, res) => {
    const desc = getDescriptor(addon.config, JIRA_MODULES);
    res.json(desc);
  });
}

function getConfDescAbsUrl(addonConfig) {
  return addonConfig.localBaseUrl() + CONF_DESC_PATH;
}

function getJiraDescAbsUrl(addonConfig) {
  return addonConfig.localBaseUrl() + JIRA_DESC_PATH;
}

function getDescriptor(addonConfig, validModules) {
  const descJson = fs.readFileSync('atlassian-connect.json', 'utf8');
  const desc = JSON.parse(descJson);
  desc.modules = filterObjectByKeys(desc.modules, validModules);
  desc.baseUrl = addonConfig.localBaseUrl();
  return desc;
}

function filterObjectByKeys(obj, validKeys) {
  const allKeys = Object.keys(obj);
  const ret = {};

  // Copy things in obj to ret
  allKeys.forEach(key => {
    if (validKeys.includes(key)) {
      ret[key] = obj[key]
    }
  });

  return ret;
}