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.
# 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
- create a new directory or
cd
wherever you make projects - run
npm create svelte@latest .
ornpm create svelte@latest fancy-app
- follow the prompts
- move into the project and
npm install
- check your sanity by running
npm run dev
and open up localhost:5173:
Let's go to town with a few Tailwind / DaisyUI packages to make this even easier.
- run
npx svelte-add@latest tailwindcss --tailwindcss-forms --tailwindcss-typography --tailwindcss-daisyui
- run
npm install
- run
npm run dev
# 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:
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.
- create a new file —
app/.env
— and be sure to add it to your.gitignore
file if you're using Git - add the following environment variables, which SvelteKit will pick up automatically
- be sure to use private and public vars where needed
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:
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:
# 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.
|
|
|
|
|
|
|
|
|
|
Here on line 3
we list the Jira projects we'll be working with and import the ReleaseForm
component. Which we'll add next:
<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:
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:
@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:
# 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.