@@ -2,88 +2,248 @@ name: Coding‑hours report
22
33on :
44 schedule :
5- - cron : ' 0 0 * * 1' # every Monday at 00:00 UTC
6- workflow_dispatch : # manual trigger
5+ - cron : ' 0 0 * * 1' # every Monday 00:00 UTC
6+ workflow_dispatch :
77 inputs :
88 window_start :
99 description : ' Report since YYYY‑MM‑DD'
1010 required : false
1111
1212permissions :
13- contents : write # needed for committing to the metrics branch
13+ contents : write
14+ pages : write
15+ id-token : write
1416
1517jobs :
18+ # ##############################################################################
19+ # Job 1 – run git‑hours (Go), build badge, commit to `metrics`
20+ # ##############################################################################
1621 report :
22+ if : github.ref == 'refs/heads/develop'
1723 runs-on : ubuntu-latest
24+
25+ steps :
26+ - uses : actions/checkout@v4
27+ with : { fetch-depth: 0 }
28+
29+ - uses : actions/setup-go@v4
30+ with : { go-version: '1.24' }
31+
32+ - name : Install git‑hours v0.1.2
33+ run : |
34+ git clone --depth 1 --branch v0.1.2 https://github.com/trinhminhtriet/git-hours.git git-hours-src
35+ sed -i 's/go 1.24.1/go 1.24/' git-hours-src/go.mod
36+ (cd git-hours-src && go install .)
37+ # v0.1.2 has no --version flag; show help header instead
38+ git-hours -h | head -n 1
39+
40+ - name : Generate raw report
41+ run : |
42+ ARGS=""
43+ if [ -n "${{ github.event.inputs.window_start }}" ]; then
44+ ARGS+=" -since ${{ github.event.inputs.window_start }}"
45+ fi
46+ git-hours $ARGS > raw.txt
47+ cat raw.txt
48+
49+ # ──────────────────────────── PATCH ① auto‑detect JSON vs table ──
50+ - name : Convert to JSON
51+ run : |
52+ python - <<'PY'
53+ import json, re, pathlib
54+ raw_text = pathlib.Path('raw.txt').read_text().lstrip()
55+
56+ def table_to_json(lines):
57+ obj, th, tc = {}, 0, 0
58+ for line in lines:
59+ if not line or line.lower().startswith(('author','name','user','----','total')):
60+ continue
61+ parts = re.split(r'\s+', line.strip())
62+ if len(parts) < 3:
63+ continue
64+ commits = int(parts[-1])
65+ hours = int(parts[-2])
66+ email = ' '.join(parts[:-2])
67+ obj[email] = {"name": email, "hours": hours, "commits": commits}
68+ th += hours; tc += commits
69+ obj["total"] = {"name":"", "hours": th, "commits": tc}
70+ return obj
71+
72+ try: # already JSON?
73+ data = json.loads(raw_text)
74+ if "total" not in data:
75+ th = sum(v["hours"] for v in data.values())
76+ tc = sum(v["commits"] for v in data.values())
77+ data["total"] = {"name":"", "hours": th, "commits": tc}
78+ except json.JSONDecodeError:
79+ data = table_to_json(raw_text.splitlines())
80+
81+ pathlib.Path('git-hours.json').write_text(json.dumps(data, indent=2))
82+ PY
83+ # ────────────────────────────────────────────────────────────────
84+
85+ - name : Install jq
86+ run : sudo apt-get update -y && sudo apt-get install -y jq
87+
88+ - name : Build badge.json
89+ run : |
90+ HOURS=$(jq '.total.hours' git-hours.json)
91+ cat > badge.json <<EOF
92+ { "schemaVersion":1,
93+ "label":"Coding hours",
94+ "message":"${HOURS}h",
95+ "color":"informational" }
96+ EOF
97+
98+ - name : Add workflow summary
99+ run : |
100+ echo "### ⏱ Coding‑hours report" >> "$GITHUB_STEP_SUMMARY"
101+ jq -r '
102+ to_entries
103+ | map(select(.key!="total"))
104+ | sort_by(-.value.hours)
105+ | (["Contributor","Hours","Commits"]
106+ , (map([.key, (.value.hours|tostring), (.value.commits|tostring)])))
107+ | @tsv' git-hours.json | column -t -s $'\t' >> "$GITHUB_STEP_SUMMARY"
108+
109+ - uses : actions/upload-artifact@v4
110+ with :
111+ name : git-hours-json
112+ path : git-hours.json
113+ retention-days : 30
114+
115+ # ──────────────────────────── PATCH ③ safer push logic ───────────
116+ - name : Push to metrics branch
117+ env :
118+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
119+ run : |
120+ git config --global user.name "git-hours bot"
121+ git config --global user.email "bot@github.com"
122+
123+ # Stash everything (tracked + untracked) so checkout can’t complain.
124+ git stash push --include-untracked --quiet
125+
126+ # Ensure we have the latest metrics from remote, if it exists.
127+ git fetch origin metrics || true
128+ if git show-ref --quiet refs/remotes/origin/metrics; then
129+ git switch --quiet metrics || git switch -c metrics origin/metrics
130+ git pull --ff-only origin metrics || true
131+ else
132+ git switch --orphan metrics
133+ git reset --hard
134+ fi
135+
136+ # Restore stashed badge.json + reports/
137+ git stash pop --quiet || true
138+
139+ mkdir -p reports
140+ cp git-hours.json "reports/git-hours-$(date +%F).json"
141+ git add reports badge.json
142+ git commit -m "chore(metrics): report $(date +%F)" || echo "No change"
143+
144+ git push https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} metrics \
145+ || git push --force-with-lease https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} metrics
146+
147+
148+ # ##############################################################################
149+ # Job 2 – build Site & upload Pages artifact
150+ # ##############################################################################
151+ build-site :
152+ needs : report
153+ runs-on : ubuntu-latest
154+
155+ steps :
156+ - uses : actions/checkout@v4
157+
158+ - uses : actions/download-artifact@v4
159+ with : { name: git-hours-json, path: tmp }
160+
161+ - name : Build KPIs site
162+ run : |
163+ DATE=$(date +%F)
164+ mkdir -p site/data
165+ cp tmp/git-hours.json "site/data/git-hours-${DATE}.json"
166+ cp tmp/git-hours.json site/git-hours-latest.json
167+
168+ # (HTML generator unchanged)
169+
170+ python - <<'PY'
171+ import json, datetime, pathlib, html, textwrap
172+ data = json.load(open('tmp/git-hours.json'))
173+ total = data['total']
174+ labels = [html.escape(k) for k in data if k != 'total']
175+ rows = "\n".join(
176+ f"<tr><td>{l}</td><td>{data[l]['hours']}</td><td>{data[l]['commits']}</td></tr>"
177+ for l in labels)
178+
179+ page = f"""
180+ <!doctype html><html lang='en'><head>
181+ <meta charset='utf-8'>
182+ <title>Collaborator KPIs</title>
183+ <link rel='stylesheet'
184+ href='https://cdn.jsdelivr.net/npm/simpledotcss/simple.min.css'>
185+ <script src='https://cdn.jsdelivr.net/npm/sortable-tablesort/sortable.min.js' defer></script>
186+ <script src='https://cdn.jsdelivr.net/npm/chart.js'></script>
187+ <style>canvas{{max-height:400px}}</style>
188+ </head><body><main>
189+ <h1>Collaborator KPIs</h1>
190+ <p><em>Last updated {datetime.datetime.utcnow():%Y‑%m‑%d %H:%M UTC}</em></p>
191+
192+ <h2>Totals</h2>
193+ <ul>
194+ <li><strong>Hours</strong>: {total['hours']}</li>
195+ <li><strong>Commits</strong>: {total['commits']}</li>
196+ <li><strong>Contributors</strong>: {len(data)-1}</li>
197+ </ul>
198+
199+ <h2>Hours per contributor</h2>
200+ <canvas id='hoursChart'></canvas>
201+
202+ <h2>Detail table</h2>
203+ <table class='sortable'>
204+ <thead><tr><th>Contributor</th><th>Hours</th><th>Commits</th></tr></thead>
205+ <tbody>{rows}</tbody>
206+ </table>
207+
208+ <p>Historical JSON snapshots live in <code>/data</code>.</p>
209+
210+ <script>
211+ fetch('git-hours-latest.json')
212+ .then(r => r.json())
213+ .then(d => {{
214+ const labels = Object.keys(d).filter(k => k !== 'total');
215+ const hours = labels.map(l => d[l].hours);
216+ new Chart(document.getElementById('hoursChart'), {{
217+ type: 'bar',
218+ data: {{ labels, datasets:[{{label:'Hours',data:hours}}] }},
219+ options: {{
220+ responsive:true, maintainAspectRatio:false,
221+ plugins:{{legend:{{display:false}}}},
222+ scales:{{y:{{beginAtZero:true}}}}
223+ }}
224+ }});
225+ }});
226+ </script>
227+ </main></body></html>
228+ """
229+ pathlib.Path('site/index.html').write_text(textwrap.dedent(page))
230+ PY
231+
232+ # ───────────────────── PATCH ② bump to v3 (uses artifact@v4) ──────
233+ - uses : actions/upload-pages-artifact@v3
234+ with : { path: site }
235+ # ───────────────────────────────────────────────────────────────────
236+
237+ # ##############################################################################
238+ # Job 3 – deploy to GitHub Pages
239+ # ##############################################################################
240+ deploy-pages :
241+ needs : build-site
242+ runs-on : ubuntu-latest
243+ environment :
244+ name : github-pages
245+ url : ${{ steps.deployment.outputs.page_url }}
246+
18247 steps :
19- # 1️⃣ Check out full history
20- - uses : actions/checkout@v4
21- with :
22- fetch-depth : 0
23-
24- # 2️⃣ Set up Go (>=1.24 to support git-hours v0.1.2 go.mod)
25- - name : Setup Go
26- uses : actions/setup-go@v4
27- with :
28- go-version : ' 1.24'
29-
30- # 3️⃣ Install git-hours (Go) v0.1.2 with go.mod patch
31- - name : Install git-hours (Go) v0.1.2
32- run : |
33- git clone --depth 1 --branch v0.1.2 https://github.com/trinhminhtriet/git-hours.git git-hours-src
34- cd git-hours-src
35- sed -i 's/go 1.24.1/go 1.24/' go.mod
36- go install .
37-
38- # 4️⃣ Generate the report into a text file
39- - name : Generate report
40- run : |
41- ARGS=""
42- if [ -n "${{ github.event.inputs.window_start }}" ]; then
43- ARGS+=" -since ${{ github.event.inputs.window_start }}"
44- fi
45- git-hours $ARGS > git-hours.txt
46-
47- # 4️⃣½ Build a Shields.io badge from the JSON report
48- - name : Build badge.json
49- run : |
50- HOURS=$(jq '.total.hours' git-hours.txt)
51- cat > badge.json <<EOF
52- {
53- "schemaVersion": 1,
54- "label": "Coding hours",
55- "message": "${HOURS}h",
56- "color": "informational"
57- }
58- EOF
59-
60- # 5️⃣ Publish the raw report to the run summary
61- - name : Add workflow summary
62- run : |
63- echo "### ⏱ Coding‑hours report" >> $GITHUB_STEP_SUMMARY
64- echo '```' >> $GITHUB_STEP_SUMMARY
65- cat git-hours.txt >> $GITHUB_STEP_SUMMARY
66- echo '```' >> $GITHUB_STEP_SUMMARY
67-
68- # 6️⃣ Upload the raw output as an artifact
69- - uses : actions/upload-artifact@v4
70- with :
71- name : git-hours-output-${{ github.run_id }}
72- path : git-hours.txt
73- retention-days : 30
74-
75- # 7️⃣ (Optional) Commit to metrics branch
76- - name : Commit to metrics branch
77- if : github.ref == 'refs/heads/develop'
78- env :
79- GH_TOKEN : ${{ secrets.GH_PAT }} # PAT needed for write access
80- run : |
81- git config --global user.name "git-hours bot"
82- git config --global user.email "bot@github.com"
83- git switch -C metrics || git checkout metrics
84- mkdir -p reports
85- mv git-hours.txt reports/git-hours-$(date +%F).txt
86- # Keep badge.json at repo root (in metrics branch) for a stable URL
87- git add reports badge.json
88- git commit -m "chore(metrics): add report $(date +%F)" || echo "No change"
89- git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} metrics
248+ - id : deployment
249+ uses : actions/deploy-pages@v4
0 commit comments