Published: Aug 29, 2023 by Isaac Johnson
I’ve covered issue collectors before, such as using Azure DevOps to propegate Azure Work Items and then Github Issues.
Recently, I’ve needed to revisit this and likely include JIRA as well. Can we come up with a simplified Issue Collector that could be used to create Azure Work Items as well as JIRA Tickets (for external visibility)? In my case, I’ll be looking to have a very detailed Azure Work Item but then a simplified JIRA ticket that other teams can use to track progress.
Setting up Azure Work Item
Let’s start with creating a custom work item we’ll be using to collect some user details.
I’ll start with a HelloWorld project in AzDO
I’m already using a customized Agile flow
The modifications to Work Iteam Templates actually happens at the organization level. There we can create a new Inherited Process or use the one I already created
We can see I made some custom types during a demo years ago. I’ll be creating a new Work Item type for this project by clicking “+ New work item type”
I can give it a name, icon and colour
Adding and editing fields is pretty easy:
Now we have a basic work item template:
Azure Webhook
To collect issues, we’ll need an exposed Azure DevOps Webhook.
These are exposed as “Service Connections” in the project
We can use the type “Incoming Webhook”
The secret is optional and just used to calculate a checksum.
I can now see it listed
Feedback Form
I’ll need a feedback form to use. Since I’ll be sticking in AzDO for this project, I’ll pull down a copy of submitformtojsonapi and use an Azure Repo.
The first step is to clone this Github project - submitformjsonapi
builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/glorious73/submitformtojsonapi.git mysubmitformtojsonapi
Cloning into 'mysubmitformtojsonapi'...
remote: Enumerating objects: 63, done.
remote: Counting objects: 100% (63/63), done.
remote: Compressing objects: 100% (38/38), done.
remote: Total 63 (delta 24), reused 56 (delta 17), pack-reused 0
Unpacking objects: 100% (63/63), 92.97 KiB | 1.19 MiB/s, done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd mysubmitformtojsonapi/
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$
In Azure Repos, Let’s create a new repo
Make sure to uncheck “Add a README” which is checked by default
We now get an empty repo page
You’ll need GIT creds for the next step, so get them now
We can now add an ‘origin2’ and push up to Azure Repos
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git remote add origin2 https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git push -u origin2 --all
fatal: Authentication failed for 'https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi/'
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git push -u origin2 --all
Username for 'https://princessking.visualstudio.com': isaac.johnson
Password for 'https://isaac.johnson@princessking.visualstudio.com':
Enumerating objects: 63, done.
Counting objects: 100% (63/63), done.
Delta compression using up to 16 threads
Compressing objects: 100% (55/55), done.
Writing objects: 100% (63/63), 93.26 KiB | 9.33 MiB/s, done.
Total 63 (delta 24), reused 0 (delta 0)
remote: Analyzing objects... (63/63) (23 ms)
remote: Storing packfile... done (83 ms)
remote: Storing index... done (63 ms)
To https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin2'.
Which now shows up
We’ll want to change these lines to point to our webhook
We can see the changes we’ll need to update; two javascript files, a css, an HTML and package.json
The key files here:
renamed index.html to issue.html and changed the blocks to
<div class="container card card-color">
<form action="" id="sampleForm">
<h2>Create a feedback task</h2>
<div class="form-row">
<label for="userId">Email Address</label>
<input type="email" class="input-text input-text-block w-100" id="userId" name="userId" value="anonymous@dontemailme.com">
</div>
<div class="form-row">
<label for="summary">Summary</label>
<input type="text" class="input-text input-text-block w-100" id="summary" name="summary">
</div>
<div class="form-row">
<label for="description">Description or Details</label>
<textarea class="input-text input-text-block ta-100" id="description" name="description"></textarea>
</div>
<div class="form-row mx-auto">
<button type="submit" class="btn-submit" id="btnSubmit">
The css to add:
.ta-100 {
width: 100%;
height: 200px;
}
updates to package.json
"description": "demo to submit html forms to APIs",
"main": "index.html",
"scripts": {
"dev": "npm run clean && parcel src/*.html --out-dir dev issue.html",
"build": "parcel build src/*.html --public-url ./",
"clean": "rimraf ./dev && rimraf -rf ./.cache"
},
I commented out and returns just the raw response in performPostHttpRequest
async performPostHttpRequest(fetchLink, headers, body) {
if(!fetchLink || !headers || !body) {
throw new Error("One or more POST request parameters was not passed.");
}
try {
const rawResponse = await fetch(fetchLink, {
method: "POST",
headers: headers,
body: JSON.stringify(body)
});
//const content = await rawResponse.json();
//return content;
// Webhooks are NOT JSON
console.log(rawResponse);
return rawResponse;
}
catch(err) {
console.error(`Error at fetch POST: ${err}`);
throw err;
}
}
Lastly, we updated app.js for the URL
async function submitForm(e, form) {
// 1. Prevent reloading page
e.preventDefault();
// 2. Submit the form
// 2.1 User Interaction
const btnSubmit = document.getElementById('btnSubmit');
btnSubmit.disabled = true;
setTimeout(() => btnSubmit.disabled = false, 2000);
// 2.2 Build JSON body
const jsonFormData = buildJsonFormData(form);
// 2.3 Build Headers
const headers = buildHeaders();
// 2.4 Request & Response
const response = await fetchService.performPostHttpRequest(`https://dev.azure.com/princessking/_apis/public/distributedtask/webhooks/issuecollector?api-version=6.0-preview`, headers, jsonFormData); // Uses JSON Placeholder
console.log(response);
// 2.5 Inform user of result - we do not get any feedback json objects from webhook so just use the HTML
if(response)
window.location = `/success.html`;
else
alert(`An error occured.`);
}
Pipeline
Let’s head over to Pipelines to create a Pipeline
We’ll choose Azure Repos
and select the new repo
We can just select Node.js (or starter pipeline as we’ll modify it)
We initially get
# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
pool:
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build
displayName: 'npm install and build'
Let’s add some steps to package the output so we can at least view it.
# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
pool:
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build
# copy to artifact staging
cp -rf ./dist $(Build.ArtifactStagingDirectory)
displayName: 'npm install and build'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
I’ll click “save and run”
The defaults are fine
I had an error due to spacing. You can use the three-dot menu to validate and find issues if you have a copy/paste error or typo
This should kick off a run
Which, for me, built without issue
Let’s go look at the published artifact
Here we can see the generated files
We can download to view locally
In this first pass, I found it generated the original index.html which was not what I expected
I realized that the source file needs to be index.html
Running locally
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ npm run dev
> submittingformstoapis@1.0.0 dev /home/builder/Workspaces/mysubmitformtojsonapi
> npm run clean && parcel src/*.html --out-dir dev issue.html
> submittingformstoapis@1.0.0 clean /home/builder/Workspaces/mysubmitformtojsonapi
> rimraf ./dev && rimraf -rf ./.cache
Server running at http://localhost:1234
⠙ Building app.js...Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db
Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating
Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db
Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating
⠹ Building app.js...Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db
Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating
⠸ Building index.js...Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db
Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating
✨ Built in 870ms.
I can see the rendered file
What I realized is that parcel has an output option that can specify the file to create
The updated package.json
{
"name": "submittingformstoapis",
"version": "1.0.0",
"description": "demo to submit html forms to APIs",
"main": "issue.html",
"scripts": {
"dev": "npm run clean && parcel src/*.html --out-dir dev issue.html",
"build": "parcel build src/*.html --public-url ./ -o issue.html",
"clean": "rimraf ./dev && rimraf -rf ./.cache"
},
"keywords": [
"form",
"html",
"json",
"api",
"fetch",
"post",
"request"
],
"author": "Amjad Abujamous",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6",
"@babel/plugin-transform-runtime": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"parcel-bundler": "^1.12.4",
"parcel-plugin-html-partials": "0.0.6",
"save": "^2.4.0"
}
}
Also, we need to NOT have ./dist checked in as it will create an errant index.html when we don’t want that.
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git status
On branch master
Your branch is up to date with 'origin2/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: dist/app.66f9fcf1.js
deleted: dist/app.66f9fcf1.js.map
deleted: dist/index.html
deleted: dist/styles.c6edcaf7.css
deleted: dist/styles.c6edcaf7.css.map
deleted: dist/success.html
modified: package.json
renamed: src/issue.html -> src/index.html
Untracked files:
(use "git add <file>..." to include in what will be committed)
dist/
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git commit -m updates
[master 6f4d326] updates
8 files changed, 1 insertion(+), 24 deletions(-)
delete mode 100644 dist/app.66f9fcf1.js
delete mode 100644 dist/app.66f9fcf1.js.map
delete mode 100644 dist/index.html
delete mode 100644 dist/styles.c6edcaf7.css
delete mode 100644 dist/styles.c6edcaf7.css.map
delete mode 100644 dist/success.html
rename src/{issue.html => index.html} (100%)
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 378 bytes | 378.00 KiB/s, done.
Total 4 (delta 3), reused 0 (delta 0)
remote: Analyzing objects... (4/4) (8 ms)
remote: Storing packfile... done (53 ms)
remote: Storing index... done (101 ms)
To https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi
31601d7..6f4d326 master -> master
Which now looks good
Form Processor
Let’s first get a pipeline started that can listen to the webhook.
I’ll just create as a YAML in the repo:
$ cat form-pipelines.yml
# Payload as sent by Web Form
resources:
webhooks:
- webhook: issuecollector
connection: issuecollector
pool:
vmImage: ubuntu-latest
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: $"
echo "summary: $"
echo "description: $"
cat >rawDescription <<EOOOOL
$
EOOOOL
cat >rawSummary <<EOOOOT
$
EOOOOT
cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
I’ll add and push (I also added “dist/” to .gitignore)
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git add form-pipelines.yml
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ vi .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git status
On branch master
Your branch is up to date with 'origin2/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: form-pipelines.yml
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git add .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git commit -m "add form pipeline"
[master ab52e17] add form pipeline
2 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 form-pipelines.yml
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 693 bytes | 693.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Analyzing objects... (4/4) (26 ms)
remote: Storing packfile... done (50 ms)
remote: Storing index... done (44 ms)
To https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi
6f4d326..ab52e17 master -> master
I’ll now add pipelines. Like before, I’ll select new pipeline and select Azure Repos
This time I’ll select an Existing Azure Pipelines YAML file
It should detect the form pipelines YAML
This time I won’t run, I’ll just save
I can test with a local snapshot of the issue.html
We can see the output in the job log:
Creating Issues
We now have a form that triggers a pipeline that can be parsed. However, to save issues we need a PAT that can update Work Items
We can create that in the Personal Access Tokens area
I can give, at most, a year on a PAT
Later, we’ll need some kind of reminder to refresh this or in a year it will stop working
I tend to like to use Group Variables here so I can re-use shared tokens.
Let’s add a Group Variable in Library
I’ll add the token here
(Don’t forget to hit save at the top)
I can now use this in the pipeline
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git diff form-pipelines.yml
diff --git a/form-pipelines.yml b/form-pipelines.yml
index 45a722e..d10f389 100644
--- a/form-pipelines.yml
+++ b/form-pipelines.yml
@@ -7,6 +7,9 @@ resources:
pool:
vmImage: ubuntu-latest
+variables:
+- group: AZDOAutomations
+
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
@@ -26,3 +29,24 @@ steps:
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
+
+ cat >$(Pipeline.Workspace)/createwi.sh <<EOL
+ set -x
+ export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
+ az boards work-item create --title '`cat inputSummary`' --type Feature --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --fields System.Description='`cat inputDescription | tr -d '\n'`' > azresp.json
+ EOL
+ chmod u+x createwi.sh
+
+ echo "createwi.sh:"
+ cat $(Pipeline.Workspace)/createwi.sh
+
+ echo "have a nice day."
+ displayName: 'Check webhook payload'
+
+- task: AzureCLI@2
+ displayName: 'Create feature ticket'
+ inputs:
+ azureSubscription: 'Pay-As-You-Go(d955c0ba-asdf-asdf-asdf-asdfasdfasdf)'
+ scriptType: 'bash'
+ scriptLocation: 'scriptPath'
+ scriptPath: '$(Pipeline.Workspace)/createwi.sh'
\ No newline at end of file
I’ll push that up
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git add form-pipelines.yml
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git commit -m 'update'
[master 6f02504] update
1 file changed, 24 insertions(+)
builder@DESKTOP-QADGF36:~/Workspaces/mysubmitformtojsonapi$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 1.01 KiB | 1.01 MiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Analyzing objects... (3/3) (7 ms)
remote: Storing packfile... done (133 ms)
remote: Storing index... done (46 ms)
To https://princessking.visualstudio.com/HelloWorldPrj/_git/mysubmitformtojsonapi
ab52e17..6f02504 master -> master
I’ll now fire a test
Note, we need to approve use of this pipeline
Specifically, we need to permit use of the Group Variable and Azure Sub
After working on an Azure Sub connection, it dawned on me that it was a completely unnecessary step. I reworked to just use a Bash step instead
resources:
webhooks:
- webhook: issuecollector
connection: issuecollector
pool:
vmImage: ubuntu-latest
variables:
- group: AZDOAutomations
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: $"
echo "summary: $"
echo "description: $"
cat >rawDescription <<EOOOOL
$
EOOOOL
cat >rawSummary <<EOOOOT
$
EOOOOT
cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
cat >$(Pipeline.Workspace)/createwi.sh <<EOL
set -x
export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
az boards work-item create --title '`cat inputSummary`' --type Feature --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --fields System.Description='`cat inputDescription | tr -d '\n'`' > azresp.json
EOL
chmod u+x $(Pipeline.Workspace)/createwi.sh
echo "createwi.sh:"
cat $(Pipeline.Workspace)/createwi.sh
echo "have a nice day."
displayName: 'Check webhook payload'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createwi.sh'
workingDirectory: '$(Pipeline.Workspace)'
This created a feature, which is what the az command requested.
But we want to use our new issue type
I just added a step to parse the email and add an RSVP field
# if they left "dontemailme" assume they do not want RSVP
export USERTLD=`echo "" | sed 's/^.*@//'`
if [[ "$USERTLD" == "dontemailme.com" ]]; then
export RSVP="--fields Custom.RespondezSVP=false"
else
export RSVP="--fields Custom.RespondezSVP=true"
fi
cat >$(Pipeline.Workspace)/createwi.sh <<EOL
set -x
export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
az boards work-item create --title '`cat inputSummary`' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --fields System.Description='`cat inputDescription | tr -d '\n'`' $RSVP > azresp.json
EOL
chmod u+x $(Pipeline.Workspace)/createwi.sh
I tested
Lastly, I updated to show output
# Payload as sent by Web Form
resources:
webhooks:
- webhook: issuecollector
connection: issuecollector
pool:
vmImage: ubuntu-latest
variables:
- group: AZDOAutomations
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: $"
echo "summary: $"
echo "description: $"
cat >rawDescription <<EOOOOL
$
EOOOOL
cat >rawSummary <<EOOOOT
$
EOOOOT
cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
# if they left "dontemailme" assume they do not want RSVP
export USERTLD=`echo "" | sed 's/^.*@//'`
if [[ "$USERTLD" == "dontemailme.com" ]]; then
export RSVP="--fields Custom.RespondezSVP=false"
else
export RSVP="--fields Custom.RespondezSVP=true"
fi
cat >$(Pipeline.Workspace)/createwi.sh <<EOL
set -x
export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
az boards work-item create --title '`cat inputSummary`' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --fields System.Description='`cat inputDescription | tr -d '\n'`' $RSVP > azresp.json
EOL
chmod u+x $(Pipeline.Workspace)/createwi.sh
echo "createwi.sh:"
cat $(Pipeline.Workspace)/createwi.sh
echo "have a nice day."
displayName: 'Check webhook payload'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createwi.sh'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Execute Create Script'
- task: Bash@3
inputs:
targetType: 'inline'
script: 'cat $(Pipeline.Workspace)/azresp.json'
workingDirectory: '$(Pipeline.Workspace)'-
displayName: 'Show create output'
And indeed, it all works
JIRA
We got this far with just AzDO and Forms. Let’s tackle JIRA ticket creation now next.
Our goal is to add tickets to an existing board at the same time as we do Azure Work Items
To engage with JIRA, we need an API key. We’ll find that under our Profile in the Security Tab
Click “Create API Token”
Give it a name, then click Create
We can test that API token with a CURL command
$ curl -D- -u isaac@freshbrewed.science:c2VyaW91c2x5IHRob3VnaC4gSSBhbSBub3QgYSBmYW4gb2YgSklSQSBhbnkgbW9yZS4gSXQgaXMgcmVhbGx5IHBvd2VyZnVsIGJ1dCB0aGV5IGtlZXAgbWFraW5nIGJhc2ljIGZlYXR1cmVzIHByZW1pdW0uIGJ1cm5lZCBhd2F5IG15IGdvb2Qgd2lsbAo= -X GET -H "Content-Type: application/json" https://freshbrewed.atlassian.net/rest/api/2/issue/createmeta
HTTP/2 200
date: Mon, 28 Aug 2023 11:10:53 GMT
content-type: application/json;charset=UTF-8
server: AtlassianEdge
timing-allow-origin: *
x-arequestid: 5bf0a520ad01da63f7e48252317bdd21
set-cookie: atlassian.xsrf.token=3f1e825fa8f1e0eeec6a652c2358d28935afbfaa_lin; Path=/; SameSite=None; Secure
x-aaccountid: 618742213ae5230069d074cf
cache-control: no-cache, no-store, no-transform
vary: Accept-Encoding
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
atl-traceid: aa00b08d6c79b359
report-to: {"endpoints": [{"url": "https://dz8aopenkvv6s.cloudfront.net"}], "group": "endpoint-1", "include_subdomains": true, "max_age": 600}
nel: {"failure_fraction": 0.001, "include_subdomains": true, "max_age": 600, "report_to": "endpoint-1"}
strict-transport-security: max-age=63072000; includeSubDomains; preload
{"expand":"projects","projects":[{"self":"https://freshbrewed.atlassian.net/rest/api/2/project/10000","id":"10000","key":"TPK","name":"MyJIRAProject","avatarUrls":{"48x48":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425","24x24":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=small","16x16":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=xsmall","32x32":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=medium"},"issuetypes":[{"self":"https://freshbrewed.atlassian.net/rest/api/2/issuetype/10002","id":"10002","description":"A small, distinct piece of work.","iconUrl":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","name":"Task","untranslatedName":"Task","subtask":false},{"self":"https://freshbrewed.atlassian.net/rest/api/2/issuetype/10003","id":"10003","description":"A small piece of work that's part of a larger task.","iconUrl":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","name":"Sub-task","untranslatedName":"Sub-task","subtask":true},{"self":"https://freshbrewed.atlassian.net/rest/api/2/issuetype/10001","id":"10001","description":"Functionality or a feature expressed as a user goal.","iconUrl":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","name":"Story","untranslatedName":"Story","subtask":false},{"self":"https://freshbrewed.atlassian.net/rest/api/2/issuetype/10004","id":"10004","description":"A problem or error.","iconUrl":"https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium","name":"Bug","untranslatedName":"Bug","subtask":false},{"self":"https://freshbre
Showing that it works, let’s see if we can create an issue with REST.
We sued to be able to just go to the URL with /rest/api/2
and get some nice docs, but that seems gone now in the hosted instance. That said, we can find latest docs for creating issues in the developer portal
I’m not going to nit-pick an essentially free product, but it says for the fields, we can use any field and its any additional property. That’s as helpful as a screen door on a submarine.
It does, however, give an example, when selecting ‘curl’ in the upper right that might help us out
curl --request POST \
--url 'https://your-domain.atlassian.net/rest/api/2/issue' \
--user 'email@example.com:<api_token>' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"assignee": {
"id": "5b109f2e9729b51b54dc274d"
},
"components": [
{
"id": "10000"
}
],
"customfield_10000": "09/Jun/19",
"customfield_20000": "06/Jul/19 3:25 PM",
"customfield_30000": [
"10000",
"10002"
],
"customfield_40000": "Occurs on all orders",
"customfield_50000": "Could impact day-to-day work.",
"customfield_60000": "jira-software-users",
"customfield_70000": [
"jira-administrators",
"jira-software-users"
],
"customfield_80000": {
"value": "red"
},
"description": "Order entry fails when selecting supplier.",
"duedate": "2019-03-11",
"environment": "UAT",
"fixVersions": [
{
"id": "10001"
}
],
"issuetype": {
"id": "10000"
},
"labels": [
"bugfix",
"blitz_test"
],
"parent": {
"key": "PROJ-123"
},
"priority": {
"id": "20000"
},
"project": {
"id": "10000"
},
"reporter": {
"id": "5b10a2844c20165700ede21g"
},
"security": {
"id": "10000"
},
"summary": "Main order flow broken",
"timetracking": {
"originalEstimate": "10",
"remainingEstimate": "5"
},
"versions": [
{
"id": "10000"
}
]
},
"update": {
"worklog": [
{
"add": {
"started": "2019-07-05T11:05:00.000+0000",
"timeSpent": "60m"
}
}
]
}
}'
I’ve always found that in more complicated situations as such, using a curl to get something similar helps flesh out the important fields
Let’s pull down an existing issue somewhat similar to that which I wish to create
$ curl --request GET -u isaac@freshbrewed.science:c2VyaW91c2x5IHRob3VnaC4gSSBhbSBub3QgYSBmYW4gb2YgSklSQSBhbnkgbW9yZS4gSXQgaXMgcmVhbGx5IHBvd2VyZnVsIGJ1dCB0aGV5IGtlZXAgbWFraW5nIGJhc2ljIGZlYXR1cmVzIHByZW1pdW0uIGJ1cm5lZCBhd2F5IG15IGdvb2Qgd2lsbAo= -H "Content-Type: application/json" https://freshbrewed.atlassian.net/rest/api/2/issue/TPK-1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 17470 0 17470 0 0 44794 0 --:--:-- --:--:-- --:--:-- 44794
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations,customfield_10010.requestTypePractice",
"id": "10000",
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000",
"key": "TPK-1",
"fields": {
"statuscategorychangedate": "2022-05-26T19:29:22.701-0500",
"issuetype": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/issuetype/10001",
"id": "10001",
"description": "Functionality or a feature expressed as a user goal.",
"iconUrl": "https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium",
"name": "Story",
"subtask": false,
"avatarId": 10315,
"hierarchyLevel": 0
},
"timespent": null,
"customfield_10030": null,
"customfield_10031": null,
"project": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/project/10000",
"id": "10000",
"key": "TPK",
"name": "MyJIRAProject",
"projectTypeKey": "software",
"simplified": false,
"avatarUrls": {
"48x48": "https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425",
"24x24": "https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=small",
"16x16": "https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=xsmall",
"32x32": "https://freshbrewed.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10425?size=medium"
}
},
"customfield_10032": null,
"customfield_10033": null,
"fixVersions": [],
"aggregatetimespent": null,
"customfield_10034": null,
"resolution": null,
"customfield_10035": null,
"customfield_10036": null,
"customfield_10029": null,
"resolutiondate": null,
"workratio": -1,
"lastViewed": "2023-08-28T06:17:29.298-0500",
"issuerestriction": {
"issuerestrictions": {},
"shouldDisplay": false
},
"watches": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/TPK-1/watchers",
"watchCount": 1,
"isWatching": true
},
"created": "2022-05-26T19:29:21.958-0500",
"customfield_10020": null,
"customfield_10021": null,
"customfield_10022": null,
"priority": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/priority/3",
"iconUrl": "https://freshbrewed.atlassian.net/images/icons/priorities/medium.svg",
"name": "Medium",
"id": "3"
},
"customfield_10023": null,
"customfield_10024": null,
"customfield_10025": null,
"customfield_10026": null,
"labels": [],
"customfield_10016": null,
"customfield_10017": null,
"customfield_10018": {
"hasEpicLinkFieldDependency": false,
"showField": false,
"nonEditableReason": {
"reason": "PLUGIN_LICENSE_ERROR",
"message": "The Parent Link is only available to Jira Premium users."
}
},
"customfield_10019": "0|hzzzzz:",
"aggregatetimeoriginalestimate": null,
"timeestimate": null,
"versions": [],
"issuelinks": [],
"assignee": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"updated": "2022-05-27T07:21:38.407-0500",
"status": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/status/10000",
"description": "",
"iconUrl": "https://freshbrewed.atlassian.net/",
"name": "Backlog",
"id": "10000",
"statusCategory": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/statuscategory/2",
"id": 2,
"key": "new",
"colorName": "blue-gray",
"name": "To Do"
}
},
"components": [],
"timeoriginalestimate": null,
"description": "Just a simple story",
"customfield_10010": null,
"customfield_10014": null,
"timetracking": {},
"customfield_10015": null,
"customfield_10005": null,
"customfield_10006": null,
"security": null,
"customfield_10007": null,
"customfield_10008": null,
"customfield_10009": null,
"aggregatetimeestimate": null,
"attachment": [],
"summary": "A Quick Story",
"creator": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"subtasks": [],
"reporter": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"aggregateprogress": {
"progress": 0,
"total": 0
},
"customfield_10001": null,
"customfield_10002": null,
"customfield_10003": null,
"customfield_10004": null,
"environment": null,
"duedate": null,
"progress": {
"progress": 0,
"total": 0
},
"votes": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/TPK-1/votes",
"votes": 0,
"hasVoted": false
},
"comment": {
"comments": [
{
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000/comment/10000",
"id": "10000",
"author": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"body": "[Isaac Johnson|https://gitlab.com/isaac.johnson] mentioned this issue in [a commit|https://gitlab.com/isaac.johnson/dockerWithTests2/-/commit/03d55c307675be860bb8204f4f2468b1bda44e31] of [Isaac Johnson / dockerWithTests2|https://gitlab.com/isaac.johnson/dockerWithTests2] on branch [isaac.johnson-main-patch-27196|https://gitlab.com/isaac.johnson/dockerWithTests2/-/tree/isaac.johnson-main-patch-27196]:{quote}Make a change for TPK-1.{quote}",
"updateAuthor": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"created": "2022-05-26T19:33:41.200-0500",
"updated": "2022-05-26T19:33:41.200-0500",
"jsdPublic": true
},
{
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000/comment/10001",
"id": "10001",
"author": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"body": "[Isaac Johnson|https://gitlab.com/isaac.johnson] mentioned this issue in [a merge request|https://gitlab.com/isaac.johnson/dockerWithTests2/-/merge_requests/6] of [Isaac Johnson / dockerWithTests2|https://gitlab.com/isaac.johnson/dockerWithTests2] on branch [isaac.johnson-main-patch-27196|https://gitlab.com/isaac.johnson/dockerWithTests2/-/tree/isaac.johnson-main-patch-27196]:{quote}Make a change for TPK-1.{quote}",
"updateAuthor": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"created": "2022-05-26T19:34:20.510-0500",
"updated": "2022-05-26T19:34:20.510-0500",
"jsdPublic": true
},
{
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000/comment/10002",
"id": "10002",
"author": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"body": "[Isaac Johnson|https://gitlab.com/isaac.johnson] mentioned this issue in [a commit|https://gitlab.com/isaac.johnson/dockerWithTests2/-/commit/091e47a83aeb6e81297592bad38607c45db9da31] of [Isaac Johnson / dockerWithTests2|https://gitlab.com/isaac.johnson/dockerWithTests2]:{quote}Make a change for TPK-1.{quote}",
"updateAuthor": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"created": "2022-05-27T07:21:37.467-0500",
"updated": "2022-05-27T07:21:37.467-0500",
"jsdPublic": true
},
{
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000/comment/10003",
"id": "10003",
"author": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"body": "[Isaac Johnson|https://gitlab.com/isaac.johnson] mentioned this issue in [a commit|https://gitlab.com/isaac.johnson/dockerWithTests2/-/commit/d91d3ecb18264ec37cad9860b0f00ae5c5591dfd] of [Isaac Johnson / dockerWithTests2|https://gitlab.com/isaac.johnson/dockerWithTests2] on branch [main|https://gitlab.com/isaac.johnson/dockerWithTests2/-/tree/main]:{quote}Merge branch 'isaac.johnson-main-patch-27196' into 'main'{quote}",
"updateAuthor": {
"self": "https://freshbrewed.atlassian.net/rest/api/2/user?accountId=618742213ae5230069d074cf",
"accountId": "618742213ae5230069d074cf",
"emailAddress": "isaac@freshbrewed.science",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"24x24": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"16x16": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png",
"32x32": "https://secure.gravatar.com/avatar/cec493a212fa197022dcdb32fa9c2a3b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIJ-2.png"
},
"displayName": "Isaac Johnson",
"active": true,
"timeZone": "America/Chicago",
"accountType": "atlassian"
},
"created": "2022-05-27T07:21:37.766-0500",
"updated": "2022-05-27T07:21:37.766-0500",
"jsdPublic": true
}
],
"self": "https://freshbrewed.atlassian.net/rest/api/2/issue/10000/comment",
"maxResults": 4,
"total": 4,
"startAt": 0
},
"worklog": {
"startAt": 0,
"maxResults": 20,
"total": 0,
"worklogs": []
}
}
}
I’ll trim the list way down and create a basic medium priority issue. Some things, like Parent, are locked out since I guess creating a parent link is a “premium feature” 🙄
$ curl --request POST \
--url 'https://freshbrewed.atlassian.net/rest/api/2/issue' \
--user 'isaac@freshbrewed.science:c2VyaW91c2x5IHRob3VnaC4gSSBhbSBub3QgYSBmYW4gb2YgSklSQSBhbnkgbW9yZS4gSXQgaXMgcmVhbGx5IHBvd2VyZnVsIGJ1dCB0aGV5IGtlZXAgbWFraW5nIGJhc2ljIGZlYXR1cmVzIHByZW1pdW0uIGJ1cm5lZCBhd2F5IG15IGdvb2Qgd2lsbAo=' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"components": [],
"description": "My Test Description.",
"issuetype": {
"id": "10001"
},
"labels": [
"AzDOFORM"
],
"priority": {
"name": "Medium",
"id": "3"
},
"project": {
"id": "10000"
},
"reporter": {
"id": "618742213ae5230069d074cf"
},
"summary": "This is a Test Issue"
}
}'
{"id":"10010","key":"TPK-11","self":"https://freshbrewed.atlassian.net/rest/api/2/issue/10010"}
I can now see an issue was created!
Adding to Azure Pipeline
Before I jump in to editing the pipeline, let’s save this to the group variable so we have a secret we can re-use
I’ll now be able to use -u $(JIRAAPIUSER):$(JIRAAPIKEY)
in the pipeline
I’ll basically add a new ‘createjira.sh’ similar to the WI and call that out in a second BASH step
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: $"
echo "summary: $"
echo "description: $"
cat >rawDescription <<EOOOOL
$
EOOOOL
cat >rawSummary <<EOOOOT
$
EOOOOT
cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
# if they left "dontemailme" assume they do not want RSVP
export USERTLD=`echo "" | sed 's/^.*@//'`
if [[ "$USERTLD" == "dontemailme.com" ]]; then
export RSVP="--fields Custom.RespondezSVP=false"
else
export RSVP="--fields Custom.RespondezSVP=true"
fi
cat >$(Pipeline.Workspace)/createwi.sh <<EOL
set -x
export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
az boards work-item create --title '`cat inputSummary`' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --fields System.Description='`cat inputDescription | tr -d '\n'`' $RSVP > azresp.json
EOL
chmod u+x $(Pipeline.Workspace)/createwi.sh
echo "createwi.sh:"
cat $(Pipeline.Workspace)/createwi.sh
# Create JIRA Issue
cat >$(Pipeline.Workspace)/createjira.sh <<EOT
set -x
curl --request POST \
--url 'https://freshbrewed.atlassian.net/rest/api/2/issue' \
--user '$(JIRAAPIUSER):$(JIRAAPIKEY)' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"components": [],
"description": "`cat inputDescription | tr -d '\n'` requested by $",
"issuetype": {
"id": "10001"
},
"labels": [
"AzDOFORM",
"$"
],
"priority": {
"name": "Medium",
"id": "3"
},
"project": {
"id": "10000"
},
"reporter": {
"id": "618742213ae5230069d074cf"
},
"summary": "`cat inputSummary`"
}
}'
EOT
chmod u+x $(Pipeline.Workspace)/createjira.sh
echo "have a nice day."
displayName: 'Check webhook payload'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createwi.sh'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Execute Create WI Script'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createjira.sh'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Execute Create JIRA Script'
- task: Bash@3
inputs:
targetType: 'inline'
script: 'cat $(Pipeline.Workspace)/azresp.json'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Show create output'
I’ll give it a test
This triggered a job
I found it created a Work Item (albeit missing description)
But the JIRA ticket was created exactly as I had hoped
Something that I’m realizing is that while the Description comes back with a GET as “system.description”, using it for the create doesn’t work as well
az boards work-item create --title 'This is a NEW test' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by isaac.johnson@gmail.com' --fields 'System.Description=Hopefully this will create both an Azure DevOps Work Item AND a JIRA ticket' --fields Custom.RespondezSVP=true
From the docs, we should be using “–description” (or “-d”).
I changed the line in the pipelines file to:
az boards work-item create --title '`cat inputSummary`' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --description '`cat inputDescription | tr -d '\n'`' $RSVP > azresp.json
Then it worked
As not to make you have to scroll up to piece that all together, the end result for our pipeline is
# Payload as sent by Web Form
resources:
webhooks:
- webhook: issuecollector
connection: issuecollector
pool:
vmImage: ubuntu-latest
variables:
- group: AZDOAutomations
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: $"
echo "summary: $"
echo "description: $"
cat >rawDescription <<EOOOOL
$
EOOOOL
cat >rawSummary <<EOOOOT
$
EOOOOT
cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
echo "input summary: `cat inputSummary`"
echo "input description: `cat inputDescription`"
# if they left "dontemailme" assume they do not want RSVP
export USERTLD=`echo "" | sed 's/^.*@//'`
if [[ "$USERTLD" == "dontemailme.com" ]]; then
export RSVP="--fields Custom.RespondezSVP=false"
else
export RSVP="--fields Custom.RespondezSVP=true"
fi
cat >$(Pipeline.Workspace)/createwi.sh <<EOL
set -x
export AZURE_DEVOPS_EXT_PAT=$(AZDOWIToken)
az boards work-item create --title '`cat inputSummary`' --type ExternalUserRequest --org https://dev.azure.com/princessking --project HelloWorldPrj --discussion 'requested by $' --description '`cat inputDescription | tr -d '\n'`' $RSVP > azresp.json
EOL
chmod u+x $(Pipeline.Workspace)/createwi.sh
echo "createwi.sh:"
cat $(Pipeline.Workspace)/createwi.sh
# Create JIRA Issue
cat >$(Pipeline.Workspace)/createjira.sh <<EOT
set -x
curl --request POST \
--url 'https://freshbrewed.atlassian.net/rest/api/2/issue' \
--user '$(JIRAAPIUSER):$(JIRAAPIKEY)' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"fields": {
"components": [],
"description": "`cat inputDescription | tr -d '\n'` requested by $",
"issuetype": {
"id": "10001"
},
"labels": [
"AzDOFORM",
"$"
],
"priority": {
"name": "Medium",
"id": "3"
},
"project": {
"id": "10000"
},
"reporter": {
"id": "618742213ae5230069d074cf"
},
"summary": "`cat inputSummary`"
}
}'
EOT
chmod u+x $(Pipeline.Workspace)/createjira.sh
echo "have a nice day."
displayName: 'Check webhook payload'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createwi.sh'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Execute Create WI Script'
- task: Bash@3
inputs:
filePath: '$(Pipeline.Workspace)/createjira.sh'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Execute Create JIRA Script'
- task: Bash@3
inputs:
targetType: 'inline'
script: 'cat $(Pipeline.Workspace)/azresp.json'
workingDirectory: '$(Pipeline.Workspace)'
displayName: 'Show create output'
Which leverages a group variable with our AzDO PAT, JIRA API Username and Key
Summary
This was mostly to illustrate a point about creating JIRA keys and Azure Work Items using forms. I used some of the content last week in an internal DevDays presentation. It was slated to post on the 22nd but frankly, the JIRA parts were not completed so I needed to wait till today.
The idea I want to use this for is to pair up Azure Work Items with JIRA, which many organizations use for tracking, but developers, at least in my case, are largely locked out of modifying (so it’s not very usable).
That way I can leverage Work Item Automations using something I control (Azure DevOps) with a system of record used to track team projects (JIRA). I could see an argument about only using one or the other. Frankly, from a design perspective, two sources of truth is foolish.
For me, to wholly link into JIRA, means creating a fleet of checks. Those might include:
- JIRA is still up
- New (mandatory) fields haven’t cropped up
- States are the same
- Projects haven’t changed
- API Keys still active (this is true of AzDO too)
I will own that a part of me is just old-man curmudgeon and doesn’t trust externally managed services to be a basis for a flow. To some degree, I need to ‘get over it’.
That said, I will still advise, from an architect (and old man yelling to get off his lawn) point of view; IF you base key automations on external ticketing/state systems, wrap a WHOLE LOT of checks around them so you can handle outages and updates gracefully.