Initial monorepo structure

This commit is contained in:
2026-06-30 20:37:40 +02:00
commit 2f2b295531
121 changed files with 39171 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"specId": "d26ccd62-45f2-47f0-8fbb-e15f4f79916d", "workflowType": "requirements-first", "specType": "feature"}
+579
View File
@@ -0,0 +1,579 @@
# Design Document: PAT Renewal
## Overview
The PAT Renewal system automates Personal Access Token lifecycle management across GitLab, Jira, Confluence, and OrgMyLife services. It runs as a GitHub Actions scheduled workflow (weekly, Mondays at 06:00 UTC) that:
1. Reads a PAT Registry (JSON) tracking all managed tokens
2. Checks each token's expiry status via service-specific API calls
3. Auto-rotates GitLab PATs using the GitLab Personal Access Tokens API
4. Creates OrgMyLife tasks or sends email alerts for tokens requiring manual renewal
5. Produces a summary report in the GitHub Actions run log
The system integrates with the existing GitHub Actions infrastructure already used in the workspace (deploy workflows, OrgMyLife sync) and leverages the OrgMyLife API for task creation alerts.
**Key Design Decisions:**
- **Python-based workflow script**: Matches the workspace's primary language (Python 3.11+) and allows reuse of patterns from the AI-Orchestrator project (httpx for HTTP, pytest for testing)
- **JSON registry file**: Simple, version-controllable, human-readable format stored in the repository
- **GitHub Actions secrets for token storage**: Leverages existing secure secret management without introducing new infrastructure
- **Idempotent alerting**: Prevents duplicate OrgMyLife tasks by checking for existing open tasks before creating new ones
## Architecture
```mermaid
graph TD
subgraph "GitHub Actions"
CRON[Cron Trigger<br/>Monday 06:00 UTC] --> WF[pat-check Workflow]
DISPATCH[workflow_dispatch] --> WF
WF --> SCRIPT[pat_manager.py]
end
subgraph "PAT Manager Script"
SCRIPT --> LOAD[Load PAT Registry]
LOAD --> CHECK[Expiry Checker]
CHECK --> CLASSIFY{Classify PATs}
CLASSIFY -->|expiring soon + auto| ROTATE[GitLab Rotator]
CLASSIFY -->|expiring soon + manual| ALERT[Alert Sender]
CLASSIFY -->|expired| ALERT
CLASSIFY -->|healthy| REPORT[Summary Reporter]
ROTATE --> SECRET_UPDATE[Update GitHub Secret]
ROTATE -->|failure| ALERT
SECRET_UPDATE --> REPORT
ALERT --> REPORT
end
subgraph "External Services"
CHECK --> GITLAB_API[GitLab API]
CHECK --> JIRA_API[Jira API]
CHECK --> CONFLUENCE_API[Confluence API]
CHECK --> ORGMYLIFE_API[OrgMyLife API]
ROTATE --> GITLAB_API
SECRET_UPDATE --> GH_API[GitHub API]
ALERT --> ORGMYLIFE_API
ALERT --> EMAIL[Email / SMTP]
end
subgraph "Storage"
LOAD --> REGISTRY[pat-registry.json]
REPORT --> LOG[Workflow Run Log]
end
```
### Component Interaction Flow
```mermaid
sequenceDiagram
participant GHA as GitHub Actions
participant PM as pat_manager.py
participant REG as pat-registry.json
participant GL as GitLab API
participant JIRA as Jira API
participant OML as OrgMyLife API
participant GH as GitHub API
GHA->>PM: Trigger (cron/dispatch)
PM->>REG: Load registry
REG-->>PM: PAT entries
loop For each PAT
alt GitLab PAT
PM->>GL: GET /personal_access_tokens
GL-->>PM: expires_at
else Jira/Confluence/OrgMyLife PAT
PM->>JIRA: Authenticated API call
JIRA-->>PM: 200 OK / 401 Unauthorized
end
PM->>PM: Classify (healthy/expiring/expired/check failed)
end
alt GitLab PAT expiring soon
PM->>GL: POST /personal_access_tokens/{id}/rotate
GL-->>PM: New token + expiry
PM->>GH: Update secret
PM->>REG: Update expiry_date
end
alt Manual token expiring/expired
PM->>OML: POST /api/tasks (create alert task)
end
PM->>GHA: Summary report (log output)
```
## Components and Interfaces
### 1. PAT Registry Module (`registry.py`)
Responsible for loading, validating, and persisting the PAT registry JSON file.
```python
@dataclass
class PatEntry:
service: str # "GitLab" | "Jira" | "Confluence" | "OrgMyLife"
token_name: str # max 128 chars
expiry_date: str | None # ISO 8601 (YYYY-MM-DD) or None
renewal_method: str # "auto" | "manual"
class PatRegistry:
def load(self, path: Path) -> list[PatEntry]: ...
def validate_entry(self, entry: dict) -> PatEntry | ValidationError: ...
def save(self, path: Path, entries: list[PatEntry]) -> None: ...
def create_empty(self, path: Path) -> None: ...
```
**Validation Rules:**
- `service` must be one of: `GitLab`, `Jira`, `Confluence`, `OrgMyLife`
- `token_name` must be non-empty, max 128 characters
- `expiry_date` must be valid ISO 8601 (YYYY-MM-DD) or null
- `renewal_method` must match service: GitLab → "auto", others → "manual"
- No duplicate `(service, token_name)` combinations
### 2. Expiry Checker Module (`checker.py`)
Queries each service API to determine PAT status.
```python
class PatStatus(Enum):
HEALTHY = "healthy"
EXPIRING_SOON = "expiring soon"
EXPIRED = "expired"
CHECK_FAILED = "check failed"
@dataclass
class CheckResult:
entry: PatEntry
status: PatStatus
days_remaining: int | None
error_message: str | None
class ExpiryChecker:
def __init__(self, expiry_window_days: int = 14, timeout: int = 30, max_retries: int = 2): ...
async def check_all(self, entries: list[PatEntry], secrets: dict[str, str]) -> list[CheckResult]: ...
async def check_gitlab(self, entry: PatEntry, token: str) -> CheckResult: ...
async def check_jira(self, entry: PatEntry, token: str) -> CheckResult: ...
async def check_confluence(self, entry: PatEntry, token: str) -> CheckResult: ...
async def check_orgmylife(self, entry: PatEntry, token: str) -> CheckResult: ...
```
**Service-Specific Logic:**
- **GitLab**: `GET /personal_access_tokens` → read `expires_at` field, compare with window
- **Jira**: Authenticated call → HTTP 401 = expired
- **Confluence**: Authenticated call → HTTP 401 = expired
- **OrgMyLife**: Authenticated call → HTTP 401/403 = expired
### 3. GitLab Rotator Module (`rotator.py`)
Handles automatic token rotation for GitLab PATs.
```python
@dataclass
class RotationResult:
success: bool
new_expiry_date: str | None
error_message: str | None
class GitLabRotator:
def __init__(self, timeout: int = 30): ...
async def rotate(self, token_id: str, current_token: str) -> RotationResult: ...
```
### 4. Secret Updater Module (`secret_updater.py`)
Updates GitHub Actions secrets with new token values.
```python
class SecretUpdater:
def __init__(self, github_token: str, repo_owner: str, repo_name: str): ...
async def update_secret(self, secret_name: str, secret_value: str) -> bool: ...
```
### 5. Alert Sender Module (`alerter.py`)
Sends alerts via OrgMyLife task creation or email.
```python
class AlertChannel(Enum):
ORGMYLIFE = "orgmylife"
EMAIL = "email"
@dataclass
class AlertResult:
sent: bool
channel: AlertChannel
error_message: str | None
class AlertSender:
def __init__(self, channel: AlertChannel, config: dict): ...
async def send_alert(self, result: CheckResult) -> AlertResult: ...
async def check_existing_task(self, service: str, token_name: str, expiry_date: str) -> bool: ...
```
**OrgMyLife Integration:**
- Uses `POST /api/tasks` with `X-API-Key` header for authentication
- Task title format: `[PAT Renewal] {service} - {token_name}`
- Includes days remaining and renewal instructions in description
- Checks for existing open tasks before creating duplicates
### 6. Summary Reporter Module (`reporter.py`)
Produces the workflow summary log.
```python
class SummaryReporter:
def report(self, results: list[CheckResult], rotations: list[RotationResult]) -> str: ...
```
### 7. Main Orchestrator (`pat_manager.py`)
Entry point that coordinates all modules.
```python
async def main() -> int:
"""Returns exit code: 0 for success, 1 for failures."""
...
```
## Data Models
### PAT Registry JSON Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"version": { "type": "string", "const": "1.0" },
"tokens": {
"type": "array",
"items": {
"type": "object",
"properties": {
"service": {
"type": "string",
"enum": ["GitLab", "Jira", "Confluence", "OrgMyLife"]
},
"token_name": {
"type": "string",
"maxLength": 128,
"minLength": 1
},
"expiry_date": {
"type": ["string", "null"],
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"renewal_method": {
"type": "string",
"enum": ["auto", "manual"]
}
},
"required": ["service", "token_name", "renewal_method"],
"additionalProperties": false
}
}
},
"required": ["version", "tokens"]
}
```
### Example PAT Registry
```json
{
"version": "1.0",
"tokens": [
{
"service": "GitLab",
"token_name": "ci-pipeline-token",
"expiry_date": "2025-08-15",
"renewal_method": "auto"
},
{
"service": "Jira",
"token_name": "jira-api-access",
"expiry_date": "2025-07-20",
"renewal_method": "manual"
},
{
"service": "Confluence",
"token_name": "confluence-bot",
"expiry_date": "2025-09-01",
"renewal_method": "manual"
},
{
"service": "OrgMyLife",
"token_name": "orchestrator-api-key",
"expiry_date": null,
"renewal_method": "manual"
}
]
}
```
### Check Result Data Model
```python
@dataclass
class CheckResult:
entry: PatEntry
status: PatStatus # healthy | expiring soon | expired | check failed
days_remaining: int | None # None if check failed or no expiry date
error_message: str | None # Populated only for check_failed
stale_warning: bool = False # True if API succeeds but stored date has passed
```
### GitHub Actions Workflow Configuration
```yaml
name: PAT Lifecycle Check
on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
workflow_dispatch: {}
jobs:
check-pats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install httpx
- name: Run PAT Manager
env:
GITLAB_PAT: ${{ secrets.GITLAB_PAT }}
JIRA_PAT: ${{ secrets.JIRA_PAT }}
CONFLUENCE_PAT: ${{ secrets.CONFLUENCE_PAT }}
ORGMYLIFE_API_KEY: ${{ secrets.ORGMYLIFE_API_SECRET }}
ORGMYLIFE_USER: ${{ secrets.ORGMYLIFE_USER }}
ORGMYLIFE_PASS: ${{ secrets.ORGMYLIFE_PASS }}
GH_TOKEN: ${{ secrets.PAT_MANAGER_TOKEN }}
ALERT_CHANNEL: orgmylife
run: python scripts/pat_manager.py
```
### Environment Variables / Secrets Mapping
| Secret Name | Purpose | Used By |
|---|---|---|
| `GITLAB_PAT` | GitLab API access + rotation source | Checker, Rotator |
| `JIRA_PAT` | Jira authenticated check | Checker |
| `CONFLUENCE_PAT` | Confluence authenticated check | Checker |
| `ORGMYLIFE_API_SECRET` | OrgMyLife API key for task creation | Alerter |
| `ORGMYLIFE_USER` | OrgMyLife basic auth username | Alerter |
| `ORGMYLIFE_PASS` | OrgMyLife basic auth password | Alerter |
| `PAT_MANAGER_TOKEN` | GitHub API token for secret updates | SecretUpdater |
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Registry serialization round-trip
*For any* valid list of PAT entries, serializing the registry to JSON and deserializing it back SHALL produce an equivalent list of entries with identical field values.
**Validates: Requirements 1.1**
### Property 2: Validation accepts only valid entries
*For any* dictionary representing a PAT entry, the validator SHALL accept it if and only if it contains all required fields (`service`, `token_name`, `expiry_date`, `renewal_method`), the `service` is one of the four supported values, the `token_name` is non-empty and at most 128 characters, the `expiry_date` is either null or a valid ISO 8601 date string, and the `renewal_method` matches the service (GitLab → "auto", others → "manual").
**Validates: Requirements 1.2, 1.4**
### Property 3: Invalid entries and duplicates preserve registry state
*For any* PAT registry and any entry that is either invalid (fails validation) or a duplicate (same service + token_name already exists), attempting to add that entry SHALL leave the registry unchanged — the entry count and all existing entries remain identical.
**Validates: Requirements 1.3, 1.5**
### Property 4: Expiry classification is deterministic and correct
*For any* PAT entry with an expiry date and any reference date, the classification SHALL be:
- "expired" if `expiry_date < reference_date`
- "expiring soon" if `reference_date <= expiry_date <= reference_date + expiry_window`
- "healthy" if `expiry_date > reference_date + expiry_window`
This holds regardless of whether the expiry date comes from the service API or the stored registry value.
**Validates: Requirements 2.2, 2.3, 2.4, 6.5**
### Property 5: Classification completeness
*For any* PAT registry with N entries, after running the expiry check, the system SHALL produce exactly N classification results, each with exactly one status from the set {"healthy", "expiring soon", "expired", "check failed"}.
**Validates: Requirements 2.7**
### Property 6: Registry update after successful rotation
*For any* GitLab PAT entry and any valid rotation response containing a new expiry date, after successful rotation the registry entry's `expiry_date` SHALL equal the new expiry date from the rotation response.
**Validates: Requirements 3.3**
### Property 7: Alert triggering correctness
*For any* set of classified PATs:
- If ALL PATs have status "healthy", THEN zero alerts SHALL be sent
- For each manual-renewal PAT with status "expiring soon" or "expired", exactly one alert SHALL be triggered
**Validates: Requirements 4.1, 5.4**
### Property 8: Alert message completeness
*For any* PAT entry requiring an alert:
- If the alert channel is OrgMyLife, the task title SHALL contain both the service name and token identifier
- If the alert channel is email, the email body SHALL contain the service name, token identifier, expiration date, and a renewal instructions URL
**Validates: Requirements 4.2, 4.3**
### Property 9: Days remaining calculation
*For any* expiry date and reference date, the computed `days_remaining` SHALL equal `max(0, (expiry_date - reference_date).days)` — yielding 0 for already-expired tokens and the correct positive integer for future dates.
**Validates: Requirements 4.4**
### Property 10: Alert idempotence
*For any* PAT that requires an alert, if an open OrgMyLife task already exists for the same service, token_name, and expiry_date, THEN no new task SHALL be created. Running the alert flow twice for the same PAT state SHALL produce at most one task.
**Validates: Requirements 4.5**
### Property 11: Summary report completeness
*For any* list of check results, the generated summary report SHALL contain one line per result, and each line SHALL include the service name, token identifier, and classification status.
**Validates: Requirements 5.3**
### Property 12: Non-auth HTTP errors yield "check failed"
*For any* HTTP response status code that is not 200-299, 401, or 403, the classification SHALL be "check failed". This includes 4xx errors (other than 401/403), 5xx errors, and timeout conditions.
**Validates: Requirements 6.6**
## Error Handling
### Retry Strategy
| Operation | Max Retries | Backoff | Fallback |
|---|---|---|---|
| Service API health check | 2 | 5s linear | Classify as "check failed" |
| GitLab token rotation | 1 | None | Manual alert flow |
| GitHub secret update | 0 | N/A | Alert + non-zero exit |
| OrgMyLife task creation | 1 | 2s | Log error + non-zero exit |
| Email sending | 1 | 2s | Log error + non-zero exit |
### Error Classification
```python
class PatManagerError(Exception):
"""Base exception for PAT Manager errors."""
pass
class RegistryValidationError(PatManagerError):
"""Raised when a registry entry fails validation."""
def __init__(self, entry: dict, failed_fields: list[str]): ...
class ServiceCheckError(PatManagerError):
"""Raised when a service API check fails after retries."""
def __init__(self, service: str, token_name: str, reason: str): ...
class RotationError(PatManagerError):
"""Raised when GitLab token rotation fails."""
def __init__(self, token_name: str, http_status: int | None, reason: str): ...
class SecretUpdateError(PatManagerError):
"""Raised when GitHub secret update fails."""
def __init__(self, secret_name: str, reason: str): ...
class AlertError(PatManagerError):
"""Raised when alert delivery fails."""
def __init__(self, channel: str, reason: str): ...
```
### Exit Codes
| Code | Meaning |
|---|---|
| 0 | All checks passed, all actions succeeded |
| 1 | One or more alerts could not be delivered |
| 2 | Registry validation failed (malformed registry file) |
### Security Error Handling
- Token values are masked via `::add-mask::{value}` before any operation that might log output
- On rotation failure, the old token remains valid — no service disruption
- On secret update failure, the new token exists in GitLab but the workflow still uses the old one — alert is critical
- All HTTP errors are logged without including request/response bodies that might contain tokens
## Testing Strategy
### Property-Based Tests (Hypothesis)
The project uses Python 3.11+ with pytest. Property-based tests will use the **Hypothesis** library, which is the standard PBT framework for Python.
**Configuration:**
- Minimum 100 examples per property test (`@settings(max_examples=100)`)
- Each test tagged with a comment referencing the design property
- Tag format: `# Feature: pat-renewal, Property {number}: {property_text}`
**Properties to implement:**
1. Registry round-trip serialization
2. Validation correctness (accepts valid, rejects invalid)
3. Invalid/duplicate entries preserve state
4. Expiry classification determinism
5. Classification completeness
6. Registry update after rotation
7. Alert triggering correctness
8. Alert message completeness
9. Days remaining calculation
10. Alert idempotence
11. Summary report completeness
12. Non-auth HTTP errors → "check failed"
**Generators needed:**
- `pat_entries()` — generates valid PatEntry instances with random services, token names, dates
- `invalid_entries()` — generates dictionaries missing fields or with invalid values
- `expiry_dates(relative_to)` — generates dates in various ranges (past, within window, beyond window)
- `http_status_codes()` — generates non-success, non-auth status codes
- `check_results()` — generates lists of CheckResult with various statuses
### Unit Tests (pytest)
Example-based tests for specific scenarios:
- Empty registry file creation (Req 1.6)
- Successful rotation logging format (Req 3.6)
- Expired GitLab PAT skips rotation (Req 3.7)
- Stale expiry_date warning (Req 6.7)
- Token masking with `::add-mask::` (Req 7.2)
### Integration Tests (pytest + httpx mock)
Mock-based tests for external service interactions:
- GitLab API expiry check with mocked response (Req 6.1)
- Jira/Confluence 401 interpretation (Req 6.2, 6.3)
- OrgMyLife 401/403 interpretation (Req 6.4)
- GitLab rotation API call + retry on failure (Req 3.1, 3.4)
- GitHub secret update after rotation (Req 3.2)
- Rotation success + secret update failure → alert (Req 3.5)
- Alert channel unreachable → non-zero exit (Req 4.6)
- Service timeout after retries → "check failed" (Req 2.5)
- Null expiry_date classification (Req 2.6, 6.8)
### Smoke Tests
- Workflow YAML contains correct cron schedule (Req 5.1)
- Workflow YAML contains workflow_dispatch trigger (Req 5.2)
- All API URLs use HTTPS scheme (Req 7.3)
- No token values written to non-secret locations (Req 7.1)
### Test Dependencies
```
hypothesis>=6.100
pytest>=8.0
pytest-asyncio>=0.23
pytest-cov>=5.0
respx>=0.21 # httpx mock library
```
+117
View File
@@ -0,0 +1,117 @@
# Requirements Document
## Introduction
This feature provides automated Personal Access Token (PAT) lifecycle management across all services used in the workspace: GitLab, Jira, Confluence, and OrgMyLife. The system tracks PAT expiration dates, auto-renews tokens where the service API supports it, and alerts the user via OrgMyLife task creation or email when manual intervention is required.
**Recommended Architecture:** A GitHub Actions scheduled workflow (runs weekly) that:
- Checks PAT expiry via each service's API
- Auto-rotates GitLab PATs (GitLab API supports this natively)
- Creates OrgMyLife tasks or sends email alerts for tokens that cannot be auto-renewed (Jira, Confluence)
- Stores a PAT registry (JSON) tracking token names, services, and expiration dates
This approach keeps the logic centralized, runs on schedule without needing the AI-Orchestrator to be online, and integrates with existing GitHub Actions infrastructure already in use.
## Glossary
- **PAT_Manager**: The automated system (GitHub Actions workflow) responsible for checking, renewing, and alerting about PAT status
- **PAT_Registry**: A JSON configuration file that stores metadata about all managed PATs (service, token name, expiration date, renewal method)
- **Token_Expiry_Window**: The number of days before expiration at which the PAT_Manager begins renewal or alerting (default: 14 days)
- **Auto_Renewable_Token**: A PAT whose service API supports programmatic rotation (e.g., GitLab PATs via the Personal Access Tokens API)
- **Manual_Renewal_Token**: A PAT whose service does not support programmatic rotation and requires human intervention (e.g., Jira, Confluence)
- **Alert_Channel**: The notification destination for expiry warnings — either OrgMyLife task creation or email
## Requirements
### Requirement 1: PAT Registry Management
**User Story:** As a developer, I want a central registry of all my PATs with their metadata, so that I have a single source of truth for token lifecycle information.
#### Acceptance Criteria
1. THE PAT_Manager SHALL maintain a PAT_Registry JSON file containing the service name, token identifier (maximum 128 characters), expiration date in ISO 8601 format (YYYY-MM-DD), and renewal method (value of "auto" or "manual") for each managed PAT
2. WHEN a new PAT is added to the PAT_Registry, THE PAT_Manager SHALL validate that the entry contains all required fields (service, token_name, expiry_date in ISO 8601 format, renewal_method) and that the service value matches one of the supported services
3. IF a PAT_Registry entry contains invalid or missing fields, THEN THE PAT_Manager SHALL reject the entry, leave the PAT_Registry unchanged, and output a validation error to the workflow log identifying the malformed entry and which fields failed validation
4. THE PAT_Manager SHALL accept only the following service values in the PAT_Registry: GitLab (renewal_method: "auto"), Jira (renewal_method: "manual"), Confluence (renewal_method: "manual"), and OrgMyLife (renewal_method: "manual")
5. IF a PAT entry is added with a service and token_name combination that already exists in the PAT_Registry, THEN THE PAT_Manager SHALL reject the duplicate entry and output a validation error to the workflow log indicating the conflict
6. WHEN the PAT_Registry file does not exist at workflow execution time, THE PAT_Manager SHALL create an empty PAT_Registry file with a valid JSON structure
### Requirement 2: PAT Expiry Checking
**User Story:** As a developer, I want the system to check whether any of my PATs are expired or approaching expiration, so that I can take action before services break.
#### Acceptance Criteria
1. WHEN the scheduled check runs, THE PAT_Manager SHALL query each service API to determine the current expiration status of each registered PAT, applying a timeout of 30 seconds per service call
2. WHEN a PAT expiration date is within the Token_Expiry_Window (default: 14 days), THE PAT_Manager SHALL classify the PAT as "expiring soon"
3. WHEN a PAT expiration date has passed, THE PAT_Manager SHALL classify the PAT as "expired"
4. WHEN a PAT is not expired and its expiration date is beyond the Token_Expiry_Window, THE PAT_Manager SHALL classify the PAT as "healthy"
5. IF a service API call fails due to connection timeout, DNS resolution failure, or HTTP 5xx response after 2 retry attempts, THEN THE PAT_Manager SHALL log the failure reason and classify the PAT as "check failed"
6. IF a PAT_Registry entry has no expiration date configured, THEN THE PAT_Manager SHALL classify the PAT as "healthy" provided the authenticated API call succeeds, or "expired" if the API call returns an authentication failure
7. WHEN the expiry check completes, THE PAT_Manager SHALL produce a classification result for every PAT in the PAT_Registry such that each PAT is assigned exactly one status: "healthy", "expiring soon", "expired", or "check failed"
### Requirement 3: Automatic PAT Renewal (GitLab)
**User Story:** As a developer, I want GitLab PATs to be automatically rotated before they expire, so that my CI/CD pipelines and scripts continue working without manual intervention.
#### Acceptance Criteria
1. WHEN a GitLab PAT is classified as "expiring soon", THE PAT_Manager SHALL call the GitLab Personal Access Tokens API (`POST /personal_access_tokens/{id}/rotate`) to rotate the token
2. WHEN the GitLab API returns a new token value after rotation, THE PAT_Manager SHALL update the stored token in GitHub Actions secrets using the GitHub API within 30 seconds of receiving the new value
3. WHEN the GitLab API returns a new token value after rotation, THE PAT_Manager SHALL update the PAT_Registry entry with the new expiration date as returned by the GitLab API response
4. IF the GitLab token rotation API call fails with an HTTP error status, a network timeout after 30 seconds, or a connection error, THEN THE PAT_Manager SHALL retry the rotation once and, if the retry also fails, fall back to the manual renewal alert flow
5. IF the GitLab token rotation succeeds but the subsequent GitHub Actions secret update fails, THEN THE PAT_Manager SHALL immediately trigger the manual renewal alert flow indicating that the token was rotated but the secret could not be updated
6. WHEN a GitLab PAT is successfully rotated and the secret is updated, THE PAT_Manager SHALL log the rotation event with the old and new expiration dates
7. IF a GitLab PAT is classified as "expired", THEN THE PAT_Manager SHALL skip automatic rotation and fall back to the manual renewal alert flow
### Requirement 4: Manual Renewal Alerting
**User Story:** As a developer, I want to be alerted when PATs that cannot be auto-renewed are approaching expiration, so that I can manually renew them in time.
#### Acceptance Criteria
1. WHEN a Manual_Renewal_Token has 14 or fewer days until expiration or is already expired, THE PAT_Manager SHALL send an alert via the configured Alert_Channel
2. IF the configured Alert_Channel is OrgMyLife, THEN THE PAT_Manager SHALL create a task with the title containing the service name and token identifier
3. IF the configured Alert_Channel is email, THEN THE PAT_Manager SHALL send an email containing the service name, token identifier, expiration date, and renewal instructions URL
4. THE PAT_Manager SHALL include the remaining days until expiration (or indicate already expired with 0 days remaining) in each alert message
5. THE PAT_Manager SHALL not create a new OrgMyLife task for a PAT if an open task for the same PAT and the same expiration date already exists
6. IF the configured Alert_Channel is unreachable or returns an error, THEN THE PAT_Manager SHALL log the failure and exit the workflow run with a non-zero exit code
### Requirement 5: Scheduled Execution
**User Story:** As a developer, I want the PAT checks to run automatically on a weekly schedule, so that I do not need to remember to check token status manually.
#### Acceptance Criteria
1. THE PAT_Manager SHALL execute as a GitHub Actions workflow on a weekly cron schedule, running once every Monday at 06:00 UTC
2. THE PAT_Manager SHALL support manual triggering via workflow_dispatch, performing the same check-and-report sequence as the scheduled run
3. WHEN the workflow completes (whether triggered by schedule or workflow_dispatch), THE PAT_Manager SHALL produce a summary report in the GitHub Actions run log listing each managed PAT with its service name, token identifier, and classification status (healthy, expiring soon, expired, or check failed)
4. IF all PATs are classified as "healthy", THEN THE PAT_Manager SHALL complete without creating OrgMyLife tasks or sending email alerts, while still logging the summary report
5. IF one or more PATs are classified as "expiring soon", "expired", or "check failed", THEN THE PAT_Manager SHALL proceed with the applicable renewal or alerting flow and still produce the summary report
### Requirement 6: Service-Specific Expiry Detection
**User Story:** As a developer, I want the system to correctly detect expiry for each service type, so that checks work regardless of how each platform reports token status.
#### Acceptance Criteria
1. WHEN checking a GitLab PAT, THE PAT_Manager SHALL use the GitLab Personal Access Tokens API (`GET /personal_access_tokens`) to retrieve the `expires_at` field and compare it against the current date and Token_Expiry_Window to classify the PAT as "healthy", "expiring soon", or "expired"
2. WHEN checking a Jira PAT, THE PAT_Manager SHALL attempt an authenticated API call within a timeout of 30 seconds and interpret HTTP 401 as an expired or revoked token
3. WHEN checking a Confluence PAT, THE PAT_Manager SHALL attempt an authenticated API call within a timeout of 30 seconds and interpret HTTP 401 as an expired or revoked token
4. WHEN checking an OrgMyLife token, THE PAT_Manager SHALL attempt an authenticated API call within a timeout of 30 seconds and interpret HTTP 401 or HTTP 403 as an expired or revoked token
5. WHEN a service does not expose an expiry date via API and the authenticated call succeeds, THE PAT_Manager SHALL compare the expiry_date stored in the PAT_Registry against the current date and Token_Expiry_Window to classify the PAT
6. IF a service API returns an HTTP error other than 401 or 403 or the request times out, THEN THE PAT_Manager SHALL classify the PAT as "check failed" and log an error message indicating the service name, token identifier, and the response status or timeout condition
7. IF the authenticated API call succeeds but the expiry_date stored in the PAT_Registry has passed, THEN THE PAT_Manager SHALL classify the PAT as "expired" based on the stored date and include a warning that the stored expiry_date may be stale
8. IF a Manual_Renewal_Token has no expiry_date in the PAT_Registry, THEN THE PAT_Manager SHALL classify the PAT based solely on the authenticated API call result — "healthy" if the call succeeds, or "expired" if the call returns 401 or 403
### Requirement 7: Security
**User Story:** As a developer, I want PAT values to remain secure throughout the renewal process, so that tokens are not exposed in logs or artifacts.
#### Acceptance Criteria
1. THE PAT_Manager SHALL store PAT values exclusively in GitHub Actions secrets or encrypted environment variables and SHALL NOT write token values to workflow artifacts, step summaries, or PR comments
2. THE PAT_Manager SHALL mask token values by registering them with the workflow log masking mechanism so that any occurrence in stdout, stderr, or workflow annotations is replaced with `***`
3. THE PAT_Manager SHALL transmit PAT values only over HTTPS connections
4. IF a token rotation produces a new value, THEN THE PAT_Manager SHALL update the GitHub Actions secret using the GitHub API with a token that has the `admin:org` or `repo` scope required for secrets write access
5. IF the GitHub API secret update fails, THEN THE PAT_Manager SHALL retain the previous secret value unchanged, report the failure in the workflow log without exposing the new token value, and exit the workflow run with a non-success status
+204
View File
@@ -0,0 +1,204 @@
# 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"] }
]
}
```