Providers
GitHub
Uses the Ahena CLI (beta). Install it with npm install -g @ahena/cli, or prefix commands with npx @ahena/cli. See Installation. The dashboard covers projects, connections, Doctor, the Stack Graph, plans and approvals without it.
Overview
Ahena checks the GitHub repository your app is built and deployed from: its visibility, default
branch protection, deployment environments, GitHub Actions variables and secret names, webhooks
and Pages. It creates environments, sets Actions variables, adds required status checks and
reviews through its own ruleset, and creates a webhook, each after you approve the plan. Secret
values never go through Ahena: Doctor confirms each declared secret name exists and gives you
the gh secret set command for any that don't.
GitHub isn't called by your app at runtime, so there's nothing to generate: Ahena has no
ahena generate github. Ahena only talks to GitHub's management API (api.github.com), never
to your repository's contents.
Package: @ahena/provider-github · Category: repository · Pinned API version: 2026-03-10
(sent as X-GitHub-Api-Version)
Capabilities: repository-inspection, environments, actions-variables, actions-secrets,
branch-protection, webhooks, pages
Maturity: verified against a fake of the API; not yet verified against the real service.
Connection
| Field | Secret | Notes |
|---|---|---|
GITHUB_TOKEN |
yes | A fine-grained personal access token (github_pat_…) limited to this one repository (recommended). A classic token (ghp_…) also works, but it reaches every repository you can; Doctor warns about it in production. |
GITHUB_OWNER |
no | The user or organization, e.g. leo-labs. Offered from the repositories the token can see. |
GITHUB_REPO |
no | The repository name, e.g. leo. Offered from the repositories the token can see. |
GITHUB_TOKEN=github_pat_… ahena connect github -e production --set GITHUB_OWNER=leo-labs --set GITHUB_REPO=leo
GitHub App installation tokens aren't supported: they expire after an hour, and refreshing them would mean giving Ahena the app's private key.
Permissions
Create the fine-grained token with Repository access: Only select repositories (just this one) and these repository permissions (Read and write includes Read):
| Permission | Access | Needed for |
|---|---|---|
| Metadata | Read-only | Always included: the repository, rulesets on the default branch |
| Actions | Read-only | Listing environments |
| Administration | Read and write | Creating environments and the Ahena ruleset; reading classic branch protection and whether Actions is enabled (Read-only is enough if you don't let Ahena change environments or protection) |
| Environments | Read and write | Environment variables and environment secret names (Read-only if you don't declare environment variables) |
| Variables | Read and write | Repository variables (Read-only if you don't declare any) |
| Secrets | Read-only | Repository secret names (GitHub never returns values) |
| Webhooks | Read and write | Creating the declared webhook (Read-only if you don't declare one) |
| Pages | Read-only | Reporting Pages status |
Ahena never needs Contents, Pull requests, Issues or Workflows permissions, and never reads or writes code.
How Ahena checks permissions (permissionCheck: "checked"). Fine-grained tokens don't list
their permissions anywhere. When you connect, Ahena asks with reads that can't change anything
(a GET per area: environments, Actions settings, variables, secrets, webhooks, Pages, and
environment variables inside an existing environment). GitHub answers 2xx when the token has
the read permission and 403 ("Resource not accessible by personal access token") when it
doesn't, so each read is reported as granted or missing. Write permissions can't be probed
without writing, so they're reported as …:write:unknown (or missing when even the read is
refused); a missing write shows up as a 403 naming the permission (from GitHub's
X-Accepted-GitHub-Permissions header) when you apply. For a classic token Ahena reads the
OAuth scopes GitHub reports (X-OAuth-Scopes): repo grants everything above.
Capabilities
| Capability | Access | Verification | Changes | What Ahena does |
|---|---|---|---|---|
repository-inspection |
read-only | Visibility, default branch, archived, Actions enabled. | ||
environments |
writable | read-back | CONFIRMATION_REQUIRED | Creates the declared GitHub environment; reports its protection rules and deployment branch policy. |
actions-variables |
writable | read-back | CONFIRMATION_REQUIRED | Sets repository and environment variables to the declared values. |
actions-secrets |
manual | existence only | Checks declared secret names exist (with when they were updated); gives the gh secret set command for missing ones. |
|
branch-protection |
writable | read-back | CONFIRMATION_REQUIRED | Adds required status checks and approving reviews on the default branch through the ruleset Ahena: default branch. |
webhooks |
writable | read-back | CONFIRMATION_REQUIRED | Creates the declared webhook (JSON, TLS verified) with a new signing secret. |
pages |
read-only | Reports Pages status and whether HTTPS is enforced. |
Refused outright: changing repository visibility; deleting, archiving, renaming or transferring the repository; deleting environments, variables, rulesets or webhooks; changing environment protection rules; removing or lowering required checks or reviews, or disabling a ruleset; editing classic branch protection or rulesets Ahena didn't create; changing existing webhooks or creating one that skips TLS verification; Actions variables whose names look like secrets; reading secret values (GitHub never returns them).
Network: only api.github.com (enforced). The webhook route is probed through Ahena's
credential-free probe. ahena lock records visibility, default branch, environments, variable
values, secret names (never values), default-branch protection and webhooks for drift.
Configure
repository: {
provider: "github",
owner: "leo-labs",
repo: "leo",
variables: { NODE_VERSION: "22" }, // repository-level Actions variables
secrets: ["TURBO_TOKEN"], // repository-level secret NAMES that must exist
environments: { // keyed by Ahena environment
production: {
name: "production", // the GitHub environment (defaults to the key)
variables: { API_URL: "https://api.leo.app" },
secrets: ["DEPLOY_TOKEN"],
},
staging: { variables: { API_URL: "https://api.staging.leo.app" } },
},
protection: { requiredChecks: ["ci"], requiredReviews: 1 }, // default branch
webhook: { production: { url: "https://leo.app/api/webhooks/github", events: ["push", "pull_request"] } },
},
Each Ahena environment's GitHub connection manages the GitHub environment declared for it, plus
the repository-level settings (variables, protection, webhook). Repository-level settings
are the same in every environment, so applying them from a second environment changes nothing.
webhook may be a single value or keyed by environment. Declaring environments.production
makes Doctor require default-branch protection.
| Change | Classification | Notes |
|---|---|---|
| Create a GitHub environment | CONFIRMATION_REQUIRED | Created with no protection rules; add reviewers or deployment branches in GitHub (Doctor reminds you for production). Never sent for an environment that exists. |
| Set an Actions variable (repository or environment, production included) | CONFIRMATION_REQUIRED | The diff shows old and new values. Variables not in ahena.config.ts are left alone. |
Create the ruleset Ahena: default branch |
CONFIRMATION_REQUIRED | Targets the default branch; adds required_status_checks and pull_request rules. Created only when existing protection (any ruleset, any level, or classic protection) doesn't already require them. |
| Extend that ruleset | CONFIRMATION_REQUIRED | Adds missing checks and raises the review count; keeps every existing rule and setting, and sets it back to active if it was disabled (the diff says so). Refused at apply if the ruleset changed after the plan. |
| Create the webhook | CONFIRMATION_REQUIRED | Idempotent by URL: planned only when no webhook has that URL, and re-checked right before creating. A random signing secret is stored encrypted as GITHUB_WEBHOOK_SECRET. |
| Set an Actions secret | MANUAL | gh secret set <NAME> --repo <owner>/<repo> [--env <environment>] (prompts for the value). |
Ahena never proposes removals: asking for fewer checks or reviews than GitHub already requires proposes nothing. Archived repositories are refused (GitHub rejects changes to them).
Why secrets are manual: GitHub only accepts secrets encrypted with libsodium sealed boxes (X25519 + XSalsa20-Poly1305), which WebCrypto doesn't provide, and plans never carry secret values. So Ahena doesn't send secret values to GitHub; it checks names and you set values with the GitHub CLI.
Limitations
- Secret values can't be read back: GitHub never returns them, so Ahena verifies existence (and
the last-updated date in
ahena inspect), never content. - Ahena doesn't set secret values (see above) or Dependabot/Codespaces secrets.
- Environment protection rules (required reviewers, wait timers, deployment branch policies) are reported, not changed.
- Rulesets and branch protection on private repositories need GitHub Pro, Team or Enterprise; on GitHub Free, GitHub refuses the ruleset (Ahena shows GitHub's message).
- Classic branch protection is read (with Administration read) but never written.
- Organization-level variables, secrets and rulesets are counted where GitHub reports them on the repository (rulesets on the default branch) but never managed.
- Webhook events of an existing webhook aren't changed; Doctor lists missing events.
- GitHub App installation tokens aren't supported.
- Nothing is deleted: removing something from
ahena.config.tsleaves it in GitHub.
Manual steps
- Set secret values:
gh secret set <NAME> --repo <owner>/<repo>(add--env <environment>for environment secrets). Doctor prints the exact commands. - Protect the production environment in GitHub (Settings › Environments › production: Deployment branches "Protected branches only" and/or Required reviewers).
- Organization repositories: an owner may need to approve the fine-grained token (Organization settings › Personal access tokens › Pending requests).
- Regenerate the token before it expires (Doctor warns 14 days ahead) and reconnect.
Doctor checks
| Id | Severity | Meaning |
|---|---|---|
github.token |
PASS / FAIL | The token authenticates (401 → reconnect). |
github.token.expiration |
PASS / INFO / WARNING / FAIL | From GitHub-Authentication-Token-Expiration: WARNING within 14 days, FAIL within 3; INFO when GitHub reports no expiry. |
github.token.kind |
INFO / WARNING | A classic token (WARNING in production): replace with a fine-grained one. |
github.repository |
PASS / FAIL | The repository is reachable (GitHub answers 404 for repositories the token can't see). |
github.repository.match |
FAIL | The connection is for a different repository than ahena.config.ts names. |
github.repository.archived |
WARNING | Archived: nothing can be applied. |
github.actions |
PASS / WARNING | GitHub Actions is enabled for the repository. |
github.protection |
PASS / FAIL | Production declared: the default branch has some protection (rulesets at any level, or classic). |
github.protection.checks |
PASS / FAIL | Declared required checks and review count are in force. |
github.environment |
PASS / FAIL | The declared GitHub environment exists (with its protection rules as evidence). |
github.environment.protection |
WARNING | The production environment has no protection rules or branch policy. |
github.variables / github.environment.variables |
PASS / WARNING / FAIL | Declared variables exist (FAIL) and match (WARNING). |
github.secrets / github.environment.secrets |
PASS / FAIL | Declared secret names exist; MANUAL gh secret set steps otherwise. |
github.secrets.values |
INFO | Not verified: secret values (GitHub never returns them). |
github.webhook |
PASS / WARNING / FAIL | Webhook to the declared URL exists, is active, and has the declared events. |
github.webhook.endpoint |
PASS / WARNING / FAIL | Unsigned-request probe to your route. |
github.secret.github_webhook_secret |
PASS / FAIL | The webhook signing secret is stored in this environment (checked by name). |
github.pages |
INFO / WARNING | Pages status; WARNING when a custom domain doesn't enforce HTTPS. |
github.config |
FAIL | repository in ahena.config.ts isn't valid. |
A check the token can't read is reported as WARNING "Not verified: … (the token can't read it)" with the permission to add, never as passing.
Set up step by step
Create the token. On GitHub: your avatar › Settings › Developer settings › Personal access tokens › Fine-grained tokens › Generate new token (https://github.com/settings/personal-access-tokens/new). Choose the resource owner (the organization that owns the repository), an expiration, and Only select repositories → your repository.
Grant the minimum permissions from the table above: Metadata (read), Actions (read), Administration, Environments, Variables, Webhooks (read and write, or read-only for what you don't let Ahena change), Secrets (read), Pages (read). Generate the token and copy it.
Connect:
GITHUB_TOKEN=github_pat_… ahena connect github -e production --set GITHUB_OWNER=… --set GITHUB_REPO=…Verify the connection:
ahena inspect github -e productionshows the repository, environments, variables, secret names, protection and webhooks. The connection's granted scopes list each permission as granted or:unknown.Run Doctor:
ahena doctor -e production.Plan:
ahena plan -e production(all providers), orahena configure github -e productionfor GitHub alone. Review each action; every change is CONFIRMATION_REQUIRED.Apply:
ahena apply -e production. Changes that matter are approved in the dashboard. If a webhook was created, its signing secret is stored asGITHUB_WEBHOOK_SECRET(ahena env reveal production GITHUB_WEBHOOK_SECRETto see it).Verify: Ahena re-plans after applying; an empty plan means GitHub shows the desired state. Then set any missing secrets with the
gh secret setcommands Doctor prints, and runahena doctor -e productionagain.ahena lockrecords the state forahena drift.Troubleshooting:
Symptom Cause Fix GitHub rejected the tokenExpired, revoked or mistyped Regenerate it and reconnect. The token can't see owner/repo(404)Repository not selected for the token, wrong name, or the organization hasn't approved the token Edit the token's repository access; ask an org owner to approve it. 403 … needs variables=writeThe token lacks that permission Edit the token and grant it (the permission named in the message). Upgrade to GitHub Pro or make this repository publicRulesets on a private repository on GitHub Free Upgrade the plan, or protect the branch another way. the ruleset … changed after this plan was madeSomeone edited Ahena: default branchRun ahena planagain.rate limiting this tokenGitHub's rate limit Wait until the time shown and retry. github.secretsFAIL after setting a secretSet at the wrong level Repository secrets: no --env; environment secrets:--env <name>.Disconnect and revoke:
ahena disconnect github -e productionremoves the stored token. Then delete the token at https://github.com/settings/personal-access-tokens (classic tokens: https://github.com/settings/tokens).
Disconnect behavior
Removes the connection and the token Ahena stored; Ahena can't revoke a personal access token itself. Delete it at https://github.com/settings/personal-access-tokens. Environments, variables, secrets, the Ahena ruleset and webhooks stay in GitHub.