Auto‑closing “Sin commits hoy” ClickUp tasks after repo activity
TL;DR: I added a lightweight webhook in src/main.py that pings ClickUp and a small state‑check in src/clickup_notifier.py to automatically close the “Sin commits hoy” task when a repository receives a new commit. The fix removes stale daily‑no‑commit tickets and keeps the board clean without manual intervention.
The Problem
Our weekly workflow uses ClickUp to track “no‑commit” days. Every morning a task titled “Sin commits hoy” is created for each repo. The task should be auto‑closed as soon as any commit lands, but the previous implementation only closed the task when the CI pipeline succeeded. If a repo had activity outside the CI run (e.g., a manual push to a branch that skips CI), the task stayed open, cluttering the board with false‑positive “no‑commit” alerts.
The symptom showed up in ClickUp as:
Task: Sin commits hoy – repo: content-automation
Status: Open
Last activity: 2026‑10‑02 08:12 (no commits detected)
Even though git log -1 for content-automation reported a commit at 2026‑10‑02 07:45. The automation that checks the repo’s activity was never triggered because it was tied to the CI job’s success event.
What I Tried First
My first attempt was to add a post‑commit hook in the repository that called the existing ClickUp notifier:
# .git/hooks/post-commit
python src/clickup_notifier.py --repo $PWD --action close-no-commit
I quickly ran into two issues:
- Hook not shared – Git hooks are not versioned, so every clone needed manual setup.
- Race condition – The hook executed before the commit was pushed to the remote, so ClickUp never saw the new SHA and kept the task open.
I also tried to modify the CI workflow to run a “check‑commits” job on every push, but the CI still skipped runs for branches with [skip ci] in the commit message, which is a common pattern in our automation scripts.
Both approaches failed to guarantee that the “Sin commits hoy” task would close reliably.
The Implementation
The final solution lives in two files:
-
src/main.py– a tiny Flask webhook that listens for GitHub push events. -
src/clickup_notifier.py– a helper that talks to ClickUp’s API and decides whether to close the task.
1. Adding the webhook endpoint
src/main.py now exposes /github/push:
# src/main.py
from flask import Flask, request, abort
from clickup_notifier import close_no_commit_task
app = Flask(__name__)
@app.route("/github/push", methods=["POST"])
def github_push():
payload = request.json
if not payload or "repository" not in payload:
abort(400, "Invalid GitHub payload")
repo_name = payload["repository"]["name"]
# Only act on pushes that contain at least one commit
if payload.get("commits"):
close_no_commit_task(repo_name)
return "", 204
if __name__ == "__main__":
# Run locally with `flask run` or deploy to Vercel / Render
app.run(host="0.0.0.0", port=5000)
Why Flask? It’s already a dependency of the project (used for the internal dashboard), so adding a single route adds negligible overhead. The endpoint is stateless, which makes it easy to deploy as a serverless function.
2. Updating the notifier logic
src/clickup_notifier.py now contains the core logic to locate and close the daily task:
# src/clickup_notifier.py
import os
import requests
from datetime import datetime
CLICKUP_TOKEN = os.getenv("CLICKUP_TOKEN")
CLICKUP_SPACE_ID = os.getenv("CLICKUP_SPACE_ID")
HEADERS = {"Authorization": CLICKUP_TOKEN, "Content-Type": "application/json"}
def _find_task(repo_name: str) -> dict | None:
"""Search for an open task named 'Sin commits hoy' for the given repo."""
url = f"https://api.clickup.com/api/v2/space/{CLICKUP_SPACE_ID}/task"
params = {"archived": "false", "page": 0}
resp = requests.get(url, headers=HEADERS, params=params)
resp.raise_for_status()
for task in resp.json()["tasks"]:
if (task["name"] == "Sin commits hoy" and
task.get("custom_fields", [{}])[0].get("value") == repo_name):
return task
return None
def close_no_commit_task(repo_name: str) -> None:
"""Close the 'Sin commits hoy' task if it exists and is still open."""
task = _find_task(repo_name)
if not task:
print(f"[clickup_notifier] No open no‑commit task for {repo_name}")
return
task_id = task["id"]
close_url = f"https://api.clickup.com/api/v2/task/{task_id}"
payload = {"status": "closed", "close_reason": "Commit detected"}
try:
r = requests.put(close_url, json=payload, headers=HEADERS)
r.raise_for_status()
print(f"[clickup_notifier] Closed task {task_id} for {repo_name}")
except requests.HTTPError as e:
print(f"[clickup_notifier] Failed to close task: {e}")
Key changes compared to the previous version:
- Removed CI dependency – The function is now called directly from the webhook, not from a CI step.
- Added repository filter – The custom field “Repo” (index 0) is used to match the task to the correct repo, preventing cross‑repo closures.
- Graceful fallback – If the task cannot be found, we simply log and exit; no exception bubbles up to break the webhook.
3. Wiring everything together
In the repository’s GitHub settings I added a Push webhook pointing to the deployed URL (e.g., https://content-automation.vercel.app/github/push). The webhook payload includes the full list of commits, which the endpoint checks before invoking close_no_commit_task.
I also updated the CI config (.github/workflows/auto-close.yml) to skip the old task‑close step, avoiding duplicate API calls:
# .github/workflows/auto-close.yml
name: Auto‑close no‑commit task
on:
push:
branches: [ main ]
jobs:
noop:
runs-on: ubuntu-latest
steps:
- name: No‑op (webhook handles it)
run: echo "Task closure now handled by webhook"
4. Verifying the fix
After pushing a new commit to content-automation, the webhook logs show:
[clickup_notifier] Closed task 1234567890 for content-automation
And the ClickUp board instantly reflects the task status change to Closed. No stale “Sin commits hoy” tickets appear after subsequent pushes, even when CI is skipped.
Key Takeaway
Decouple state‑changing automation from CI pipelines and move it to an event‑driven webhook. By listening to the source of truth (GitHub push events) you guarantee that the action runs exactly when the relevant change occurs, regardless of CI configuration or skip flags.
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/content-automation · 2026-10-03
#playadev #buildinpublic
Top comments (0)