Integrating SvelteKit with Jira for Efficient Cross-Project Releases

For software development projects, many teams contribute to the same applications and mono-repositories. Sometimes, a single release can contain Frontend, Backend, Infrastructure, Configuration, and service-level API updates. Translating that into "Fix" versions in Jira can be an interesting challenge...

# Problem

The inability to share "Fix" Versions across multiple Jira projects can lead to increased manual work, potential for errors, and hindered visibility across projects.

There is a concept of "Cross-project" releases within Jira's Advanced Roadmaps feature, but the overhead of creating releases for each project is a torturous task that will quickly drain precious time with menial pointing and clicking.

# Solution

We're going to use the Jira API and create a frontend in SvelteKit. We'll make a UI in which we can provide a Fix Version (Release) title and description, select from a list of projects we'd like that release created for, and POST to the Jira Versions API via our proxy.

Creating cross-project releases is essential when working on large, cross-team Jira Epics and User Stories. Naming them all the same makes querying issues with JQL easier, and fun!

SvelteKit is a great place to start. Its enjoyable developer experience and quickly visible results leave only some API integration.

Everything "just works", and the npm-enabled tools like svelte-add make standing up a new project wonderfully straightforward. Let's get to work and build ourselves a release-cascading SvelteKit Jira-API integration app.

# tl;dr — A Demonstration

Here we start on the Jira dashboard page after logging in, navigate to the Advanced Road Maps "Plans" area, where I've previously set up a cross-project release. This way we can see all of our Jira projects in their sad, empty, releaseless-existence.

Still frame: A stil image of the Jira dashboard page.
Animated A demo of cascading Jira releases.

# SvelteKit

What follows is a great example of what can be done in two hours (maybe less!). This GitHub repo has the final results in its release-cascade directory.

# Setting up SvelteKit for Jira Integration

  1. create a new directory or cd wherever you make projects
  2. run npm create svelte@latest . or npm create svelte@latest fancy-app
  3. follow the prompts
  4. move into the project and npm install
  5. check your sanity by running npm run dev and open up localhost:5173:

Barebones SvelteKit welcome page.

Let's go to town with a few Tailwind / DaisyUI packages to make this even easier.

  1. run npx svelte-add@latest tailwindcss --tailwindcss-forms --tailwindcss-typography --tailwindcss-daisyui
  2. run npm install
  3. run npm run dev

Tailwind-styled SvelteKit welcome page.

# API Proxy Route

After having something to look at, I like to set up the API and — to get around CORS — a proxy route in SvelteKit. We'll essentially be building this:

The API infograpnic depicting client request to SvelteKit proxy server, server proxy request to Jira API, response to SvelteKit proxy server, and finally a response back to the client UI.

Working with the Jira API is pretty straightforward, and thanks to API Evangelist, very easy to explore via this Postman Collection for Jira Cloud. The Jira Server API is similar, but there are nuanced differences.

This project assumes the use of Personal Access Tokens (PAT) to authenticate the Jira API. Follow their guide for Server or Cloud to generate a PAT and proceed with building out the necessary endpoints. As mentioned, our focus below is Jira Server specifically.

# Secrets, Secrets

Let's add some API credentials to Authenticate with the Jira API — we'll set things up for Jira content as well.

  1. create a new file — app/.env — and be sure to add it to your .gitignore file if you're using Git
  2. add the following environment variables, which SvelteKit will pick up automatically
  3. be sure to use private and public vars where needed
.env
JIRA_API_URL="http://localhost:8080"
JIRA_API_PATH="/rest/api/2/version"
PUBLIC_PROXY_PATH="/api/v1/create-release"
JIRA_API_BEARER_TOKEN="ABcdefGHIjklmnOpQrSTUVwx+yZ"

Note

localhost:8080 assumes we're using a local, Dockerized Jira instance. Read more about how to run Jira in Docker in this post: Engineering in Program Management — Run Jira Server in Docker

# Server Route

Set up the POST endpoint, which is just another SvelteKit route on the server:

app/src/routes/api/v1/create-release/+server.js
import { JIRA_API_URL, JIRA_API_BEARER_TOKEN, JIRA_API_PATH } from '$env/static/private';

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
	const params = await request.json();
	const { name, description, project } = params;

	const headers = new Headers();
	headers.append('authorization', `Bearer ${JIRA_API_BEARER_TOKEN}`);
	headers.append('accept', 'application/json');
	headers.append('content-type', 'application/json');

	const options = {
		method: 'POST',
		headers
	};
	const body = JSON.stringify({
		archived: false,
		description,
		name,
		project,
		released: false
	});

	try {
		const response = await fetch(`${JIRA_API_URL}${JIRA_API_PATH}`, {
			...options,
			body
		});

		if (!response.ok) {
			throw new Error('Network response was not ok');
		}

		const responseData = await response.json();

		// TODO: this should be replaced with a real logging solution
		console.log(responseData);
		return new Response(JSON.stringify(responseData), {
			status: 200,
			headers: {
				'content-type': 'application/json'
			}
		});
	} catch (error) {
		console.error(error);
		return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
			status: 500,
			headers: {
				'content-type': 'application/json'
			}
		});
	}
}

As a non-functional test, check if SvelteKit is informing you that there is no GET method on this route. Open localhost to confirm the endpoint is (not) functioning:

The SvelteKit API Proxy endpoint returning a GET method not allowed message.

# UI, submitHandler, and createRelease API

Let's build out the main UI route, form component, and the client API for our backend. The previously generated +layout.svelte can stay as is.

app/src/routes/+page.svelte
1
<script>
2
	// your script goes here
3
	const projects = ['UIR', 'PIN', 'DAN', 'APE', 'DEVO', 'RM'];
4
	import ReleaseForm from '$lib/comp/ReleaseForm.svelte';
5
</script>
6

7
<svelte:head>
8
	<title>Jira Release Cascade</title>
9
</svelte:head>
10
<ReleaseForm {projects} />

Here on line 3 we list the Jira projects we'll be working with and import the ReleaseForm component. Which we'll add next:

app/src/components/ReleaseForm.svelte
<script>
	import { deserialize } from '$app/forms';
	import { createRelease } from '$lib/api/createRelease';
	export let projects;
	$: selectedProjects = [];
	$: releaseName = '';
	$: releaseDescription = '';
	$: success = false;

	const selectAllProjects = function toggleAllFieldsetCheckboxes(e) {
		selectedProjects = e.target.checked ? [...projects] : [];
	};

	const toggleProject = function toggleProject(node) {
		const value = node.value;

		if (node.checked && !selectedProjects.includes(value)) {
			selectedProjects.push(value);
		} else if (!node.checked && selectedProjects.includes(value)) {
			selectedProjects = selectedProjects.filter((project) => project !== value);
		}
	};

	const submitHandler = async function submitHandler(e) {
		e.preventDefault();
		success = 'pending';
		try {
			// Generate an array of promises for each selected project
			const postPromises = selectedProjects.map((project) =>
				createRelease(releaseName, releaseDescription, project)
			);

			// Execute all promises concurrently and wait for them to settle
			const results = await Promise.all(postPromises);

			// NOTE: This is a great place for reporting middleware
			results.forEach((result) => {
				console.log(result);
			});
			success = true;
			setTimeout(function () {
				success = false;
			}, 2000);
		} catch (error) {
			console.error('Error:', error);
			success = 'failed';
		}
	};
</script>

<h1 class="pb-9 text-3xl">Create a Cascading Release</h1>
<form id="release-form" on:submit={submitHandler} class="text-center">
	<h2 class="text-xl">Projects</h2>
	<fieldset class="grid grid-cols-2 justify-items-start md:grid-cols-3 lg:grid-cols-4">
		<label class="label col-span-full cursor-pointer">
			<input type="checkbox" class="checkbox" on:change={selectAllProjects} />
			<span class="label-text">Select All</span>
		</label>
		{#each projects as project}
			<label class="label cursor-pointer">
				<input
					type="checkbox"
					class="checkbox"
					bind:group={selectedProjects}
					on:change={toggleProject}
					name={project}
					value={project}
				/>
				<span class="label-text">{project}</span>
			</label>
		{/each}
	</fieldset>
	<fieldset>
		<h2 class="pb-1 pt-9">Release/Version Name</h2>
		<input
			name="release-name"
			type="text"
			placeholder="KEY-R24.04.10.0 or KEY-R24.Q1.1.0 etc."
			class="input input-bordered w-full"
			bind:value={releaseName}
			required
		/>
		<div class="label">
			<span class="label-text-alt"
				>Check out the <a href="/">Release and Version Naming documentation</a>.
			</span>
		</div>
		<h2 class="pb-1 pt-5">Release/Version Description</h2>
		<input
			name="release-description"
			type="text"
			placeholder="Add a description, if you'd like."
			class="input input-bordered w-full"
			bind:value={releaseDescription}
		/>
	</fieldset>
	<button
		class="align-self btn btn-primary mt-4 w-[333px] transition-colors {success
			? `btn-success`
			: null} {success === 'pending' ? `btn-info` : success === 'failed' ? `btn-error` : null}"
	>
		{#if success === true}
			Success!
			<svg
				xmlns="http://www.w3.org/2000/svg"
				class="h-6 w-6"
				fill="none"
				viewBox="0 0 24 24"
				stroke="currentColor"
				><path
					stroke-linecap="round"
					stroke-linejoin="round"
					stroke-width="2"
					d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
				/></svg
			>
		{:else if success === 'pending'}
			Creating Releases
			<span class="loading loading-spinner"></span>
		{:else if success === 'failed'}
			Something went wrong.
		{:else}
			Go
		{/if}
	</button>
</form>

Lastly, let's create the client-side API for the Jira proxy route:

app/src/lib/api/createRelease.js
import { PUBLIC_PROXY_PATH as PROXY } from '$env/static/public';
export async function createRelease(name, description, project) {
	const headers = new Headers();
	headers.append('content-type', 'application/json');
	const options = {
		method: 'POST',
		headers
	};

	const body = JSON.stringify({
		archived: false,
		description,
		name,
		project,
		released: false
	});

	try {
		// Make the API POST call
		const response = await fetch(PROXY, {
			...options,
			body
		});

		if (response.ok) {
			return 'success'; // Return 'success' if the API call succeeds
		} else {
			throw new Error('API call failed');
		}
	} catch (error) {
		console.error('Error:', error);
		throw error; // Rethrow the error
	}
}

# Additional Layout

Let's open up app/src/app.pcss and add some general layout to the form:

app.pcss
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body {
	width: 100vw;
	height: 100vh;
}

body {
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
}

form {
	width: 77%;
	max-width: 800px;
}

a {
	color: rgb(160, 145, 221);
	text-decoration: underline;
	transition: color 133ms linear;
}

fieldset {
	padding-top: 1rem;
	display: block;
	text-align: left;
	&:first-child {
		padding-top: 0;
	}
	h2:not(first-child) {
		display: block;
	}
	.label-text {
		padding-left: 0.5rem;
	}
}

We've made some changes that SvelteKit may not yet be aware of, so let's restart the server with npm run dev and have a look at beautiful release form:

The finished Jira Release Cascade app.

# Final Adjustments

The Jira projects set on line 3 of +page.svelte, highlighted above, will be specific to Jira instance. Add the project keys specific the Jira Server.

The included project keys align with some Jira data __mocks__I've included in the main directory of this project's GitHub Repo. Here's a link to the tree hash where this data will always exist.

# Conclusion

Now that we have an easy way to POST a single release to the API, populating Jira projects in one fell swoop, we can look forward to saving time, reducing errors, and improving coordination across teams.

Back to Top