Skip to content
Merged
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
66 changes: 66 additions & 0 deletions .github/workflows/points.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Points Allocation

on:
pull_request_review:
types: [submitted]
issue_comment:
types: [created]

permissions:
contents: write
pull-requests: write

jobs:
assign-points:
runs-on: ubuntu-latest
# Only run for PR reviews or comments on PRs (not regular issues)
if: >
github.event_name == 'pull_request_review' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request != null)
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install dependencies
run: pip install pyyaml

- name: Run points script
id: assign_points
run: |
set +e # Don't exit on error
python scripts/assign_points.py
exit_code=$?
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT

# Exit codes:
# 0 = Success (points awarded)
# 2 = No-op (no points, but not an error)
# 1 or other = Actual error

if [ $exit_code -eq 0 ] || [ $exit_code -eq 2 ]; then
exit 0
else
exit $exit_code
fi

- name: Update leaderboard markdown
if: steps.assign_points.outputs.exit_code == '0'
run: python scripts/update_leaderboard.py

- name: Commit and push leaderboard changes
if: steps.assign_points.outputs.exit_code == '0'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add leaderboard.json LEADERBOARD.md
git diff --staged --quiet || git commit -m "Update leaderboard [skip ci]"
git diff --staged --quiet || git push origin main
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Microsoft Agent 365 SDK Samples and Prompts

![Top Contributor](https://img.shields.io/badge/dynamic/json?color=blue&label=Top%20Contributor&query=$.top&url=https://raw.githubusercontent.com/microsoft/Agent365-Samples/main/leaderboard.json)

This repository contains sample agents and prompts for building with the Microsoft Agent 365 SDK. The Microsoft Agent 365 SDK extends the Microsoft 365 Agents SDK with enterprise-grade capabilities for building sophisticated agents. It provides comprehensive tooling for observability, notifications, runtime utilities, and development tools that help developers create production-ready agents for platforms including M365, Teams, Copilot Studio, and Webchat.

- **Sample agents** are available in C# (.NET), Python, and Node.js/TypeScript
Expand Down
1 change: 1 addition & 0 deletions leaderboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
286 changes: 286 additions & 0 deletions scripts/assign_points.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
# Copyright (c) Microsoft. All rights reserved.

import os
import json
import yaml
import sys

# Exit codes used by this script:
# 0 = Success - points were awarded and leaderboard updated
# 1 = Error - something went wrong (missing config, permissions, etc.)
# 2 = No-op - no points awarded, but not an error (duplicate event, no criteria matched)

# Path to config file inside scripts folder
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config_points.yml')
PROCESSED_FILE = os.path.join(os.path.dirname(__file__), 'processed_ids.json')
# Path to leaderboard in repository root (one level up from scripts/)
LEADERBOARD_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'leaderboard.json')

def load_config():
"""
Load the points configuration from config_points.yml.

Returns:
dict: Configuration dictionary with 'points' section

Exits:
1 if config file is missing or contains invalid YAML
"""
if not os.path.exists(CONFIG_FILE):
print(f"ERROR: Config file not found: {CONFIG_FILE}", file=sys.stderr)
print("Expected location: scripts/config_points.yml", file=sys.stderr)
sys.exit(1)

try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
except yaml.YAMLError as e:
print(f"ERROR: Invalid YAML syntax in config file: {e}", file=sys.stderr)
print(f"File location: {CONFIG_FILE}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR: Failed to read config file: {e}", file=sys.stderr)
sys.exit(1)

# Validate that config has the expected structure
if not isinstance(config, dict) or 'points' not in config:
print(f"ERROR: Invalid config structure in {CONFIG_FILE}", file=sys.stderr)
print("Expected format: { points: { basic_review: 5, ... } }", file=sys.stderr)
sys.exit(1)

return config

def load_event():
event_path = os.getenv('GITHUB_EVENT_PATH')
if not event_path:
print("ERROR: GITHUB_EVENT_PATH is not set.")
sys.exit(1)
if not os.path.exists(event_path):
print(f"ERROR: Event file not found: {event_path}")
sys.exit(1)
with open(event_path, 'r', encoding='utf-8') as f:
event = json.load(f)

# Validate that this is a PR-related event, not a regular issue comment
if 'issue' in event and 'pull_request' not in event.get('issue', {}):
print("INFO: Skipping - this is a comment on a regular issue, not a pull request.")
sys.exit(2) # Exit code 2 = no-op

return event

def load_processed_ids():
if os.path.exists(PROCESSED_FILE):
with open(PROCESSED_FILE, 'r', encoding='utf-8') as f:
try:
return json.load(f)
except json.JSONDecodeError:
return []
return []

def save_processed_ids(ids):
"""
Save processed event IDs to prevent duplicate scoring.

This is critical for data integrity - if this fails after points
are awarded, the same event could be scored multiple times on retry.
"""
try:
with open(PROCESSED_FILE, 'w', encoding='utf-8') as f:
json.dump(ids, f, indent=2)
except PermissionError as e:
print(f"ERROR: Permission denied when saving processed IDs to {PROCESSED_FILE}: {e}", file=sys.stderr)
print("Check file permissions and ensure the workflow has write access.", file=sys.stderr)
sys.exit(1)
except IOError as e:
print(f"ERROR: Failed to write processed IDs to {PROCESSED_FILE}: {e}", file=sys.stderr)
print("This may be due to disk space issues or file system problems.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR: Unexpected error saving processed IDs: {e}", file=sys.stderr)
sys.exit(1)

def extract_user(event):
"""
Extract user login from GitHub event with multiple fallback strategies.

Priority:
1. review.user.login (for pull_request_review events)
2. comment.user.login (for issue_comment events)
3. sender.login (top-level event sender)

Returns:
tuple: (user_login: str, source: str) or (None, None) if extraction fails
"""
# Try review user first
review = event.get('review')
if review and isinstance(review, dict):
review_user = review.get('user')
if review_user and isinstance(review_user, dict):
login = review_user.get('login')
if login:
return login, 'review.user'

# Try comment user second
comment = event.get('comment')
if comment and isinstance(comment, dict):
comment_user = comment.get('user')
if comment_user and isinstance(comment_user, dict):
login = comment_user.get('login')
if login:
return login, 'comment.user'

# Fallback to top-level sender (most reliable)
sender = event.get('sender')
if sender and isinstance(sender, dict):
login = sender.get('login')
if login:
return login, 'sender'

# All extraction methods failed
return None, None

def detect_points(event, cfg):
"""
Calculate points for a GitHub event based on review content and actions.

All keyword matching is CASE-INSENSITIVE. Contributors can use any capitalization.

Scoring Rules:
1. Review types (mutually exclusive - only the highest applies):
- Include "detailed" anywhere in your review = detailed_review points (10)
- Include "basic review" anywhere in your review = basic_review points (5)
- If both keywords present, only "detailed" counts (higher value)

2. Bonus points (additive - can stack with review types):
- Include "performance" anywhere = performance_improvement bonus (+4)
- Approve the PR (state=approved) = approve_pr bonus (+3)

Keyword Examples (all case-insensitive):
- "detailed", "Detailed", "DETAILED" all work
- "basic review", "Basic Review", "BASIC REVIEW" all work
- "performance", "Performance", "PERFORMANCE" all work

Scoring Examples:
- "This is a basic review" = 5 points
- "This is a DETAILED analysis" = 10 points (case doesn't matter)
- "detailed performance review" = 10 + 4 = 14 points
- Approved PR with "Basic Review" = 5 + 3 = 8 points
- Approved PR with "Detailed PERFORMANCE review" = 10 + 4 + 3 = 17 points
"""
action = event.get('action', '')
review = event.get('review') or {}
comment = event.get('comment') or {}

# Convert to lowercase for case-insensitive matching
review_body = (review.get('body') or '').lower()
review_state = (review.get('state') or '').lower()
comment_body = (comment.get('body') or '').lower()

user, source = extract_user(event)

if not user:
print("ERROR: Unable to extract user from event. Checked review.user, comment.user, and sender fields.")
print("Event structure:", json.dumps({
'has_review': 'review' in event,
'has_comment': 'comment' in event,
'has_sender': 'sender' in event,
'action': action
}, indent=2))
sys.exit(1)

print(f"User identified: {user} (source: {source})")

points = 0
scoring_breakdown = []

# Review type scoring (mutually exclusive - detailed takes precedence)
# All matching is case-insensitive due to .lower() above
if "detailed" in review_body:
points += cfg['points']['detailed_review']
scoring_breakdown.append(f"detailed_review: +{cfg['points']['detailed_review']}")
elif "basic review" in review_body:
points += cfg['points']['basic_review']
scoring_breakdown.append(f"basic_review: +{cfg['points']['basic_review']}")

# Performance improvement bonus (additive)
if "performance" in comment_body or "performance" in review_body:
points += cfg['points']['performance_improvement']
scoring_breakdown.append(f"performance_improvement: +{cfg['points']['performance_improvement']}")

# PR approval bonus (additive)
if action == "submitted" and review_state == "approved":
points += cfg['points']['approve_pr']
scoring_breakdown.append(f"approve_pr: +{cfg['points']['approve_pr']}")

# Log scoring breakdown for transparency
if scoring_breakdown:
print(f"Scoring breakdown: {', '.join(scoring_breakdown)} = {points} total")
else:
print("No scoring criteria matched.")

return points, user

def update_leaderboard(user, points):
"""
Update the leaderboard with awarded points for a user.

Args:
user: GitHub username
points: Points to award
"""
leaderboard = {}

if os.path.exists(LEADERBOARD_FILE):
with open(LEADERBOARD_FILE, 'r', encoding='utf-8') as f:
try:
leaderboard = json.load(f)
except json.JSONDecodeError:
leaderboard = {}

leaderboard[user] = leaderboard.get(user, 0) + points

try:
with open(LEADERBOARD_FILE, 'w', encoding='utf-8') as f:
json.dump(leaderboard, f, indent=2)
except PermissionError as e:
print(f"ERROR: Permission denied when saving leaderboard to {LEADERBOARD_FILE}: {e}", file=sys.stderr)
print("Check file permissions and ensure the workflow has write access.", file=sys.stderr)
sys.exit(1)
except IOError as e:
print(f"ERROR: Failed to write leaderboard to {LEADERBOARD_FILE}: {e}", file=sys.stderr)
print("This may be due to disk space issues or file system problems.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR: Unexpected error saving leaderboard: {e}", file=sys.stderr)
sys.exit(1)

def main():
cfg = load_config()
event = load_event()
points, user = detect_points(event, cfg)

# Extract unique ID for duplicate prevention
event_id = event.get('review', {}).get('id') or event.get('comment', {}).get('id')
if not event_id:
print("No unique ID found in event. Skipping duplicate check.")
sys.exit(2) # Exit code 2 = no-op (not an error)

processed_ids = load_processed_ids()
if event_id in processed_ids:
print(f"Event {event_id} already processed. Skipping scoring.")
sys.exit(2) # Exit code 2 = no-op (not an error)

if points <= 0:
print("No points awarded for this event.")
sys.exit(2) # Exit code 2 = no-op (not an error)

# Update leaderboard first, then mark as processed
# This order ensures we can retry if processed_ids save fails
update_leaderboard(user, points)
processed_ids.append(event_id)
save_processed_ids(processed_ids)
print(f"Points awarded: {points} to {user}")
sys.exit(0) # Exit code 0 = success (points awarded)

if __name__ == "__main__":
main()
25 changes: 25 additions & 0 deletions scripts/config_points.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Contributor Points Configuration
#
# All keyword matching is CASE-INSENSITIVE. Contributors can use any capitalization.
# For example: "detailed", "Detailed", "DETAILED" all work the same way.
#
# Scoring Rules:
# 1. Review types are mutually exclusive (only highest value applies)
# - "detailed" keyword awards detailed_review points
# - "basic review" keyword awards basic_review points (only if "detailed" not present)
# 2. Bonuses are additive and can stack with review types:
# - "performance" keyword adds performance_improvement bonus
# - Approved PR (state=approved) adds approve_pr bonus
#
# Examples (all case-insensitive):
# - "basic review" OR "Basic Review" OR "BASIC REVIEW" = 5 points
# - "detailed analysis" OR "Detailed Analysis" = 10 points
# - "detailed performance review" OR "DETAILED PERFORMANCE REVIEW" = 10 + 4 = 14 points
# - Approved PR with "basic review" = 5 + 3 = 8 points
# - Approved PR with "detailed performance review" = 10 + 4 + 3 = 17 points

points:
basic_review: 5 # Simple review with "basic review" keyword (case-insensitive)
detailed_review: 10 # In-depth review with "detailed" keyword (case-insensitive, overrides basic)
performance_improvement: 4 # Bonus for mentioning "performance" (case-insensitive, additive)
approve_pr: 3 # Bonus for approving a PR (additive)
Loading