Someone opens a bug report on Monday. Nobody sees it, because GitHub's notification emails are buried under everything else. By the time you check, it has been sitting there for nine days.
Small projects don't usually fail loudly. They drift. Issues pile up, a post brings in a burst of visitors you never notice, and a week of silence turns into a month.
A short weekly summary fixes a lot of that. This guide builds one: a Python script and a GitHub Actions workflow that post a digest to Discord every Monday. It's free and needs no server.
What the digest looks like
Here's an example of what lands in your Discord channel (the numbers ar
e made up):
Weekly digest: yourname/your-project
Stars: 124 total, +5 this week
Forks: 12 total, +1 this week
Issues: 7 open, 2 opened and 3 closed this week
Views: 340 | Clones: 22 (this week)
Top referrers (last 14 days): dev.to (80), Google (40), github.com (10)
https://github.com/yourname/your-project
It's seven lines. You can read it in ten seconds, and it answers the questions people actually have: is anyone noticing this project, and is anything waiting for me?
A digest tells you what changed. It doesn't tell you when something breaks. For that, a monitor is the better tool, and I covered how to build one in my guide to a free Python uptime monitor. The two work well together.
What you need
- A GitHub repository to put the script in. It can be the project you want to track
- A Discord server where you can add a webhook (a private one just for you works well)
- Optional: a personal access token, if you want the traffic numbers
The script uses only what Python already includes, so there's nothing to install.
A note on traffic numbers
Stars, forks and issues are easy to read. Traffic is the awkward one, and it's worth understanding before you start.
The built-in token that every workflow gets can't read a repository's traffic. GitHub only allows it with a personal access token that has "Administration" read permission, and there's no narrower option. The permission is read-only, but it does sound bigger than it is.
GitHub also keeps traffic data for only the last 14 days. That's why the digest labels the referrers "last 14 days".
So you have two choices:
- Skip traffic. Everything else still works with the built-in token, and the digest says traffic was skipped.
- Add a token. I'll show how in Step 3. Limit it to the repositories you need, and give it an expiry date.
Either way is fine. Many people start without traffic and add it later.
Step 1: Add the script
Create a file called weekly_digest.py in your repository:
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
API = os.environ.get("GITHUB_API_URL", "https://api.github.com") # set automatically in GitHub Actions
TOKEN = os.environ["GH_TOKEN"]
REPOS = (os.environ.get("REPOS") or os.environ["GITHUB_REPOSITORY"]).split(",")
DAYS = int(os.environ.get("DAYS", "7"))
WEBHOOK = os.environ.get("DISCORD_WEBHOOK", "")
since = datetime.now(timezone.utc) - timedelta(days=DAYS)
since_time = since.strftime("%Y-%m-%dT%H:%M:%SZ")
since_day = since.strftime("%Y-%m-%d")
period = "this week" if DAYS == 7 else f"in the last {DAYS} days"
STAR_FORMAT = "application/vnd.github.star+json" # adds the date each star was given
def api(path, accept="application/vnd.github+json"):
request = urllib.request.Request(API + path, headers={
"Authorization": f"Bearer {TOKEN}",
"Accept": accept,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "weekly-digest",
})
with urllib.request.urlopen(request, timeout=20) as response:
return json.load(response), response.headers
def new_stars(repo):
_, headers = api(f"/repos/{repo}/stargazers?per_page=100", STAR_FORMAT)
last = re.search(r'[?&]page=(\d+)>; rel="last"', headers.get("Link", ""))
page, count = (int(last.group(1)) if last else 1), 0
while page >= 1: # start with the newest stars and work backwards
stars, _ = api(f"/repos/{repo}/stargazers?per_page=100&page={page}", STAR_FORMAT)
count += sum(1 for s in stars if s["starred_at"] >= since_time)
if any(s["starred_at"] < since_time for s in stars):
break # everything before this page is older still
page -= 1
return count
def new_forks(repo):
forks, _ = api(f"/repos/{repo}/forks?sort=newest&per_page=100")
return sum(1 for f in forks if f["created_at"] >= since_time)
def count_issues(repo, filter_):
query = urllib.parse.quote(f"repo:{repo} is:issue {filter_}")
data, _ = api(f"/search/issues?per_page=1&q={query}")
return data["total_count"]
def days_total(data, key):
return sum(day["count"] for day in data[key] if day["timestamp"][:10] > since_day)
def traffic_lines(repo):
try:
views, _ = api(f"/repos/{repo}/traffic/views")
clones, _ = api(f"/repos/{repo}/traffic/clones")
sources, _ = api(f"/repos/{repo}/traffic/popular/referrers")
except urllib.error.HTTPError as error:
if error.code not in (403, 404):
raise
return ["Traffic: skipped (this token can't read it)"]
lines = [f"Views: {days_total(views, 'views')} | Clones: {days_total(clones, 'clones')} ({period})"]
if sources:
top = ", ".join(f"{s['referrer']} ({s['count']})" for s in sources[:3])
lines.append(f"Top referrers (last 14 days): {top}")
return lines
def build_digest(repo):
info, _ = api(f"/repos/{repo}")
return "\n".join([
f"**Weekly digest: {repo}**",
f"Stars: {info['stargazers_count']} total, +{new_stars(repo)} {period}",
f"Forks: {info['forks_count']} total, +{new_forks(repo)} {period}",
f"Issues: {count_issues(repo, 'is:open')} open, "
f"{count_issues(repo, f'created:>={since_day}')} opened and "
f"{count_issues(repo, f'closed:>={since_day}')} closed {period}",
*traffic_lines(repo),
info["html_url"],
])
def send(text):
if not WEBHOOK:
print(text + "\n")
return
body = json.dumps({"content": text[:1900]}).encode()
# Discord turns away requests with Python's default User-Agent, so we set our own
request = urllib.request.Request(WEBHOOK, data=body, headers={
"Content-Type": "application/json", "User-Agent": "weekly-digest/1.0"})
urllib.request.urlopen(request, timeout=15).close()
errors = []
for repo in (r.strip() for r in REPOS):
try:
send(build_digest(repo))
except Exception as error:
errors.append(f"{repo}: {error}")
if errors:
print("Problems:", *errors, sep="\n- ")
sys.exit(1)
The script talks to GitHub through its API, the set of web addresses that tools use to ask GitHub for data. If that's new to you, my beginner's guide to what an API is explains the idea.
Here's what it does for each repository:
- Stars. It looks at when each star was given and counts the ones from the last 7 days. It starts from the newest stars and stops as soon as it reaches older ones, so big repositories stay fast.
- Forks. It counts the new forks in the same period.
- Issues. It counts how many are open, how many were opened and how many were closed. Pull requests aren't included, only issues.
- Traffic. It adds views, clones and top referrers, if the token allows it.
- Delivery. It posts the result to Discord.
If one repository fails, for example because of a typo in its name, the others are still sent and the run is marked as failed at the end so you hear about it.
Step 2: Add the workflow
Create .github/workflows/weekly-digest.yml:
name: Weekly repo digest
on:
schedule:
- cron: '37 7 * * 1' # Mondays at 07:37 UTC
workflow_dispatch: # adds a "Run workflow" button for testing
permissions:
contents: read
issues: read
jobs:
digest:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4 # use the latest major version
- name: Send the digest
env:
GH_TOKEN: ${{ secrets.DIGEST_TOKEN || github.token }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
# REPOS: owner/repo-one,owner/repo-two # optional, see below
run: python3 weekly_digest.py
A few notes:
-
Which repository? By default it reports on the repository the workflow lives in. To watch others, uncomment
REPOSand list them, separated by commas. -
The token line.
secrets.DIGEST_TOKEN || github.tokenmeans "use my own token if I added one, otherwise use the built-in one". That's how traffic stays optional. - Limited permissions. The workflow can read your code and issues and nothing else.
- The odd start time avoids the top of the hour, when GitHub is busiest and scheduled runs are more likely to be delayed.
-
The schedule.
1at the end means Monday. Each run takes a few seconds and is billed as one minute, so a weekly digest uses about 4 or 5 minutes a month.
Step 3: Add your secrets
In your repository, open Settings, then Secrets and variables, then Actions, and add:
DISCORD_WEBHOOK (required). A webhook is a private address that lets a script post into a Discord channel. In Discord, open the channel's settings, go to Integrations, then Webhooks, create one and copy its address. If webhooks are new to you, here's what a webhook is and how it works. Treat the address like a password, because anyone who has it can post to your channel.
DIGEST_TOKEN (optional, for traffic). To create it:
- On GitHub, open your account Settings, then Developer settings, then Personal access tokens, then Fine-grained tokens
- Create a new token and give it a clear name, like "weekly digest"
- Set an expiry date
- Under repository access, choose "Only select repositories" and pick the ones you want to track
- Under repository permissions, set "Administration" to read-only
- Create the token, copy it straight away and save it as the
DIGEST_TOKENsecret
The token is read-only and limited to your chosen repositories. It will expire, so put a reminder in your calendar. When it does, the workflow fails with an authentication error, and you create a new token and update the secret.
Step 4: Test it
Open the Actions tab in your repository, choose "Weekly repo digest" and click "Run workflow". Within a minute or so, the digest should appear in Discord.
If you'd rather see the output first, run it on your computer with the webhook left out, and the digest prints in your terminal. Set GH_TOKEN to a token and GITHUB_REPOSITORY to your repository, for example yourname/your-project.
Reading the numbers
A digest is only useful if it changes what you do.
- Stars are a mood, not a goal. A jump usually means someone shared your project. Look at the referrers to find out where.
- Referrers tell you what's working. If most visitors arrive from one article or site, that's where your next post should go.
- Views and clones are different things. Views are people looking at your pages. Clones can include automated systems, like build servers, so don't read them as readers.
- Opened vs closed matters more than open. If more issues are opened than closed for several weeks in a row, the pile is growing. That's the signal to set aside an hour.
- Quiet weeks are normal. Zero stars and two views is a perfectly ordinary week for a small project.
The script leaves out unique visitors on purpose. GitHub counts them per day, so adding the days together would count the same person several times.
Is this worth automating?
For most people with a project, yes. A digest is a good example of something that suits automation: it's repetitive, it only reads data, and a mistake costs almost nothing. The worst case is a wrong number in a Discord message.
I wrote about how to judge other tasks in When Not to Automate: 6 Signs a Task Isn't Worth It.
What can go wrong
Traffic says "skipped". The token can't read it. Check that DIGEST_TOKEN exists, covers that repository, and has Administration set to read-only.
The run fails with an authentication error. Your token has probably expired, or it was deleted. Create a new one and update the secret.
A private repository returns "not found". The token doesn't have access to it. Add the repository to the token's list.
Too many repositories. The issue counts use GitHub's search, which has a lower request limit than the rest of the API. A handful of repositories is fine. A very long list may run into it.
The digest stops arriving. In a public repository, GitHub switches off scheduled workflows after 60 days without repository activity. If Monday comes and goes with nothing in Discord, check the Actions tab. I go into this more in how to monitor your automations when something breaks.
Quick answers
Can I get this by email instead?
Sending email from a workflow needs mail account details, which is more setup. Discord, Slack or Telegram are simpler because they only need a webhook or a bot token.
Can I use Slack instead of Discord?
Yes, if you change the message format. Slack's incoming webhooks expect {"text": "..."} instead of Discord's {"content": "..."}, so you'd edit the send function.
Can I track someone else's repository?
You can read the stars, forks and issues of public repositories. Traffic is only available for repositories you have access to.
Why do the traffic numbers only go back 14 days?
That's GitHub's limit. If you want a longer history, you'd need to save the numbers somewhere each week.
Does it cost anything?
No. Public repositories don't use up any free minutes, and a weekly run is tiny on a private one.
The short version
Most projects don't need a dashboard. They need a short message once a week that says what changed and what's waiting.
Start without the token, since stars, forks and issues are enough to be useful. Add traffic later if you want it.
You can find more practical automation guides on Procwire.
Top comments (0)