Skip to content

feat: add usage-stats.sh — one-command adoption metrics - #98

Merged
FIERsity merged 1 commit into
mainfrom
feat/usage-stats
Aug 6, 2026
Merged

feat: add usage-stats.sh — one-command adoption metrics#98
FIERsity merged 1 commit into
mainfrom
feat/usage-stats

Conversation

@FIERsity

@FIERsity FIERsity commented Aug 6, 2026

Copy link
Copy Markdown
Owner

What

Adds scripts/usage-stats.sh, a one-command dashboard of adoption signals across every public surface of EnvOrigin:

Source Signal Status
crates.io API downloads (total / 90d / per version) live — 224 total
Homebrew analytics API installs 30d/90d/365d activates after next release (formula currently 404s)
GitHub traffic API views / clones, unique live
Release assets prebuilt-binary downloads live — 79
Code search public repos with uses: FIERsity/envorigin-action live — 5 hits (incl. our dogfood jobs)
HN Algolia mentions on Hacker News live — 0

Why

We need an adoption baseline before promoting the project. These are all public APIs — no user telemetry is collected. The script labels which numbers are inflated by our own CI, so the trend can be read honestly.

Notes

  • Shell-only, no new dependencies; bash -n + full run verified locally.
  • scripts/ is excluded from the published crate, so this does not affect packaging.
  • Release-gate impact: none — the script is not part of any test target.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a command-line reporting tool that gathers product adoption and usage metrics from multiple public sources.
    • Reports package, repository, release download, code search, and community activity.
    • Provides clear handling for unavailable data and defaults missing metrics safely.

Tracks real usage across every public surface from a single command:
crates.io downloads, Homebrew analytics, GitHub traffic, release
binary downloads, public `uses:` of the action, and HN mentions.
All signals come from public APIs — no user telemetry needed.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added scripts/usage-stats.sh. The strict-mode Bash script queries public adoption metrics from crates.io, Homebrew, GitHub, public code search, and Hacker News. It reports fallback messages for unavailable Homebrew formulas and missing Hacker News counts.

Changes

Usage metrics reporting

Layer / File(s) Summary
Script setup
scripts/usage-stats.sh
Defines strict Bash execution, repository constants, usage documentation, and formatted output sections.
Service metric collection
scripts/usage-stats.sh
Queries crates.io, Homebrew, GitHub, public code search, and Hacker News. It reports fallback values for unavailable or empty responses.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the addition of a one-command script for adoption metrics.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/usage-stats

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/usage-stats.sh`:
- Around line 32-36: Update the Homebrew analytics jq expression in the
usage-stats section to bind `.analytics.install` as `$i`, then read the 30d,
90d, and 365d values independently from `$i` for their corresponding labels,
preserving 0 as the fallback for missing values.
- Line 17: Update scripts/usage-stats.sh so 403 responses from GitHub traffic
and clones requests do not terminate the script under set -e; catch those
endpoint failures, print an unavailable status, and continue producing the
remaining adoption metrics.
- Line 20: Update the usage-counting logic in scripts/usage-stats.sh to fetch
matching code-search items, require workflow uses: references to ACTION_REPO,
retain only public repositories, and deduplicate by .repository.full_name before
counting. Remove the hard-coded “minus 2-3” adjustment and ensure the query
references ACTION_REPO rather than a fixed action name.
- Around line 52-54: Update the Hacker News count assignment for hn to use jq’s
alternative operator, defaulting a missing or null .nbHits value to 0 before
assignment; keep the existing curl request and output behavior unchanged.
- Around line 45-46: Update the release download aggregation command in the
release binary downloads section to use gh api pagination with slurped results
before reducing over all release assets, ensuring the download total includes
every release page.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c9970a9-8f82-464d-9740-08698768c2dd

📥 Commits

Reviewing files that changed from the base of the PR and between fd10c48 and 1eea2d9.

📒 Files selected for processing (1)
  • scripts/usage-stats.sh

Comment thread scripts/usage-stats.sh
# and our own dogfood jobs inflate raw counts. The code-search hit count
# is the strongest signal — it means someone actually wired the action
# into their CI. Its hits include our own repos' dogfood jobs.
set -euo pipefail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)usage-stats\.sh$|scripts/usage-stats.sh' || true

echo "== usage-stats.sh =="
if [ -f scripts/usage-stats.sh ]; then
  cat -n scripts/usage-stats.sh
fi

echo "== gh command references =="
rg -n 'gh api|GH_|GITHUB_|traffic|views|traffic' scripts/usage-stats.sh .github README.md package.json 2>/dev/null | head -200 || true

echo "== diff stat/name-status if available (focused) =="
git diff -- scripts/usage-stats.sh | sed -n '1,220p' || true

Repository: FIERsity/envorigin

Length of output: 4957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== bash set -e behavior probe (gh API not invoked) =="
bash -c 'set -e
function failing_cmd { echo >&2 gh: API error; return 4; }
echo before
failing_cmd || true
echo after'
bash -c 'set -e
function failing_cmd { echo >&2 gh: API error; return 4; }
echo before
failing_cmd
echo after' 2>&1; echo "probe_exit=$?"

echo "== gh API 403 behavior probe from current repo if gh available =="
if command -v gh >/dev/null 2>&1; then
  git config --get remote.origin.url
  echo "gh version:"
  gh --version | head -3
  echo "gh api /repos/FIERsity/envorigin/traffic/views:"
  gh api "repos/FIERsity/envorigin/traffic/views" --jq '"  views: \(.count)  unique visitors: \(.uniques)"' 2>&1 || true
  echo "gh api search code:"
  gh api "search/code?q=FIERsity%2Fenvorigin-action&per_page=1" --jq '.total_count' 2>&1 || true
  echo "gh api public crates download_count:"
  gh api "repos/FIERsity/envorigin/releases" --jq 'reduce .[].assets[]? as $a (0; . + $a.download_count) | "  prebuilt binaries downloaded: \(.)"' 2>&1 || true
else
  echo "gh not available"
fi

Repository: FIERsity/envorigin

Length of output: 258


🌐 Web query:

GitHub REST API views traffic clones access require read permission public repository https://docs.github.com/rest/traffic

💡 Result:

To access repository traffic data—such as clones (GET /repos/{owner}/{repo}/traffic/clones) and page views (GET /repos/{owner}/{repo}/traffic/views)—via the GitHub REST API, you must have push-level access to the repository [1][2][3]. Even for public repositories, read-only access is insufficient; the API requires you to be a repository collaborator or owner with push-equivalent permissions [3][4]. Requests made without these privileges will return a 403 Forbidden error indicating that push access is required [3][4][5]. When using personal access tokens (PATs), you must ensure your token has the necessary permissions. For fine-grained PATs, this typically requires Administration: Read access (or higher) for the repository [6][7][8]. For classic PATs, the repo scope is generally required [3][8]. Note that the traffic API only exposes data for the most recent 14 days [1][7][8].

Citations:


Do not treat missing GitHub traffic permissions as a script failure.

The traffic and clones endpoints require push-level GitHub access, even on public repositories. With set -e, a 403 from those calls exits scripts/usage-stats.sh before the rest of the adoption metrics print. Catch these results and print unavailable status, or document the required permissions in the script header.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/usage-stats.sh` at line 17, Update scripts/usage-stats.sh so 403
responses from GitHub traffic and clones requests do not terminate the script
under set -e; catch those endpoint failures, print an unavailable status, and
continue producing the remaining adoption metrics.

Comment thread scripts/usage-stats.sh
set -euo pipefail

REPO="FIERsity/envorigin"
ACTION_REPO="FIERsity/envorigin-action"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked files matching usage-stats:"
fd -a 'usage-stats\.sh$' . || true

echo
if [ -f scripts/usage-stats.sh ]; then
  echo "Line count:"
  wc -l scripts/usage-stats.sh
  echo
  echo "Relevant source:"
  cat -n scripts/usage-stats.sh | sed -n '1,120p'
fi

echo
echo "Search for ACTION_REPO/usage-count/action usage in repo:"
rg -n "ACTION_REPO|total_count|usage|code-search|api\.github\.com|/search/code" . || true

Repository: FIERsity/envorigin

Length of output: 4135


🌐 Web query:

GitHub REST Search Code API total_count matches count files is_inclusive private repositories search syntax context

💡 Result:

The GitHub REST API search code endpoint operates using specific constraints regarding results, indexing, and syntax as of August 6, 2026. Total Count and Results The REST API's total_count field indicates the estimated number of search results [1]. However, the API is limited to returning up to 1,000 results for any given search query [2]. Additionally, the API currently utilizes a legacy search engine that differs from the modern web-based search experience; consequently, total_count values returned by the API may differ from results observed in the GitHub web interface [3]. If a search takes too long, the API returns an incomplete_results boolean as true [2]. Private Repository Access To search private repositories, you must authenticate with a token (such as a Personal Access Token or GitHub App token) that has the appropriate repository permissions [4][5][6]. The API searches up to 4,000 private repositories to which you have access, specifically prioritizing the most recently updated ones among the first 10,000 accessible repositories [4]. Search Syntax and Behavior - Range Operators: The range operator (..) is inclusive on both sides [7][8]. For example, stars:1..3 includes repositories with 1, 2, or 3 stars [8]. - Excluding Results: - Qualifiers: You can exclude results matching a specific qualifier by prefixing it with a hyphen (-), such as -language:python [9][10]. - Keywords: You can exclude files containing specific words using the NOT operator (e.g., hello NOT world) [9][11]. - Search Scoping: Code searches require at least one user, organization, or repository qualifier to be specified in the query [4][6]. Context and Limitations The REST API code search endpoint is subject to strict rate limits (10 requests per minute) and requires authentication [1][2]. It also only indexes repositories with fewer than 500,000 files, only searches default branches, and restricts file size to under 384 KB [4]. Because the REST API still relies on legacy infrastructure, modern search features available on the GitHub website (like semantic search) may not be reflected in REST API results [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Read search code matching item shape from installed gh help/docs if available:"
if command -v gh >/dev/null 2>&1; then
  gh help api search/code --help 2>/dev/null || true
  echo
  gh search code --help 2>/dev/null || true
else
  echo "gh not available"
fi

echo
echo "Behavioral probe: parse current jq query from script and inspect jq available fields without calling GitHub (static only)."
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("scripts/usage-stats.sh")
s = p.read_text()
m = re.search(r'count=\$"\([^"$\n]*\.total_count"\)', s)
print(m.group(0) if m else "jq query containing .total_count not found")
print(s[s.index("== Public repos using the action"):s.index("== Hacker News mentions")])
PY

Repository: FIERsity/envorigin

Length of output: 3609


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect remaining actions workflow usage to understand context for dogfood adjustment"
find . -type f \( -path '*/.github/workflows/*' -o -path '*workflows*' \) -print | sort | head -200
rg -n --hidden --glob '!*.lock' --glob '!node_modules/**' 'envorigin-action|FIERsity/envorigin|envorigin' .github . || true

Repository: FIERsity/envorigin

Length of output: 23339


Count distinct public repositories that actually use the action.

search/code returns matching files, and total_count is not a distinct repository count. The query also does not require .github/workflows + uses: syntax, and an authenticated request can include private repositories available to the token. Fetch matching items, filter for public repositories with workflow uses: action entries, deduplicate .repository.full_name, and remove the hard-coded “minus 2-3” adjustment. Use ACTION_REPO for the action reference.

🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 20-20: ACTION_REPO appears unused. Verify use (or export if used externally).

(SC2034)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/usage-stats.sh` at line 20, Update the usage-counting logic in
scripts/usage-stats.sh to fetch matching code-search items, require workflow
uses: references to ACTION_REPO, retain only public repositories, and
deduplicate by .repository.full_name before counting. Remove the hard-coded
“minus 2-3” adjustment and ensure the query references ACTION_REPO rather than a
fixed action name.

Source: Linters/SAST tools

Comment thread scripts/usage-stats.sh
Comment on lines +32 to +36
section "== Homebrew (brew install envorigin) =="
code="$(curl -s -o /dev/null -w '%{http_code}' https://formulae.brew.sh/api/formula/envorigin.json)"
if [ "$code" = "200" ]; then
curl -s https://formulae.brew.sh/api/formula/envorigin.json \
| jq -r '.analytics.install["30d"] as $a | " 30d: \($a["envorigin"] // 0) 90d: \($a["envorigin"] // 0) 365d: \($a["envorigin"] // 0)"'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file excerpt ---\n'
sed -n '1,70p' scripts/usage-stats.sh

printf '\n--- jq expression target file shape (live read-only probe) ---\n'
python3 - <<'PY'
import json, urllib.request
url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
    data = json.load(r)
analytics = data.get("analytics", {}) or {}
install = analytics.get("install", {}) or {}
for k in ["30d", "90d", "365d"]:
    v = install.get(k, {}) or {}
    print(f"{k}:", v.get("envorigin"), type(v.get("envorigin")))
PY

Repository: FIERsity/envorigin

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file excerpt ---\n'
sed -n '1,70p' scripts/usage-stats.sh

printf '\n--- jq expression target file shape (live read-only probe) ---\n'
python3 - <<'PY'
import json, urllib.request
url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
    data = json.load(r)
analytics = data.get("analytics", {}) or {}
install = analytics.get("install", {}) or {}
for k in ["30d", "90d", "365d"]:
    v = install.get(k, {}) or {}
    print(f"{k}: {v.get('envorigin')} {type(v.get('envorigin'))}")
PY

Repository: FIERsity/envorigin

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- file excerpt ---'
sed -n '1,70p' scripts/usage-stats.sh

printf '%s\n' ''
printf '%s\n' '--- jq expression target file shape read-only probe ---'
python3 - <<'PY'
import json
import urllib.request

url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
    data = json.load(r)
analytics = data.get("analytics") or {}
install = analytics.get("install") or {}
for k in ["30d", "90d", "365d"]:
    v = install.get(k) or {}
    print(f"{k}: {v.get('envorigin')} {type(v.get('envorigin')).__name__}")
PY

Repository: FIERsity/envorigin

Length of output: 5331


🌐 Web query:

Homebrew formulae API analytics install object contains 30d 90d 365d

💡 Result:

The Homebrew Analytics JSON API provides aggregate usage data for formulae and other events, typically structured by category and a specified timeframe [1][2]. The "install" event category data is accessible via endpoints that incorporate the time range as a path parameter, specifically: 30d (30 days), 90d (90 days), and 365d (365 days) [1][3]. The general structure of these API endpoints follows the pattern: https://formulae.brew.sh/api/analytics/${CATEGORY}/${DAYS}.json [1] Where: - ${CATEGORY}: The event category (e.g., install, install-on-request, build-error) [1][4]. - ${DAYS}: The duration of the data period, which must be 30d, 90d, or 365d [1]. For example, to retrieve install analytics for the last 365 days, one would access: https://formulae.brew.sh/api/analytics/install/365d.json [5] Data is also available grouped by specific subsets, such as homebrew-core: https://formulae.brew.sh/api/analytics/${CATEGORY}/homebrew-core/${DAYS}.json [1][2] These reports are derived from raw events stored in InfluxDB, which Homebrew retains for 365 days [3][6]. The JSON responses typically contain metadata including the category, start and end dates, total event counts, and a breakdown of items (formulae) with their respective installation counts [1][6].

Citations:


Read each Homebrew analytics period independently.

Lines 34-36 bind .analytics.install["30d"] to $a, then reuses that value for the 90-day and 365-day labels. Read .analytics.install["90d"] and .analytics.install["365d"] from the same $i object instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/usage-stats.sh` around lines 32 - 36, Update the Homebrew analytics
jq expression in the usage-stats section to bind `.analytics.install` as `$i`,
then read the 30d, 90d, and 365d values independently from `$i` for their
corresponding labels, preserving 0 as the fallback for missing values.

Comment thread scripts/usage-stats.sh
Comment on lines +45 to +46
section "== Release binary downloads (all releases) =="
gh api "repos/$REPO/releases" --jq 'reduce .[].assets[]? as $a (0; . + $a.download_count) | " prebuilt binaries downloaded: \(.)"'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching usage-stats.sh:\n'
fd -a 'usage-stats\.sh$' . || true

printf '\nTarget file excerpt:\n'
if [ -f scripts/usage-stats.sh ]; then
  nl -ba scripts/usage-stats.sh | sed -n '1,90p'
fi

printf '\nSearch for GH_API pagination flags in scripts:\n'
rg -n 'gh api|paginate|RELEASE|REPO' scripts || true

printf '\nTry locate gh (do not run repository code):\n'
command -v gh || true
printf '%s\n' 'gh version:'
gh --version 2>/dev/null || true

Repository: FIERsity/envorigin

Length of output: 302


🌐 Web query:

GitHub REST API /repos/{owner}/{repo}/releases default page size per_page pagination releases endpoint

💡 Result:

For the GitHub REST API endpoint /repos/{owner}/{repo}/releases, the default number of results returned per page is 30 [1][2][3][4]. Key details regarding pagination for this endpoint include: - Default per_page: 30 [1][2][3][4] - Maximum per_page: 100 [1][2][3][4][5] - Default page: 1 [1][2][3][4] If you request a per_page value higher than 100, the API will automatically cap the results to 100 without returning an error [5]. You can use the Link header provided in the API response to navigate through subsequent pages of results [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Target file excerpt:\n'
awk '{printf "%6d\t%s\n", NR, $0}' scripts/usage-stats.sh | sed -n '1,90p'

printf '\nSearch for GH_API pagination usage in scripts:\n'
rg -n 'gh api|--paginate|per_page|REPO|RELEASE' scripts || true

printf '\nGH availability:\n'
command -v gh || true
gh --version 2>/dev/null || true

Repository: FIERsity/envorigin

Length of output: 5718


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'current gh release count and first page artifact download counts:\n'
gh api "repos/FIERsity/envorigin/releases" -q '.[0:3] | length as $n | "first_page_count: \($n)"' --jq '.[].assets[].download_count' || true

printf '\nfirst_page_total assets asset download counts from first page only:\n'
gh api "repos/FIERsity/envorigin/releases" \
  --jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page_asset_total: \(.)"' || true

Repository: FIERsity/envorigin

Length of output: 329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'release pagination count comparison:\n'
printf '%s\n' 'first_page:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" --jq 'length'
printf '%s\n' 'first_2_pages:'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" --jq 'add | length'
printf '%s\n' 'first_page_sum_of_counts:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" --jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page: \(.)"'
printf '%s\n' 'all_pages_sum_of_counts:'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" --jq 'add | reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "all_pages: \(.)"'

Repository: FIERsity/envorigin

Length of output: 1800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

printf 'all release pages saved with --paginate --slurp:\n'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" > "$tmp/releases_slurp.json"

printf '%s\n' 'pages_length:'
jq 'length' "$tmp/releases_slurp.json"

printf '%s\n' 'first_page_sum_of_counts:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" \
  --jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page: \(.)"'

printf '%s\n' 'all_pages_sum_of_counts:'
jq 'add | reduce .[].assets[]? as $a (0; . + ($a.download_count // 0))' "$tmp/releases_slurp.json"

Repository: FIERsity/envorigin

Length of output: 291


Paginate the release list before summing downloads. Without gh api --paginate --slurp, this only processes the first page of releases, so repositories with more pages return an incomplete download total.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 46-46: Expressions don't expand in single quotes, use double quotes for that.

(SC2016)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/usage-stats.sh` around lines 45 - 46, Update the release download
aggregation command in the release binary downloads section to use gh api
pagination with slurped results before reducing over all release assets,
ensuring the download total includes every release page.

Comment thread scripts/usage-stats.sh
Comment on lines +52 to +54
section "== Hacker News mentions =="
hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')"
echo " mentions: ${hn:-0}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file context =="
sed -n '1,90p' scripts/usage-stats.sh || true

echo "== jq null behavior probe =="
if command -v jq >/dev/null 2>&1; then
  jq -r '{} | {mentions: (.nbHits // 0)}'
  jq -r '.nbHits // 0' <<EOF
{}
EOF
  jq -r '.nbHits // 0' <<EOF
{"nbHits": null}
EOF
  jq -r '.nbHits // 0' <<EOF
{"nbHits": 123}
EOF
else
  echo "jq not installed"
fi

echo "== shell parameter expansion probe =="
python3 - <<'PY'
missing=""
present="null"
print("missing:", repr(missing or "0"))
print("present:", repr(present or "0"))
PY

Repository: FIERsity/envorigin

Length of output: 3076


🌐 Web query:

jq manual null default operator // example .x // 0 jq null

💡 Result:

In jq, the // operator is the alternative operator, commonly used to provide a default value when a field is missing, null, or false [1][2][3]. The expression.x // 0 works as follows: If the expression.x produces any result other than false or null, that result is returned [4][1][3]. If.x results in false or null (or if the field.x is missing, which evaluates to null), the right-hand side of the operator (0) is returned instead [2][3]. Example usage: If your input is {"x": 10}, the filter.x // 0 returns 10. If your input is {"x": null} or {}, the filter.x // 0 returns 0 [2][3]. A subtle but important distinction exists when using generators (expressions that produce multiple values). The filter (a, b) // c will output a and b if they are not false or null; c is only output if no truthy values are produced by the left-hand side [4][5]. If you intend to provide a default for every single element in a stream, you should use the pipe operator: (a, b) |. // c [4].

Citations:


🌐 Web query:

jq null string output -r

💡 Result:

In jq, when a query results in a null value or fails to find a specified key, the output is typically the literal string "null" (when using --raw-output/-r) or the JSON value null [1][2][3]. You can control this behavior using several common techniques depending on whether you want to suppress the output, replace it with an empty string, or provide a default value [1][4][5]. To suppress output when the result is null: Use the select filter to filter out null results: jq '.field | select(.!= null)' [4][6] Alternatively, you can use the empty keyword, which returns no results [3]: jq '.field // empty' [1][4] To replace null with an empty string: Use the alternative operator (//) to provide an empty string as a default [1][7][5]: jq -r '.field // ""' [7] To handle missing keys versus null values: Accessing a missing key in jq returns null [3][8]. If you need to distinguish between a key that is missing and a key that is explicitly set to null, use the has/1 function [2][5]: jq 'if has("field") then.field else "default" end' [2] When using jq in shell scripts, if you want to detect when a command returns null as a failure, you can use the --exit-status (-e) flag [1][2]. This causes jq to exit with a non-zero status if the final output is null or false [1][3].

Citations:


Convert a missing Hacker News count in jq.

When the API response omits .nbHits, jq -r '.nbHits' prints the string null; ${hn:-0} only handles an unset variable. Use jq’s alternative operator so the fallback is applied before assignment.

Proposed fix
- hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')"
+ hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits // 0')"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
section "== Hacker News mentions =="
hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')"
echo " mentions: ${hn:-0}"
section "== Hacker News mentions =="
hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits // 0')"
echo " mentions: ${hn:-0}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/usage-stats.sh` around lines 52 - 54, Update the Hacker News count
assignment for hn to use jq’s alternative operator, defaulting a missing or null
.nbHits value to 0 before assignment; keep the existing curl request and output
behavior unchanged.

@FIERsity
FIERsity merged commit 7409425 into main Aug 6, 2026
5 checks passed
@FIERsity
FIERsity deleted the feat/usage-stats branch August 6, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant