Skip to content

Coding‑hours report #5

Coding‑hours report

Coding‑hours report #5

Workflow file for this run

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