Coding‑hours report #25
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: Coding‑hours report | |
on: | |
schedule: | |
- cron: '0 0 * * 1' # every Monday 00:00 UTC | |
workflow_dispatch: | |
inputs: | |
window_start: | |
description: 'Report since YYYY‑MM‑DD' | |
required: false | |
permissions: | |
contents: write | |
pages: write | |
id-token: write | |
jobs: | |
############################################################################### | |
# Job 1 – run git‑hours (Go), build badge, commit to `metrics` | |
############################################################################### | |
report: | |
if: github.ref == 'refs/heads/develop' | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
with: { fetch-depth: 0 } | |
- uses: actions/setup-go@v4 | |
with: { go-version: '1.24' } | |
- name: Install git‑hours v0.1.2 | |
run: | | |
git clone --depth 1 --branch v0.1.2 https://github.com/trinhminhtriet/git-hours.git git-hours-src | |
sed -i 's/go 1.24.1/go 1.24/' git-hours-src/go.mod | |
(cd git-hours-src && go install .) | |
# v0.1.2 has no --version flag; show help header instead | |
git-hours -h | head -n 1 | |
- name: Generate raw report | |
run: | | |
ARGS="" | |
if [ -n "${{ github.event.inputs.window_start }}" ]; then | |
ARGS+=" -since ${{ github.event.inputs.window_start }}" | |
fi | |
git-hours $ARGS > raw.txt | |
cat raw.txt | |
# ──────────────────────────── PATCH ① auto‑detect JSON vs table ── | |
- name: Convert to JSON | |
run: | | |
python - <<'PY' | |
import json, re, pathlib | |
raw_text = pathlib.Path('raw.txt').read_text().lstrip() | |
def table_to_json(lines): | |
obj, th, tc = {}, 0, 0 | |
for line in lines: | |
if not line or line.lower().startswith(('author','name','user','----','total')): | |
continue | |
parts = re.split(r'\s+', line.strip()) | |
if len(parts) < 3: | |
continue | |
commits = int(parts[-1]) | |
hours = int(parts[-2]) | |
email = ' '.join(parts[:-2]) | |
obj[email] = {"name": email, "hours": hours, "commits": commits} | |
th += hours; tc += commits | |
obj["total"] = {"name":"", "hours": th, "commits": tc} | |
return obj | |
try: # already JSON? | |
data = json.loads(raw_text) | |
if "total" not in data: | |
th = sum(v["hours"] for v in data.values()) | |
tc = sum(v["commits"] for v in data.values()) | |
data["total"] = {"name":"", "hours": th, "commits": tc} | |
except json.JSONDecodeError: | |
data = table_to_json(raw_text.splitlines()) | |
pathlib.Path('git-hours.json').write_text(json.dumps(data, indent=2)) | |
PY | |
# ──────────────────────────────────────────────────────────────── | |
- name: Install jq | |
run: sudo apt-get update -y && sudo apt-get install -y jq | |
- name: Build badge.json | |
run: | | |
HOURS=$(jq '.total.hours' git-hours.json) | |
cat > badge.json <<EOF | |
{ "schemaVersion":1, | |
"label":"Coding hours", | |
"message":"${HOURS}h", | |
"color":"informational" } | |
EOF | |
- name: Add workflow summary | |
run: | | |
echo "### ⏱ Coding‑hours report" >> "$GITHUB_STEP_SUMMARY" | |
jq -r ' | |
to_entries | |
| map(select(.key!="total")) | |
| sort_by(-.value.hours) | |
| (["Contributor","Hours","Commits"] | |
, (map([.key, (.value.hours|tostring), (.value.commits|tostring)]))) | |
| @tsv' git-hours.json | column -t -s $'\t' >> "$GITHUB_STEP_SUMMARY" | |
- uses: actions/upload-artifact@v4 | |
with: | |
name: git-hours-json | |
path: git-hours.json | |
retention-days: 30 | |
# ──────────────────────────── PATCH ③ safer push logic ─────────── | |
- name: Push to metrics branch | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
git config --global user.name "git-hours bot" | |
git config --global user.email "[email protected]" | |
# Stash everything (tracked + untracked) so checkout can’t complain. | |
git stash push --include-untracked --quiet | |
# Ensure we have the latest metrics from remote, if it exists. | |
git fetch origin metrics || true | |
if git show-ref --quiet refs/remotes/origin/metrics; then | |
git switch --quiet metrics || git switch -c metrics origin/metrics | |
git pull --ff-only origin metrics || true | |
else | |
git switch --orphan metrics | |
git reset --hard | |
fi | |
# Restore stashed badge.json + reports/ | |
git stash pop --quiet || true | |
mkdir -p reports | |
cp git-hours.json "reports/git-hours-$(date +%F).json" | |
git add reports badge.json | |
git commit -m "chore(metrics): report $(date +%F)" || echo "No change" | |
git push https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} metrics \ | |
|| git push --force-with-lease https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} metrics | |
############################################################################### | |
# Job 2 – build Site & upload Pages artifact | |
############################################################################### | |
build-site: | |
needs: report | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: { name: git-hours-json, path: tmp } | |
- name: Build KPIs site | |
run: | | |
DATE=$(date +%F) | |
mkdir -p site/data | |
cp tmp/git-hours.json "site/data/git-hours-${DATE}.json" | |
cp tmp/git-hours.json site/git-hours-latest.json | |
# (HTML generator unchanged) | |
python - <<'PY' | |
import json, datetime, pathlib, html, textwrap | |
data = json.load(open('tmp/git-hours.json')) | |
total = data['total'] | |
labels = [html.escape(k) for k in data if k != 'total'] | |
rows = "\n".join( | |
f"<tr><td>{l}</td><td>{data[l]['hours']}</td><td>{data[l]['commits']}</td></tr>" | |
for l in labels) | |
page = f""" | |
<!doctype html><html lang='en'><head> | |
<meta charset='utf-8'> | |
<title>Collaborator KPIs</title> | |
<link rel='stylesheet' | |
href='https://cdn.jsdelivr.net/npm/simpledotcss/simple.min.css'> | |
<script src='https://cdn.jsdelivr.net/npm/sortable-tablesort/sortable.min.js' defer></script> | |
<script src='https://cdn.jsdelivr.net/npm/chart.js'></script> | |
<style>canvas{{max-height:400px}}</style> | |
</head><body><main> | |
<h1>Collaborator KPIs</h1> | |
<p><em>Last updated {datetime.datetime.utcnow():%Y‑%m‑%d %H:%M UTC}</em></p> | |
<h2>Totals</h2> | |
<ul> | |
<li><strong>Hours</strong>: {total['hours']}</li> | |
<li><strong>Commits</strong>: {total['commits']}</li> | |
<li><strong>Contributors</strong>: {len(data)-1}</li> | |
</ul> | |
<h2>Hours per contributor</h2> | |
<canvas id='hoursChart'></canvas> | |
<h2>Detail table</h2> | |
<table class='sortable'> | |
<thead><tr><th>Contributor</th><th>Hours</th><th>Commits</th></tr></thead> | |
<tbody>{rows}</tbody> | |
</table> | |
<p>Historical JSON snapshots live in <code>/data</code>.</p> | |
<script> | |
fetch('git-hours-latest.json') | |
.then(r => r.json()) | |
.then(d => {{ | |
const labels = Object.keys(d).filter(k => k !== 'total'); | |
const hours = labels.map(l => d[l].hours); | |
new Chart(document.getElementById('hoursChart'), {{ | |
type: 'bar', | |
data: {{ labels, datasets:[{{label:'Hours',data:hours}}] }}, | |
options: {{ | |
responsive:true, maintainAspectRatio:false, | |
plugins:{{legend:{{display:false}}}}, | |
scales:{{y:{{beginAtZero:true}}}} | |
}} | |
}}); | |
}}); | |
</script> | |
</main></body></html> | |
""" | |
pathlib.Path('site/index.html').write_text(textwrap.dedent(page)) | |
PY | |
# ───────────────────── PATCH ② bump to v3 (uses artifact@v4) ────── | |
- uses: actions/upload-pages-artifact@v3 | |
with: { path: site } | |
# ─────────────────────────────────────────────────────────────────── | |
############################################################################### | |
# Job 3 – deploy to GitHub Pages | |
############################################################################### | |
deploy-pages: | |
needs: build-site | |
runs-on: ubuntu-latest | |
environment: | |
name: github-pages | |
url: ${{ steps.deployment.outputs.page_url }} | |
steps: | |
- id: deployment | |
uses: actions/deploy-pages@v4 |