GitHubの「About(私について)」は第一印象を左右すると思います。
とはいったもののGithubのProfileをカスタマイズできることを私も最近知ったのでメモベースで共有します。
この記事ではモダンな見た目とアニメーションを保ちつつ、最近のプロジェクト/活動履歴/今月の活動グラフをGitHub公式APIのみで自動更新する実装を解説します。
前提・要件
- プロフィール用リポジトリ
<your_id>/<your_id>を用意(READMEがプロフィールに表示される仕組み)
→https://github.com/kskmasa/kskmasa
こんなかんじの - GitHub Actions が使えること
完成イメージ(簡潔)
- ヒーロー部:名前・肩書・簡潔なタグライン(中央寄せ)
- 自動更新3部:
- 🔄 現在の開発中プロジェクト(直近Push順に3件、言語・スター・相対時刻)
- 🏃 最近の活動(過去14日のコミット/PR/Issueから5件)
- 🕓 今月のアクティビティ(過去30日の貢献数をアニメーションSVG棒グラフ)
- 任意:GitHub Readme StatsやTyping SVGなどの装飾を加えてもよい(SaaS依存に注意)
実装手順
1. READMEに自動差し込み用マーカーを置く
後述のスクリプトが次のマーカー間を置換する。配置場所は自由。
## 🔄 現在の開発中プロジェクト
<!-- PROJECTS:START -->
<!-- PROJECTS:END -->
## 🏃 最近の活動
<!-- ACTIVITY:START -->
<!-- ACTIVITY:END -->
## 🕓 今月のアクティビティ
<!-- MONTHLY_GRAPH:START -->
<!-- MONTHLY_GRAPH:END -->
2. ワークフローを作成(毎日自動更新)
- 実行はUTC基準。日本時間(JST=UTC+9)で09:00頃にしたい場合、
0:00 UTCを指定。GitHubのキュー状況で数分ずれることがある。
name: Update Profile README
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *" # 毎日 09:00 JST 相当(UTC 00:00)
permissions:
contents: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout profile repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install deps
run: |
pip install requests python-dateutil
- name: Generate sections & SVG
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_USER: ${{ github.repository_owner }}
run: |
python scripts/update_readme.py
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add README.md assets/monthly_activity.svg
git diff --staged --quiet || git commit -m "chore: auto-update README sections"
git push
3. 更新スクリプト(GraphQL→Markdown差し替え→SVG生成)
- 必要権限:デフォルトの
GITHUB_TOKENでGraphQL読み取りOK(公開活動のみ)。 - 取得対象:
-
repositories(orderBy: PUSHED_AT, isFork:false)で最近更新TOP3 -
contributionsCollectionから直近14日のコミット/PR/Issue - 過去30日の貢献数を配列化しアニメーションSVGで描画
-
scripts/update_readme.py(全文)
import os, json, re, sys, math
from datetime import datetime, timedelta, timezone
import requests
from dateutil import parser
TOKEN = os.getenv("GITHUB_TOKEN")
USER = os.getenv("GH_USER")
API_GQL = "https://api.github.com/graphql"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
ROOT = os.getcwd()
ASSETS_DIR = os.path.join(ROOT, "assets")
SVG_PATH = os.path.join(ASSETS_DIR, "monthly_activity.svg")
README_PATH = os.path.join(ROOT, "README.md")
# ---------- helpers ----------
def gql(query, variables=None):
r = requests.post(API_GQL, json={"query": query, "variables": variables or {}}, headers=HEADERS, timeout=30)
r.raise_for_status()
j = r.json()
if "errors" in j:
raise RuntimeError(j["errors"])
return j["data"]
def humanize(dt_iso):
dt = parser.isoparse(dt_iso).replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
diff = now - dt
s = diff.total_seconds()
if s < 60: return f"{int(s)}s ago"
if s < 3600: return f"{int(s//60)}m ago"
if s < 86400: return f"{int(s//3600)}h ago"
return f"{int(s//86400)}d ago"
def replace_between(text, start_marker, end_marker, new_body):
pattern = re.compile(
r"(?P<start><!--\s*"+re.escape(start_marker)+r"\s*-->)(?P<body>.*?)(?P<end><!--\s*"+re.escape(end_marker)+r"\s*-->)",
re.DOTALL
)
return pattern.sub(lambda m: f"{m.group('start')}\n{new_body}\n{m.group('end')}", text)
# ---------- queries ----------
QUERY_RECENT_REPOS = """
query($login:String!) {
user(login:$login){
repositories(privacy:PUBLIC, first: 20, orderBy:{field: PUSHED_AT, direction: DESC}, isFork:false) {
nodes {
name description stargazerCount
primaryLanguage { name color }
pushedAt url
}
}
}
}
"""
QUERY_EVENTS = """
query($login:String!, $from:DateTime!, $to:DateTime!) {
user(login:$login) {
contributionsCollection(from:$from, to:$to) {
commitContributionsByRepository(maxRepositories: 10) {
repository { name url }
contributions(first: 5) {
edges { node { occurredAt commitCount } }
}
}
pullRequestContributions(first:10){
edges{ node{ occurredAt pullRequest { title url } } }
}
issueContributions(first:10){
edges{ node{ occurredAt issue { title url } } }
}
}
}
}
"""
QUERY_MONTH = """
query($login:String!, $from:DateTime!, $to:DateTime!) {
user(login:$login){
contributionsCollection(from:$from, to:$to){
contributionCalendar{
weeks{ contributionDays{ date contributionCount } }
}
}
}
}
"""
# ---------- builders ----------
def build_projects_section():
data = gql(QUERY_RECENT_REPOS, {"login": USER})
nodes = data["user"]["repositories"]["nodes"]
top = nodes[:3]
lines = ["### 🔄 Currently Building"]
for n in top:
lang = n["primaryLanguage"]["name"] if n["primaryLanguage"] else "-"
color = n["primaryLanguage"]["color"] if n["primaryLanguage"] else "#cccccc"
desc = (n["description"] or "No description").strip()
pushed = humanize(n["pushedAt"])
lines.append(
f"- **[{n['name']}]({n['url']})** — {desc}\n"
f" <sub><span style='display:inline-block;width:10px;height:10px;background:{color};"
f"border-radius:50%;vertical-align:middle;margin-right:6px'></span>{lang} • ⭐ {n['stargazerCount']} • updated {pushed}</sub>"
)
return "\n".join(lines)
def build_activity_section():
to = datetime.now(timezone.utc)
frm = to - timedelta(days=14)
data = gql(QUERY_EVENTS, {"login": USER, "from": frm.isoformat(), "to": to.isoformat()})
cc = data["user"]["contributionsCollection"]
events = []
for repoBlock in cc.get("commitContributionsByRepository", []):
repo = repoBlock["repository"]
for e in repoBlock["contributions"]["edges"]:
node = e["node"]
events.append({
"type": "commit",
"text": f"pushed **{node['commitCount']}** commit(s) to [{repo['name']}]({repo['url']})",
"when": node["occurredAt"]
})
for e in cc.get("pullRequestContributions", {}).get("edges", []):
node = e["node"]
pr = node["pullRequest"]
events.append({ "type": "pr", "text": f"opened PR: [{pr['title']}]({pr['url']})", "when": node["occurredAt"] })
for e in cc.get("issueContributions", {}).get("edges", []):
node = e["node"]
isu = node["issue"]
events.append({ "type": "issue", "text": f"opened Issue: [{isu['title']}]({isu['url']})", "when": node["occurredAt"] })
events.sort(key=lambda x: x["when"], reverse=True)
events = events[:5]
if not events:
return "_No recent public activity_"
out = ["### 🏃 Recent Activity"]
for ev in events:
out.append(f"- ⏺️ {ev['text']} <sub>{humanize(ev['when'])}</sub>")
return "\n".join(out)
def generate_month_svg():
end = datetime.now(timezone.utc).date()
start = end - timedelta(days=29)
data = gql(QUERY_MONTH, {"login": USER, "from": start.isoformat()+"T00:00:00Z", "to": end.isoformat()+"T23:59:59Z"})
weeks = data["user"]["contributionsCollection"]["contributionCalendar"]["weeks"]
counts = {}
for w in weeks:
for d in w["contributionDays"]:
date = d["date"]
if start.isoformat() <= date <= end.isoformat():
counts[date] = d["contributionCount"]
days = [(start + timedelta(days=i)).isoformat() for i in range(30)]
vals = [counts.get(day, 0) for day in days]
maxv = max(vals) if any(vals) else 1
width, height = 800, 240
padding = 30
chart_w = width - padding*2
chart_h = height - padding*2
bar_w = chart_w / len(vals)
bars = []
for i, v in enumerate(vals):
x = padding + i * bar_w
h = 0 if maxv == 0 else (v / maxv) * chart_h
y = height - padding - h
delay = i * 0.03
bars.append(f"""
<g class="bar" transform="translate({x:.2f}, {y:.2f})">
<title>{days[i]}: {v}</title>
<rect x="0" y="0" width="{bar_w*0.8:.2f}" height="{h:.2f}" rx="6" ry="6"
style="animation: grow 0.8s {delay:.2f}s ease-out forwards; transform-origin: bottom;">
</rect>
</g>
""")
svg = f"""<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
<style>
.title {{ font: 700 16px 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial; }}
.axis {{ font: 12px 'Segoe UI', Roboto, Ubuntu, Arial; fill: #888; }}
rect {{ fill: #00bcd4; opacity: 0.85; }}
rect:hover {{ opacity: 1.0; }}
@keyframes grow {{ from {{ transform: scaleY(0); }} to {{ transform: scaleY(1); }} }}
</style>
<rect x="0" y="0" width="{width}" height="{height}" fill="#0b1220" rx="16" />
<text x="{padding}" y="{padding-8}" class="title" fill="#ffffff">This Month Activity (last 30 days)</text>
<g>
{"".join(bars)}
</g>
<g class="axis">
<text x="{padding}" y="{height - 8}">{days[0][5:]}</text>
<text x="{padding + chart_w/2:.2f}" y="{height - 8}">{days[15][5:]}</text>
<text x="{padding + chart_w - 36:.2f}" y="{height - 8}">{days[-1][5:]}</text>
</g>
</svg>"""
os.makedirs(ASSETS_DIR, exist_ok=True)
with open(SVG_PATH, "w", encoding="utf-8") as f:
f.write(svg)
def build_monthly_graph_section():
return "### 🕓 This Month\n\n"
def main():
if not TOKEN or not USER:
print("Missing env GITHUB_TOKEN or GH_USER", file=sys.stderr); sys.exit(1)
projects_md = build_projects_section()
activity_md = build_activity_section()
generate_month_svg()
graph_md = build_monthly_graph_section()
with open(README_PATH, "r", encoding="utf-8") as f:
readme = f.read()
readme = replace_between(readme, "PROJECTS:START", "PROJECTS:END", projects_md)
readme = replace_between(readme, "ACTIVITY:START", "ACTIVITY:END", activity_md)
readme = replace_between(readme, "MONTHLY_GRAPH:START", "MONTHLY_GRAPH:END", graph_md)
with open(README_PATH, "w", encoding="utf-8") as f:
f.write(readme)
if __name__ == "__main__":
main()
4. 見栄えの整ったREADMEテンプレート(貼るだけ)
1.をそのまま使ってもよいし、こちらに差し替えてもよいです。
YOUR_GITHUB_ID とSNSリンクだけ置換すれば使える。
完成イメージ
→https://github.com/kskmasa
README.md(自動更新分+他テンプレ)
<!-- =========================================================
GitHub Profile README — Modern / Minimal / Animated
TODO:
1) YOUR_GITHUB_ID を自分のIDに置換
2) 各SNSリンクを差し替え
3) assets/monthly_activity.svg はワークフローが自動生成
========================================================= -->
<div align="center">
# 👋 Hi, I'm **Your Name**
*Designing systems that make life simpler and smarter.*
✨ Full-stack Engineer / Product Designer
💡 AI Automation・Productivity Apps・Creative Tools
<img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/colored.png" width="100%" alt="divider" />
<p>
<img src="https://github-readme-stats.vercel.app/api?username=YOUR_GITHUB_ID&show_icons=true&theme=tokyonight&hide_border=true" height="165" alt="stats"/>
<img src="https://github-readme-stats.vercel.app/api/top-langs/?username=YOUR_GITHUB_ID&layout=compact&theme=tokyonight&hide_border=true" height="165" alt="top langs"/>
</p>
<a href="https://git.io/typing-svg">
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&size=22&pause=1000&color=00BFFF¢er=true&vCenter=true&width=700&lines=AI+Automation+Enthusiast;Building+apps+that+empower+people;Always+learning%2C+always+creating." alt="typing animation"/>
</a>
</div>
---
### 🧠 Tech Stack






> “Design isn’t what it looks like — it’s how it works.”
---
## 🔄 現在の開発中プロジェクト
<!-- PROJECTS:START -->
<!-- PROJECTS:END -->
---
## 🏃 最近の活動
<!-- ACTIVITY:START -->
<!-- ACTIVITY:END -->
---
## 🕓 今月のアクティビティ
<!-- MONTHLY_GRAPH:START -->
<!-- MONTHLY_GRAPH:END -->
---
### 🌐 Connect
[](https://yourdomain.com)
[](https://linkedin.com/in/yourname)
[](https://x.com/yourhandle)
[](https://instagram.com/yourhandle)
<p align="center">
<img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/colored.png" width="100%" alt="divider" />
</p>
5. セットアップ(最短)
- プロフィール用リポジトリ
<your_id>/<your_id>を作成し、上記ファイルを配置:-
README.md(マーカー入り) .github/workflows/update-readme.ymlscripts/update_readme.py
-
- Actions タブを有効化し、初回は Run workflow で手動実行
- 以後は毎日自動更新(UTC基準のcron)。
検証とトラブルシュート
- 実行時刻が数分ずれる:GitHubは予定時刻の実行を保証しない。許容範囲で考えるか、頻度を上げる。
-
GITHUB_TOKENの権限不足:permissions.contents: writeを付与してから再実行。 - 活動が表示されない:private活動はGraphQLの
contributionsCollectionに含まれない(公開活動のみ)。 - SVGが生成されない:
assets/ディレクトリの書き込みとコミット処理を確認。
運用・拡張(制約・代替・セキュリティ)
- 制約:private活動の集計は不可(GitHub仕様)。代替は自前ログやセルフホストRunnerでの集計。
- テーマ変更:SVGの色・背景・サイズはスクリプト内CSSを編集。
- 代替オプション:
- GitHub Readme Stats(多テーマ・言語カード)
- readme-typing-svg(打鍵アニメ)
- Shields.io(技術スタックやリンク用バッジ)
- セキュリティ:トークンはリポジトリシークレット(デフォルトの
GITHUB_TOKEN)を使用。外部SaaSのURLはレート制限や停止のリスクを理解して利用。
まとめ
ここまでで、見た目も動きも整ったGitHubプロフィールREADMEが完成しました。
コードを書かなくても、毎日少しずつ“自分の今”が更新されるのってちょっと楽しいですよね。
最初はシンプルに始めて、
あとから「今日の活動グラフ」や「開発中のアプリ一覧」を足していくのもおすすめです。
プロフィールを“作品”として育てていく感じで、気軽に試してみてください。
参考リンク
-
Profile README(プロフィールにREADMEを表示する仕組み)
↪ Managing your profile README - GitHub Docs -
GitHub Actionsのschedule(cron/UTC/最短5分)
↪ Workflow syntax for GitHub Actions - on.schedule -
GraphQL API 概要と
contributionsCollection(コミット/PR/Issue等)
↪ GitHub GraphQL API v4 Documentation -
GitHub Readme Stats(任意の装飾)
↪ anuraghazra/github-readme-stats -
readme-typing-svg(任意のアニメーション)
↪ DenverCoder1/readme-typing-svg -
Shields.io(バッジの使い方)
↪ Shields.io Official Docs
Tweet Copy
プロフィールREADMEを“綺麗に自動更新”。
GraphQL × Actionsで、プロジェクト/活動/今月グラフを毎日生成。
外部SaaS不要で最小構成。
#GitHub #GitHubActions #GraphQL