Skip to content

Commit 21c4e40

Browse files
authored
Add daily GitHub clone count update workflow
This workflow updates the clone count of the repository daily and creates a badge in a markdown file if it doesn't exist. It handles authentication, fetches clone statistics, and manages a gist for storing historical data. Signed-off-by: Nathan Thaler <nathan.thaler@broadcom.com>
1 parent 73c0e73 commit 21c4e40

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

.github/workflows/clone.yml

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
name: GitHub Clone Count Update Everyday
2+
3+
on:
4+
schedule:
5+
- cron: 0 0 * * *
6+
workflow_dispatch:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
# Required to commit CLONE.md file to repository
13+
# Note: contents: write is the minimal permission needed for this workflow
14+
# trunk-ignore(checkov): contents: write is required to commit files to repository
15+
contents: write
16+
17+
steps:
18+
- uses: actions/checkout@v6
19+
with:
20+
ref: ${{ github.ref }}
21+
fetch-depth: 0
22+
token: ${{ secrets.CLONE_SECRET_TOKEN }}
23+
24+
- name: gh login
25+
run: echo "${{ secrets.CLONE_SECRET_TOKEN }}" | gh auth login --with-token
26+
27+
- name: parse latest clone count
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.CLONE_SECRET_TOKEN }}
30+
run: |
31+
set -euo pipefail
32+
# CLONE_SECRET_TOKEN is required because:
33+
# 1. The traffic/clones endpoint requires "Administration" (read) permissions
34+
# 2. GITHUB_TOKEN typically doesn't have access to traffic data
35+
# 3. A Personal Access Token (PAT) with repo scope is needed
36+
# Using environment variable to avoid exposing secret in process list
37+
# Note: Secret is passed via env var, not command line, for security
38+
# trunk-ignore(trufflehog): Secret properly handled via environment variable, not command line
39+
if ! curl -f --user "${{ github.actor }}:$GITHUB_TOKEN" \
40+
-H "Accept: application/vnd.github.v3+json" \
41+
"https://api.github.com/repos/${{ github.repository }}/traffic/clones" \
42+
> clone.json; then
43+
echo "Error: Failed to fetch clone statistics"
44+
exit 1
45+
fi
46+
- name: create gist and download previous count
47+
id: set_id
48+
run: |
49+
set -euo pipefail
50+
if gh secret list | grep -q "GIST_ID"
51+
then
52+
echo "GIST_ID found"
53+
echo "GIST=${{ secrets.GIST_ID }}" >> $GITHUB_OUTPUT
54+
curl -f "https://gist.githubusercontent.com/${{ github.actor }}/${{ secrets.GIST_ID }}/raw/clone.json" > clone_before.json || true
55+
if grep -q '404: Not Found' clone_before.json 2>/dev/null || [ ! -s clone_before.json ]; then
56+
echo "GIST_ID not valid anymore. Creating another gist..."
57+
gist_id=$(gh gist create clone.json | awk -F / '{print $NF}')
58+
if [ -z "$gist_id" ]; then
59+
echo "Error: Failed to create gist"
60+
exit 1
61+
fi
62+
echo "$gist_id" | gh secret set GIST_ID
63+
echo "GIST=$gist_id" >> $GITHUB_OUTPUT
64+
cp clone.json clone_before.json
65+
git rm --ignore-unmatch CLONE.md || true
66+
fi
67+
else
68+
echo "GIST_ID not found. Creating a gist..."
69+
gist_id=$(gh gist create clone.json | awk -F / '{print $NF}')
70+
if [ -z "$gist_id" ]; then
71+
echo "Error: Failed to create gist"
72+
exit 1
73+
fi
74+
echo "$gist_id" | gh secret set GIST_ID
75+
echo "GIST=$gist_id" >> $GITHUB_OUTPUT
76+
cp clone.json clone_before.json
77+
fi
78+
- name: update clone.json
79+
run: |
80+
set -euo pipefail
81+
python3 << 'EOF'
82+
import json
83+
84+
# Read current clone data from GitHub API
85+
with open('clone.json', 'r') as fh:
86+
now = json.load(fh)
87+
88+
# Read previous accumulated data
89+
with open('clone_before.json', 'r') as fh:
90+
before = json.load(fh)
91+
92+
# Create timestamp index for efficient lookup
93+
timestamps = {before['clones'][i]['timestamp']: i for i in range(len(before['clones']))}
94+
95+
# Start with previous data and merge in new data
96+
latest = dict(before)
97+
for i in range(len(now['clones'])):
98+
timestamp = now['clones'][i]['timestamp']
99+
if timestamp in timestamps:
100+
# Update existing timestamp with new data
101+
latest['clones'][timestamps[timestamp]] = now['clones'][i]
102+
else:
103+
# Add new timestamp
104+
latest['clones'].append(now['clones'][i])
105+
106+
# Recalculate totals from all clones
107+
latest['count'] = sum(map(lambda x: int(x['count']), latest['clones']))
108+
latest['uniques'] = sum(map(lambda x: int(x['uniques']), latest['clones']))
109+
110+
# Aggregate old data by month if we have more than 100 timestamps
111+
if len(timestamps) > 100:
112+
remove_this = []
113+
clones = latest['clones']
114+
for i in range(len(timestamps) - 35):
115+
clones[i]['timestamp'] = clones[i]['timestamp'][:7] # Truncate to YYYY-MM
116+
if clones[i]['timestamp'] == clones[i+1]['timestamp'][:7]:
117+
clones[i+1]['count'] += clones[i]['count']
118+
clones[i+1]['uniques'] += clones[i]['uniques']
119+
remove_this.append(clones[i])
120+
121+
for item in remove_this:
122+
clones.remove(item)
123+
124+
# Write updated data back
125+
with open('clone.json', 'w', encoding='utf-8') as fh:
126+
json.dump(latest, fh, ensure_ascii=False, indent=4)
127+
EOF
128+
- name: Update gist with latest count
129+
env:
130+
GITHUB_TOKEN: ${{ secrets.CLONE_SECRET_TOKEN }}
131+
run: |
132+
set -euo pipefail
133+
# Using environment variable to avoid exposing secret in process list
134+
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')
135+
echo '{"description": "${{ github.repository }} clone statistics", "files": {"clone.json": {"content": "'"$content"'"}}}' > post_clone.json
136+
if ! curl -f -s -X PATCH \
137+
--user "${{ github.actor }}:$GITHUB_TOKEN" \
138+
-H "Content-Type: application/json" \
139+
-d @post_clone.json "https://api.github.com/gists/${{ steps.set_id.outputs.GIST }}" > /dev/null; then
140+
echo "Error: Failed to update gist"
141+
exit 1
142+
fi
143+
if [ ! -f CLONE.md ]; then
144+
shields="https://img.shields.io/badge/dynamic/json?color=success&label=Clone&query=count&url="
145+
url="https://gist.githubusercontent.com/${{ github.actor }}/${{ steps.set_id.outputs.GIST }}/raw/clone.json"
146+
repo="https://gist.githubusercontent.com/${{ github.actor }}/${{ steps.set_id.outputs.GIST }}/raw/clone.json"
147+
echo '' > CLONE.md
148+
echo '**Markdown**' >> CLONE.md
149+
echo '```markdown' >> CLONE.md
150+
echo "[![GitHub Clones](${shields}${url}&logo=github)](${repo})" >> CLONE.md
151+
echo '```' >> CLONE.md
152+
153+
git add CLONE.md
154+
git config --global user.name "GitHub Action"
155+
git config --global user.email "action@github.com"
156+
git commit -m "create clone count badge"
157+
fi
158+
- name: Push
159+
uses: ad-m/github-push-action@master
160+
with:
161+
github_token: ${{ secrets.CLONE_SECRET_TOKEN }}
162+
branch: "gh-actions"
163+
force_with_lease: true

0 commit comments

Comments
 (0)