An issue that has come up in my professional life recently a few times has been how to take in bugs, features, etc from users external to a private Azure DevOps organization or project. Azure DevOps work items are fantastic, but Azure Boards assume those creating and commenting have access to that AzDO Project and more over, to create, users have contributor access which requires a degree of licensing.
How can we ingest work or feedback akin to the JIRA Issue Collector or similar to generic service now help desk forms?
Today we will review a method utilizing a static form that leverages javascript to create and transmit a JSON payload to an Azure DevOps webhook. This in turn will then process it into a work item on the users behalf.
Setup
First we we need to create a webhook:
I generally name the hook and the connection the same for simplicity:
Static Website
I started with this project : https://github.com/glorious73/submitformtojsonapi . It is a good example of a generated static app that can POST with a JSON payload. The key areas that need to be updated are app.js and the fields used in the form.
# from src/js/app.js
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/feedbackForm?api-version=6.0-preview`, headers, jsonFormData); // Uses JSON Placeholder
//console.log(response);
// 2.5 Inform user of result
if(response) {
window.location.href = `/success.html`;
} else {
alert(`An error occured.`);
}
}
Also, in the js/service/FetchService.js we can stub out the processing of the response (since our example does not expect proper JSON back):
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)
});
console.log(rawResponse);
// Webhooks are not JSON
// const content = await rawResponse.json();
//return content;
return rawResponse;
}
catch(err) {
console.error(`Error at fetch POST: ${err}`);
throw err;
}
}
index.html (which i renamed feedback.html):
<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">
Submit
</button>
</div>
</form>
</div>
<script src="js/app.js"></script>
I added one entry to the CSS for a larger textarea you can see referenced above
.ta-100 {
width: 100%;
height: 200px;
}
Lastly, to really test we need to expose any HTML file, so update the "dev" line in the package.json
"scripts": {
"dev": "npm run clean && parcel src/*.html --out-dir dev feedback.html",
Setting up the pipeline
You can test the code above using npm run dev
which will run a server on http://localhost:1234.
To setup build and release, I created a pipeline that could use 'npm run build' to build the "dist" folder:
trigger:
- main
pool:
vmImage: ubuntu-latest
variables:
- name: awsBucket
value: freshbrewed-test
- name: awsFinalBucket
value: freshbrewed.science
- name: awsCreds
value: freshbrewed
jobs:
- job: buildanddeploy
displayName: "Build and Deploy"
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.*'
checkLatest: true
- script: |
set -x
npm install
npm run build
# copy to artifact staging
cp -rf ./dist $(Build.ArtifactStagingDirectory)
displayName: 'Check webhook payload'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
- task: AmazonWebServices.aws-vsts-tools.S3Upload.S3Upload@1
displayName: 'S3 Upload: ${{ variables.awsBucket }}'
inputs:
awsCredentials: ${{ variables.awsCreds }}
regionName: 'us-east-1'
bucketName: ${{ variables.awsBucket }}
sourceFolder: '$(Build.ArtifactStagingDirectory)/dist'
globExpressions: '**/*.*'
filesAcl: 'public-read'
- job: waitforvalidation
pool: server
dependsOn: buildanddeploy
timeoutInMinutes: 6440
steps:
- task: ManualValidation@0
inputs:
notifyUsers: 'isaac.johnson@gmail.com'
instructions: 'Please validate the build configuration and resume'
- job: deploytoprod
dependsOn: waitforvalidation
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'drop'
downloadPath: '$(System.ArtifactsDirectory)'
- task: AmazonWebServices.aws-vsts-tools.S3Upload.S3Upload@1
displayName: 'S3 Upload: ${{ variables.awsBucket }}'
inputs:
awsCredentials: ${{ variables.awsCreds }}
regionName: 'us-east-1'
bucketName: ${{ variables.awsFinalBucket }}
sourceFolder: '$(System.ArtifactsDirectory)/drop/dist'
globExpressions: '**/*.*'
filesAcl: 'public-read'
There will be a manual intervention before it deploys to prod. Be aware if using the ManualValidation task that it must be tied to a serverless agent (like 'server').
Processing the webhook
We need a PAT just for making work items:
Note: PATs must have expiry so plan to need to renew later:
We can now save it into a new library:
Processing Pipeline
Let's now create a pipeline for processing the form
# Payload as sent by Web Form
resources:
webhooks:
- webhook: feedbackForm
connection: feedbackForm
pool:
vmImage: ubuntu-latest
variables:
- group: AZDOAutomations
steps:
- script: |
echo Add other tasks to build, test, and deploy your project.
echo "userId: ${{ parameters.feedbackForm.userId }}"
echo "summary: ${{ parameters.feedbackForm.summary }}"
echo "description: ${{ parameters.feedbackForm.description }}"
cat >rawDescription <<EOOOOL
${{ parameters.feedbackForm.description }}
EOOOOL
cat >rawSummary <<EOOOOT
${{ parameters.feedbackForm.summary }}
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=$(AZDOTOKEN)
az boards work-item create --title '`cat inputSummary`' --type Feature --org https://dev.azure.com/princessking --project ghost-blog --discussion 'requested by ${{ parameters.feedbackForm.userId }}' --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: 'My-Azure-SubNew'
scriptType: 'bash'
scriptLocation: 'scriptPath'
scriptPath: '$(Pipeline.Workspace)/createwi.sh'
Validation
We can now trigger a pipeline by filling in the form and clicking submit: http://freshbrewed-test.s3-website-us-east-1.amazonaws.com/feedback.html
which if deployed properly will reply with a success when submit is clicked
This then fires the pipeline tied to the webhook
and I immediately see an updated ticket
Satisfied all is good, i can go back and approve the manual intervention
clicking the link brings me to a pipeline for which i can reject or approve:
we can now see it deployed to production
and our form is live (go try it if you want): https://freshbrewed.science/feedback.html
Testing
and we can submit a ticket
which triggers the pipeline
which lastly creates a feature for me to consider going forward
I have in other cases setup Azure Logic Apps to trigger O365 emails. In our case, the only post notification i have presently set up is to slack:
And Datadog has an event alert setup on Production Builds
Summary
We used a simple static HTML form to create a JSON payload and transmit that to an Azure DevOps webhook. We were able to test with a basic parcel server locally (http://localhost:1234).
With webhooks, we can test them locally by making a values.json file with the expected payload and hitting the webhook with curl. e.g.
curl -v -X POST -d @values.json -H "Content-type: application/json" https://dev.azure.com/princessking/_apis/public/distributedtask/webhooks/processform?api-version=6.0-preview
We used a Manual Intervention step to pause our pipeline between dev and prod sites and lastly, the already existing service connections triggered notifications on events.
This now allows feedback externally into a Private Azure DevOps project and we can use this a scalable solution for ingestion forms.