Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions full-platform/slack-approval-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Slack Approval Gateway

## What problem this solves

Two human-approval gates exist in the delivery pipeline:

1. **The GitOps PR gate** -- every change to any cluster is a commit to `microservice-app-gitops`'s `main`, which requires one approving review (branch protection, `required_approving_review_count: 1`). This includes the automated `promote/*` PRs `promote.yml` opens.
2. **The prod-environment gate** -- each service repo's `ci.yml` has a `gate-prod` job targeting a GitHub Environment named `prod`, configured with `required_reviewers` (Juanmadiaz45, EstebanGZam, Tiago0507). It is live: org variables `ENABLE_LEGACY_DEV_PROMOTION`/`ENABLE_LEGACY_SEMANTIC_RELEASE` are both `true`.

Gate 2 was silent -- a run just sits paused with no notification anywhere until someone happens to check GitHub. `microservice-app-slack-approval-gateway` (two AWS Lambda functions behind an API Gateway HTTP API) fixes both:

- Gate 1 gets a Slack message with **Approve** / **Request changes** buttons -- clicking one calls GitHub's API for real (submits the review, merges on approve).
- Gate 2 gets a Slack **notification** (technical + non-technical summary, a direct link to GitHub's "Review deployments" screen) -- no button. GitHub's classic `required_reviewers` cannot be satisfied by a GitHub App token on behalf of a listed human (only Users/Teams can be listed reviewers; confirmed against GitHub's REST API docs), and the team chose to keep that approval exactly as it is today rather than replace it with a custom deployment-protection-rule app. Slack tells you it is waiting; you still approve it in GitHub.

Both messages carry a **technical** summary (the PR's own already-reviewed body sections, or deterministic job/run metadata -- verbatim, never re-derived) and a **non-technical** rephrasing generated by the Anthropic API, explicitly prompted to use only the facts given and never add anything unstated -- so a non-engineering stakeholder (a manager, a client) can understand what is about to happen without needing the technical detail.

## Why a third GitHub App, not one of the existing two

`microtodosuite-release` and `microtodosuite-gitops-promoter` (see `github-app-authentication.md`) exist to *act* within a specific, narrow scope (mint a release, open a promotion PR). This one exists to *read* GitHub events and *approve on a human's behalf when they click a Slack button*. Folding that into an existing App would widen its blast radius for no reason -- the same single-purpose-App principle applies here as there.

| App | Backs | Installed on | Permission | Webhook subscription |
| --- | --- | --- | --- | --- |
| `microtodosuite-slack-approver` | `github-webhook-handler` (reads) | `microservice-app-gitops` | Pull requests: write, Contents: write | `pull_request` |
| `microtodosuite-slack-approver` | `github-webhook-handler` (reads) | The five service repos | Actions: read | `workflow_job` |
| `microtodosuite-slack-approver` | `slack-interaction-handler` (acts) | `microservice-app-gitops` | (same installation/token as above) | -- |

One App, "select repositories" installation covering all six repos above (the five services for read-only notification, `microservice-app-gitops` for both notification and write access). Its webhook deliveries all point at the API Gateway's `github-webhook-handler` route; its installation token (minted only when a Slack button is actually clicked) is used by `slack-interaction-handler` to approve/merge.

Org secrets (selected-repository visibility is not used here -- see below, these are not GitHub Actions secrets at all): the App id/key live in **AWS Secrets Manager**, not as repository or organization Actions secrets, because nothing in this flow runs as a GitHub Action -- the Lambdas fetch them directly at invocation time.

## Where the credentials actually live

Unlike the release/promoter Apps (consumed by GitHub Actions, so their credentials are GitHub Actions secrets), this App's credentials are consumed by a Lambda function outside GitHub entirely. They live in AWS Secrets Manager, provisioned by the Terraform module in `microservice-app-ops` (`aws/shared/slack-approval-gateway/`), and are fetched by the Lambda at runtime via the AWS SDK -- never logged, never written to disk, cached only in the warm execution environment's memory. The same value-blind discipline `scripts/managed/bootstrap-sonarqube.sh` (spec 009 T103) applies here: a credential is read once, used once, never echoed.

| Secret | Used by |
| --- | --- |
| `SLACK_APPROVER_APP_KEY` | Both Lambdas -- mint an installation token |
| `SLACK_SIGNING_SECRET` | `slack-interaction-handler` -- verifies a click really came from Slack |
| `SLACK_BOT_TOKEN` | Both Lambdas -- post/update Slack messages |
| `GITHUB_WEBHOOK_SECRET` | `github-webhook-handler` -- verifies a webhook really came from GitHub |
| `ANTHROPIC_API_KEY` | Both Lambdas -- the non-technical rephrasing |

The App id itself (`SLACK_APPROVER_APP_ID`) is not sensitive -- a GitHub App id is public information -- so it is passed straight through as a plain Lambda environment variable rather than a Secrets Manager entry. `microservice-app-ops`'s own delivery contract (`tests/contract/aws-dev-foundation.sh`) forbids an ordinary, state-persisted Secrets Manager value anywhere in `aws/`, precisely so no one has to judge case-by-case whether a given value is "sensitive enough" to earn the write-only treatment.

## Private-key handling: mode 0600, then gone

Identical discipline to `github-app-authentication.md`: download the App's private key once, `chmod 600` it immediately, upload it straight into AWS Secrets Manager from that file (`aws secretsmanager put-secret-value --secret-id ... --secret-string file://key.pem`, or the Terraform-managed equivalent), then delete the local file the moment the secret is confirmed set.

## Rotation

Same zero-downtime pattern as the other two Apps: generate a new private key in the App's settings (the old one keeps working), upload it as the new secret value, confirm one real webhook delivery authenticates with it, then delete the old key from the App's settings.

## The authorization boundary

`config/authorized-approvers.json` in `microservice-app-slack-approval-gateway` maps a Slack user ID to a GitHub login. It intentionally lists exactly the three logins already configured as `required_reviewers` on every service repo's `prod` Environment -- this system does not grant anyone new authority, it only lets an already-authorized reviewer act through Slack instead of the GitHub UI for gate 1. Any Slack click from a user not on that list is refused, logged, and the click's own `response_url` is used to tell that person so -- it is never silently ignored, and it never reaches GitHub.

## What this does not do

- Does not replace or touch `required_reviewers` on any `prod` Environment -- gate 2 stays exactly as it is; this system only adds the missing notification.
- Does not let anyone approve gate 2 from Slack -- that would require each listed reviewer to complete a one-time GitHub OAuth user-token grant so the App could act with their own identity rather than an installation token (a real option for a future iteration, not built here).
- Does not run as a GitHub Action or inside the EKS cluster -- it is two standalone Lambda functions, chosen specifically because the economical cluster has never exposed anything to the internet (no Ingress exists anywhere in `microservice-app-gitops`; spec 004 User Story 4 deliberately limits access to `kubectl port-forward`), and building a first Ingress+ALB+ACM+DNS just for this would be a larger project than the gateway itself.
Loading