id: hacktoberfest-tracker
namespace: company.team
description: |
Track Hacktoberfest contributions in a GitHub org: count merged October PRs
per author, build a leaderboard in Python, and post a daily digest to Slack.
triggers:
- id: daily_in_october
type: io.kestra.plugin.core.trigger.Schedule
description: Run every morning during October, when Hacktoberfest happens.
cron: "0 9 * 10 *"
inputs:
- id: github_org
type: STRING
displayName: GitHub Organization
description: Organization to track (e.g. kestra-io).
defaults: kestra-io
- id: since_date
type: STRING
displayName: Season Start Date
description: Only count PRs created on or after this date (YYYY-MM-DD).
defaults: "2026-10-01"
- id: max_prs
type: INT
displayName: Maximum PRs
description: Maximum PRs to pull from the Search API per run (1-100).
defaults: 100
- id: notify_slack
type: BOOL
displayName: Notify Slack
description: Post the leaderboard digest to Slack.
defaults: true
tasks:
- id: fetch_prs
type: io.kestra.plugin.core.http.Request
description: Search October pull requests across the whole organization.
uri: "https://api.github.com/search/issues?q=org:{{ inputs.github_org
}}+type:pr+created:>={{ inputs.since_date }}&per_page={{ inputs.max_prs
}}&sort=created&order=desc"
method: GET
headers:
Accept: "application/vnd.github+json"
User-Agent: "Kestra-Hacktoberfest-Tracker"
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
retry:
type: exponential
interval: PT2S
maxInterval: PT30S
maxAttempts: 3
- id: build_leaderboard
type: io.kestra.plugin.scripts.python.Script
description: Count merged PRs per contributor (bots excluded) and format the digest.
containerImage: python:3.11-slim
inputFiles:
prs.json: "{{ outputs.fetch_prs.body | toJson }}"
script: |
import json
from collections import Counter
from kestra import Kestra
logger = Kestra.logger()
with open("prs.json") as f:
raw = f.read().strip()
data = json.loads(raw) if raw else {}
items = data.get("items", []) if isinstance(data, dict) else []
merged = Counter()
total = len(items)
for pr in items:
user = (pr.get("user") or {}).get("login", "")
if not user or user.endswith("[bot]") or user == "dependabot":
continue
info = pr.get("pull_request") or {}
if info.get("merged_at"):
merged[user] += 1
top = merged.most_common(5)
lines = [f"{i}. @{u}, {c} merged" for i, (u, c) in enumerate(top, 1)]
text = (
f"*Hacktoberfest leaderboard for `{"""{{ inputs.github_org }}"""}*`\n"
f"PRs opened since {"""{{ inputs.since_date }}"""}: {total} | contributors with merges: {len(merged)}\n\n"
+ ("\n".join(lines) if lines else "No merged PRs yet, spread the word!")
).replace('"', "'")
logger.info(f"total={total} contributors={len(merged)}")
Kestra.outputs({"total": total, "contributors": len(merged), "digest": text})
- id: maybe_notify
type: io.kestra.plugin.core.flow.If
description: Post the digest when PRs exist and notifications are enabled.
condition: "{{ inputs.notify_slack and outputs.build_leaderboard.vars.total > 0 }}"
then:
- id: notify_slack
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Deliver the Hacktoberfest leaderboard to Slack.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Hacktoberfest leaderboard for {{ inputs.github_org }}",
"blocks": [
{"type": "header", "text": {"type": "plain_text", "text": "Hacktoberfest Leaderboard 🏆"}},
{"type": "section", "text": {"type": "mrkdwn", "text": "{{ outputs.build_leaderboard.vars.digest }}"}},
{"type": "context", "elements": [{"type": "mrkdwn", "text": "Org: {{ inputs.github_org }} | Execution: {{ execution.id }} | Merged PRs only, bots excluded"}}]}
]
}
else:
- id: log_noop
type: io.kestra.plugin.core.log.Log
description: Log when there is nothing to report yet.
message: "No PRs found for {{ inputs.github_org }} since {{ inputs.since_date
}}."
errors:
- id: alert_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert Slack if the tracker itself fails.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "🚨 Hacktoberfest tracker failed for {{ inputs.github_org }} in execution {{ execution.id }}. Check GITHUB_TOKEN / rate limits."
}
outputs:
- id: total_prs
type: INT
value: "{{ outputs.build_leaderboard.vars.total }}"
- id: contributors
type: INT
value: "{{ outputs.build_leaderboard.vars.contributors }}"