diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4b89fd20..18d12126 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Manual changes to the registry require approval from the leadership team -governance/contributors.yaml @rawx18 @ronit-raj9 @parvm1102 \ No newline at end of file +governance/contributors.yaml @focuzd \ No newline at end of file diff --git a/.github/scripts/governance_engine.py b/.github/scripts/governance_engine.py index 12ac51af..0ff3995f 100644 --- a/.github/scripts/governance_engine.py +++ b/.github/scripts/governance_engine.py @@ -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 @@ -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() @@ -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 @@ -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}") @@ -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( @@ -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}") @@ -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 @@ -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") @@ -712,6 +738,7 @@ 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: @@ -719,7 +746,7 @@ def run_sync_mode(): 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) @@ -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() diff --git a/.github/workflows/gov-sync.yml b/.github/workflows/gov-sync.yml index bfd0a877..74fcb98e 100644 --- a/.github/workflows/gov-sync.yml +++ b/.github/workflows/gov-sync.yml @@ -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' || @@ -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 diff --git a/governance/bots.yaml b/governance/bots.yaml index cd42958f..8222bfb5 100644 --- a/governance/bots.yaml +++ b/governance/bots.yaml @@ -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 diff --git a/governance/contributors.yaml b/governance/contributors.yaml index 3cc79d4c..6e041df1 100644 --- a/governance/contributors.yaml +++ b/governance/contributors.yaml @@ -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 diff --git a/governance/history/bot_logs.yaml b/governance/history/bot_logs.yaml index 38070466..5cfd355c 100644 --- a/governance/history/bot_logs.yaml +++ b/governance/history/bot_logs.yaml @@ -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)' diff --git a/governance/history/bots/github-actions[bot].yaml b/governance/history/bots/github-actions[bot].yaml new file mode 100644 index 00000000..eedf4007 --- /dev/null +++ b/governance/history/bots/github-actions[bot].yaml @@ -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)' diff --git a/governance/history/ledger.yaml b/governance/history/ledger.yaml index 6955e00d..1c3fd688 100644 --- a/governance/history/ledger.yaml +++ b/governance/history/ledger.yaml @@ -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 diff --git a/governance/history/users/focuzd.yaml b/governance/history/users/focuzd.yaml index abf2e80b..1c16ed3c 100644 --- a/governance/history/users/focuzd.yaml +++ b/governance/history/users/focuzd.yaml @@ -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)' diff --git a/governance/history/users/rawx18.yaml b/governance/history/users/rawx18.yaml index 14c83ca2..f03502b7 100644 --- a/governance/history/users/rawx18.yaml +++ b/governance/history/users/rawx18.yaml @@ -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'