A Nightly Release Digest Agent on a Free Stack: A Case Study
Thursday, 2:14 AM. CI goes red because a dependency changed a default timeout in a minor release. The release notes warned about it three weeks earlier. Nobody read them.
That failure mode is common enough to automate against. This case study covers one small project end to end: a nightly agent that watches a few repositories, summarizes new releases, and flags breaking changes. The budget was zero dollars.
Background
The project is deliberately small. One cron job. One tool. One model endpoint. Those constraints shaped every decision:
- No paid APIs.
- No GPU or container orchestration.
- A free server that can run cron.
- A model endpoint that does not charge per call.
Two operator-supplied options covered the stack: MonkeyCode's free model access and its free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free tier included a 10M-token allocation at the time of writing; verify the current terms before you depend on them.
Goal
Build a nightly digest that:
- Checks a fixed list of repositories.
- Fetches releases published in the last 24 hours.
- Summarizes each release and quotes sentences with breaking-change keywords.
- Logs token usage per run.
Success criteria: under 15,000 tokens per run, under three minutes of wall-clock time, zero manual intervention.
Implementation
Architecture
cron (free server)
└─ digest_agent.py
├─ model endpoint (MonkeyCode free tier)
├─ tool: get_releases(repo, since)
└─ GitHub REST API
The agent loop is a standard tool-calling pattern. The model decides when to call get_releases. The script executes the call and feeds the result back. The loop stops when the model returns a final digest.
The agent loop
The code below is a minimal reproducible sketch. The endpoint and response shapes depend on the provider, so read the current docs before running it.
# digest_agent.py — minimal sketch
import json
import os
import urllib.request
from datetime import datetime, timedelta
REPOS = ["psf/requests", "pallets/flask", "encode/httpx"]
SINCE = (datetime.utcnow() - timedelta(days=1)).isoformat()
TOOLS = [
{
"type": "function",
"function": {
"name": "get_releases",
"description": "List GitHub releases for a repo published after a date.",
"parameters": {
"type": "object",
"properties": {
"repo": {"type": "string"},
"since": {"type": "string", "format": "date-time"},
},
"required": ["repo", "since"],
},
},
}
]
def call_model(messages):
payload = {"messages": messages, "tools": TOOLS, "tool_choice": "auto"}
req = urllib.request.Request(
os.environ["MC_ENDPOINT"],
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {os.environ['MC_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req) as resp:
return json.load(resp)
def run_tool(name, args):
if name != "get_releases":
raise ValueError(f"unknown tool: {name}")
url = f"https://api.github.com/repos/{args['repo']}/releases?per_page=10"
req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req) as resp:
releases = json.load(resp)
return [
{
"tag": r["tag_name"],
"published": r["published_at"],
"body": (r["body"] or "")[:2000],
}
for r in releases
if r["published_at"] >= args["since"]
]
SYSTEM_PROMPT = """
You are a release digest bot. For each repo, list new releases and quote
sentences that mention breaking changes, deprecations, or migration steps.
If a release body is empty, say so. Do not invent changes.
"""
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
for step in range(4): # hard cap on iterations
response = call_model(messages)
messages.append(response["message"])
tool_calls = response["message"].get("tool_calls") or []
if not tool_calls:
break
for tc in tool_calls:
args = json.loads(tc["function"]["arguments"])
result = run_tool(tc["function"]["name"], args)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": json.dumps(result),
})
Two details matter. The iteration cap prevents runaway loops when the model keeps calling tools. The [:2000] slice on release bodies keeps tool results small, because tool results are the largest token driver.
Token accounting
Every response includes usage data. Log it:
usage = response.get("usage", {})
print(",".join([
datetime.utcnow().isoformat(),
str(usage.get("prompt_tokens", "")),
str(usage.get("completion_tokens", "")),
str(usage.get("total_tokens", "")),
str(step + 1),
]))
Deployment
On the free server, one cron line runs the script at 06:00 UTC:
0 6 * * * cd /srv/digest && /usr/bin/python3 digest_agent.py >> digest.log 2>&1
The output file is the digest. The log file is the audit trail. Both live on the same box, which is fine for a personal tool and wrong for a team product.
Results
The measurement format matters more than any single number. Each run produces one CSV row:
run_at,prompt_tokens,completion_tokens,total_tokens,iterations
2026-08-24T06:00:01Z,4120,1180,5300,2
That row is illustrative, from a local test run. In practice, totals vary with repo activity. Three quiet repos produced roughly 4,000–6,000 tokens per run in my tests. One busy repo with a long changelog can triple that. Track the CSV before trusting any single number.
The digest surfaced the failure mode it was built for: a breaking-change note buried at the bottom of a long release body. That is the real result. The token count is only a means to an end.
Lessons learned
- Tool results dominate the budget. Truncate aggressively. A 2,000-character cap per release body kept runs under 10,000 tokens.
- Hard caps beat clever prompts. The four-iteration limit never triggered in normal runs, but it bounded the worst case.
- Logs are the only observability on a free server. Log token usage every run; you cannot debug a cron job you cannot see.
- Models invent changes. The system prompt forbids invention, and the digest quotes release bodies verbatim. Never let the model paraphrase a changelog from memory.
- GitHub rate limits are real. Unauthenticated requests allow 60 per hour, which is plenty for nightly runs. Add a token if you watch more than a few repos.
Limitations
This approach is not for everyone.
- It is a personal tool, not a team service. There is no auth, no queue, and no retry logic.
- It misses changes that GitHub does not publish as releases. Changelogs hidden in commit messages are invisible to it.
- Free-tier terms can change. The 10M-token allocation and the free server option were accurate at the time of writing; re-check before you build on them.
- If a dependency is security-critical, a nightly digest is not a substitute for automated vulnerability scanning.
Closing
A small agent on a free stack solved a specific, recurring problem. The total cost was zero dollars and about 200 lines of Python. MonkeyCode's free model access and free server option made that possible; the token log kept it honest.
If you try this pattern, start with three repos and one week of CSV rows. Then decide whether the digest earns its 06:00 slot.
Top comments (0)