A GitHub Action that tracks star counts across all your repositories on a schedule, generates visual reports with charts and badges, and emails you once enough has changed to be worth reading.
Documentation · Getting Started · Configuration · Examples · Troubleshooting
Important
Coming from Star History, Starchart.cc or similar? You're in the right place.
GitHub announced that access to the stargazers API is being restricted to repository admins and collaborators. Tools that chart stars for repositories they don't own will start receiving empty responses or 403 errors.
GitHub Star Tracker keeps working. It runs inside your workflow, with your token, against your repositories: exactly the access GitHub is keeping. Star counts, reports, badges, CSV and notifications are unaffected in every case.
The one thing that depends on your role rather than your token's scopes is the stargazer endpoint, and it is what star-history charts and stargazer tracking are reconstructed from. If you track a repository you do not administer (an organization repo where you are a read-only member, or any repo reached through a fine-grained token with no explicit grant on its organization), those two fall back to the stored per-run snapshots. Known Limitations has the detail.
- What You Get
- Features
- Quick Start
- Configuration
- How It Works
- Embedding in Your README
- Documentation
- Support & Contributing
- Use of AI
Every writing run commits these artefacts to a dedicated data branch, with a new snapshot appended even when no
star moved. The runs that commit nothing are a read-only run, which renders and reports in full but never
pushes, and a run whose filters matched no repository, which never opens the branch.
-
Animated SVG charts: star history, per-repo trends, top repos comparison, and growth forecasts, with automatic dark/light mode support:
-
Shields.io-style badge: embeddable star count that updates automatically:
-
Markdown report: summary tables, delta indicators, every New Repository and Removed Repository since the baseline, stargazer details, and forecast tables.
-
CSV & JSON data: machine-readable exports for dashboards, spreadsheets, or downstream pipelines.
The HTML report is the one thing that is not committed. It is written outside the data branch worktree for
the email body, and reaches you through the report-html and report-html-path outputs instead. Its charts are
QuickChart images whose URLs carry each chart's data, repository names included, so on a
private repository those names reach quickchart.io whenever the email's images load.
- Animated SVG charts: star history, per-repo trends, comparisons, and growth forecasts, with an optional Trend Line over the observed curve
- Dark/light mode: SVG charts auto-adapt to the viewer's color scheme via
prefers-color-scheme - Historical snapshots: configurable retention (default: 52 runs) with JSON persistence
- Smart filtering: by visibility, Owner, min stars, exact names or regex, archived, forks
- Stargazer tracking: see who starred your repos, with avatars and dates
- Email notifications: built-in SMTP, with a fixed or adaptive threshold and a Notification Mode that decides whether losses count
- GitHub Enterprise: GHES support, auto-detected or explicit API URL
- Multi-language: English, Spanish, Catalan, Italian
- CSV export: machine-readable output for data pipelines
- Action outputs for workflow chaining:
lost-stars,new-stargazers,new-stars,notification-sent,report,report-csv,report-html,report-html-path,should-notify,stars-changedandtotal-stars - No install step: its runtime dependencies are bundled into the committed
dist/, so the runner clones and executes, with nonpm installin your workflow (ADR 0003) - Enforced coverage: the build gate is 85% on lines, functions, branches and statements, over an extensive unit test suite
- Built around GitHub's 2026 stargazers restriction: star counts, reports, badges and notifications never touch the restricted endpoint, and the stargazer-based charts and tracking keep working wherever your token's user is an admin or collaborator, as the note at the top explains
- Go to GitHub Settings > Tokens
- Generate a classic token with
repoorpublic_reposcope, or a fine-grained token withMetadata: Read-onlyon the repositories you want tracked - Add it as a repository secret named
STAR_TRACKER_TOKEN
Note
The default GITHUB_TOKEN is not sufficient to list your repositories, but it is what pushes the data branch: with actions/checkout's default persist-credentials: true, git sends the workflow's own token, so the PAT only reads. It needs Contents: Read and write on the workflow's repository only if your checkout sets persist-credentials: false. See the PAT guide for details, including why a fine-grained token can leave the stargazer sections empty.
Create .github/workflows/star-tracker.yml:
name: Track Stars
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch:
permissions:
contents: write # git pushes the data branch with this workflow's token
concurrency:
group: star-tracker
cancel-in-progress: false
jobs:
track:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Track stars
id: tracker
uses: fbuireu/github-star-tracker@v1
with:
github-token: ${{ secrets.STAR_TRACKER_TOKEN }}contents: writeis what the push needs:actions/checkoutkeeps the workflow's own token in the checkout, and git sends that one when the action pushes the data branch. Aread-onlyrun never pushes and needs onlycontents: read.concurrencyqueues a manual run behind a scheduled one instead of letting them overlap. Two writing runs that overlap both branch from the same data-branch commit, and the second push is refused, ending that run red after its report and email have gone out.id: trackeris what later steps read the outputs through, assteps.tracker.outputs.<name>.
- Manual run: Actions > Track Stars > Run workflow
- View report: Check the
star-tracker-databranch in your repository
The first run creates that branch and has nothing to compare against, so every repository arrives as new with a delta
of zero and stars-changed is true. With email configured and the default notification-threshold, the first email
goes out listing all of them, and with track-stargazers on every existing stargazer is reported as new too. Later
runs report only what changed. How It Works has the detail.
Set options directly in the workflow or via a YAML config file. See the Configuration guide for full details.
- name: Track stars
id: tracker
uses: fbuireu/github-star-tracker@v1
with:
github-token: ${{ secrets.STAR_TRACKER_TOKEN }}
visibility: 'public' # public | private | all | owned
locale: 'es' # en | es | ca | it
include-charts: true
track-stargazers: true
min-stars: '5'
exclude-repos: 'test-repo,/^demo-.*/'
compare-against: 'last-run' # last-run | 24h | 7d | 30d
notification-threshold: '500' # 0 | N | auto
notification-mode: 'gains' # net | gainsAll Inputs
| Input | Default | Description |
|---|---|---|
github-token |
- | Required. PAT with repo or public_repo scope |
chart-animation |
true |
Animate SVG charts (true) or render them static (false) for email/static contexts |
chart-begin-at-zero |
false |
Start the Y-axis at zero (true) or zoom into the data range (false) |
chart-curve |
monotone |
Curve when smoothing: monotone (no overshoot, best for stars), catmull-rom, cubic-bezier, rounded-step. Email approximates non-monotone curves |
chart-custom-milestones |
- | Comma-separated star counts for the milestone reference lines, replacing the built-in defaults (e.g. 250, 750, 2500). Requires chart-milestones |
chart-line-color |
#dfb317 |
Hex color of primary chart line/fill/points (not comparison). Accepts hex with or without a leading # |
chart-line-width |
2.5 |
Stroke width (px, >0) of data lines in all charts |
chart-max-points |
30 |
Curve granularity: points across the full span (capped at 365); 0 reconstructs at weekly resolution. Not a time window (see chart-range) |
chart-milestones |
true |
Show milestone reference lines on the main star-history chart (true) or hide them (false) |
chart-range |
all |
Time window plotted: 30d, 90d, 1y or all |
chart-show-points |
true |
Draw a marker on each data point (true) or hide them for a cleaner dense line (false) |
chart-smoothing |
true |
Smooth curve (true) or straight segments to show spikes; applies to email charts too |
chart-theme |
auto |
Color theme for the SVG charts: auto (follows prefers-color-scheme), light or dark |
chart-trend-line |
false |
Overlay a dashed moving-average trend line on the main chart |
chart-y-axis-side |
left |
Y-axis labels side: left or right |
compare-against |
last-run |
Snapshot used as comparison baseline: last-run, 24h, 7d or 30d. A window takes the newest snapshot at least that old, with 6 hours of slack so a late scheduled run still finds its own. If history is shorter than the window, the oldest stored snapshot is used and the report's date shows how far back it really goes |
config-path |
star-tracker.yml |
Path to YAML config file |
data-branch |
star-tracker-data |
Branch for tracking data |
email-from |
localized | Sender name or address; defaults to a localized sender name |
email-theme |
auto |
Color theme for the HTML email and its charts: auto (same as chart-theme), light or dark. Email charts are images, so a dark-mode reader needs this to resolve to dark |
email-to |
- | Recipient address |
exclude-orgs |
- | Owners to exclude (exact name or /regex/) |
exclude-repos |
- | Repos to exclude (exact name or /regex/) |
github-api-url |
- | GitHub API base URL (for GHES), https only. Auto-detected on GHES runners |
include-archived |
false |
Include archived repos |
include-charts |
true |
Generate star trend charts |
include-forks |
false |
Include forked repos |
locale |
en |
en, es, ca, or it |
max-history |
52 |
Max snapshots to keep |
min-stars |
0 |
Min stars to track |
notification-mode |
net |
How the threshold measures that change: net (absolute change, so a large drop also fires) or gains (upward movement only) |
notification-threshold |
0 |
0 (every run with changes), N (accumulated change since the last notification), or auto (adaptive) |
only-orgs |
- | Only track repos under these Owners (exact name or /regex/) |
only-repos |
- | Only track these repos (exact name or /regex/). When set, it skips the archived, fork, exclude and min-stars filters; only only-orgs still narrows first |
read-only |
false |
Make this a Read-Only Run: it still fetches, reports, sets outputs and emails, it just never commits or pushes. Use it for another workflow that shares a data branch with the workflow that writes it |
send-on-no-changes |
false |
Email even with no changes |
smart-sampling |
false |
Sample stargazer pages for high-star repos (avoids rate limits) |
smart-sampling-pages |
30 |
Max evenly-spaced stargazer pages per sampled repo |
smart-sampling-threshold |
1500 |
Star count above which a repo is sampled |
smtp-host |
- | SMTP hostname (enables email) |
smtp-password |
- | SMTP password |
smtp-port |
587 |
SMTP port: 465 means implicit TLS, anything else STARTTLS |
smtp-username |
- | SMTP username |
top-repos |
10 |
Top repos in charts/forecasts |
track-stargazers |
false |
Track individual stargazers |
velocity-metrics |
false |
Add a growth-velocity section (stars/day, % growth, days to next milestone) to the report |
visibility |
all |
public, private, all, or owned |
[!IMPORTANT]
notification-thresholddecides when you get an email;compare-againstdecides what period the report body covers. The two are independent: the threshold accumulates from the Notification Baseline, while the report diffs against a stored snapshot. So a threshold that takes several runs to trip still produces a report covering only thecompare-againstwindow. Set them to match if you want the email body to span what the threshold accumulated.
See notification-threshold for how the counter behaves on a fresh data branch, when you raise the value, and on a read-only run.
Every tracking option in this table can also go in the YAML config file, with its key written with either dashes or underscores (include-charts and include_charts are both accepted). The rest are workflow-only and the file ignores them: the credentials and plumbing (github-token, github-api-url, config-path, every smtp-* input, email-from and email-to) and send-on-no-changes. email-theme is a tracking option and works in the file.
Outputs
| Output | Description |
|---|---|
lost-stars |
Stars lost vs. the comparison baseline. Per run, not cumulative |
new-stargazers |
New stargazers vs. the stored stargazer list, which every writing run rewrites - not driven by compare-against, and 0 when track-stargazers is off |
new-stars |
Stars gained vs. the comparison baseline. Per run, not cumulative |
notification-sent |
true / false - whether an email was actually delivered. should-notify is the decision; this is the outcome |
report |
Full Markdown report |
report-csv |
CSV report (for data pipelines) |
report-html |
HTML report (for email) |
report-html-path |
File path to the HTML report (for large reports / custom mailers) |
should-notify |
true / false - the cumulative threshold signal, driven by notification-threshold and notification-mode (and requires an actual change) |
stars-changed |
true / false - anything changed vs. the comparison baseline (per run) |
total-stars |
Total star count |
To email on every N stars, use notification-threshold: 'N' with notification-mode: 'gains' and gate the step on if: steps.tracker.outputs.should-notify == 'true'. Gating on new-stars >= N would require N stars inside a single run, which on a daily schedule almost never happens.
Outputs are set at the very end of a successful run. A run that fails sets none of them, even when it fails after its email went out, as a refused push of the data branch does, so a later step gated on an output does not run.
API Reference: Complete inputs, outputs, and data formats
---
config:
look: handDrawn
layout: dagre
---
flowchart TD
trigger(["Workflow trigger"])
config["Load the configuration: inputs, then star-tracker.yml, then defaults"]
client["Build the GitHub client from github-token and github-api-url"]
fetch["List repositories, GET /user/repos, paginated"]
filter["Resolve the Tracked Set from the filters"]
empty["Empty report and HTML report, every output zeroed"]
probe{"Data branch on the remote?"}
attach["Fetch it, add a worktree"]
orphan["Create it as an orphan branch with an empty commit"]
read["Read stars-data.json"]
measure["Measure the Run: Baseline Snapshot, deltas, new Snapshot, threshold"]
wantStargazers{"include-charts or track-stargazers?"}
stargazers["Fetch stargazers with starred_at, one repository at a time"]
wantDiff{"track-stargazers?"}
diff["Diff against stargazers.json"]
reconstruct["Reconstruct the star history, compute the forecast"]
render["Render Markdown, HTML, CSV, badge and SVG charts"]
due{"SMTP set, and a Notification due or send-on-no-changes?"}
send["Send the email"]
settle["Advance or hold the Notification Baseline"]
htmlReport["Write the HTML report to RUNNER_TEMP"]
publish["Write every artefact into the worktree, prune old charts"]
readOnly{"read-only?"}
push["Commit and push the data branch"]
outputs["Set every output"]
green(["Run ends green"])
red(["Run fails: Star Tracker failed"])
trigger --> config --> client --> fetch --> filter
filter -->|no repositories matched| empty --> green
filter --> probe
probe -->|yes| attach --> read
probe -->|"no, writing run"| orphan --> read
read --> measure --> wantStargazers
wantStargazers -->|yes| stargazers --> wantDiff
wantStargazers -->|no| reconstruct
wantDiff -->|yes| diff --> reconstruct
wantDiff -->|no| reconstruct
reconstruct --> render --> due
due -->|yes| send --> settle
due -->|no| settle
settle --> htmlReport --> publish --> readOnly
readOnly -->|no| push --> outputs
readOnly -->|yes| outputs
outputs --> green
config -.->|"bad visibility or data-branch, unreadable file"| red
client -.->|"no token, or an API URL that is not https"| red
fetch -.->|"any failed request, a 403 rate limit included"| red
probe -.->|"no checkout, a failed probe, or absent on a read-only run"| red
attach -.->|"git fetch or worktree add fails"| red
orphan -.->|"worktree add, checkout --orphan or the empty commit fails"| red
read -.->|unreadable history| red
diff -.->|"stargazers.json is not JSON"| red
push -.->|"push refused, after the email"| red
classDef start stroke:#1565c0,stroke-width:3px
classDef setup stroke:#e65100,stroke-width:3px
classDef measuring stroke:#7b1fa2,stroke-width:3px
classDef rendering stroke:#2e7d32,stroke-width:3px
classDef delivering stroke:#c2185b,stroke-width:3px
classDef fatal stroke:#d73a49,stroke-width:3px,stroke-dasharray:6 4
class trigger,green start
class config,client,fetch,filter,empty setup
class probe,attach,orphan,read,measure,wantStargazers,stargazers,wantDiff,diff,reconstruct measuring
class render rendering
class due,send,settle,htmlReport,publish,readOnly,push,outputs delivering
class red fatal
The dashed edges end a run red, and nothing after them runs: no later step, and no output is set. Two of them come after
a probe that succeeded: fetching the data branch and adding its worktree fails, or creating the orphan does, on a network
error or on a leftover .<data-branch> path git cannot reuse. Everything else that can go wrong only thins the run, and
it still ends green: a repository whose stargazers cannot be read loses its reconstructed chart and its new-stargazer
diff, and a failed email holds the Notification Baseline for the next run.
Note where the email sits. It goes out before the data branch is pushed, so a refused push ends red a run that has
already emailed you.
How It Works: The full execution pipeline, phase by phase, with what is fatal, degraded or only a warning in each phase. Architecture: the DDD(ish) layering, one ubiquitous language and a pure core, and how much of the tactical catalogue is taken
The charts plot the real historical curve: every star is placed on the date it was actually given. Each stargazer carries a starred_at timestamp (GitHub's application/vnd.github.star+json media type), and the action reconstructs the cumulative star count over real time from those dates, so the timeline runs from a repo's very first star up to now, regardless of when you started running the action.
The per-run snapshots on the data branch are still kept for the report's delta tables and notifications ("how many stars changed against the comparison baseline", which compare-against selects), but the charts themselves no longer depend on them.
One caveat: GitHub caps the stargazers listing at roughly 40,000 per repo (oldest first), so on a very large repo the most recent stars fall outside the Reachable Stargazers and the Covered Stars behind the curve stop short of the true total. The reachable portion is drawn accurately and the recent tail is bridged with a straight ramp up to the current total, so the early curve stays honest and the chart never goes flat at the end. Pair this with smart-sampling to keep the request cost bounded on big repos.
The line style is configurable via chart-curve (monotone by default, plus catmull-rom, cubic-bezier and rounded-step), along with theme, colors, milestones, point markers, the time window and more. See the examples gallery for a rendered comparison of every option.
Tip
SVG charts automatically adapt to dark and light mode. No extra configuration needed: they use prefers-color-scheme to match the viewer's theme.
Viewing Reports: All access methods (data branch, badges, outputs, email)
| Guide | Description |
|---|---|
| Getting Started | Setup from token to first run |
| How It Works | Execution flow, phase by phase |
| Architecture | DDD(ish) layers, and where the DDD stops |
| Configuration | All options and settings |
| API Reference | Inputs, outputs, and data formats |
| Examples | Real-world workflow configurations |
| Star Trend Charts | Chart types, embedding, and customization |
| Email Notifications | Built-in SMTP and external action setup |
| Viewing Reports | Data branch, badges, outputs, raw data |
| Data Management | Storage, rotation, and manual management |
| Internationalization | Multi-language support |
| Personal Access Token | Classic and fine-grained token setup |
| Technical Stack | Technologies and design decisions |
| Known Limitations | Constraints and workarounds |
| Troubleshooting | Common issues and solutions |
For why the action is built the way it is, one decision per file, see the architecture decision records. They include the AGPL-3.0-only licence and what it asks of anyone redistributing or hosting a modified version.
If you find this project useful, consider supporting its development:
This project uses AI assistance primarily for documentation purposes. AI tools (GitHub Copilot, Claude) were used to:
- Write and improve documentation (README, wiki pages)
- Generate boilerplate code and configuration files
- Assist with code reviews and suggestions
The core logic, architecture decisions, and implementation were developed by the maintainer. All AI-generated content has been reviewed and validated.
AGPL-3.0 © Made with 🤘🏼 by Ferran Buireu