Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions scripts/usage-stats.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# usage-stats.sh — one-command adoption metrics for EnvOrigin.
#
# Tracks real usage signals across every public surface:
# crates.io downloads — installs via cargo install / Cargo.toml
# Homebrew analytics — installs via `brew install envorigin`
# GitHub traffic — page views and clones (last 14 days)
# Release binaries — prebuilt binary downloads (all releases)
# Code search — public repos with `uses: FIERsity/envorigin-action`
# HN Algolia — mentions on Hacker News
#
# No telemetry is collected from users — these are all public APIs.
# Read the numbers as a trend, not an exact headcount: CI runs, mirrors,
# 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.


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

UA="envorigin-usage-stats"

section() { printf '\n\033[1m%s\033[0m\n' "$1"; }

section "== crates.io (cargo install / Cargo.toml) =="
crates="$(curl -sf -H "User-Agent: $UA" https://crates.io/api/v1/crates/envorigin \
| jq -r '"total downloads: \(.crate.downloads) last 90d: \(.crate.recent_downloads) versions: \(.versions|length)"')"
printf '%s\n' "$crates"
curl -sf -H "User-Agent: $UA" https://crates.io/api/v1/crates/envorigin \
| jq -r '.versions[0:3][] | " v\(.num): \(.downloads) downloads"'

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)"'
Comment on lines +32 to +36

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.

else
echo " formula not on formulae.brew.sh yet (HTTP $code) — appears after the next release"
fi

section "== GitHub traffic (last 14 days) =="
gh api "repos/$REPO/traffic/views" --jq '" views: \(.count) unique visitors: \(.uniques)"'
gh api "repos/$REPO/traffic/clones" --jq '" clones: \(.count) unique cloners: \(.uniques)"'

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

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.


section "== Public repos using the action (uses: FIERsity/envorigin-action) =="
count="$(gh api "search/code?q=FIERsity%2Fenvorigin-action&per_page=1" --jq '.total_count')"
echo " hits: $count (includes our own dogfood jobs; minus 2-3 for real adoption)"

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

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.

Loading