Initial monorepo structure
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"specId": "d26ccd62-45f2-47f0-8fbb-e15f4f79916d", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user