# github_cli

AWS Lambda function and developer CLI for automating GitHub tasks during employee offboarding. It searches Terraform files across a GitHub org for references to a departing user, then creates one cleanup issue per affected repository. Part of the employee offboarding automation Step Functions workflow.

## Features

- Search all Terraform files in a GitHub org for any text (email, username, full name)
- Paginated org-wide search with automatic rate-limit handling
- Create one unassigned cleanup issue per affected repository (engineers pick these up or assign Copilot manually)
- Supports both `offboard` (delete) and `suspend` operations, which change the issue wording
- `dry_run` mode skips issue creation without failing
- Runs as an AWS Lambda handler or as a local CLI

## Requirements

- Python 3.14+
- [uv](https://docs.astral.sh/uv/) package manager
- A GitHub personal access token with `repo` and `read:org` scopes

## Setup

```bash
cd lambda/github_cli
uv sync --group dev
export GITHUB_TOKEN=ghp_your_token_here
```

## Environment variables

| Variable | Description | Example |
|---|---|---|
| `GITHUB_TOKEN` | GitHub token with `repo` scope (locally the token value; in Lambda the Secrets Manager secret *name*) | `ghp_abc123...` |
| `ENVIRONMENT` | Deployment environment (used for Sentry) | `dev`, `qa`, `prod` |
| `SENTRY_DSN` | Sentry DSN for error reporting (optional) | — |
| `AWS_ACCESS_KEY_ID` | AWS access key (Lambda runtime, for Secrets Manager) | — |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key | — |
| `AWS_SESSION_TOKEN` | AWS session token (for assumed roles) | — |

## CLI usage

### Search for a user across org Terraform files

```bash
uv run github-cli search-text-in-org \
  --org theorchard \
  --search-text "john.doe@example.com"
```

Optionally narrow the search to a single repository:

```bash
uv run github-cli search-text-in-org \
  --org theorchard \
  --search-text "john.doe@example.com" \
  --repo-filter terraform-infra
```

### Create a Copilot offboarding issue

```bash
uv run github-cli \
  --repo theorchard/terraform-infra \
  offboard-user-with-copilot \
  --issue-description "Remove all Terraform references for this user." \
  --user-email john.doe@example.com \
  --user-full-name "John Doe"
```

### Create a single issue (debugging)

```bash
uv run github-cli --repo theorchard/terraform-infra \
  create-issue \
  --title "Cleanup" \
  --body "Remove references" \
  --assignees octocat
```

**Global options** (must come _before_ the subcommand):

| Option | Default | Description |
|--------|---------|-------------|
| `--repo` | `theorchard/terraform-infra` | Target repository (`owner/repo`) |
| `--base-branch` | `master` | Stored on CLI state; currently unused by the subcommands (no branch/PR logic) |
| `--debug` | off | Enable debug logging |

## Lambda handler

`github_client/app.py` is the Lambda entry point. It routes on `event["action"]`:

### `search-text-in-org`

Searches org Terraform files for both the user's email and full name, deduplicates results, and returns a compact payload for Step Functions.

Request:

```json
{
  "action": "search-text-in-org",
  "org": "theorchard",
  "email": "john.doe@example.com",
  "full_name": "John Doe",
  "repo_filter": "terraform-infra",
  "ticket_id": "SYS-1234"
}
```

Response:

```json
{
  "ticket_id": "SYS-1234",
  "email": "john.doe@example.com",
  "full_name": "John Doe",
  "terraform_hits": [
    {
      "path": "modules/iam/main.tf",
      "html_url": "https://github.com/...",
      "repository": "theorchard/terraform-infra",
      "confidence": "high"
    }
  ]
}
```

Each hit carries a `confidence` of `high` (precise identifiers like the full email or local-part) or `low` (the derived initial+lastname form, which produces more false positives).

### `offboard-user-with-copilot`

Creates **one cleanup issue per affected repository** (grouping `terraform_hits` by repo), summarising the Auth0 results and Terraform references. Issues are unassigned. The `operation` field selects the wording: `offboard` (default — user deleted) or `suspend` (user access gated, account preserved). Set `"dry_run": true` to skip issue creation and return empty `issue_urls`/`issue_numbers` lists. If there are no `terraform_hits`, no issue is created.

Request:

```json
{
  "action": "offboard-user-with-copilot",
  "ticket_id": "SYS-1234",
  "email": "john.doe@example.com",
  "full_name": "John Doe",
  "last_working_day": "2026-03-28",
  "repo": "theorchard/terraform-infra",
  "base_branch": "master",
  "operation": "offboard",
  "auth0_results": [
    { "tenant": "prod-orchard", "user_id": "auth0|abc123", "operation": "deleted" }
  ],
  "terraform_hits": [
    { "path": "modules/iam/main.tf", "html_url": "https://...", "repository": "theorchard/terraform-infra", "confidence": "high" }
  ],
  "dry_run": false
}
```

Response:

```json
{
  "ticket_id": "SYS-1234",
  "issue_urls": ["https://github.com/theorchard/terraform-infra/issues/42"],
  "issue_numbers": [42],
  "dry_run": false
}
```

## Docker

Start the Lambda locally and invoke it with curl:

```bash
docker compose up --build function

curl -X POST http://localhost:9000/2015-03-31/functions/function/invocations \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "search-text-in-org",
    "org": "theorchard",
    "email": "john.doe@example.com",
    "full_name": "John Doe",
    "repo_filter": "terraform-infra",
    "ticket_id": "SYS-1234"
  }'
```

Run unit tests and lint inside Docker (mirrors CI):

```bash
make docker_test
make docker_test_integration
```

## Testing

```bash
uv run pytest ./tests/              # All tests
uv run pytest ./tests/unit/         # Unit tests only
uv run pytest ./tests/integration/  # Integration tests only
uv run pytest ./tests/unit/test_github_client.py                            # Single file
uv run pytest ./tests/unit/test_github_client.py::test_create_issue_success # Single test
```

## Lint & format

```bash
uv run ruff check github_client/ tests/
uv run ruff format github_client/ tests/
uv run mypy github_client/
```

## Architecture

### GitHub client (`github_client/github_client.py`)

Wraps the GitHub REST API for code search (`GET /search/code`, scoped to `language:Terraform`, with pagination and rate-limit backoff) and issue creation (`POST /repos/{repo}/issues`). Accepts a pre-built session so tests can inject a mock.

### Configuration (`config.py`)

`GitHubConfig` uses `pydantic-settings`. Locally, `GITHUB_TOKEN` is read directly from the environment. In Lambda (detected via `AWS_LAMBDA_FUNCTION_NAME`), the env var holds the Secrets Manager secret *name* and the token is fetched at startup by a `@model_validator`.

## Project structure

```
lambda/github_cli/
├── config.py                   # GitHubConfig (reads GITHUB_TOKEN env var / Secrets Manager)
├── dev.py                      # Typer CLI entry point (github-cli script)
├── github_client/
│   ├── __init__.py
│   ├── app.py                  # Lambda handler
│   ├── github_client.py        # GitHub REST API client
│   ├── secrets_manager.py      # AWS Secrets Manager client
│   └── utils.py                # Helpers (e.g. split_full_name_safely)
└── tests/
    ├── unit/                   # Isolated unit tests
    └── integration/            # Handler-level tests with mocked HTTP
```

