Issue Collection Forms with AzDO and JIRA

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

/content/images/2023/08/azdojira-01.png

I’m already using a customized Agile flow

/content/images/2023/08/azdojira-02.png

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

/content/images/2023/08/azdojira-03.png

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”

/content/images/2023/08/azdojira-04.png

I can give it a name, icon and colour

/content/images/2023/08/azdojira-05.png

Adding and editing fields is pretty easy:

Now we have a basic work item template:

/content/images/2023/08/azdojira-06.png

Azure Webhook

To collect issues, we’ll need an exposed Azure DevOps Webhook.

These are exposed as “Service Connections” in the project

/content/images/2023/08/azdojira-07.png

We can use the type “Incoming Webhook”

/content/images/2023/08/azdojira-08.png

The secret is optional and just used to calculate a checksum.

/content/images/2023/08/azdojira-09.png

I can now see it listed

/content/images/2023/08/azdojira-10.png

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

/content/images/2023/08/azdojira-11.png

Make sure to uncheck “Add a README” which is checked by default

/content/images/2023/08/azdojira-12.png

We now get an empty repo page

/content/images/2023/08/azdojira-13.png

You’ll need GIT creds for the next step, so get them now

/content/images/2023/08/azdojira-14.png

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

/content/images/2023/08/azdojira-15.png

We’ll want to change these lines to point to our webhook

/content/images/2023/08/azdojira-16.png

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

/content/images/2023/08/azdojira-18.png

  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

/content/images/2023/08/azdojira-19.png

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

/content/images/2023/08/azdojira-20.png

We’ll choose Azure Repos

/content/images/2023/08/azdojira-21.png

and select the new repo

/content/images/2023/08/azdojira-22.png

We can just select Node.js (or starter pipeline as we’ll modify it)

/content/images/2023/08/azdojira-23.png

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”

/content/images/2023/08/azdojira-24.png

The defaults are fine

/content/images/2023/08/azdojira-25.png

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

/content/images/2023/08/azdojira-26.png

This should kick off a run

/content/images/2023/08/azdojira-27.png

Which, for me, built without issue

/content/images/2023/08/azdojira-28.png

Let’s go look at the published artifact

/content/images/2023/08/azdojira-29.png

Here we can see the generated files

/content/images/2023/08/azdojira-30.png

We can download to view locally

/content/images/2023/08/azdojira-31.png

In this first pass, I found it generated the original index.html which was not what I expected

/content/images/2023/08/azdojira-32.png

I realized that the source file needs to be index.html

/content/images/2023/08/azdojira-33.png

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

/content/images/2023/08/azdojira-34.png

What I realized is that parcel has an output option that can specify the file to create

/content/images/2023/08/azdojira-35.png

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

/content/images/2023/08/azdojira-36.png

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

/content/images/2023/08/azdojira-37.png

This time I’ll select an Existing Azure Pipelines YAML file

/content/images/2023/08/azdojira-38.png

It should detect the form pipelines YAML

/content/images/2023/08/azdojira-39.png

This time I won’t run, I’ll just save

/content/images/2023/08/azdojira-40.png

I can test with a local snapshot of the issue.html

We can see the output in the job log:

/content/images/2023/08/azdojira-41.png

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

/content/images/2023/08/azdojira-42.png

I can give, at most, a year on a PAT

/content/images/2023/08/azdojira-43.png

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

/content/images/2023/08/azdojira-44.png

I’ll add the token here

/content/images/2023/08/azdojira-45.png

(Don’t forget to hit save at the top)

I can now use this in the pipeline

/content/images/2023/08/azdojira-46.png

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

/content/images/2023/08/azdojira-47.png

Note, we need to approve use of this pipeline

/content/images/2023/08/azdojira-48.png

Specifically, we need to permit use of the Group Variable and Azure Sub

/content/images/2023/08/azdojira-49.png

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)'
    

/content/images/2023/08/azdojira-50.png

This created a feature, which is what the az command requested.

/content/images/2023/08/azdojira-51.png

But we want to use our new issue type

/content/images/2023/08/azdojira-52.png

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

/content/images/2023/08/azdojira-53.png

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

/content/images/2023/08/azdojira-54.png

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

/content/images/2023/08/azdojira-55.png

To engage with JIRA, we need an API key. We’ll find that under our Profile in the Security Tab

/content/images/2023/08/azdojira-56.png

Click “Create API Token”

/content/images/2023/08/azdojira-57.png

Give it a name, then click Create

/content/images/2023/08/azdojira-58.png

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.

/content/images/2023/08/azdojira-59.png

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

/content/images/2023/08/azdojira-60.png

$ 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!

/content/images/2023/08/azdojira-61.png

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

/content/images/2023/08/azdojira-62.png

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

/content/images/2023/08/azdojira-63.png

This triggered a job

/content/images/2023/08/azdojira-64.png

I found it created a Work Item (albeit missing description)

/content/images/2023/08/azdojira-65.png

But the JIRA ticket was created exactly as I had hoped

/content/images/2023/08/azdojira-66.png

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

/content/images/2023/08/azdojira-67.png

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

/content/images/2023/08/azdojira-68.png

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.

AzDO AzureDevOps JIRA Forms

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes