Skip to content

Repository files navigation

    ███████╗██╗  ██╗██████╗  ██████╗ ████████╗
    ╚══███╔╝██║ ██╔╝██╔══██╗██╔═══██╗╚══██╔══╝
  ███╔╝ █████╔╝ ██████╔╝██║   ██║   ██║
 ███╔╝  ██╔═██╗ ██╔══██╗██║   ██║   ██║
███████╗██║  ██╗██████╔╝╚██████╔╝   ██║
╚══════╝╚═╝  ╚═╝╚═════╝  ╚═════╝    ╚═╝

The Zookeeper's Faithful Morning Sentinel

Python GitHub Actions Jira Slack License


🦁 What Is ZKBot?

ZKBot is an automated triage assistant built for the Zookeeper team. Every weekday morning at 7:30 AM CDT, it silently wakes up, scans the PB Jira board, and delivers a clean Slack report of every ticket that needs human attention — before anyone has had their first coffee.

No dashboards to check. No filters to remember. No tickets falling through the cracks. Just a friendly nudge, right where the team already lives.

"A good bot should feel like a great teammate — one that handles the boring stuff so you can focus on the interesting stuff."


🗺️ How It Works

flowchart TD
    A([⏰ 7:30 AM CDT\nGitHub Actions Cron]) --> B[Checkout Repo]
    B --> C[Install Dependencies]
    C --> D[Run main.py]
    D --> E{Authenticate\nwith Jira API}
    E --> F1[Query Filter 18934\nBacklog, No Team]
    E --> F2[Query Filter 18935\nReviewed, No Team, Stale]
    E --> F3[Query Filter 18936\nAll PB, No Team]
    E --> F4[Query Filter 20496\nStale Backlog, Team Assigned]
    F1 --> G1{No linked\nissues?}
    F2 --> G2{No linked\nissues?}
    F3 --> G3{Has linked\nissues?}
    F4 --> G4{Linked work item\nstatuses?}
    G1 -- Yes --> S1[Needs Attention]
    G2 -- Yes --> S2[Reviewed, No Movement]
    G3 -- Yes --> S3[Warning]
    G4 -- None --> S4[Warning: No Linked Work]
    G4 -- Any Backlog/To Do --> S5[Warning: Pending Work]
    G4 -- All Done --> S6[Info: Work Complete]
    S1 --> M1[Build Triage Report]
    S2 --> M1
    S3 --> M1
    S4 --> M2[Build Backlog Report]
    S5 --> M2
    S6 --> M2
    M1 --> N{Any sections\nnon-empty?}
    M2 --> N
    N -- Yes --> O[POST to Slack\nWorkflow Webhook]
    N -- No --> P[POST friendly\nall-clear message]
    O --> Q([📬 Zookeeper team\nreceives Slack message])
    P --> Q
Loading

🔍 Triage Logic

Triage criteria live entirely in Jira saved filters — no hardcoded JQL in the code. Each filter is managed directly in Jira and Python does a secondary pass to check for linked issues.

Filter Link What it captures Python keeps
18934 — ZKBot - Backlogged items Open in Jira Backlog status, no tech team tickets with no linked issues
18935 — ZKBot - Reviewed/More Info Needed Open in Jira Reviewed or More Info Needed, no tech team, not updated in 2+ days tickets with no linked issues
18936 — ZKBot - No Tech Team Open in Jira All PB board items, no tech team tickets WITH linked issues
20496 — ZKBot - Stale Backlog, Tech Team Assigned Open in Jira Backlog status, tech team assigned split three ways by linked work item status

Why linked issues in Python and not in the filter? Standard Jira Cloud JQL has no operator for filtering by whether linked issues exist. If ScriptRunner is ever added to the instance, issueFunction in issuesWithoutLinks() could move this check into the filter itself.

Filter 20496 — Stale Backlog Detection

Filter 20496 JQL: project in ("PB") AND status in (Backlog) AND "Technical Team[Dropdown]" IS NOT EMPTY ORDER BY created DESC

Tickets from this filter are further split in Python by the status of their linked work items:

Case Condition Bucket
A No linked work items ⚠️ Warning — same treatment as "no tech team"
B Has linked work items, all have status name exactly Done ℹ️ Informational — still open but work is complete
C Has linked work items, any has status Backlog or To Do ⚠️ Warning — pending work still assigned
Neither (e.g. only "In Progress" linked items) Omitted — not reported

C is checked before B, since a ticket can't simultaneously have a pending linked item and have all linked items done.


📬 The Slack Message

ZKBot posts via a Slack Workflow Builder webhook. The workflow receives a single text variable — zkbotpayload — and routes it to the Zookeeper team's channel.

When tickets need attention:

Triage Report

:red_circle: 2 ticket(s) need attention — Backlog && No Tickets && No Team
- PB-2300 (5d) — Some unassigned backlog ticket
  https://pagerinc.atlassian.net/browse/PB-2300

:hourglass_flowing_sand: 1 ticket(s) need attention - Reviewed | More Information Needed - No Recent Updates && No Tickets && No Team
- PB-2150 (3d) — Stale reviewed ticket
  https://pagerinc.atlassian.net/browse/PB-2150

:warning: 2 ticket(s) have linked tickets but No Tech Team Assigned
- PB-2090 — [INT] Some linked but unassigned ticket
  https://pagerinc.atlassian.net/browse/PB-2090


Backlog Report

:warning: 1 ticket(s) — No Linked Work Items
- CHA PB-2200 (56d) — Some stale backlog ticket, team assigned, nothing linked
  https://pagerinc.atlassian.net/browse/PB-2200

:hourglass_flowing_sand: 1 ticket(s) — Pending Work Items (Backlog/To Do)
- MIR PB-2210 (19d) — Some stale backlog ticket with a linked item still in To Do
  https://pagerinc.atlassian.net/browse/PB-2210

:information_source: 1 ticket(s) — Still open but all Linked Work Items are Done
- MIR PB-2220 (54d) — Some backlog ticket whose linked work is all Done
  https://pagerinc.atlassian.net/browse/PB-2220

Each group header (Triage Report, Backlog Report) only appears if that group has at least one non-empty section — a quiet week for one group doesn't print an empty header.

Tech Team tag: When a ticket has a Technical Team assigned (custom field customfield_10199), the abbreviation before the colon (e.g. MIR from MIR: Member Intake & Recommendations) is shown alongside the ticket key — in Backlog Report sections it's shown before the key (e.g. MIR PB-2210).

When everything is triaged:

:white_check_mark: All tickets are in good shape — nothing to triage!

🏗️ Project Structure

ZKBot/
├── .github/
│   └── workflows/
│       └── zkbot.yml        # GitHub Actions cron schedule & job
├── main.py                  # Entry point — orchestrates the run
├── jira_client.py           # Jira authentication, querying & serialization
├── slack_notifier.py        # Message formatting & Slack webhook POST
├── find_field.py            # Utility: discover custom Jira field IDs
├── requirements.txt         # Python dependencies
├── .env.example             # Environment variable reference
└── .gitignore               # Keeps secrets out of version control

⚙️ Configuration

All secrets are stored as GitHub Actions Secrets — never committed to the repository.

Secret Description
JIRA_URL Your Atlassian base URL, e.g. https://yourcompany.atlassian.net
JIRA_EMAIL Email address associated with your Jira account
JIRA_API_TOKEN API token from id.atlassian.com
SLACK_WEBHOOK_URL Workflow Builder webhook URL

The Jira filter IDs (18934, 18935, 18936, 20496) are not secrets — they are hardcoded constants in main.py.


🚀 Running Locally

# Clone the repo
git clone https://github.com/TimVallier-Pager/ZKBot.git
cd ZKBot

# Set up environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Copy and fill in credentials
cp .env.example .env
# edit .env with your values

# Dry run — prints the Slack message without posting it
DRY_RUN=1 .venv/bin/python main.py

# Live run
.venv/bin/python main.py

⏰ Schedule

ZKBot runs automatically via GitHub Actions cron:

- cron: "30 12 * * 1-5"  # 7:30 AM CDT (UTC-5), Monday–Friday
Day Time
Monday–Friday 7:30 AM CDT
Saturday–Sunday 💤 resting

Daylight Saving Note: The cron runs at 12:30 UTC year-round. During CST (UTC-6, November–March) this shifts to 6:30 AM local time. Adjust the cron to 30 13 * * 1-5 during those months if you want a strict 7:30 AM.

You can also trigger the bot manually at any time:

GitHub → Actions → ZKBot Triage Report → Run workflow


🧰 Tech Stack

Python dependencies
Package Purpose
jira Official Jira Python SDK — handles auth, pagination, and field deserialization
requests HTTP client for posting to the Slack webhook
python-dotenv Loads .env file for local development
Infrastructure
Service Role
GitHub Actions Hosts and schedules the bot — no server required
GitHub Secrets Secure storage for API credentials
Jira Cloud REST API Source of truth for ticket data
Slack Workflow Builder Receives the webhook payload and routes it to the team channel

🤓 Nerdy Details

Why not a Slack bot token instead of a webhook?

Slack offers two integration paths: incoming webhooks and full bot tokens. A bot token is more powerful — it can read channels, react to messages, and post anywhere — but it also requires creating a Slack App, managing OAuth scopes, and maintaining a token with broad permissions.

For ZKBot's use case (one-way, one-channel notifications), a Workflow Builder webhook is the perfect fit. It's scoped to exactly one action in exactly one place, with zero ongoing maintenance.

Why does markdown not render in the Slack message?

Slack's mrkdwn formatting (*bold*, <url|label>) only renders in messages sent via the Chat API or Block Kit. When a message arrives through a Workflow Builder text variable, Slack treats it as plain text and doesn't apply mrkdwn processing — with one exception: bare URLs are automatically hyperlinked.

This is actually fine for ZKBot. The plain text format is clean, scannable, and renders consistently across all Slack clients.

How does Jira pagination work here?

The Jira Cloud REST API caps results at 50 issues per request by default. ZKBot handles this with a simple while loop:

while True:
    batch = client.search_issues(jql, startAt=start, maxResults=50)
    # process batch...
    start += len(batch)
    if start >= batch.total:
        break

This ensures ZKBot never misses a ticket, even on boards with hundreds of open issues.

Why GitHub Actions instead of a local cron job?

A local cron job requires the machine to be on and awake at the scheduled time. GitHub Actions runs in the cloud, is free for public repos and generously free for private ones, produces a full audit log of every run, and can be triggered manually from anywhere. It's a strictly better fit for a lightweight scheduled task like this.


🛠️ Built With

This project was designed and built with Claude Code (Anthropic's AI coding assistant) in a single session — from blank directory to deployed production bot. The original implementation, including Jira integration, Slack formatting iteration, GitHub Actions setup, secret configuration, and this README, was produced collaboratively without writing a single line of code by hand.


Built with care for the Zookeeper team

View on GitHub · Report an Issue · Trigger a Run

About

Zkbot Things - setting managed in terraform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages