diff --git a/README.md b/README.md index cbbc055..2c99eec 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Set up GitHub Actions for automatic syncing every 6 hours - see [Automation](#au - Organization scope: Sync teams from specific GitHub orgs - Enterprise scope: Sync all teams across the enterprise - Automatic cost center creation with bracket notation naming -- Orphaned user detection and removal +- Full sync mode (removes users who left teams) - Single assignment (multi-team users get last team's assignment) ### Additional Features @@ -146,7 +146,7 @@ teams: - "your-org" auto_create_cost_centers: true - remove_orphaned_users: true + remove_users_no_longer_in_teams: true ``` **Cost Center Naming:** @@ -335,7 +335,7 @@ Check `logs/populate_cost_centers.log` for detailed traces. Use `--verbose` for - [TEAMS_QUICKSTART.md](TEAMS_QUICKSTART.md) - Teams Mode setup guide - [TEAMS_INTEGRATION.md](TEAMS_INTEGRATION.md) - Teams Mode reference -- [ORPHANED_USERS_FEATURE.md](ORPHANED_USERS_FEATURE.md) - Orphaned users documentation +- [REMOVED_USERS_FEATURE.md](REMOVED_USERS_FEATURE.md) - Full sync mode documentation ## License @@ -343,4 +343,4 @@ This project is licensed under the MIT License. See [LICENSE](LICENSE) file for --- -**Latest Features:** Teams-based assignment (organization & enterprise scope), orphaned user detection, bracket notation naming, enhanced logging, incremental processing +**Latest Features:** Teams-based assignment (organization & enterprise scope), full sync mode (removes users who left teams), bracket notation naming, enhanced logging, incremental processing diff --git a/ORPHANED_USERS_FEATURE.md b/REMOVED_USERS_FEATURE.md similarity index 60% rename from ORPHANED_USERS_FEATURE.md rename to REMOVED_USERS_FEATURE.md index 9709534..d2c864c 100644 --- a/ORPHANED_USERS_FEATURE.md +++ b/REMOVED_USERS_FEATURE.md @@ -1,16 +1,16 @@ -# Orphaned User Detection and Removal Feature +# Full Sync Mode - Removed Users Feature ## Overview -This feature automatically detects and optionally removes "orphaned users" from cost centers. Orphaned users are those who are assigned to a cost center but are no longer members of the corresponding GitHub team. +This feature automatically detects and removes users from cost centers when they're no longer members of the corresponding GitHub team. This "full sync" mode ensures cost centers stay perfectly synchronized with actual team membership. ## Why This Feature? When teams change over time (members leave, are removed, or switch teams), their cost center assignments can become stale. This feature keeps cost centers synchronized with actual team membership by: 1. **Detecting** users in cost centers who are no longer in the corresponding team -2. **Reporting** these orphaned users with warnings -3. **Optionally removing** them based on configuration +2. **Reporting** these users with warnings +3. **Automatically removing** them to maintain sync ## How It Works @@ -19,13 +19,13 @@ When teams change over time (members leave, are removed, or switch teams), their For each cost center being managed: 1. Fetch the **expected members** from the GitHub team 2. Fetch the **current members** from the cost center API -3. Calculate orphaned users: `current_members - expected_members` -4. Log warnings for any orphaned users found +3. Calculate users no longer in teams: `current_members - expected_members` +4. Log warnings for any users found who left teams ### Removal Logic (when enabled) -If `teams.remove_orphaned_users: true`: -- Automatically remove orphaned users from their cost centers +If `teams.remove_users_no_longer_in_teams: true` (default): +- Automatically remove users who left teams from their cost centers - Log success/failure for each removal - Provide summary statistics @@ -41,59 +41,60 @@ teams: scope: "enterprise" # or "organization" mode: "auto" - # Orphaned user handling - remove_orphaned_users: false # Set to true to enable automatic removal + # Full sync mode + remove_users_no_longer_in_teams: true # Set to false to disable automatic removal ``` ### Default Behavior -- **Default**: `false` (disabled) -- **When disabled**: Orphaned users are detected and logged but NOT removed -- **When enabled**: Orphaned users are detected and automatically removed +- **Default**: `true` (enabled - full sync mode) +- **When enabled**: Users who left teams are detected and automatically removed +- **When disabled**: Users who left teams are detected and logged but NOT removed ## Usage Examples ### Plan Mode (Preview) ```bash -# See what would happen (with removal disabled) +# With full sync enabled (default) python main.py --teams-mode --assign-cost-centers --mode plan # Output shows: -# MODE=plan: Orphaned user detection is DISABLED -# Users in cost centers but not in teams will remain assigned +# MODE=plan: Full sync mode is ENABLED +# In apply mode, users no longer in teams will be removed from cost centers +# (Cannot show specific removed users in plan mode - cost centers don't exist yet) ``` ```bash -# With removal enabled in config +# With full sync disabled in config python main.py --teams-mode --assign-cost-centers --mode plan # Output shows: -# MODE=plan: Orphaned user detection is ENABLED -# In apply mode, users in cost centers but not in teams will be removed +# MODE=plan: Full sync mode is DISABLED +# Users in cost centers but not in teams will remain assigned ``` ### Apply Mode (Execution) ```bash -# Apply with removal disabled (default) +# Apply with full sync enabled (default) python main.py --teams-mode --assign-cost-centers --mode apply --yes -# Adds users to cost centers but leaves orphaned users alone +# Output includes: +# [INFO] Checking for users in cost centers who are no longer in teams... +# [WARNING] ⚠️ Found 3 users no longer in team for cost center '[enterprise team] Frontend' +# [WARNING] ⚠️ alice is in cost center but not in team +# [WARNING] ⚠️ bob is in cost center but not in team +# [INFO] Removing 3 users from '[enterprise team] Frontend'... +# [INFO] ✅ Successfully removed 3 users from cost center +# [INFO] 📊 Removed users summary: Found 3 users no longer in teams, successfully removed 3 ``` ```bash -# Apply with removal enabled +# Apply with full sync disabled python main.py --teams-mode --assign-cost-centers --mode apply --yes -# Output includes: -# [INFO] Checking for orphaned users... -# [WARNING] ⚠️ Found 3 orphaned users in cost center 'Team: Frontend' -# [WARNING] ⚠️ alice is in cost center but not in team -# [WARNING] ⚠️ bob is in cost center but not in team -# [INFO] Removing 3 orphaned users from 'Team: Frontend'... -# [INFO] ✅ Successfully removed 3 users from cost center -# [INFO] 📊 Orphaned users summary: Found 3 orphaned users, successfully removed 3 +# Adds users to cost centers but leaves users who left teams alone ``` ## API Methods Added @@ -142,25 +143,26 @@ results = github_manager.remove_users_from_cost_center( - Added `remove_users_from_cost_center()` method 2. **`src/config_manager.py`** - - Added `teams_remove_orphaned_users` configuration property + - Added `teams_remove_users_no_longer_in_teams` configuration property + - Backward compatibility with old `remove_orphaned_users` key 3. **`src/teams_cost_center_manager.py`** - - Added `_remove_orphaned_users()` private method - - Modified `sync_team_assignments()` to call orphaned user detection - - Added orphaned user handling in apply mode + - Added `_remove_users_no_longer_in_teams()` private method + - Modified `sync_team_assignments()` to call removed user detection + - Added removed user handling in apply mode 4. **`config/config.yaml` & `config/config.example.yaml`** - - Added `remove_orphaned_users` configuration option with documentation + - Added `remove_users_no_longer_in_teams` configuration option with documentation 5. **`main.py`** - - Display `remove_orphaned_users` status in configuration output + - Display full sync mode status in configuration output ### Logging The feature provides comprehensive logging: - **INFO**: General operation status -- **WARNING**: Orphaned users detected +- **WARNING**: Users no longer in teams detected - **ERROR**: API failures or removal failures - **DEBUG**: Detailed member counts @@ -181,10 +183,10 @@ The feature provides comprehensive logging: - Cost reporting is inaccurate - Manual cleanup required -**With this feature**: +**With full sync mode**: ```yaml teams: - remove_orphaned_users: true + remove_users_no_longer_in_teams: true # Default ``` - Users automatically removed from old cost center - Added to new cost center @@ -222,17 +224,20 @@ Always run with `--mode plan` to see what changes would be made: python main.py --teams-mode --assign-cost-centers --mode plan ``` -### 2. Enable Gradually +### 2. Test Before Enabling Full Sync -Start with `remove_orphaned_users: false` to see how many orphaned users exist: +If you want to see users who left teams without removing them first: ```bash -# Step 1: Run once to see orphaned users (they'll be logged as warnings) +# Step 1: Disable full sync temporarily +# Set remove_users_no_longer_in_teams: false in config + +# Step 2: Run once to see users who left (they'll be logged as warnings) python main.py --teams-mode --assign-cost-centers --mode apply --yes -# Step 2: Review logs for orphaned users -# Step 3: If comfortable, enable removal -# Step 4: Run again with removal enabled +# Step 3: Review logs for users who left teams +# Step 4: If comfortable, enable full sync (set to true) +# Step 5: Run again with full sync enabled ``` ### 3. Regular Sync Schedule @@ -247,22 +252,22 @@ Run teams sync regularly to keep cost centers up-to-date: ### 4. Monitor Logs Review execution logs for: -- Number of orphaned users found +- Number of users who left teams found - Removal success rates - Any API errors ### 5. Consider Impact -Before enabling `remove_orphaned_users: true`, consider: +Full sync mode is enabled by default. Before using it, consider: - Do you have users manually added to cost centers outside of team membership? - Are there legitimate reasons for users to be in cost centers but not teams? - Do you have adequate logging/monitoring? ## Limitations -1. **Plan Mode**: Cannot show specific orphaned users in plan mode because cost centers may not exist yet. Orphaned user detection only runs in apply mode after cost centers are created/resolved. +1. **Plan Mode**: Cannot show specific users who left teams in plan mode because cost centers may not exist yet. Detection only runs in apply mode after cost centers are created/resolved. -2. **Manual Assignments**: If you manually add users to cost centers outside of this tool, they will be detected as orphaned and removed if they're not in the corresponding team. +2. **Manual Assignments**: If you manually add users to cost centers outside of this tool, they will be detected as "not in team" and removed if they're not in the corresponding team. 3. **API Rate Limits**: Checking cost center membership adds API calls. Large numbers of cost centers may hit rate limits. @@ -270,14 +275,14 @@ Before enabling `remove_orphaned_users: true`, consider: ## Troubleshooting -### Orphaned users not being removed +### Users who left teams not being removed **Check**: -1. Is `remove_orphaned_users: true` in config? +1. Is `remove_users_no_longer_in_teams: true` in config? (Should be default) 2. Running in `--mode apply` (not plan)? 3. Check logs for API errors -### False positives (users incorrectly identified as orphaned) +### False positives (users incorrectly identified as having left) **Cause**: User is not in the team being synced @@ -290,25 +295,25 @@ Before enabling `remove_orphaned_users: true`, consider: **Cause**: Cost center doesn't exist yet (plan mode issue) -**Solution**: This is expected in plan mode. Orphaned detection only runs in apply mode. +**Solution**: This is expected in plan mode. Detection only runs in apply mode. ## Future Enhancements Potential improvements: -1. **Dry-run for orphaned users**: Show what would be removed without actually removing +1. **Dry-run for removed users**: Show what would be removed without actually removing 2. **Whitelist**: Configure specific users to never be removed -3. **Notification**: Send alerts when orphaned users are found/removed -4. **Audit log export**: Export orphaned user reports to CSV +3. **Notification**: Send alerts when users who left teams are found/removed +4. **Audit log export**: Export removed user reports to CSV ## Summary -The orphaned user detection and removal feature helps maintain clean, accurate cost center assignments by: +The full sync mode (removed users feature) helps maintain clean, accurate cost center assignments by: -- ✅ **Detecting** users who shouldn't be in cost centers +- ✅ **Detecting** users who left teams but are still in cost centers - ✅ **Reporting** discrepancies with clear warnings -- ✅ **Removing** orphaned users automatically (when enabled) +- ✅ **Removing** users who left teams automatically (enabled by default) - ✅ **Working** with both organization and enterprise team scopes -- ✅ **Configurable** - enable/disable as needed -- ✅ **Safe** - disabled by default, clear logging, error handling +- ✅ **Configurable** - disable if needed +- ✅ **Safe** - clear logging, error handling, backward compatible This keeps your cost center data synchronized with actual team membership over time with minimal manual intervention. diff --git a/TEAMS_QUICKSTART.md b/TEAMS_QUICKSTART.md index b6fd8fc..603bc94 100644 --- a/TEAMS_QUICKSTART.md +++ b/TEAMS_QUICKSTART.md @@ -1,246 +1,303 @@ # Teams Integration - Quick Start Guide -Get started with GitHub Teams Integration for cost center automation in 5 minutes! +Get started with GitHub Teams Integration for cost center automation in 5 minutes using GitHub Actions! -## Step 1: Verify Prerequisites +## Overview -Ensure you have: -- ✅ GitHub Enterprise Cloud admin access -- ✅ Personal Access Token with: - - `manage_billing:enterprise` scope - - `read:org` scope (NEW for teams mode) -- ✅ Python 3.8+ installed -- ✅ Repository cloned and dependencies installed +This guide helps you set up **automated cost center sync** that: +- ✅ Syncs with your **enterprise teams** (default) +- ✅ Creates one cost center per team automatically +- ✅ Removes users from cost centers when they leave teams (full sync) +- ✅ Runs on a schedule via GitHub Actions -```bash -cd cost-center-automation -pip install -r requirements.txt +## Step 1: Create Your GitHub Token + +1. Go to [GitHub Settings → Tokens (classic)](https://github.com/settings/tokens/new) +2. Create a token with these scopes: + - ✅ `manage_billing:enterprise` (required) + - ✅ `read:org` (required for teams) +3. Copy the token - you'll need it in the next step + +## Step 2: Set Up GitHub Actions + +### Add Repository Secret + +1. Go to your repository **Settings → Secrets and variables → Actions** +2. Click **New repository secret** +3. Name: `COST_CENTER_TOKEN` +4. Value: Paste your token from Step 1 +5. Click **Add secret** + +### Create Workflow File + +Create `.github/workflows/cost-center-sync.yml`: + +```yaml +name: Cost Center Sync + +on: + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: # Allow manual runs + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Sync cost centers with enterprise teams + env: + GITHUB_TOKEN: ${{ secrets.COST_CENTER_TOKEN }} + GITHUB_ENTERPRISE: ${{ github.repository_owner }} + run: | + python main.py --teams-mode --assign-cost-centers --mode apply --yes ``` -## Step 2: Configure Organizations +**Note:** This uses `github.repository_owner` as the enterprise name. If your enterprise name is different, replace it with your actual enterprise name. + +## Step 3: Configure Teams Integration -Edit `config/config.yaml` and add your organizations: +Edit `config/config.yaml`: ```yaml teams: enabled: true - mode: "auto" # Start with auto mode - - # Add your organizations here - organizations: - - "your-github-org" - + scope: "enterprise" # Sync with enterprise teams (default) + mode: "auto" # Create one cost center per team auto_create_cost_centers: true - cost_center_name_template: "Team: {team_name}" -``` - -## Step 3: Set Environment Variables - -```bash -# Set your GitHub token and enterprise -export GITHUB_TOKEN="ghp_your_token_here" -export GITHUB_ENTERPRISE="your-enterprise-name" + remove_users_no_longer_in_teams: true # Full sync: remove users who left teams ``` -Or create a `.env` file: -``` -GITHUB_TOKEN=ghp_your_token_here -GITHUB_ENTERPRISE=your-enterprise-name -``` +**That's it!** With these defaults, you get: +- ✅ Enterprise team sync (no need to list organizations) +- ✅ Automatic cost center creation per team +- ✅ Full sync mode (users removed when they leave teams) -## Step 4: Test with Plan Mode +## Step 4: Test Your Setup -Run in plan mode to see what would happen (no changes made): +### Manual Test Run -```bash -python main.py --teams-mode --assign-cost-centers --mode plan -``` +Trigger the workflow manually to test: -You should see: -- List of teams found in your organization -- Cost centers that would be created -- Users that would be assigned -- Summary of assignments +1. Go to **Actions** tab in your repository +2. Click **Cost Center Sync** workflow +3. Click **Run workflow** dropdown +4. Click **Run workflow** button -## Step 5: Review the Plan +### Check the Output -Check the output for: -- ✅ All expected teams are listed -- ✅ Cost center names look correct -- ✅ Team member counts are accurate -- ⚠️ Review any warnings about users in multiple teams -- ✅ No unexpected errors +Watch the workflow run and verify: +- ✅ All enterprise teams are discovered +- ✅ Cost centers are created with format `[enterprise team] {team-name}` +- ✅ Users are assigned correctly +- ✅ No errors in the logs -Example output: -``` -2025-10-06 10:00:00 [INFO] Found 5 teams in your-github-org -2025-10-06 10:00:01 [INFO] Team Frontend (your-github-org/frontend) → Cost Center 'Team: Frontend': 12 members -2025-10-06 10:00:02 [INFO] Team Backend (your-github-org/backend) → Cost Center 'Team: Backend': 8 members -... -2025-10-06 10:00:04 [WARNING] ⚠️ Found 3 users who are members of multiple teams. -2025-10-06 10:00:04 [WARNING] ⚠️ alice is in multiple teams [org/frontend, org/mobile] → will be assigned to 'Team: Mobile' -2025-10-06 10:00:05 [INFO] Team assignment summary: 5 cost centers, 42 unique users (each assigned to exactly ONE cost center) -``` +### Verify in GitHub -## Step 6: Apply Changes +1. Go to `https://github.com/enterprises/YOUR-ENTERPRISE/billing/cost_centers` +2. Verify cost centers were created for each team +3. Click into each cost center to verify user assignments -When you're ready to apply: +## Step 5: Review Full Sync Behavior -```bash -# With confirmation prompt -python main.py --teams-mode --assign-cost-centers --mode apply +**Full sync mode is enabled by default** (`remove_users_no_longer_in_teams: true`): -# Or skip confirmation (for automation) -python main.py --teams-mode --assign-cost-centers --mode apply --yes -``` +- ✅ Users **added** to a team → automatically assigned to that team's cost center +- ✅ Users **removed** from a team → automatically removed from that team's cost center +- ✅ Users **moved** between teams → reassigned to the new team's cost center -## Step 7: Verify Results +### Check the Logs -Check your GitHub Enterprise cost centers: -1. Go to `https://github.com/enterprises/YOUR-ENTERPRISE/billing/cost_centers` -2. Verify new cost centers were created -3. Click into each cost center to see assigned users +After the first run, review the logs for users who left teams: -## Next Steps +``` +[INFO] Checking for users in cost centers who are no longer in teams... +[INFO] Found 3 users no longer in teams across 2 cost centers +[INFO] Removing users from cost centers... +[INFO] Removed alice from cost center '[enterprise team] Frontend' +``` -### Option A: Keep Auto Mode -If you're happy with one cost center per team, you're done! Just run regularly: +## Advanced Configuration -```bash -# Daily sync (cron example) -0 2 * * * cd /path/to/repo && python main.py --teams-mode --assign-cost-centers --mode apply --yes -``` +### Option 1: Use Organization Teams Instead -### Option B: Switch to Manual Mode -For more control over team-to-cost-center mappings: +If you prefer organization-level teams: ```yaml teams: - enabled: true - mode: "manual" - + scope: "organization" organizations: - - "your-github-org" - - team_mappings: - "your-github-org/frontend": "Engineering: Frontend" - "your-github-org/backend": "Engineering: Backend" - "your-github-org/mobile-ios": "Engineering: Mobile" - "your-github-org/mobile-android": "Engineering: Mobile" # Same cost center + - "your-org-1" + - "your-org-2" ``` -### Option C: Use Custom Naming Template -Customize how cost centers are named in auto mode: +Cost centers will be named: `[org team] {org-name}/{team-name}` -```yaml -teams: - mode: "auto" - # Include org name in cost center - cost_center_name_template: "{org} - {team_name}" - # Or use team slug - cost_center_name_template: "Team: {team_slug}" - # Or get creative - cost_center_name_template: "[{org}] {team_name}" -``` +### Option 2: Disable Full Sync -## Common Scenarios +If you want to keep users in cost centers even after they leave teams: -### Scenario 1: Multiple Organizations ```yaml teams: - organizations: - - "org1" - - "org2" - - "org3" - cost_center_name_template: "{org}: {team_name}" + remove_users_no_longer_in_teams: false ``` -### Scenario 2: Existing Cost Centers (Manual Mode) +⚠️ **Warning:** This can cause cost centers to grow indefinitely. + +### Option 3: Manual Mode + +For complete control over team-to-cost-center mappings: + ```yaml teams: mode: "manual" - auto_create_cost_centers: false # Use existing IDs team_mappings: - "my-org/team-a": "CC-001-EXISTING" - "my-org/team-b": "CC-002-EXISTING" + "frontend": "Engineering: Frontend" # Enterprise teams use just slug + "backend": "Engineering: Backend" + "mobile-ios": "Engineering: Mobile" + "mobile-android": "Engineering: Mobile" # Multiple teams → same cost center ``` -### Scenario 3: Mixed Approach +### Option 4: Custom Schedule + +Adjust the cron schedule in your workflow: + ```yaml -teams: - mode: "manual" - auto_create_cost_centers: true # Create if needed - team_mappings: - "my-org/frontend": "CC-FRONTEND-001" # Existing - "my-org/backend": "Engineering: Backend" # Will be created +on: + schedule: + - cron: '0 2 * * *' # Daily at 2 AM UTC + # - cron: '0 */6 * * *' # Every 6 hours + # - cron: '0 2 * * 1' # Weekly on Monday at 2 AM ``` ## Troubleshooting -### Error: "Teams mode requires organizations to be configured" -**Fix**: Add organizations to config: -```yaml -teams: - organizations: - - "your-org" +### Workflow Fails: "Authentication failed" + +**Causes:** +- ❌ Token not set or expired +- ❌ Token missing required scopes + +**Fix:** +1. Verify secret name matches: `COST_CENTER_TOKEN` +2. Check token has `manage_billing:enterprise` and `read:org` scopes +3. Regenerate token if expired + +### Error: "Failed to fetch enterprise teams" + +**Causes:** +- ❌ Wrong enterprise name in workflow +- ❌ Token doesn't have access to enterprise + +**Fix:** +1. Check the enterprise name in your workflow matches your actual enterprise +2. Verify token owner has enterprise admin access + +### Warning: "User in multiple teams" + +**This is normal.** Users can only belong to ONE cost center. If someone is in multiple teams, they'll be assigned to the LAST team processed. + +**Review the logs** to see assignments: ``` +[WARNING] alice is in multiple teams [frontend, mobile] → will be assigned to '[enterprise team] mobile' +``` + +**Solutions:** +- Accept this behavior (user's primary team wins) +- Use manual mode to control which teams are included +- Restructure team membership to have clear primary teams + +### Want to See What Would Change? -### Error: "Failed to fetch teams for org X" -**Possible causes**: -- Token missing `read:org` scope → Regenerate token with correct scope -- Wrong org name → Check spelling -- No access to org → Verify token has access +Add a plan-only workflow for testing: -### Warning: "No mapping found for team org/team-slug in manual mode" -**Fix**: Add mapping for the team: ```yaml -teams: - team_mappings: - "org/team-slug": "Cost Center Name" +name: Cost Center Sync (Plan Only) + +on: + workflow_dispatch: + +jobs: + plan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: pip install -r requirements.txt + - env: + GITHUB_TOKEN: ${{ secrets.COST_CENTER_TOKEN }} + GITHUB_ENTERPRISE: ${{ github.repository_owner }} + run: | + python main.py --teams-mode --assign-cost-centers --mode plan ``` -### Warning: User in multiple teams -**This is a conflict notification.** Users can only belong to ONE cost center. If a user is in multiple teams, they'll be assigned to the LAST team's cost center processed. Review the warning logs to see which cost center each multi-team user will get. +## Local Testing (Optional) + +If you want to test locally before using Actions: + +```bash +# Clone and setup +git clone +cd cost-center-automation +pip install -r requirements.txt -**Solution:** If this is not desired: -1. Use manual mode to control which teams are processed -2. Ensure users have one primary team for cost allocation -3. Review team membership structure +# Set environment variables +export GITHUB_TOKEN="your_token_here" +export GITHUB_ENTERPRISE="your-enterprise" + +# Run in plan mode (dry run) +python main.py --teams-mode --assign-cost-centers --mode plan + +# Apply changes +python main.py --teams-mode --assign-cost-centers --mode apply --yes +``` ## Getting Help -- 📖 Full documentation: `README.md` -- 🔍 Detailed reference: `TEAMS_INTEGRATION.md` -- 💡 Implementation details: `IMPLEMENTATION_SUMMARY.md` +- 📖 Full documentation: [README.md](README.md) +- 🔍 Detailed reference: [TEAMS_INTEGRATION.md](TEAMS_INTEGRATION.md) - 🐛 Report issues: GitHub Issues ## Quick Command Reference - ```bash -# Show configuration +# Show current configuration python main.py --teams-mode --show-config -# Plan (dry run) +# Plan (dry run) - see what would change python main.py --teams-mode --assign-cost-centers --mode plan -# Apply with confirmation +# Apply changes with confirmation prompt python main.py --teams-mode --assign-cost-centers --mode apply -# Apply without confirmation +# Apply changes without confirmation (for automation) python main.py --teams-mode --assign-cost-centers --mode apply --yes -# Generate summary report -python main.py --teams-mode --summary-report - # Verbose mode (for debugging) python main.py --teams-mode --assign-cost-centers --mode plan --verbose - -# Note: Incremental mode is NOT supported for teams mode. All team members are processed every run. ``` --- -**🎉 That's it! You're now using GitHub Teams Integration for cost center automation.** +**🎉 That's it! Your cost centers now sync automatically with your enterprise teams via GitHub Actions.** + +Every day (or on your chosen schedule), the workflow will: +1. Discover all enterprise teams +2. Create/update cost centers for each team +3. Assign users based on team membership +4. Remove users who left teams (full sync) -For advanced usage, automation setup, and best practices, see the full documentation in `TEAMS_INTEGRATION.md`. +For advanced usage and customization, see [TEAMS_INTEGRATION.md](TEAMS_INTEGRATION.md). diff --git a/config/config.example.yaml b/config/config.example.yaml index fd47b4f..95d3aef 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -32,7 +32,7 @@ teams: enabled: false # Scope: 'organization' (org-level teams) or 'enterprise' (enterprise-level teams) - scope: "organization" # Options: "organization" or "enterprise" + scope: "enterprise" # Options: "organization" or "enterprise" # Mode: 'auto' (one cost center per team) or 'manual' (use mappings below) mode: "auto" # Options: "auto" or "manual" @@ -49,8 +49,8 @@ teams: # - Organization scope: [org team] {org-name}/{team-name} # - Enterprise scope: [enterprise team] {team-name} - # Orphaned user handling: remove users from cost centers if they're no longer in the team - remove_orphaned_users: true # Set to true to automatically remove orphaned users + # Full sync: remove users from cost centers when they're no longer in the team + remove_users_no_longer_in_teams: true # Set to false to keep users in cost centers even after leaving teams # Manual team-to-cost-center mappings (only used when mode is 'manual') # Format: "org/team-slug": "cost_center_id" or "cost_center_name" (if auto_create enabled) diff --git a/config/config.yaml b/config/config.yaml index b78950e..3a64ab9 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -28,7 +28,7 @@ teams: enabled: false # Scope: 'organization' (org-level teams) or 'enterprise' (enterprise-level teams) - scope: "organization" # Options: "organization" or "enterprise" + scope: "enterprise" # Options: "organization" or "enterprise" # Mode: 'auto' (one cost center per team) or 'manual' (use mappings below) mode: "auto" # Options: "auto" or "manual" @@ -44,8 +44,8 @@ teams: # - Organization scope: [org team] {org-name}/{team-name} # - Enterprise scope: [enterprise team] {team-name} - # Orphaned user handling: remove users from cost centers if they're no longer in the team - remove_orphaned_users: true # Set to true to automatically remove orphaned users + # Full sync: remove users from cost centers when they're no longer in the team + remove_users_no_longer_in_teams: true # Set to false to keep users in cost centers even after leaving teams # Manual team-to-cost-center mappings (only used when mode is 'manual') # Format: "org/team-slug": "cost_center_id" or "cost_center_name" (if auto_create enabled) diff --git a/main.py b/main.py index 1e2523b..b7b5e03 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,9 @@ - Organization scope: Sync teams from specific GitHub organizations - Enterprise scope: Sync teams across the entire GitHub Enterprise - Automatic cost center creation and naming - - Orphaned user detection and removal + - Team-based assignment (organization & enterprise teams) + - Full sync mode (removes users who left teams) + - Cost center auto-creation """ import argparse @@ -149,7 +151,7 @@ def _handle_teams_mode(args, config: ConfigManager, teams_manager, logger) -> No print(f"Organizations: {', '.join(config.teams_organizations)}") print(f"Auto-create cost centers: {config.teams_auto_create}") - print(f"Remove orphaned users: {config.teams_remove_orphaned_users}") + print(f"Full sync (remove users who left teams): {config.teams_remove_users_no_longer_in_teams}") if config.teams_mode == "auto": if teams_scope == "enterprise": diff --git a/src/config_manager.py b/src/config_manager.py index 9961f18..4c1c544 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -110,12 +110,16 @@ def _load_config(self): # Teams integration configuration teams_config = config_data.get("teams", {}) self.teams_enabled = teams_config.get("enabled", False) - self.teams_scope = teams_config.get("scope") # Required: "organization" or "enterprise" + self.teams_scope = teams_config.get("scope", "enterprise") # Default: "enterprise" self.teams_mode = teams_config.get("mode", "auto") # "auto" or "manual" self.teams_organizations = teams_config.get("organizations", []) self.teams_auto_create = teams_config.get("auto_create_cost_centers", True) self.teams_mappings = teams_config.get("team_mappings", {}) - self.teams_remove_orphaned_users = teams_config.get("remove_orphaned_users", False) + # Support both new and old config key names for backward compatibility + self.teams_remove_users_no_longer_in_teams = teams_config.get( + "remove_users_no_longer_in_teams", + teams_config.get("remove_orphaned_users", True) # Fallback to old key + ) # Store full config for other methods self.config = config_data diff --git a/src/teams_cost_center_manager.py b/src/teams_cost_center_manager.py index 4cceb46..187e2a3 100644 --- a/src/teams_cost_center_manager.py +++ b/src/teams_cost_center_manager.py @@ -354,11 +354,11 @@ def sync_team_assignments(self, mode: str = "plan") -> Dict[str, Dict[str, bool] for cost_center_id, usernames in id_based_assignments.items(): self.logger.info(f" {cost_center_id}: {len(usernames)} users") - # In plan mode, show that orphaned users would be checked if the option is enabled - if self.config.teams_remove_orphaned_users: - self.logger.info("\nMODE=plan: Orphaned user detection is ENABLED") - self.logger.info(" In apply mode, users in cost centers but not in teams will be removed") - self.logger.info(" (Cannot show specific orphaned users in plan mode - cost centers don't exist yet)") + # In plan mode, show that removed user cleanup would be performed if enabled + if self.config.teams_remove_users_no_longer_in_teams: + self.logger.info("\nMODE=plan: Full sync mode is ENABLED") + self.logger.info(" In apply mode, users no longer in teams will be removed from cost centers") + self.logger.info(" (Cannot show specific removed users in plan mode - cost centers don't exist yet)") return {} @@ -366,44 +366,45 @@ def sync_team_assignments(self, mode: str = "plan") -> Dict[str, Dict[str, bool] self.logger.info("Syncing team-based assignments to GitHub Enterprise...") results = self.github_manager.bulk_update_cost_center_assignments(id_based_assignments) - # Always check for orphaned users (detection), but only remove if configured - self.logger.info("Checking for orphaned users (users in cost centers but not in teams)...") - orphaned_results = self._remove_orphaned_users( + # Always check for users no longer in teams (detection), but only remove if configured + self.logger.info("Checking for users in cost centers who are no longer in teams...") + removed_user_results = self._remove_users_no_longer_in_teams( id_based_assignments, cost_center_id_map, - remove=self.config.teams_remove_orphaned_users + remove=self.config.teams_remove_users_no_longer_in_teams ) - # Merge orphaned user removal results into main results (if removal was enabled) - if self.config.teams_remove_orphaned_users: - for cost_center_id, user_results in orphaned_results.items(): + # Merge removed user results into main results (if removal was enabled) + if self.config.teams_remove_users_no_longer_in_teams: + for cost_center_id, user_results in removed_user_results.items(): if cost_center_id not in results: results[cost_center_id] = {} results[cost_center_id].update(user_results) return results - def _remove_orphaned_users(self, expected_assignments: Dict[str, List[str]], + def _remove_users_no_longer_in_teams(self, expected_assignments: Dict[str, List[str]], cost_center_id_map: Dict[str, str], remove: bool = True) -> Dict[str, Dict[str, bool]]: """ - Detect and optionally remove orphaned users from cost centers. + Detect and optionally remove users who are no longer in teams from cost centers. - Orphaned users are those who are in a cost center but not in the corresponding team. + These are users who are currently assigned to a cost center but are no longer + members of the corresponding GitHub team. Args: expected_assignments: Dict mapping cost_center_id -> list of expected usernames cost_center_id_map: Dict mapping cost_center_name -> cost_center_id - remove: If True, remove orphaned users. If False, only detect and log. + remove: If True, remove users no longer in teams. If False, only detect and log. Returns: Dict mapping cost_center_id -> Dict mapping username -> removal success status """ removal_results = {} - total_orphaned = 0 - total_removed = 0 + total_removed_users = 0 + total_successfully_removed = 0 - self.logger.info(f"Checking {len(expected_assignments)} cost centers for orphaned users...") + self.logger.info(f"Checking {len(expected_assignments)} cost centers for users no longer in teams...") for cost_center_id, expected_users in expected_assignments.items(): # Get current members of the cost center @@ -414,12 +415,12 @@ def _remove_orphaned_users(self, expected_assignments: Dict[str, List[str]], self.logger.debug(f" Current: {sorted(current_members)}") self.logger.debug(f" Expected: {sorted(expected_users)}") - # Find orphaned users (in cost center but not in expected team members) + # Find users no longer in teams (in cost center but not in expected team members) expected_users_set = set(expected_users) current_members_set = set(current_members) - orphaned_users = current_members_set - expected_users_set + users_no_longer_in_team = current_members_set - expected_users_set - if orphaned_users: + if users_no_longer_in_team: # Find the cost center name for logging cost_center_name = None for name, cc_id in cost_center_id_map.items(): @@ -430,47 +431,47 @@ def _remove_orphaned_users(self, expected_assignments: Dict[str, List[str]], display_name = cost_center_name or cost_center_id self.logger.warning( - f"⚠️ Found {len(orphaned_users)} orphaned users in cost center '{display_name}' " + f"⚠️ Found {len(users_no_longer_in_team)} users no longer in team for cost center '{display_name}' " f"(in cost center but not in team)" ) - for username in sorted(orphaned_users): + for username in sorted(users_no_longer_in_team): self.logger.warning(f" ⚠️ {username} is in cost center but not in team") - total_orphaned += len(orphaned_users) + total_removed_users += len(users_no_longer_in_team) - # Remove orphaned users if configured + # Remove users no longer in teams if configured if remove: - self.logger.info(f"Removing {len(orphaned_users)} orphaned users from '{display_name}'...") + self.logger.info(f"Removing {len(users_no_longer_in_team)} users from '{display_name}'...") removal_status = self.github_manager.remove_users_from_cost_center( cost_center_id, - list(orphaned_users) + list(users_no_longer_in_team) ) removal_results[cost_center_id] = removal_status successful_removals = sum(1 for success in removal_status.values() if success) - total_removed += successful_removals + total_successfully_removed += successful_removals - if successful_removals < len(orphaned_users): - failed = len(orphaned_users) - successful_removals + if successful_removals < len(users_no_longer_in_team): + failed = len(users_no_longer_in_team) - successful_removals self.logger.warning( - f"Failed to remove {failed}/{len(orphaned_users)} orphaned users from '{display_name}'" + f"Failed to remove {failed}/{len(users_no_longer_in_team)} users from '{display_name}'" ) else: - self.logger.info(f"⚠️ Orphaned user removal is DISABLED - users will remain in cost center") + self.logger.info(f"⚠️ Full sync is DISABLED - users will remain in cost center") - if total_orphaned > 0: + if total_removed_users > 0: if remove: self.logger.info( - f"📊 Orphaned users summary: Found {total_orphaned} orphaned users, " - f"successfully removed {total_removed}" + f"📊 Removed users summary: Found {total_removed_users} users no longer in teams, " + f"successfully removed {total_successfully_removed}" ) else: self.logger.warning( - f"📊 Orphaned users summary: Found {total_orphaned} orphaned users (NOT removed - removal disabled)" + f"📊 Removed users summary: Found {total_removed_users} users no longer in teams (NOT removed - full sync disabled)" ) else: - self.logger.info("✅ No orphaned users found - all cost centers are in sync with teams") + self.logger.info("✅ No users found who left teams - all cost centers are in sync with teams") return removal_results