GitHub Clone Count Update Everyday #7
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: GitHub Clone Count Update Everyday | |
| on: | |
| schedule: | |
| - cron: 0 0 * * * | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # Required to commit CLONE.md file to repository | |
| # Note: contents: write is the minimal permission needed for this workflow | |
| # trunk-ignore(checkov): contents: write is required to commit files to repository | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.ref }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.CLONE_SECRET_TOKEN }} | |
| - name: gh login | |
| run: echo "${{ secrets.CLONE_SECRET_TOKEN }}" | gh auth login --with-token | |
| - name: parse latest clone count | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.CLONE_SECRET_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # CLONE_SECRET_TOKEN is required because: | |
| # 1. The traffic/clones endpoint requires "Administration" (read) permissions | |
| # 2. GITHUB_TOKEN typically doesn't have access to traffic data | |
| # 3. A Personal Access Token (PAT) with repo scope is needed | |
| # Using environment variable to avoid exposing secret in process list | |
| # Note: Secret is passed via env var, not command line, for security | |
| # trunk-ignore(trufflehog): Secret properly handled via environment variable, not command line | |
| if ! curl -f --user "${{ github.actor }}:$GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| "https://api.github.com/repos/${{ github.repository }}/traffic/clones" \ | |
| > clone.json; then | |
| echo "Error: Failed to fetch clone statistics" | |
| exit 1 | |
| fi | |
| - name: create gist and download previous count | |
| id: set_id | |
| run: | | |
| set -euo pipefail | |
| if gh secret list | grep -q "GIST_ID" | |
| then | |
| echo "GIST_ID found" | |
| echo "GIST=${{ secrets.GIST_ID }}" >> $GITHUB_OUTPUT | |
| curl -f "https://gist.githubusercontent.com/${{ github.actor }}/${{ secrets.GIST_ID }}/raw/clone.json" > clone_before.json || true | |
| if grep -q '404: Not Found' clone_before.json 2>/dev/null || [ ! -s clone_before.json ]; then | |
| echo "GIST_ID not valid anymore. Creating another gist..." | |
| gist_id=$(gh gist create clone.json | awk -F / '{print $NF}') | |
| if [ -z "$gist_id" ]; then | |
| echo "Error: Failed to create gist" | |
| exit 1 | |
| fi | |
| echo "$gist_id" | gh secret set GIST_ID | |
| echo "GIST=$gist_id" >> $GITHUB_OUTPUT | |
| cp clone.json clone_before.json | |
| git rm --ignore-unmatch CLONE.md || true | |
| fi | |
| else | |
| echo "GIST_ID not found. Creating a gist..." | |
| gist_id=$(gh gist create clone.json | awk -F / '{print $NF}') | |
| if [ -z "$gist_id" ]; then | |
| echo "Error: Failed to create gist" | |
| exit 1 | |
| fi | |
| echo "$gist_id" | gh secret set GIST_ID | |
| echo "GIST=$gist_id" >> $GITHUB_OUTPUT | |
| cp clone.json clone_before.json | |
| fi | |
| - name: update clone.json | |
| run: | | |
| set -euo pipefail | |
| python3 << 'EOF' | |
| import json | |
| # Read current clone data from GitHub API | |
| with open('clone.json', 'r') as fh: | |
| now = json.load(fh) | |
| # Read previous accumulated data | |
| with open('clone_before.json', 'r') as fh: | |
| before = json.load(fh) | |
| # Create timestamp index for efficient lookup | |
| timestamps = {before['clones'][i]['timestamp']: i for i in range(len(before['clones']))} | |
| # Start with previous data and merge in new data | |
| latest = dict(before) | |
| for i in range(len(now['clones'])): | |
| timestamp = now['clones'][i]['timestamp'] | |
| if timestamp in timestamps: | |
| # Update existing timestamp with new data | |
| latest['clones'][timestamps[timestamp]] = now['clones'][i] | |
| else: | |
| # Add new timestamp | |
| latest['clones'].append(now['clones'][i]) | |
| # Recalculate totals from all clones | |
| latest['count'] = sum(map(lambda x: int(x['count']), latest['clones'])) | |
| latest['uniques'] = sum(map(lambda x: int(x['uniques']), latest['clones'])) | |
| # Aggregate old data by month if we have more than 100 timestamps | |
| if len(timestamps) > 100: | |
| remove_this = [] | |
| clones = latest['clones'] | |
| for i in range(len(timestamps) - 35): | |
| clones[i]['timestamp'] = clones[i]['timestamp'][:7] # Truncate to YYYY-MM | |
| if clones[i]['timestamp'] == clones[i+1]['timestamp'][:7]: | |
| clones[i+1]['count'] += clones[i]['count'] | |
| clones[i+1]['uniques'] += clones[i]['uniques'] | |
| remove_this.append(clones[i]) | |
| for item in remove_this: | |
| clones.remove(item) | |
| # Write updated data back | |
| with open('clone.json', 'w', encoding='utf-8') as fh: | |
| json.dump(latest, fh, ensure_ascii=False, indent=4) | |
| EOF | |
| - name: Update gist with latest count | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.CLONE_SECRET_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Using environment variable to avoid exposing secret in process list | |
| content=$(sed -e 's/\\/\\\\/g' -e 's/\t/\\t/g' -e 's/\"/\\"/g' -e 's/\r//g' "clone.json" | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g') | |
| echo '{"description": "${{ github.repository }} clone statistics", "files": {"clone.json": {"content": "'"$content"'"}}}' > post_clone.json | |
| if ! curl -f -s -X PATCH \ | |
| --user "${{ github.actor }}:$GITHUB_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d @post_clone.json "https://api.github.com/gists/${{ steps.set_id.outputs.GIST }}" > /dev/null; then | |
| echo "Error: Failed to update gist" | |
| exit 1 | |
| fi | |
| if [ ! -f CLONE.md ]; then | |
| shields="https://img.shields.io/badge/dynamic/json?color=success&label=Clone&query=count&url=" | |
| url="https://gist.githubusercontent.com/${{ github.actor }}/${{ steps.set_id.outputs.GIST }}/raw/clone.json" | |
| repo="https://gist.githubusercontent.com/${{ github.actor }}/${{ steps.set_id.outputs.GIST }}/raw/clone.json" | |
| echo '' > CLONE.md | |
| echo '**Markdown**' >> CLONE.md | |
| echo '```markdown' >> CLONE.md | |
| echo "[](${repo})" >> CLONE.md | |
| echo '```' >> CLONE.md | |
| git add CLONE.md | |
| git config --global user.name "GitHub Action" | |
| git config --global user.email "[email protected]" | |
| git commit -m "create clone count badge" | |
| fi | |
| - name: Push | |
| uses: ad-m/github-push-action@master | |
| with: | |
| github_token: ${{ secrets.CLONE_SECRET_TOKEN }} | |
| branch: "gh-actions" | |
| force_with_lease: true |