Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Manual changes to the registry require approval from the leadership team
governance/contributors.yaml @rawx18 @ronit-raj9 @parvm1102
governance/contributors.yaml @focuzd
146 changes: 95 additions & 51 deletions .github/scripts/governance_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def get_now_ist_str():
return datetime.now(IST).strftime("%Y-%m-%dT%H:%M:%S+05:30")

def get_merged_prs(since_date=None, per_page=100):
"""Fetches merged PRs. If since_date is provided, fetches only PRs merged after that date."""
"""Fetches merged PRs from the canonical repo. If since_date is provided, fetches only PRs merged after that date."""
all_prs = []
page = 1

Expand All @@ -107,11 +107,11 @@ def get_merged_prs(since_date=None, per_page=100):
print(f"Warning: Could not parse since_date '{since_date}'. Fetching all.")
since_dt = None

print(f"Fetching merged PRs{' since ' + since_date if since_date else ' (ALL)'}...")
print(f"Fetching merged PRs from {CANONICAL_REPO}{' since ' + since_date if since_date else ' (ALL)'}...")

while True:
# Fetch closed PRs, sorted by updated desc to get recent ones first
url = f"https://api.github.com/repos/{REPO}/pulls?state=closed&sort=updated&direction=desc&per_page={per_page}&page={page}"
url = f"https://api.github.com/repos/{CANONICAL_REPO}/pulls?state=closed&sort=updated&direction=desc&per_page={per_page}&page={page}"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
Expand Down Expand Up @@ -160,14 +160,21 @@ def get_merged_prs(since_date=None, per_page=100):
return all_prs

def get_last_activity_date(username):
"""Queries GitHub Events API to find the user's latest public action."""
"""Queries GitHub Events API to find the user's latest public action in the canonical repo."""
url = f"https://api.github.com/users/{username}/events/public"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
events = response.json()
if events and isinstance(events, list):
return events[0]['created_at'].split('T')[0]
for event in events:
if event.get('repo', {}).get('name') == CANONICAL_REPO:
if event['type'] == 'PushEvent':
return event['created_at'].split('T')[0]
elif event['type'] == 'PullRequestEvent':
if event.get('payload', {}).get('action') == 'closed' and \
event.get('payload', {}).get('pull_request', {}).get('merged') == True:
return event['created_at'].split('T')[0]
except Exception as e:
print(f"Error fetching activity for {username}: {e}")
return None
Expand Down Expand Up @@ -556,10 +563,8 @@ def main():
run_sync_mode()

def run_sync_mode():
# --- FORK PROTECTION CHECK ---
if REPO != CANONICAL_REPO:
print(f"Skipping governance sync: Current repo '{REPO}' is a fork or doesn't match '{CANONICAL_REPO}'.")
return
print(f"Running governance sync in test mode on fork '{REPO}'. Fetching data from '{CANONICAL_REPO}'.")

if not os.path.exists(REGISTRY_PATH):
print(f"Registry not found at {REGISTRY_PATH}")
Expand Down Expand Up @@ -610,7 +615,7 @@ def run_sync_mode():
# Fetch ALL merged PRs
prs_to_process = get_merged_prs(since_date=None)

# Re-initialize history for existing contributors
# Re-initialize history for existing contributors (ROLE_ASSIGNMENT)
for contributor in contributors:
username = contributor['username']
update_user_history(
Expand All @@ -635,9 +640,6 @@ def run_sync_mode():
f"Bot tracked in registry. Added by: {bot.get('added_by', 'unknown')}",
is_bot=True
)
# We don't add to bot_logs here unless we want an init event.
# But the 'Added by' event is already in the main ledger if added via command.
# If migrating, it's just state.

else:
print(f"Performing INCREMENTAL SYNC since {last_sync}")
Expand All @@ -649,49 +651,74 @@ def run_sync_mode():
prs_to_process.sort(key=lambda x: x['merged_at'])

for pr in prs_to_process:
username = pr['username']
merged_at = pr['merged_at']
pr_url = pr['url']

is_bot = username.lower() in bot_usernames

if is_bot:
# Log bot activity to Bot Ledger/History only
update_user_history(username, "PR_MERGED", f"Merged PR #{pr['number']}: {pr['title']} ({pr['url']})", is_bot=True)
update_ledger("PR_MERGED", username, f"PR #{pr['number']}: {pr['title']}", is_bot=True)
# Do NOT add to contributors list
continue

# HUMAN Processing
pr_number = pr['number']
pr_title = pr['title']
pr_url = pr['url'] # html_url

# 1. New Contributor Detection
if username.lower() not in existing_usernames:
print(f"New contributor detected: {username}")
new_contributor = {
"username": username, # Keep original casing for display
"role": "Newbie Contributor",
"team": "CE",
"status": "active",
"assigned_by": "GitMesh-Steward-bot",
"assigned_at": merged_at,
"last_activity": merged_at.split('T')[0],
"notes": "Automatically onboarded after first merged PR."
}
contributors.append(new_contributor)
existing_usernames.add(username.lower())
# 1. Fetch Merge Details (Who merged it?)
# We need to fetch the single PR endpoint to get 'merged_by'
try:
pr_details_resp = requests.get(f"https://api.github.com/repos/{CANONICAL_REPO}/pulls/{pr_number}", headers=headers)
pr_details_resp.raise_for_status()
pr_details = pr_details_resp.json()
merged_by_user = pr_details.get('merged_by')
merged_by_login = merged_by_user['login'] if merged_by_user else None
except Exception as e:
print(f"Error fetching PR details for #{pr_number}: {e}")
merged_by_login = None

# 2. Fetch Commits (Who committed?)
commits = []
try:
commits_url = f"https://api.github.com/repos/{CANONICAL_REPO}/pulls/{pr_number}/commits?per_page=100"
commits_resp = requests.get(commits_url, headers=headers)
commits_resp.raise_for_status()
commits = commits_resp.json()
except Exception as e:
print(f"Error fetching commits for #{pr_number}: {e}")

# 3. Log MERGE Event
if merged_by_login:
is_merger_bot = merged_by_login.lower() in bot_usernames
# If merger is a bot, log to bot ledger?
# Or if user wants "merge event in the ledger of the person who did the merge",
# and that person is a bot, it goes to bot ledger.

update_user_history(username, "ONBOARDING", f"Achieved Newbie status via merged PR: {pr_url}", is_bot=False)
update_ledger("ONBOARDING", username, f"First merged PR: {pr_url}", is_bot=False)

# 2. Log PR to History
update_user_history(username, "PR_MERGED", f"Merged PR #{pr['number']}: {pr['title']} ({pr['url']})", is_bot=False)
update_ledger("PR_MERGED", username, f"PR #{pr['number']}: {pr['title']}", is_bot=False)
# Check if merger is new contributor?
if not is_merger_bot and merged_by_login.lower() not in existing_usernames:
print(f"New contributor (merger) detected: {merged_by_login}")
onboard_new_contributor(merged_by_login, contributors, existing_usernames, pr['merged_at'], pr_url)

log_msg = f"Merged PR #{pr_number}: {pr_title}"
update_user_history(merged_by_login, "MERGE", log_msg, is_bot=is_merger_bot)
update_ledger("MERGE", merged_by_login, log_msg, is_bot=is_merger_bot)

# 4. Log COMMIT Events
for commit in commits:
author = commit.get('author')
if not author:
continue # No GitHub user associated

author_login = author['login']
commit_msg = commit['commit']['message'].split('\n')[0] # First line
sha_short = commit['sha'][:7]

is_committer_bot = author_login.lower() in bot_usernames

# Check if committer is new contributor?
if not is_committer_bot and author_login.lower() not in existing_usernames:
print(f"New contributor (committer) detected: {author_login}")
onboard_new_contributor(author_login, contributors, existing_usernames, pr['merged_at'], pr_url)

log_msg = f"Commit {sha_short}: {commit_msg} (PR #{pr_number})"
update_user_history(author_login, "COMMIT", log_msg, is_bot=is_committer_bot)
update_ledger("COMMIT", author_login, log_msg, is_bot=is_committer_bot)

# 3. Update Activity Status (for all contributors)
print("\nUpdating activity status...")
for entry in contributors:
username = entry['username']
# Double check if bot (in case they were added to bots but not removed from contributors manually)
# Double check if bot
if username.lower() in bot_usernames:
continue

Expand All @@ -701,8 +728,7 @@ def run_sync_mode():
old_activity = entry.get('last_activity')
entry['last_activity'] = last_act

if old_activity != last_act:
update_user_history(username, "ACTIVITY_UPDATE", f"Last activity updated to {last_act}", is_bot=False)
# Removed ACTIVITY_UPDATE logging

# Inactivity Check (90 days)
last_act_dt = datetime.strptime(last_act, "%Y-%m-%d")
Expand All @@ -712,14 +738,15 @@ def run_sync_mode():
if diff > timedelta(days=90):
if entry.get('status') != "inactive":
entry['status'] = "inactive"
# Status change is type of Role Change/Assignment in broad sense
update_user_history(username, "STATUS_CHANGE", "Flagged as inactive due to 90 days of no activity.", is_bot=False)
update_ledger("STATUS_CHANGE", username, "Marked as inactive (90+ days)", is_bot=False)
else:
if entry.get('status') == "inactive":
entry['status'] = "active"
update_user_history(username, "STATUS_CHANGE", "Reactivated status due to new activity.", is_bot=False)
update_ledger("STATUS_CHANGE", username, "Reactivated due to new activity", is_bot=False)

# 4. Update Metadata
registry['metadata']['last_sync'] = get_now_ist_str()
registry['metadata']['total_contributors'] = len(contributors)
Expand All @@ -732,6 +759,23 @@ def run_sync_mode():
print("\nGovernance sync completed successfully.")
print(f"Total contributors: {len(contributors)}")
print(f"Active contributors: {registry['metadata']['active_contributors']}")

def onboard_new_contributor(username, contributors, existing_usernames, timestamp, pr_url):
new_contributor = {
"username": username,
"role": "Newbie Contributor",
"team": "CE",
"status": "active",
"assigned_by": "GitMesh-Steward-bot",
"assigned_at": timestamp,
"last_activity": timestamp.split('T')[0],
"notes": "Automatically onboarded."
}
contributors.append(new_contributor)
existing_usernames.add(username.lower())

update_user_history(username, "ROLE_ASSIGNMENT", f"Assigned Newbie Contributor (via PR {pr_url})", is_bot=False)
update_ledger("ROLE_ASSIGNMENT", username, f"Onboarded as Newbie Contributor", is_bot=False)

if __name__ == "__main__":
main()
10 changes: 6 additions & 4 deletions .github/workflows/gov-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ jobs:
# Run on schedule, manual trigger, or issue comment (commands)
# IMPORTANT: Skip if triggered by the bot's own commits or PRs to prevent infinite loops
if: |
(github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'issue_comment') &&
(
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/gov') && github.event.sender.type != 'Bot')
) &&
(github.event.head_commit.author.email != '41898282+github-actions[bot]@users.noreply.github.com' ||
github.event.head_commit.author.email == null) &&
(github.event.pull_request.title != 'chore(gov): automated governance registry & history sync' ||
Expand Down Expand Up @@ -100,7 +102,7 @@ jobs:

# Extract Code Owners from .github/CODEOWNERS for notification
CODE_OWNERS=""
if [ "${{ github.repository }}" == "LF-Decentralized-Trust-labs/gitmesh" ] && [ -f ".github/CODEOWNERS" ]; then
if [ "${{ github.repository }}" == "focuzd/gitmesh" ] && [ -f ".github/CODEOWNERS" ]; then
# Find line matching governance/contributors.yaml and extract @usernames
CODE_OWNERS=$(grep "governance/contributors.yaml" .github/CODEOWNERS | grep -o "@[a-zA-Z0-9-]*" | tr '\n' ' ')
fi
Expand Down
3 changes: 3 additions & 0 deletions governance/bots.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ bots:
- username: dependabot[bot]
added_at: '2026-01-22T16:48:49+05:30'
added_by: RAWx18
- username: github-actions[bot]
added_at: '2026-02-09T18:39:27+05:30'
added_by: focuzd
6 changes: 3 additions & 3 deletions governance/contributors.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
metadata:
version: 1.0.0
total_contributors: 12
active_contributors: 7
last_sync: '2026-01-23T07:44:03+05:30'
total_contributors: 13
active_contributors: 8
last_sync: '2026-02-09T18:32:07+05:30'
contributors:
- username: rawx18
role: CEO
Expand Down
19 changes: 19 additions & 0 deletions governance/history/bot_logs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,22 @@ events:
type: PR_MERGED
username: Copilot
details: 'PR #209: fix: handle missing labels in governance sync workflow'
- timestamp: '2026-02-09T18:32:01+05:30'
type: ROLE_ASSIGNMENT
username: github-actions[bot]
details: Onboarded as Newbie Contributor
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
username: github-actions[bot]
details: 'Commit bfaa12c: chore(gov): auto-sync activity and history logs [skip
ci] (PR #359)'
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
username: github-actions[bot]
details: 'Commit 40fc55b: chore(gov): update roles via command in PR #359 [skip
ci] (PR #359)'
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
username: github-actions[bot]
details: 'Commit c9ec335: chore(gov): update roles via command in PR #359 [skip
ci] (PR #359)'
17 changes: 17 additions & 0 deletions governance/history/bots/github-actions[bot].yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
username: github-actions[bot]
events:
- timestamp: '2026-02-09T18:32:01+05:30'
type: ROLE_ASSIGNMENT
details: Assigned Newbie Contributor (via PR https://github.com/LF-Decentralized-Trust-labs/gitmesh/pull/359)
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
details: 'Commit bfaa12c: chore(gov): auto-sync activity and history logs [skip
ci] (PR #359)'
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
details: 'Commit 40fc55b: chore(gov): update roles via command in PR #359 [skip
ci] (PR #359)'
- timestamp: '2026-02-09T18:32:01+05:30'
type: COMMIT
details: 'Commit c9ec335: chore(gov): update roles via command in PR #359 [skip
ci] (PR #359)'
17 changes: 17 additions & 0 deletions governance/history/ledger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,20 @@ events:
username: Sharkyii
details: 'Role changed from ''Newbie Contributor'' to ''Active Contributor'' by
@RAWx18 via PR #359'
- timestamp: '2026-02-09T18:32:01+05:30'
type: MERGE
username: RAWx18
details: 'Merged PR #359: chore(gov): automated governance registry & history sync'
- timestamp: '2026-02-09T18:32:02+05:30'
type: MERGE
username: RAWx18
details: 'Merged PR #360: Fix inifite looping in steward bot'
- timestamp: '2026-02-09T18:32:02+05:30'
type: COMMIT
username: focuzd
details: 'Commit 224b825: Fix inifite looping in steward bot (PR #360)'
- timestamp: '2026-02-09T18:39:28+05:30'
type: BOT_ADD
username: github-actions[bot]
details: Added @github-actions[bot] to bots (migrated from contributors if existed)
by @focuzd
3 changes: 3 additions & 0 deletions governance/history/users/focuzd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ events:
type: ROLE_CHANGE
details: 'Role changed from ''Active Contributor'' to ''Core Contributor'' by @RAWx18
via PR #359'
- timestamp: '2026-02-09T18:32:02+05:30'
type: COMMIT
details: 'Commit 224b825: Fix inifite looping in steward bot (PR #360)'
6 changes: 6 additions & 0 deletions governance/history/users/rawx18.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,9 @@ events:
- timestamp: '2026-01-23T07:43:58+05:30'
type: PR_MERGED
details: 'Merged PR #358: fix updates (https://github.com/LF-Decentralized-Trust-labs/gitmesh/pull/358)'
- timestamp: '2026-02-09T18:32:01+05:30'
type: MERGE
details: 'Merged PR #359: chore(gov): automated governance registry & history sync'
- timestamp: '2026-02-09T18:32:02+05:30'
type: MERGE
details: 'Merged PR #360: Fix inifite looping in steward bot'