# MR Auto-Approver

LLMS index: [llms.txt](/llms.txt) | Full content: [llms-full.txt](/llms-full.txt)

---

An AWS Lambda function that auto-approves GitLab merge requests based on
configurable per-project rules. Replaces CI/CD-based approval jobs and moves
approval tokens out of GitLab CI/CD variables.

## Features

- **Rules-Based Config**: YAML config with per-project ordered rules matching
  on user IDs, username patterns, branch patterns, and Konflux pipeline status
- **First-Match-Wins**: Rules evaluated in order; first match determines action
  (approve or deny)
- **Konflux Integration**: Optional check that Konflux pipelines have posted
  commit statuses before approving
- **Fork Rejection**: Unconditionally rejects MRs from forked projects
- **User Verification**: Matches against the authenticated webhook event user,
  not forgeable Git commit metadata
- **Stateless & Idempotent**: No queues or stored state; re-approving is a
  no-op

## Architecture

The Lambda subscribes to the existing SNS topic (from gitlab-event-forwarder)
with a filter for `merge_request` and `pipeline` events:

```text
GitLab Webhook → gitlab-event-forwarder → SNS Topic → mr-auto-approver → GitLab API
```

**Merge request events** (`open`, `update`): evaluate rules using the
authenticated pusher from the webhook payload. If matched and no Konflux check
needed, approve immediately. If Konflux check needed, check statuses now and
approve only if present and not failed.

**Pipeline events** (`success`, `failed`): look up the MR from the pipeline
event via the GitLab API, evaluate rules, and approve if Konflux statuses are
ready. This handles the case where Konflux posts commit statuses after the
initial MR event.

## Prerequisites

- AWS CLI configured with appropriate credentials (IAM permissions for Lambda,
  SNS, CloudFormation, CloudWatch Logs)
- Podman or Docker (for containerized SAM build/deploy)
- Python 3.11 or later (for development)
- GitLab API token(s) with `api` scope and permission to approve MRs on target
  projects
- Target project webhooks must send `merge_requests_events` and
  `pipeline_events` to the gitlab-event-forwarder endpoint

## Deployment

Build and deploy using containerized AWS SAM CLI:

```bash
cd mr-auto-approver
make build     # Build Lambda package
make deploy    # First deployment (interactive/guided)
make redeploy  # Subsequent deployments (non-interactive)
```

### Parameters

| Parameter        | Description                                | Default               |
|------------------|--------------------------------------------|-----------------------|
| `ResourcePrefix` | Prefix for resources                       | `myapp-prod`          |
| `SnsTopicArn`    | SNS topic ARN                              | (required)            |
| `GitLabUrl`      | GitLab instance URL                        | `https://gitlab.com`  |
| `ConfigPath`     | Path to YAML config (bundled with Lambda)  | `config.yaml`         |
| `ApprovalTokens` | JSON map of env var names to tokens        | `{}`                  |
| `SentryDsn`      | Optional Sentry DSN                        | ``                    |

**Resource naming:** Lambda follows `{ResourcePrefix}-lambda-approver` pattern.

**Prerequisites:** Requires an existing SNS topic. Deploy
[hummingbird-events-topic][het-docs] first to create the topic, then use its
ARN for the `SnsTopicArn` parameter.

[het-docs]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/documentation/hummingbird-events-topic.md

## Configuration

The Lambda uses a YAML config file bundled at deploy time. The config defines
per-project rules:

```yaml
settings:
  gitlab_url: https://gitlab.com

projects:
  org/group/repo:
    token_env: APPROVAL_GITLAB_TOKEN_REPO
    rules:
      - branch_regexes: ["renovate/skip/.*"]
        action: deny
      - user_ids: [12345678]
        branch_regexes: ["renovate/.*"]
      - user_ids: [87654321]
        check_konflux: true
```

### Rule fields

| Field            | Type      | Default   | Description                              |
|------------------|-----------|-----------|------------------------------------------|
| `user_ids`       | list[int] | `[]`      | GitLab user IDs (immutable, preferred)   |
| `user_regexes`   | list[str] | `[]`      | Fullmatch regexes for username           |
| `branch_regexes` | list[str] | `[]`      | Fullmatch regexes for source branch      |
| `check_konflux`  | bool      | `false`   | Require Konflux statuses before approval |
| `action`         | str       | `approve` | `approve` or `deny`                      |

**Match logic**: AND across field types, OR within a field. Empty fields match
anything. First matching rule wins. `user_ids` and `user_regexes` are both user
identity constraints -- if either is set, the user must match at least one entry
from across both lists. Prefer `user_ids` over `user_regexes` to prevent
username confusion attacks.

### Adding a new project

1. Create a project-scoped GitLab token (Developer, `api` scope) in Vault
2. Add the token to `vars.sh` and the `jq` command that builds `APPROVAL_TOKENS`
3. Add a project entry to `config.yaml` in the infrastructure repo with
   `token_env` and rules
4. Deploy the Lambda
5. Ensure the project's GitLab webhook sends events to the
   gitlab-event-forwarder endpoint

### Environment variables

| Variable           | Description                                        |
|--------------------|----------------------------------------------------|
| `CONFIG_PATH`      | Path to YAML config file                           |
| `GITLAB_URL`       | GitLab instance URL                                |
| `APPROVAL_TOKENS`  | JSON object mapping env var names to GitLab tokens |
| `SENTRY_DSN`       | Optional Sentry DSN                                |

## Security

- Fork MRs are unconditionally rejected before any rule evaluation
  (`source_project_id != target_project_id`)
- User matching supports immutable `user.id` (preferred) in addition to
  `user.username` from the webhook payload, preventing username confusion
  attacks. Never uses Git commit author/committer metadata
- For pipeline events, the Lambda looks up `head_pipeline.user` (both `id` and
  `username`) via the GitLab API to verify the last pusher
- Approval tokens stored as CloudFormation parameters with `NoEcho: true`
- SNS subscription filter policy limits events to `merge_request` and `pipeline`
- CloudWatch logs capture all approval decisions (7-day retention)

## Development

See the main [README][readme] for development workflows.

```bash
make setup     # Install dependencies
make check     # Lint code (ruff)
make fmt       # Format code
make test      # Run unit tests
make coverage  # Run tests with coverage
```

## License

This project is licensed under the GNU General Public License v3.0 or later -
see the [LICENSE][license] file for details.

[readme]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/README.md
[license]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/LICENSE
