How we eliminated "what are we deploying?" from our release process without sacrificing our manual approval gate
The 3 PM Panic
We've all been there. It's 3 PM on a Thursday, and the deployment dashboard shows a green build waiting for production approval. The release manager opens Slack and types the dreaded question:
"What are we deploying?"
What follows is 15 minutes of frantic channel scrolling, digging through commit histories, and playing detective across Jira, GitHub, and Slack. Eventually, someone pieces together that the release contains three bug fixes, two features, andβsurpriseβa database migration that needs special attention.
This scene played out in our team at least twice a week. We had a robust manual approval gate for production (security requirements meant we couldn't automate that final step), but we had zero visibility into what we were actually approving.
The manual approval wasn't the problem. The context gap was.
The Problem We Were Actually Solving
Let's be clear: we weren't trying to remove the manual approval gate. In our regulated industry, that wasn't an option. But the approval was meaningless when approvers had to become digital archaeologists just to understand what they were signing off on.
Our pain points were specific:
Context switching overhead - Approvers spent 5-10 minutes gathering information per deployment
Incomplete release notes - We relied on developers remembering to update a CHANGELOG
Missing dependencies - Database migrations, environment variable changes, or third-party service updates often got lost in the shuffle
Audit trail gaps - When something went wrong, tracing back to the original requirement was painful
"Surprise" deployments - Team members would discover their changes were in production only when a user reported a bug
The manual approval gate wasn't going anywhere. But the chaos around it? That we could fix.
The Solution: Automated Jira Ticket Linking
The idea was simple: make every deployment self-documenting by automatically pulling in Jira ticket context.
Here's what we built:
- Jira Integration in CI/CD We added a step in our CI pipeline (we use GitHub Actions) that:
Parses commit messages for Jira ticket keys (e.g., PROJ-123)
Fetches ticket metadata via Jira REST API
Aggregates all unique tickets into a deployment manifest
yaml
.github/workflows/deploy.yml snippet
name: Fetch Jira Tickets
id: jira
run: |
TICKETS=$(git log ${{ github.sha }} --pretty=format:"%s" | grep -oE '[A-Z]+-[0-9]+' | sort -u)
echo "tickets=$TICKETS" >> $GITHUB_OUTPUT-
name: Get Ticket Details
if: steps.jira.outputs.tickets != ''
run: |Fetch details for each ticket
for ticket in ${{ steps.jira.outputs.tickets }}; do
curl -H "Authorization: Bearer $JIRA_TOKEN" \
"https://jira.company.com/rest/api/2/issue/$ticket" \
| jq '.fields | {summary: .summary, status: .status.name, assignee: .assignee.displayName, priority: .priority.name}'
done The Deployment Manifest
Instead of just a version number, our deployment pipeline now generates a rich manifest:
json
{
"deployment_id": "deploy-20240115-001",
"version": "v2.3.1",
"timestamp": "2024-01-15T14:30:00Z",
"jira_tickets": [
{
"key": "PROJ-123",
"summary": "Fix payment calculation for EU customers",
"status": "Done",
"assignee": "Alex",
"priority": "High",
"components": ["Payment API", "Backend"],
"fixVersion": "v2.3.1"
},
{
"key": "PROJ-124",
"summary": "Add dark mode toggle to dashboard",
"status": "Done",
"assignee": "Jamie",
"priority": "Medium",
"components": ["UI", "Frontend"],
"fixVersion": "v2.3.1"
}
],
"release_notes": "Bug fixes: PROJ-123. Features: PROJ-124.",
"db_migrations": ["ALTER TABLE payments ADD COLUMN tax_rate"],
"env_changes": ["New env var: PAYMENT_TAX_ENABLED=true"]
}
- Automated Release Notes The pipeline generates formatted release notes from the Jira data and posts them to:
A dedicated Slack channel
The GitHub release page
Our internal documentation site
Crucially: the manual approval request itself
- Approval with Context When an approver now receives the manual approval request, they see:
text
π¦ Deployment v2.3.1 to Production
ββββββββββββββββββββββββββββββββ
π Requested: 2024-01-15 14:30 UTC
π€ Requestor: deploy-bot
π Tickets in this release:
β
PROJ-123: Fix payment calculation for EU customers (High)
β
PROJ-124: Add dark mode toggle to dashboard (Medium)
β
PROJ-125: Update error logging for API timeouts (Low)
β οΈ Database Changes:
- ALTER TABLE payments ADD COLUMN tax_rate (auto-rollback available)
βοΈ Environment Changes:
- PAYMENT_TAX_ENABLED=true (new)
π View full details: https://deployments.internal/release/v2.3.1
[Approve] [Reject] [View Details]
No more context gathering. No more Slack panic.
What We Built (The Technical Details)
Here's the architecture we settled on:
Pipeline Integration
text
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Code Push ββββββΆβ CI Build ββββββΆβ Jira Fetch β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Approval βββββββ Deploy to βββββββ Generate β
β Request β β Staging β β Manifest β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ
β Deploy to β
β Production β
βββββββββββββββ
Key Components
- Git Commit Parser
python
import re
from typing import Set
def extract_jira_tickets(commits: List[str]) -> Set[str]:
"""Extract JIRA ticket keys from commit messages."""
pattern = r'[A-Z]{2,10}-\d+'
tickets = set()
for commit in commits:
tickets.update(re.findall(pattern, commit))
return tickets
- Jira API Client
python
class JiraClient:
def get_ticket_details(self, ticket_key: str) -> dict:
response = requests.get(
f"{self.base_url}/rest/api/2/issue/{ticket_key}",
headers={"Authorization": f"Bearer {self.token}"}
)
data = response.json()
return {
"key": ticket_key,
"summary": data["fields"]["summary"],
"status": data["fields"]["status"]["name"],
"assignee": data["fields"]["assignee"]["displayName"] if data["fields"]["assignee"] else None,
"priority": data["fields"]["priority"]["name"],
"components": [c["name"] for c in data["fields"]["components"]],
"fixVersion": data["fields"]["fixVersions"][0]["name"] if data["fields"]["fixVersions"] else None
}
- Manifest Generator
python
def generate_deployment_manifest(environment, version, tickets):
manifest = {
"deployment_id": f"deploy-{datetime.now().strftime('%Y%m%d-%H%M')}",
"environment": environment,
"version": version,
"timestamp": datetime.now().isoformat(),
"jira_tickets": [JiraClient().get_ticket_details(t) for t in tickets],
"release_notes": generate_release_notes(tickets),
"db_migrations": detect_migrations(), # Custom detection logic
"env_changes": detect_env_changes() # Custom detection logic
}
return manifest
- Slack Notification
python
def send_approval_request(manifest):
blocks = [
{
"type": "header",
"text": {"type": "plain_text", "text": f"π¦ Deployment {manifest['version']} to Production"}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"Tickets: {len(manifest['jira_tickets'])}"},
{"type": "mrkdwn", "text": f"DB Changes: {len(manifest['db_migrations'])}"},
]
}
]
# Add ticket details
for ticket in manifest['jira_tickets'][:5]: # Show top 5
blocks.append({
"type": "section",
"text": {"type": "mrkdwn", "text": f"β’ *{ticket['key']}*: {ticket['summary']} ({ticket['priority']})"}
})
if len(manifest['jira_tickets']) > 5:
blocks.append({
"type": "section",
"text": {"type": "mrkdwn", "text": f"... and {len(manifest['jira_tickets']) - 5} more"}
})
# Approval buttons
blocks.append({
"type": "actions",
"elements": [
{"type": "button", "text": {"type": "plain_text", "text": "β
Approve"},
"value": f"approve_{manifest['deployment_id']}", "style": "primary"},
{"type": "button", "text": {"type": "plain_text", "text": "β Reject"},
"value": f"reject_{manifest['deployment_id']}", "style": "danger"},
]
})
return blocks
The Results (4 Months Later)
What Improved
Approval time decreased from ~8 minutes to under 1 minute - Approvers no longer needed to hunt for context. Everything they needed was in the request.
"What are we deploying?" questions dropped by 95% - The first week we deployed this, I kept waiting for the Slack message. It never came.
Audit compliance became effortless - Every deployment now has a complete audit trail linking commits β tickets β approvals β production.
On-call incidents became easier to triage - When something broke, we could instantly see which tickets were in the release and roll back specific changes.
Developers started writing better commit messages - Since commit messages are parsed, devs began naturally including ticket keys even more consistently.
What We Learned
Ticket keys in commits are non-negotiable - We added a pre-commit hook to prevent commits without ticket keys in main branches.
Not every deployment needs Jira tickets - Hotfixes and infrastructure changes don't always map to tickets. We added the ability to skip ticket linking with [NO-TICKET] in the commit message.
Database migrations need special handling - We added explicit detection for migration files and flagged them prominently in the approval request.
The manifest is now a single source of truth - Teams started referencing the deployment manifest for everything from rollback decisions to release notes generation.
The Code (If You Want to Try This)
We open-sourced the core components. Here's the simplified version you can adapt:
GitHub Action for Jira Linking
yaml
name: Generate Deployment Context
on:
push:
branches: [main, staging]
jobs:
generate-context:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Get all commit history
- name: Extract tickets from commits
id: tickets
run: |
COMMITS=$(git log ${{ github.sha }} --pretty=format:"%s")
TICKETS=$(echo "$COMMITS" | grep -oE '[A-Z]+-[0-9]+' | sort -u | tr '\n' ',')
echo "tickets=${TICKETS%,}" >> $GITHUB_OUTPUT
- name: Fetch Jira Details
if: steps.tickets.outputs.tickets != ''
uses: actions/github-script@v6
with:
script: |
const tickets = '${{ steps.tickets.outputs.tickets }}'.split(',');
// Call Jira API for each ticket
// Store results in deployment-manifest.json
- name: Create Deployment Manifest
run: |
echo "Deployment manifest created with Jira context"
- name: Upload Manifest
uses: actions/upload-artifact@v3
with:
name: deployment-manifest
path: deployment-manifest.json
Simple Jira Fetch Script
bash
!/bin/bash
fetch-jira.sh - Fetch ticket details from Jira
TICKETS=$(git log origin/main..HEAD --pretty=format:"%s" | grep -oE '[A-Z]+-[0-9]+' | sort -u)
if [ -z "$TICKETS" ]; then
echo "No Jira tickets found in commits"
exit 0
fi
echo "Found tickets: $TICKETS"
for TICKET in $TICKETS; do
echo "Fetching $TICKET..."
curl -s -H "Authorization: Bearer $JIRA_TOKEN" \
"https://jira.company.com/rest/api/2/issue/$TICKET" \
| jq '.key + ": " + .fields.summary'
done
The Bigger Picture
This project taught us something important: automation isn't just about speedβit's about clarity.
We didn't remove the manual approval gate because it serves a legitimate purpose. But we made it meaningful by providing approvers with the context they need to make informed decisions.
The result isn't just faster deployments. It's better deployments. When people understand what they're approving, they catch issues earlier. They spot dependencies. They ask better questions.
And when 3 PM rolls around on a Thursday, there's no Slack panic. Just a clear, informed approval request that says: "Here's exactly what we're deploying, and here's why."
Want to Build Your Own?
Start small:
Parse commit messages for ticket keys
Display that list in your CI/CD dashboard
Add ticket summaries to Slack approval messages
Build a full manifest with ticket details
Add automated release notes
Protip: Don't over-engineer it. A simple comma-separated list of tickets in your approval request is already a huge improvement over nothing.
Have you solved a similar problem? How did you handle the balance between automation and human oversight? Drop your thoughts in the comments!
Top comments (0)