I've been tracking 107 OSS repos daily for the open-alternative-to directory since April. Each morning a GitHub Actions job fetches release counts, open issue totals, and archived status for every listed repo, writing results to evidence.json. Three months of this data has taught me more about what "OSS health" actually means than any blog post about it.
The short version: stars are useless for real-time health assessment. Release cadence, combined with open issue counts and archived status, tells a much more honest story — but each signal has failure modes that took me a while to understand.
Why I stopped trusting star count
Stars accumulate but don't decay. A project from 2021 with 12,000 stars and a last release in 2023 looks healthy on any list sorted by popularity. The star count is preserved history, not current state.
The clearest example in my dataset: papercups-io/papercups has 176 open issues and its last release was August 2020 — nearly six years ago. A star-sorted query would surface it above projects released this week. That's not useful for a directory visitor trying to find maintained alternatives.
I wrote about four GitHub activity signals early in the project but at the time I had only seeded a handful of repos. After 107 repos and daily refresh, the star problem became undeniable. Stars went into the database — I still store stargazers_count — but they don't appear in the directory UI. Only release-derived signals do.
What releases_last_year measures, and what it doesn't
releases_last_year counts tagged GitHub releases (the "Releases" section, not tags) created in the past 365 days. I fetch it via the GitHub releases API rather than the repo metadata endpoint, which means it reflects intentional publish events rather than any repository activity.
That distinction matters. The pushed_at field on the repo endpoint updates on every push — bot commits, dependency bumps, automated security patches. It's a noisy signal that overstates activity. Releases require a human to make a deliberate decision to tag and publish. In practice, a project with pushed_at from last week and zero releases in the past year is usually stale, not active.
The one complication: some maintainers don't use GitHub releases at all. They tag commits directly or publish to package managers without a GitHub release entry. For those projects, releases_last_year reads as zero even when the project is active. I handle this by checking latest_release_at — if it's null, I fall back to checking the tag list instead of penalizing the project. I haven't automated that fallback yet; it's a known gap I track in the pipeline health monitor.
I cap releases_last_year at 30 and set a releases_last_year_capped boolean to true for anything above. Seven repos in my current dataset hit this cap. They're doing continuous deployment — PostHog at 100 releases/year is the clearest example. That's not the same behavior as a project releasing quarterly, and treating them identically in a health score would be misleading. The cap flag tells the UI: "interpret with context."
The bimodal open_issues problem
Open issue count is not a simple "lower is healthier" signal. After looking at the data, it breaks into roughly four quadrants:
| Zero/low releases | Regular releases | |
|---|---|---|
| Low open issues | Dead (no users filing bugs) | Well-maintained |
| High open issues | Abandoned mid-stream | Active but overwhelmed |
The bottom-left quadrant — high issues, zero releases — is the most concerning. papercups-io/papercups sits here: 176 issues, last release August 2020. The project was abandoned while people were still filing bugs. The three-tier content quality ladder I built for content generation handles this by skipping the AI content step for repos in this state and showing a "last maintained in X" note instead.
The top-right quadrant looks alarming but isn't. PostHog has 5,654 open issues and 100 releases last year. RocketChat has 3,949 issues and 69 releases. These are healthy, active projects where issue volume reflects user base, not neglect. A naïve "penalize for open issues" rule would wrongly rank them below a quiet, barely-maintained project.
The pattern I use now: flag only when releases_last_year === 0 AND open_items > 50. That catches the abandoned-mid-stream cases without punishing large active projects. It's imperfect — keepassxreboot/keepassxc has 890 open issues and just 2 releases last year, which is slow but deliberate for a security-focused project — but it's better than any single-variable rule.
Archived is different from stale
There is exactly one archived repo in my current dataset: TriliumNext/Notes. It has one open issue, its last release was June 2025, and its releases_last_year is zero. On every signal except archived, it looks merely stale. But it's been explicitly given up — the maintainer closed the repo.
I treat archived: true as a hard disqualifier. When the evidence job finds it, that repo gets marked as inactive in the database and is excluded from directory rendering without any other check. Everything else gets a warning, not a removal — a zero-release count shows a badge, but the entry stays. archived alone triggers removal.
The distinction matters for UI trust. If I showed archived projects with a warning badge next to current ones, visitors would have to evaluate whether the badge matters. Removing them entirely means the directory only surfaces maintained options — which is the promise I want to keep. I wrote about related filtering decisions for the AI tools directory with similar logic: some signals warrant filtering, others warrant labeling.
The two-layer architecture: why evidence.json is separate from the database
The OSS alternatives directory has two data layers. The database (Turso/libSQL — see the tradeoffs I wrote about early on) stores the stable, expensive-to-generate layer: AI-generated intro text, comparison notes, migration tips. That content is written once by Claude Haiku per repo and only regenerated if explicitly cleared.
evidence.json is the volatile layer. It refreshes daily with current GitHub state: releases_last_year, open_items, latest_release_at, archived. This data changes — issues get filed and closed, new releases ship, a maintainer decides to archive — so fetching it daily is cheap and necessary.
At build time, the Astro SSG joins the two layers by repo slug. The AI-generated text comes from the database; the health signals come from evidence.json. Neither knows about the other at generation time, which means:
- Refreshing evidence doesn't require regenerating AI content (expensive).
- Regenerating AI content for a repo doesn't require re-fetching GitHub data (rate-limited).
- If one source is stale or missing, the other degrades gracefully instead of blocking the build.
The join happens in a data loader that logs a warning and falls back to "unavailable" badges when evidence.json lacks an entry. I described a similar fallback-tolerant pattern in the three places ON CONFLICT matters post, applied here to file-based data instead of SQL.
What I'd add with another month
Three things I know I'm missing:
Issue close rate. A project closing 80% of filed issues in 30 days is healthier than one closing 20%. I have open_items but no closed_in_last_30_days. The GitHub search API can provide this but it's a separate authenticated call, not the simple repo endpoint I currently use. I'm holding off until I understand the rate-limit cost at 107 repos/day.
Recency weighting for releases. Three releases this month counts the same as 12 releases spread evenly over the past year in my current releases_last_year count. A project that was very active 10 months ago and went quiet recently looks the same as one maintaining steady monthly releases. I'd want to weight the last 90 days more heavily.
Core code vs. automated commits. Some high-pushed_at projects are mostly dependency bumps from Renovate or Dependabot. When I see pushed_at last week but releases_last_year = 0, I can't currently distinguish "active maintainers doing real work" from "automated bots keeping dependencies current while the project stagnates." Checking commit author in the API responses would help but adds complexity.
None of these are blockers — the current signals catch the obvious cases. But they're on my list as the directory grows beyond 107 repos. At that scale the margin between "barely maintained" and "dead" compresses, and I'll need more precision.
FAQ
Why not use commit frequency instead of release frequency?
Commit frequency includes noise — CI fixes, automated PRs, documentation typos. Releases require intentional human action. They're lower noise at the cost of missing projects that don't use GitHub releases.
What do you do with repos that tag their releases rather than using GitHub Releases?
Currently, latest_release_at comes back as null and releases_last_year reads zero. I show a "release data unavailable" badge instead of a health score. Long-term fix is to fall back to the GitHub tags API.
How often should evidence data refresh?
I refresh daily. Weekly would miss newly-archived repos for up to seven days — not catastrophic, but visitors might find a recently-abandoned project in the directory for too long. Daily seems right for a directory that makes explicit health claims.
What's the performance cost of fetching releases for 107 repos?
One call per repo per day, authenticated. At GitHub's 5,000-requests/hour limit, 107 calls is trivial. I add a short sleep between calls to avoid burst spikes — the same pattern I described in three public APIs I use without registration.
Do you show any of this data directly to visitors?
Yes: I show latest_release_tag, days since last release, and a color-coded freshness badge. The raw numbers (releases_last_year, open_items) are used internally for ranking and filtering but not displayed — they'd require too much context to be meaningful to a non-technical visitor.
I'll post updated numbers once I've run this for six months. The 107-repo dataset is real; the patterns described here come from three months of daily collection, not intuition.
Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.
Top comments (0)