It is Saturday, 9pm. Your nightly jobs failed forty times this week. Nobody read the emails. You want one readable digest before Monday.
That is the whole project. One input, one output, one scheduled run. This build log keeps it that small.
1. Write the cut list before the code
Most weekend projects die from a second feature. So decide what you will not build, in writing.
| In scope | Out of scope | Why the cut |
|---|---|---|
| One weekly digest file | Slack bot, email, web UI | Delivery is solved; formatting is the real work |
| Failures from one pipeline | Multi-repo aggregation | Doubles auth surface for zero added insight |
| A model call to group failures | A model call to fix failures | Fixes need context you should not send |
| One cron entry | A scheduler service | A cron line survives reboots and code review |
If a task is not in the left column, it waits until next weekend. Say that out loud once.
2. Fix the output shape before the input
Your digest has to be readable in fifteen seconds. So define the file first, not the parser.
# Nightly digest - {{week}}
## Needs a human ({{n}})
- {{job}} - first seen {{first_seen}} - {{signal}}
## Already known noise ({{noise_count}})
- {{pattern}} x {{count}}
## Not triaged ({{skipped_count}})
- {{job}} - {{reason}}
Commit that as digest.template.md. Now your tests can assert on structure instead of vibes. The third section is not decoration. It is how you avoid pretending coverage you do not have.
3. Build the fixture before you touch the network
Record one real API response into fixtures/failures.json. Strip tokens, emails, and hostnames first. Then run everything from the file.
# fixture_run.py
import json, pathlib, subprocess
FIXTURE = pathlib.Path('fixtures/failures.json')
OUT = pathlib.Path('out/digest.md')
def load():
jobs = json.loads(FIXTURE.read_text())
assert isinstance(jobs, list) and jobs, 'fixture must be a non-empty list'
return jobs
if __name__ == '__main__':
jobs = load()
print(f'fixture jobs: {len(jobs)}')
subprocess.run(['python', 'digest.py', '--dry-run'], check=True)
text = OUT.read_text()
assert text.startswith('# Nightly digest'), 'template drifted'
assert '@' not in text, 'redaction failed'
print('fixture run OK')
That last assert earns its keep. Redaction regressions are silent and expensive. One line catches them forever.
4. Guard the budget with a ledger
Free access is still a finite resource. A ledger is cheaper than regret, and it takes about fifteen lines.
# ledger.py
import json, os, pathlib, time
LEDGER = pathlib.Path('state/ledger.json')
CAP = int(os.environ.get('TOKEN_CAP', '120000')) # per run, your own limit
def estimate(text):
return max(1, len(text) // 4) # rough, fine for a guard
def load():
if LEDGER.exists():
return json.loads(LEDGER.read_text())
return {'runs': []}
def spend(prompt, completion):
state = load()
used = sum(r['tokens'] for r in state['runs'])
cost = estimate(prompt) + estimate(completion)
if used + cost > CAP:
raise SystemExit(f'budget guard tripped: {used}+{cost} > {CAP}')
state['runs'].append({'at': time.time(), 'tokens': cost})
LEDGER.write_text(json.dumps(state, indent=2))
Now test the only thing that matters for cost. Force the guard to trip.
TOKEN_CAP=1 python digest.py --dry-run; echo "exit=$?"
You want a non-zero exit and no output file. If the digest still appears, your guard runs after the call, not before it.
5. Constrain the model with a plain contract
Long prompts invite creative formatting. Keep the contract short and boring.
prompt.txt
You group failing CI jobs.
Return JSON with one key, groups, a list of objects.
Each object has signal, jobs, and first_seen.
One group per distinct error signature.
No prose. No markdown fences.
Then validate the reply instead of trusting it.
def parse(reply):
data = json.loads(reply)
groups = data.get('groups')
if not isinstance(groups, list):
raise ValueError('shape mismatch')
return groups
Parse, retry once, then fall back to a raw job list. The fallback is not failure. It is the path that keeps Monday intact.
6. Ship one cron entry and stop
This workload is one process per week with no inbound traffic. The free server option covers that shape comfortably. Check the project page for current limits rather than trusting a blog post.
# crontab -e
0 6 * * 1 cd /srv/digest && TOKEN_CAP=120000 python digest.py >> log/run.log 2>&1
Keep the log. It is your only audit trail when a digest looks wrong. Rotate it monthly if it grows.
Decision table: triage it or skip it
| Situation | Action | Reason |
|---|---|---|
| Same signature three weeks running | Group, hide | Known noise, no new information |
| Logs contain tokens or personal data | Skip, name only | Sending them leaks credentials |
| Single flaky timeout | Group, no model call | The pattern is obvious already |
| New signature right after a deploy | Send to a human first | Highest information value per row |
| Malformed reply twice | Raw list fallback | The digest must never block |
Where free access actually earns its place
This digest is small, weekly, and by design not urgent. Paying for capacity to run it would be hard to justify, and hobby budgets are real. Free model access plus a free server option is what keeps this a weekend build instead of a subscription decision. That is the entire role the product plays here.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
One honest caveat. Free tiers change. Your ledger, your fixture, and your fallback path are what keep the project alive when a quota does.
What I skipped on purpose
- No web UI. Terminals survive; dashboards get abandoned.
- No auto-fix. A wrong fix costs more than a failed job.
- No vector store. Signature grouping is enough at forty rows.
- No cross-repo auth. One pipeline, one token, one blast radius.
- No custom model routing. One call, one contract, one validator.
Who should not build this
Skip it if your logs contain customer data you cannot redact. Skip it if you need alerts within minutes, because weekly cron is the wrong tool. Skip it above a few thousand rows, where error signatures stop clustering. And skip it if you cannot accept that a free tier may change without notice.
The Monday check
Three commands, run in order, before you trust the cron entry.
python fixture_run.py
TOKEN_CAP=1 python digest.py --dry-run; echo "exit=$?"
tail -n 20 log/run.log
All three pass, you schedule the job. Any of them fail, you keep the fallback and fix nothing else.
Your backlog does not need a platform. It needs one file, one guard, and one Monday morning. Start with the free tier, then tell me which section of the digest you ended up deleting.
Top comments (0)