Pull plugin stats #1676
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Pull plugin stats | |
| on: | |
| schedule: | |
| - cron: "0 0 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| concurrency: | |
| group: plugin-stat | |
| cancel-in-progress: false | |
| env: | |
| STATS_URL: https://community.obsidian.md/assets/community-plugin-stats.json | |
| STATS_FILE: community-plugin-stats.json | |
| # New file must retain at least this fraction of the current file's entry count. | |
| MIN_RETENTION_PCT: 95 | |
| # Hard floor guards against the percent-check passing on an already-small file. | |
| STATS_MIN_ABSOLUTE: 4000 | |
| jobs: | |
| pull-stats: | |
| if: github.repository == 'obsidianmd/obsidian-releases' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: "22.x" | |
| - name: Fetch plugin stats JSON | |
| run: | | |
| curl -fsSL --retry 5 --retry-delay 10 --max-time 60 \ | |
| -o /tmp/stats.new "$STATS_URL" | |
| - name: Validate plugin stats JSON | |
| run: | | |
| set -euo pipefail | |
| NEW=/tmp/stats.new | |
| OLD="$STATS_FILE" | |
| jq -e 'type == "object"' "$NEW" > /dev/null \ | |
| || { echo "::error::stats: top-level is not a JSON object"; exit 1; } | |
| # Every entry is an object with numeric downloads/updated, and every value | |
| # within each entry (per-version counts included) is a number. | |
| jq -e ' | |
| all(.[]; | |
| type == "object" | |
| and (.downloads | type) == "number" | |
| and (.updated | type) == "number" | |
| and all(.[]; type == "number")) | |
| ' "$NEW" > /dev/null \ | |
| || { echo "::error::stats: one or more entries are malformed"; exit 1; } | |
| NEW_COUNT=$(jq 'length' "$NEW") | |
| OLD_COUNT=$(jq 'length' "$OLD") | |
| MIN_RETAINED=$(( OLD_COUNT * MIN_RETENTION_PCT / 100 )) | |
| echo "stats: new=$NEW_COUNT old=$OLD_COUNT min_retained=$MIN_RETAINED absolute_floor=$STATS_MIN_ABSOLUTE" | |
| if [ "$NEW_COUNT" -lt "$MIN_RETAINED" ]; then | |
| echo "::error::stats: new count $NEW_COUNT < ${MIN_RETENTION_PCT}% of current $OLD_COUNT" | |
| exit 1 | |
| fi | |
| if [ "$NEW_COUNT" -lt "$STATS_MIN_ABSOLUTE" ]; then | |
| echo "::error::stats: new count $NEW_COUNT below absolute floor $STATS_MIN_ABSOLUTE" | |
| exit 1 | |
| fi | |
| - name: Compute diff summary | |
| run: | | |
| set -euo pipefail | |
| DIFF=$(jq -n \ | |
| --slurpfile old "$STATS_FILE" \ | |
| --slurpfile new /tmp/stats.new \ | |
| ' | |
| ($old[0]) as $o | | |
| ($new[0]) as $n | | |
| ($o | keys) as $ok | | |
| ($n | keys) as $nk | | |
| { | |
| total_old: ($ok | length), | |
| total_new: ($nk | length), | |
| added: ($nk - $ok | length), | |
| removed: ($ok - $nk | length), | |
| downloads_old: ([$o[].downloads // 0] | add), | |
| downloads_new: ([$n[].downloads // 0] | add) | |
| } | |
| ') | |
| echo "$DIFF" | |
| # Expose added/removed counts for the commit message. | |
| { | |
| echo "STATS_ADDED=$(jq -r '.added' <<< "$DIFF")" | |
| echo "STATS_REMOVED=$(jq -r '.removed' <<< "$DIFF")" | |
| } >> "$GITHUB_ENV" | |
| { | |
| echo "### Plugin stats diff summary" | |
| echo "" | |
| echo "| Entries (old) | Entries (new) | Added | Removed | Downloads (old) | Downloads (new) |" | |
| echo "|---:|---:|---:|---:|---:|---:|" | |
| jq -r '"| \(.total_old) | \(.total_new) | \(.added) | \(.removed) | \(.downloads_old) | \(.downloads_new) |"' <<< "$DIFF" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Stage validated file | |
| run: mv /tmp/stats.new "$STATS_FILE" | |
| - name: Format with prettier | |
| run: | | |
| npm ci --no-save | |
| npm run format | |
| - name: Commit and push if changed | |
| run: | | |
| set -euo pipefail | |
| if git diff --quiet -- "$STATS_FILE"; then | |
| echo "No changes to commit" | |
| exit 0 | |
| fi | |
| git config --local user.name 'Obsidian Bot' | |
| git config --local user.email 'admin@obsidian.md' | |
| git add "$STATS_FILE" | |
| git commit -m "chore: Update plugin stats (-${STATS_REMOVED}/+${STATS_ADDED} plugins)" | |
| git push | |
| - name: Report failure as issue | |
| if: failure() | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| BODY=$'Plugin stats mirror workflow failed.\n\nRun: '"$RUN_URL" | |
| EXISTING=$(gh issue list --label plugin-stat-failure --state open --limit 1 --json number --jq '.[0].number // empty') | |
| if [ -n "$EXISTING" ]; then | |
| gh issue comment "$EXISTING" --body "$BODY" | |
| else | |
| gh label create plugin-stat-failure --color B60205 --description "Automated plugin stats mirror workflow failed" 2>/dev/null || true | |
| gh issue create \ | |
| --title "Plugin stats mirror workflow failed" \ | |
| --label plugin-stat-failure \ | |
| --body "$BODY" | |
| fi |