The case for doing this is short. This is the implementation. One workflow file, two jobs: the first flags TODO, WORKAROUND, and TBC comments on any active pull request, the second creates a Jira ticket when any of them merge without being resolved. Four setup steps.
1. Generate a Jira API token
Jira’s API requires a token, not your password. Go to your Atlassian account security page, generate a new token, and copy it immediately – you only see it once.
https://id.atlassian.com/manage-profile/security/api-tokens
2. Add the secrets to GitHub
Go to your repository’s Settings > Secrets and variables > Actions and add three secrets. The base URL is your Atlassian instance domain with no trailing slash.
JIRA_BASE_URL https://yourcompany.atlassian.net
JIRA_USER_EMAIL you@yourcompany.com
JIRA_API_TOKEN (the token from step 1)
3. Create the workflow file
Create .github/workflows/tech-debt.yml with the content below. The workflow uses two separate triggers because pull_request_target is what gives the Jira job access to your secrets when a PR merges – using pull_request for both would work for the comment job but silently fail on the ticket creation.
Both scan jobs filter to added lines only (grep -E '^\+[^+]'), so they flag new debt introduced by the PR, not keywords already sitting in the codebase. The Jira ticket includes the matching snippets and a link back to the PR, so whoever picks up the ticket has the context they need.
name: Handle Code Technical Debt
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_target:
types: [closed]
jobs:
pr-comment:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for new keywords
id: check_keywords
run: |
git fetch origin ${{ github.base_ref }}
KEYWORDS=$(git diff origin/${{ github.base_ref }}...HEAD | grep -E '^\+[^+]' | grep -Ei 'workaround|todo|tbc' || true)
if [ -n "$KEYWORDS" ]; then
echo "MATCH_FOUND=true" >> $GITHUB_OUTPUT
FOUND_ITEMS=""
echo "$KEYWORDS" | grep -qi 'workaround' && FOUND_ITEMS="$FOUND_ITEMS WORKAROUND,"
echo "$KEYWORDS" | grep -qi 'todo' && FOUND_ITEMS="$FOUND_ITEMS TODO,"
echo "$KEYWORDS" | grep -qi 'tbc' && FOUND_ITEMS="$FOUND_ITEMS TBC,"
FOUND_ITEMS=$(echo "$FOUND_ITEMS" | sed 's/,$//')
echo "FOUND_ITEMS=$FOUND_ITEMS" >> $GITHUB_OUTPUT
fi
- name: Comment on PR
if: steps.check_keywords.outputs.MATCH_FOUND == 'true'
uses: actions/github-script@v7
with:
script: |
const found = "${{ steps.check_keywords.outputs.FOUND_ITEMS }}".trim();
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ **Technical Debt Tags Detected:** This PR introduces the following tags: **${found}**.\n\nResolve before merging if possible. Any that remain will have a Jira ticket created automatically on merge.`
})
jira-tracking:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan merged keywords
id: scan_merged
run: |
KEYWORDS=$(git diff HEAD^1 HEAD | grep -E '^\+[^+]' | grep -Ei 'workaround|todo|tbc' || true)
if [ -n "$KEYWORDS" ]; then
echo "MATCH_FOUND=true" >> $GITHUB_OUTPUT
FOUND_ITEMS=""
echo "$KEYWORDS" | grep -qi 'workaround' && FOUND_ITEMS="$FOUND_ITEMS WORKAROUND,"
echo "$KEYWORDS" | grep -qi 'todo' && FOUND_ITEMS="$FOUND_ITEMS TODO,"
echo "$KEYWORDS" | grep -qi 'tbc' && FOUND_ITEMS="$FOUND_ITEMS TBC,"
FOUND_ITEMS=$(echo "$FOUND_ITEMS" | sed 's/,$//' | xargs)
echo "FOUND_ITEMS=$FOUND_ITEMS" >> $GITHUB_OUTPUT
CLEANED_KEYWORDS=$(echo "$KEYWORDS" | sed 's/"/\\"/g' | head -n 5)
echo "JIRA_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "Outstanding debt tags merged into main (${FOUND_ITEMS}) via PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV
echo -e "\nSnippets:\n$CLEANED_KEYWORDS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
fi
- name: Create Jira Ticket
if: steps.scan_merged.outputs.MATCH_FOUND == 'true'
uses: atlassian/gajira-create@v3
with:
project: 'PROJ'
issuetype: 'Task'
summary: 'Tech Debt: Resolve ${{ steps.scan_merged.outputs.FOUND_ITEMS }} from PR #${{ github.event.pull_request.number }}'
description: ${{ env.JIRA_DESCRIPTION }}
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
4. Set your project key and issue type
Two values to change before committing. Both are in the jira-tracking job under the Create Jira Ticket step.
with:
project: 'PROJ' # your Jira project key, e.g. ENG, PLATFORM, DATA
issuetype: 'Task' # Task, Story, Bug -- whatever your backlog uses
To add more keywords, edit the grep -Ei pattern in both scan steps.
grep -Ei 'workaround|todo|tbc|fixme|hack'
5. Verify
Open a PR that adds any of the flagged keywords to a file. The pr-comment job should post a comment within a minute or two. Merge the PR and check your Jira backlog for the new ticket. If the comment fires but no ticket appears, check the Actions log for the Create Jira Ticket step – it will say exactly what Jira rejected.
The comment appears on the PR. The ticket lands in the backlog. From that point, the debt is tracked whether or not anyone remembered to track it.
Full script
# .github/workflows/tech-debt.yml
name: Handle Code Technical Debt
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_target:
types: [closed]
jobs:
pr-comment:
if: github.event_name == ‘pull_request’
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for new keywords
id: check_keywords
run: |
git fetch origin ${{ github.base_ref }}
KEYWORDS=$(git diff origin/${{ github.base_ref }}...HEAD | grep -E '^\+[^+]' | grep -Ei 'workaround|todo|tbc' || true)
if [ -n "$KEYWORDS" ]; then
echo "MATCH_FOUND=true" >> $GITHUB_OUTPUT
FOUND_ITEMS=""
echo "$KEYWORDS" | grep -qi 'workaround' && FOUND_ITEMS="$FOUND_ITEMS WORKAROUND,"
echo "$KEYWORDS" | grep -qi 'todo' && FOUND_ITEMS="$FOUND_ITEMS TODO,"
echo "$KEYWORDS" | grep -qi 'tbc' && FOUND_ITEMS="$FOUND_ITEMS TBC,"
FOUND_ITEMS=$(echo "$FOUND_ITEMS" | sed 's/,$//')
echo "FOUND_ITEMS=$FOUND_ITEMS" >> $GITHUB_OUTPUT
fi
- name: Comment on PR
if: steps.check_keywords.outputs.MATCH_FOUND == 'true'
uses: actions/github-script@v7
with:
script: |
const found = "${{ steps.check_keywords.outputs.FOUND_ITEMS }}".trim();
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ **Technical Debt Tags Detected:** This PR introduces the following tags: **${found}**.\n\nResolve before merging if possible. Any that remain will have a Jira ticket created automatically on merge.`
})
jira-tracking:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan merged keywords
id: scan_merged
run: |
KEYWORDS=$(git diff HEAD^1 HEAD | grep -E '^\+[^+]' | grep -Ei 'workaround|todo|tbc' || true)
if [ -n "$KEYWORDS" ]; then
echo "MATCH_FOUND=true" >> $GITHUB_OUTPUT
FOUND_ITEMS=""
echo "$KEYWORDS" | grep -qi 'workaround' && FOUND_ITEMS="$FOUND_ITEMS WORKAROUND,"
echo "$KEYWORDS" | grep -qi 'todo' && FOUND_ITEMS="$FOUND_ITEMS TODO,"
echo "$KEYWORDS" | grep -qi 'tbc' && FOUND_ITEMS="$FOUND_ITEMS TBC,"
FOUND_ITEMS=$(echo "$FOUND_ITEMS" | sed 's/,$//' | xargs)
echo "FOUND_ITEMS=$FOUND_ITEMS" >> $GITHUB_OUTPUT
CLEANED_KEYWORDS=$(echo "$KEYWORDS" | sed 's/"/\\"/g' | head -n 5)
echo "JIRA_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "Outstanding debt tags merged into main (${FOUND_ITEMS}) via PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV
echo -e "\nSnippets:\n$CLEANED_KEYWORDS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
fi
- name: Create Jira Ticket
if: steps.scan_merged.outputs.MATCH_FOUND == 'true'
uses: atlassian/gajira-create@v3
with:
project: 'PROJ'
issuetype: 'Task'
summary: 'Tech Debt: Resolve ${{ steps.scan_merged.outputs.FOUND_ITEMS }} from PR #${{ github.event.pull_request.number }}'
description: ${{ env.JIRA_DESCRIPTION }}
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}</code></pre>