Testing a Jira Data Center plugin like its 2024…

Marty Henderson
9 min readFeb 25, 2024

Oh hey. You may remember me from such works as https://medium.com/@martyhenderson/building-a-jira-data-center-plugin-like-its-2023-f0e573cf8146 where we learned how to setup a development flow for a modern-ish way of doing front end development inside the Jira Data Center ecosystem.

I actually have a follow up planned on that where we simplify things a lot using Vite. It’s coming.

Today we’re going to talk about testing and infrastructure. And not just the usual “here’s how to get a basic test running in <insert test framework of choice here>”.

The goal of this was to create a CI pipeline that can run Playwright tests on a Jira DC plugin in a repeatable fashion AND have it replicate how we work locally. This means:

  • Clean build of the plugin and install onto a production mode Jira
  • Restore a database with a known dataset AND a Jira license so that we don’t have to do any initial setup and configuration
  • Wait for Jira to be ready before running tests
  • Run tests and get the results

We’re going to do this with CirlceCI, Docker and Docker compose.

Docker compose and CircleCI

A couple of quick caveats and shout outs

This is heavily inspired from the excellent work done here https://github.com/collabsoft-net/example-jira-app-with-docker-compose/ to run Jira in a container and create a nice dev loop. Kudos.

CircleCI have done an awful lot of excellent work on their platform and the pricing is pretty nice. However, they have this tagline:

If it works in Docker, it works on CircleCI

https://circleci.com/docker/. I’m going to respectfully disagree just a bit with that as we’ll see later. It kinda works, but there are some big differences as to how Docker works on CircleCI than when you run it locally. And you can waste a lot of time figuring this out. Like I did.

Ok, let’s get started.

Test data

This is pretty straightforward. Spin up a Jira with a connection to your database (use Postgres to make things easy), go through the Jira installation process, grab a time-bombed license from here: https://developer.atlassian.com/platform/marketplace/timebomb-licenses-for-testing-server-apps/ and use to start things up. Configure whatever data you need for your application.

Take a pg_dump of your database and put it in a ./database directory in your plugin repository. That’s it.

Playwright

This is the config I’m using:

import { defineConfig, devices } from '@playwright/test';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
require('dotenv').config();

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',

/* Run tests in files in parallel */
fullyParallel: true,

/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,

/* Retry on CI only */
retries: process.env.CI ? 2 : 0,

/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,

/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter:[
['html'],
['junit', { outputFile: 'results.xml' }]
],

/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://jira:8080' : 'http://localhost:8080',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
launchOptions: {
slowMo: 100,
},
},

/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup']
},

{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},

{
name: 'webkit',
use: {
...devices['Desktop Safari'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
});

Nothing too complex here, we just switch on some options if in a CI environment, and setup authentication as a pre-req to any tests. I’ll leave the test writing to you.

Docker compose

This is the docker compose file I’ve eventually ended up with:

version: '3.1'

services:
jira:
build: ./.docker
environment:
- JVM_SUPPORT_RECOMMENDED_ARGS=-Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
ports:
- 5005:5005
- 8080:8080
depends_on:
postgres:
condition: service_healthy

postgres:
image: postgres:15.4
ports:
- 5432:5432
environment:
- POSTGRES_USER=jira
- POSTGRES_PASSWORD=jira
- POSTGRES_DB=jira
- LANG=C
volumes:
- ./database:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U jira"]
interval: 10s
timeout: 5s
retries: 5

playwright:
image: mcr.microsoft.com/playwright:v1.40.0-jammy
ports:
- 8000:8000
environment:
- CI=true
volumes:
- ./projectstatusclient/:/app/src/projectstatusclient
command: >
sh -c "sleep 30 &&
cd /app/src/projectstatusclient &&
timeout 300 sh -c '
until curl -sSf http://jira:8080/rest/api/2/serverInfo >/dev/null; do
echo \"Waiting for Jira to be ready...\";
sleep 5;
done' &&
npm install &&
npx playwright install &&
npx playwright test"

These 3 services come up on their own special little network and refer to each other by the service names. Let’s walk through it.

  • The Jira container. This references a Dockerfile in the .docker directory. This is what we’ll use to move the various dependencies into place. Note that we wait on PostgreSQL to be ready before starting Jira.
  • The Postgres container. We configure some basic props (do change the password…) and the key bit is that we add a health-check to the mix so that we can signal the Jira container. Also it helps to use the same version that you test on locally.
  • Restoring the database on PostgreSQL. We mount our ‘database’ directory to ‘./database:/docker-entrypoint-initdb.d’. This means that any sql files in the /database directory get executed on Postgres startup. Meaning that we can restore a database that we’ve previously setup just above.
  • The Playwright container. We use the Microsoft container and set the CI property to be true as some aspects of the playwright.config.ts file need tweaked in CI environments. We mount the directory with the front end code in it to the container. Then wait for Jira to start up before we execute the tests.
  • Playwright — we use the same trick to get the tests onto the container to get the results off. As we mount the front end directory, the test result directory is also created in there, so we immediately have access to our test results.

Here’s the other files that we use with the Jira container, these live in the .docker directory:

FROM atlassian/jira-software:latest

RUN apt-get clean -y && apt-get update -y && apt-get install postgresql-client -y;

COPY ./dbconfig.xml ./jira-data-generator-5.0.0.jar ./quickreload-5.0.2.jar ./quickreload.properties /opt/
COPY ./your-plugin.jar /opt/
COPY ./start.sh /opt/start.sh

CMD /opt/start.sh

This is the Dockerfile. It’s pretty straightforward, we update and install the psql client. We copy some files from the .docker directory onto the container. The important part here, is your-plugin.jar. You need to add a build step to maven or gradle or whatever you’re using to get a version of this into the .docker directory.

I’ll show the dbconfig.xml as it also has one important part:

<?xml version="1.0" encoding="UTF-8"?>
<jira-database-config>
<name>defaultDS</name>
<delegator-name>default</delegator-name>
<database-type>postgres72</database-type>
<schema-name>public</schema-name>
<jdbc-datasource>
<url>jdbc:postgresql://postgres:5432/jira</url>
<driver-class>org.postgresql.Driver</driver-class>
<username>jira</username>
... other options...
</jdbc-datasource>
</jira-database-config>

Look closely. We refer to the postgres instance by the service name from the docker compose file. This is important. We have to use the service names when running in this environment

The we call a start.sh script.

#!/bin/bash
mkdir -p /var/atlassian/application-data/jira/plugins/installed-plugins
cp /opt/dbconfig.xml /var/atlassian/application-data/jira/dbconfig.xml
cp /opt/quickreload.properties /var/atlassian/application-data/jira/quickreload.properties
cp /opt/quickreload-5.0.2.jar /var/atlassian/application-data/jira/plugins/installed-plugins/quickreload-5.0.2.jar
cp /opt/jira-data-generator-5.0.0.jar /var/atlassian/application-data/jira/plugins/installed-plugins/jira-data-generator-5.0.0.jar
cp /opt/your-plugin.jar /var/atlassian/application-data/jira/plugins/installed-plugins/your-plugin.jar
chown -R jira:jira /var/atlassian/application-data/jira
/entrypoint.py

Again, this is straightforward what we’re doing here is putting all the various plugins into place for Jira startup. Then we start the container.

At this point, you can run this locally and see a running Jira + Postgres and it’ll try and execute any Playwright tests you have configured. You’ll be able to access these on ‘localhost’.

In fact, you can just bring up Jira and Postgres and run playwright from the command line, i.e just two containers. This was actually my starting point for everything. Which I thought was reasonable, but read on….

Bringing this to CircleCI

If it works in Docker, it works on CircleCI.

This should all be pretty straightforward now, right? Ok, let’s do it.

Take 1: I tried to replicate what I was doing locally on CircleCI. So, my initial adventure was a docker compose of just Jira and PostgreSQL, with the Playwright tests running on my local machine. This looked like this:

version: 2.1
orbs:
node: circleci/node@5.1.0

run-playwright-tests:
docker:
- image: cimg/openjdk:11.0.21-node
environment:
COMPOSE_PROJECT_NAME: jira-dc
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true

- restore_cache:
keys:
- maven-v1-{{ checksum "pom.xml" }}

- run:
name: Package
command: mvn -s .circleci-settings.xml -B package

- save_cache:
paths:
- ~/.m2
key: maven-v1-{{ checksum "pom.xml" }}

- run:
name: Build and start Docker containers with Jira and PostgreSQL.
command: docker compose -f docker-compose-ci.yml up -d --build

- run:
name: Install Playwright
command: npx playwright install --with-deps

- run:
name: Wait for Jira to be ready
command: |
timeout=$((SECONDS + 300)) # Set timeout to 5 minutes (300 seconds)
while true; do
response=$(curl -sSf http://localhost:8080/rest/api/2/serverInfo)
if [ $? -eq 0 ]; then
echo "Jira is ready"
break
elif [ $SECONDS -gt $timeout ]; then
echo "Timeout: Jira did not become ready within 5 minutes"
exit 1
else
echo "Jira is not ready yet, retrying in 5 seconds..."
sleep 5
fi
done
- run:
name: Change to projectstatusclient directory
command: cd projectstatusclient

- run:
name: Run Playwright tests
command: npx playwright test

- store_artifacts:
path: ./playwright-report
destination: playwright-report-first

workflows:
version: 2
run-unit-tests:
jobs:
- run-playwright-tests

This runs a build, uses a build cache, brings up Jira, PostgresSQL, then tries to use the Docker container running the compose to execute the tests.

It didn’t work. The parent container couldn’t see any of the containers brought up with ‘docker compose’. I eventually found this in the docs:

The primary container runs in a separate environment from Remote Docker and the two cannot communicate directly. To interact with a running service, run a container in the service’s network.

Take 2. This is where I added in the Playwright container to the mix to have all the things on the same network and be able to interact with each other.

Here’s the CircleCI config I moved to:

version: 2.1
orbs:
node: circleci/node@5.1.0

jobs:
run-playwright-tests:
docker:
- image: cimg/openjdk:11.0.21-node
environment:
COMPOSE_PROJECT_NAME: jira-dc

steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true

- restore_cache:
keys:
- maven-v1-{{ checksum "pom.xml" }}

- run:
name: Package
command: mvn -s .circleci-settings.xml -B package -DskipTests

- save_cache:
paths:
- ~/.m2
key: maven-v1-{{ checksum "pom.xml" }}

- run:
name: Build and start Docker containers with Jira and PostgreSQL and playwright.
command: docker compose -f docker-compose-ci.yml up --build -d

- run:
name: Run Playwright tests
command: docker wait jira-dc-playwright-1

- store_artifacts:
path: ./projectstatusclient/playwright-report
destination: playwright-report-first

- store_test_results:
path: ./projectstatusclient/results.xml

- run:
name: Stop and remove Docker containers
command: docker compose -f docker-compose-ci.yml down

workflows:
version: 2
run-tests:
jobs:
- run-playwright-tests

This feels ok. Let’s try it. It looked like it vaguely worked? But nope, there were no test results. Huh? It turns out that mounting the filesystems in the docker compose file wasn’t actually doing anything. The initial signal on this was no test results.

So I re-ran the build with ssh enabled (this is really, really nice: https://support.circleci.com/hc/en-us/articles/5170139355547-How-to-rerun-a-job-with-SSH), logged into the database container to see if any of the tables had been populated. Nope. Not a thing. Cue more digging into the docs.

If you want to use Docker Compose to manage a multi-container setup with a Docker Compose file, use the machine key in your .circleci/config.yml file and use docker-compose as you would normally (see Linux VM execution environment documentation here for more details). That is, if you have a Docker Compose file that shares local directories with a container, this will work as expected.

Take 3. Let’s use the machine executor.

  run-playwright-tests:
machine:
image: ubuntu-2204:2024.01.1
resource_class: large

This worked as expected. :-) Now, we have a setup that replicates what we’re running locally and behaves as it should with the ability to configure an instance for test, run tests and get the results from that container.

Some key points to takeaway

  • If you’re working with docker compose — really, really read and internalise this: https://circleci.com/docs/docker-compose/ The info I needed was there, but it felt like it was being skimmed over with the various examples and things above. Also, a lot of the examples bring things up and run a simple health-check, but nothing ‘meaty’.
  • CircleCI’s networking with Docker, doesn’t work how you think it will. It makes sense for it to do what it does to create that isolation, but it still was confusing to start with.
  • CircleCI shared filesystems with Docker do not work with their promoted Docker executor.
  • CircleCI’s option to ‘re-run with SSH’ is super cool and very useful for debugging CI problems. https://support.circleci.com/hc/en-us/articles/5170139355547-How-to-rerun-a-job-with-SSH

Anyhoo, that’s it for today. Have fun with your CI.

--

--