# Implementation Plan: PAT Renewal ## Overview Implement an automated PAT lifecycle management system as a Python script (`scripts/pat_manager.py`) that runs via GitHub Actions. The implementation follows a modular architecture with 7 components: Registry, Expiry Checker, GitLab Rotator, Secret Updater, Alert Sender, Summary Reporter, and Main Orchestrator. All modules use Python 3.11+, httpx for HTTP, and pytest with Hypothesis for property-based testing. ## Tasks - [x] 1. Set up project structure and core data models - [x] 1.1 Create directory structure and dependencies - Create `scripts/pat_manager/` package directory with `__init__.py` - Create `scripts/pat_manager/models.py` with `PatEntry` dataclass, `PatStatus` enum, `CheckResult` dataclass, `RotationResult` dataclass, `AlertResult` dataclass, and `AlertChannel` enum - Create `scripts/pat_manager/errors.py` with all custom exception classes (`PatManagerError`, `RegistryValidationError`, `ServiceCheckError`, `RotationError`, `SecretUpdateError`, `AlertError`) - Add `requirements.txt` with `httpx>=0.27` and `requirements-dev.txt` with `hypothesis>=6.100`, `pytest>=8.0`, `pytest-asyncio>=0.23`, `pytest-cov>=5.0`, `respx>=0.21` - _Requirements: 1.1, 1.4_ - [x] 1.2 Create PAT Registry JSON schema and example file - Create `pat-registry.json` with the JSON schema structure (version "1.0", empty tokens array) - Create `pat-registry.example.json` with example entries for all four services - _Requirements: 1.1, 1.6_ - [x] 2. Implement PAT Registry module - [x] 2.1 Implement registry loading and validation (`scripts/pat_manager/registry.py`) - Implement `PatRegistry` class with `load(path)` method that reads and parses JSON - Implement `validate_entry(entry)` that checks all required fields, service enum values, token_name length (max 128 chars), expiry_date ISO 8601 format, and renewal_method matching service - Implement duplicate detection for `(service, token_name)` combinations - Implement `create_empty(path)` for creating a new empty registry file - Implement `save(path, entries)` for persisting registry changes - Raise `RegistryValidationError` with specific failed fields on invalid entries - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_ - [x] 2.2 Write property test: Registry serialization round-trip - **Property 1: Registry serialization round-trip** - **Validates: Requirements 1.1** - Use Hypothesis to generate valid `PatEntry` lists and verify `save` → `load` produces equivalent entries - [x] 2.3 Write property test: Validation accepts only valid entries - **Property 2: Validation accepts only valid entries** - **Validates: Requirements 1.2, 1.4** - Use Hypothesis to generate both valid and invalid entry dicts, verify validator accepts/rejects correctly - [x] 2.4 Write property test: Invalid entries and duplicates preserve registry state - **Property 3: Invalid entries and duplicates preserve registry state** - **Validates: Requirements 1.3, 1.5** - Use Hypothesis to generate registries and invalid/duplicate entries, verify registry unchanged after rejected add - [x] 3. Implement Expiry Checker module - [x] 3.1 Implement expiry classification logic (`scripts/pat_manager/checker.py`) - Implement `ExpiryChecker` class with configurable `expiry_window_days` (default 14), `timeout` (default 30s), and `max_retries` (default 2) - Implement `classify_by_date(expiry_date, reference_date, window)` pure function for date-based classification - Implement `check_all(entries, secrets)` async method that iterates all entries and returns `CheckResult` list - _Requirements: 2.2, 2.3, 2.4, 2.7_ - [x] 3.2 Implement service-specific check methods - Implement `check_gitlab(entry, token)` — calls `GET /personal_access_tokens`, reads `expires_at`, classifies - Implement `check_jira(entry, token)` — authenticated call, HTTP 401 = expired - Implement `check_confluence(entry, token)` — authenticated call, HTTP 401 = expired - Implement `check_orgmylife(entry, token)` — authenticated call, HTTP 401/403 = expired - Handle timeout (30s), retries (2 attempts with 5s backoff), and fallback to "check failed" - Handle null `expiry_date` entries: classify based on API call result only - Handle stale `expiry_date` warning when API succeeds but stored date has passed - _Requirements: 2.1, 2.5, 2.6, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8_ - [x] 3.3 Write property test: Expiry classification is deterministic and correct - **Property 4: Expiry classification is deterministic and correct** - **Validates: Requirements 2.2, 2.3, 2.4, 6.5** - Use Hypothesis to generate expiry dates and reference dates, verify classification matches expected rules - [x] 3.4 Write property test: Classification completeness - **Property 5: Classification completeness** - **Validates: Requirements 2.7** - Use Hypothesis to generate N-entry registries, verify exactly N results each with exactly one valid status - [x] 3.5 Write property test: Non-auth HTTP errors yield "check failed" - **Property 12: Non-auth HTTP errors yield "check failed"** - **Validates: Requirements 6.6** - Use Hypothesis to generate HTTP status codes not in {200-299, 401, 403}, verify classification is "check failed" - [x] 4. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 5. Implement GitLab Rotator module - [x] 5.1 Implement GitLab token rotation (`scripts/pat_manager/rotator.py`) - Implement `GitLabRotator` class with `rotate(token_id, current_token)` async method - Call `POST /personal_access_tokens/{id}/rotate` with the current token - Return `RotationResult` with new expiry date on success - Implement retry logic: retry once on failure, then return error result - Skip rotation for expired tokens (return error indicating manual intervention needed) - _Requirements: 3.1, 3.4, 3.7_ - [x] 5.2 Write property test: Registry update after successful rotation - **Property 6: Registry update after successful rotation** - **Validates: Requirements 3.3** - Use Hypothesis to generate rotation responses with new expiry dates, verify registry entry updated correctly - [x] 6. Implement Secret Updater module - [x] 6.1 Implement GitHub secret update (`scripts/pat_manager/secret_updater.py`) - Implement `SecretUpdater` class with `update_secret(secret_name, secret_value)` async method - Use GitHub API to encrypt and update repository secrets (libsodium sealed box encryption) - Handle failure: retain previous secret value, report error without exposing token - _Requirements: 3.2, 3.5, 7.1, 7.4, 7.5_ - [x] 7. Implement Alert Sender module - [x] 7.1 Implement alert sending (`scripts/pat_manager/alerter.py`) - Implement `AlertSender` class with configurable `AlertChannel` (OrgMyLife or email) - Implement `send_alert(result)` that creates OrgMyLife task or sends email based on channel - OrgMyLife task title format: `[PAT Renewal] {service} - {token_name}` - Include days remaining (0 for expired) in alert message - Implement `check_existing_task(service, token_name, expiry_date)` for idempotent alerting - Handle alert channel unreachable: log failure, signal non-zero exit - Implement retry logic: 1 retry with 2s backoff - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_ - [x] 7.2 Write property test: Alert triggering correctness - **Property 7: Alert triggering correctness** - **Validates: Requirements 4.1, 5.4** - Use Hypothesis to generate classified PAT sets, verify zero alerts when all healthy, one alert per expiring/expired manual token - [x] 7.3 Write property test: Alert message completeness - **Property 8: Alert message completeness** - **Validates: Requirements 4.2, 4.3** - Use Hypothesis to generate PAT entries requiring alerts, verify OrgMyLife title contains service+token, email contains all required fields - [x] 7.4 Write property test: Days remaining calculation - **Property 9: Days remaining calculation** - **Validates: Requirements 4.4** - Use Hypothesis to generate expiry/reference date pairs, verify `days_remaining == max(0, (expiry - ref).days)` - [x] 7.5 Write property test: Alert idempotence - **Property 10: Alert idempotence** - **Validates: Requirements 4.5** - Use Hypothesis to generate PAT alert scenarios, verify no duplicate task created when open task exists - [x] 8. Implement Summary Reporter module - [x] 8.1 Implement summary report generation (`scripts/pat_manager/reporter.py`) - Implement `SummaryReporter` class with `report(results, rotations)` method - Generate one line per PAT with service name, token identifier, and classification status - Include rotation results (old/new expiry dates) for rotated tokens - _Requirements: 5.3_ - [x] 8.2 Write property test: Summary report completeness - **Property 11: Summary report completeness** - **Validates: Requirements 5.3** - Use Hypothesis to generate check result lists, verify report contains one line per result with service, token_name, and status - [x] 9. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 10. Implement Main Orchestrator and GitHub Actions workflow - [x] 10.1 Implement main orchestrator (`scripts/pat_manager/__main__.py`) - Implement `main()` async function that coordinates all modules - Load registry → check all → rotate GitLab tokens → update secrets → send alerts → report summary - Mask all token values with `::add-mask::{value}` before any operations - Handle rotation success + secret update failure → trigger alert flow - Return exit code: 0 (success), 1 (alert delivery failure), 2 (registry validation failure) - Log rotation events with old and new expiration dates (Req 3.6) - _Requirements: 3.5, 3.6, 5.4, 5.5, 7.1, 7.2_ - [x] 10.2 Create GitHub Actions workflow file - Create `.github/workflows/pat-check.yml` with cron schedule `0 6 * * 1` (Monday 06:00 UTC) - Add `workflow_dispatch` trigger - Configure Python 3.11 setup, dependency installation, and script execution - Map all required secrets as environment variables (GITLAB_PAT, JIRA_PAT, CONFLUENCE_PAT, ORGMYLIFE_API_SECRET, ORGMYLIFE_USER, ORGMYLIFE_PASS, PAT_MANAGER_TOKEN, ALERT_CHANNEL) - Ensure all API URLs use HTTPS - _Requirements: 5.1, 5.2, 7.3_ - [x] 10.3 Write integration tests for orchestrator flow - Test full happy path: all healthy → no alerts, summary only - Test GitLab rotation flow with mocked APIs (respx) - Test rotation success + secret update failure → alert triggered - Test alert channel unreachable → non-zero exit code - Test service timeout after retries → "check failed" classification - Test null expiry_date classification scenarios - _Requirements: 2.5, 2.6, 3.2, 3.4, 3.5, 4.6, 5.4, 5.5, 6.8_ - [x] 11. Final checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation - Property tests validate universal correctness properties from the design document using Hypothesis - Unit/integration tests validate specific examples and edge cases using pytest + respx - The implementation uses Python 3.11+, httpx for HTTP calls, and async/await throughout - All token values must be masked in logs via GitHub Actions `::add-mask::` mechanism ## Task Dependency Graph ```json { "waves": [ { "id": 0, "tasks": ["1.1", "1.2"] }, { "id": 1, "tasks": ["2.1"] }, { "id": 2, "tasks": ["2.2", "2.3", "2.4", "3.1"] }, { "id": 3, "tasks": ["3.2", "3.3", "3.4", "3.5"] }, { "id": 4, "tasks": ["5.1", "6.1", "7.1", "8.1"] }, { "id": 5, "tasks": ["5.2", "7.2", "7.3", "7.4", "7.5", "8.2"] }, { "id": 6, "tasks": ["10.1", "10.2"] }, { "id": 7, "tasks": ["10.3"] } ] } ```