GitHub Integration
Hephaestus uses three GitHub mechanisms:
| Mechanism | Purpose | Required? |
|---|---|---|
| OAuth App | "Continue with GitHub" | Yes, unless you use only GitLab login |
| PAT or GitHub App | Repository data access and feedback delivery per workspace | One of the two |
| Webhook | Live events: PRs, reviews, pushes, and others | Strongly recommended. Without it, updates wait for scheduled sync, hourly by default in production. |
Configure login with the OAuth App setup.
Choosing PAT vs GitHub App
PAT mode fully supports synchronization and is the simplest way to evaluate a single-org installation. Before you enable feedback delivery, use a GitHub App so comments have a distinct app identity.
| PAT | GitHub App | |
|---|---|---|
| Setup effort | Paste a token in the workspace UI | Create an App. Install it. Manage a private key. |
| Environment variables | None | GH_APP_ID, GH_APP_PRIVATE_KEY, GH_APP_INSTALLATION_URL, GH_APP_CLIENT_ID, GH_APP_CLIENT_SECRET |
| API rate-limit budget | Shared with the user's other API use | Per installation. See GitHub's limits. |
| Practice feedback posted back to GitHub (PR comments, inline notes, approvals) | Uses the token's user identity. Do not use for active delivery. | Uses the App identity |
| Webhooks | Create manually, as described below | Configure once on the App |
The database stores tokens encrypted with AES-256-GCM per workspace.
PAT mode
A classic PAT needs repo, read:org, and read:project scopes.
repo includes pull request and issue write access and the check state.
A fine-grained PAT needs the organization as its resource owner and the permissions below.
PAT connections need no GitHub App credentials.
Authentication uses each workspace connection's mode.
- Create a token with the scopes or permissions for its type.
- If other workspaces use existing App configuration, keep it.
- When you create or configure the workspace in the UI, paste the token.
- Create a manual webhook.
Fine-grained token permissions
These are the core permissions without Checks, which GitHub does not offer to fine-grained tokens.
| Permission (manifest key) | Scope | Level | Without it |
|---|---|---|---|
Contents (contents) | Repository | Read | Commit sync stays empty. Practice reviews cannot check out the repository. |
Pull requests (pull_requests) | Repository | Read & write | Pull request and review sync stays empty. Feedback delivery on pull requests fails. |
Issues (issues) | Repository | Read & write | Issue sync stays empty. Feedback delivery as comments fails. |
Discussions (discussions) | Repository | Read | Discussion sync stays empty |
Commit statuses (statuses) | Repository | Read | Commit statuses do not count toward a pull request's check state |
Members (members) | Organization | Read | Organization membership and team sync stay empty |
Projects (organization_projects) | Organization | Read | Projects v2 sync stays empty |
Issue types (issue_types) | Organization | Read | Issue-type sync stays empty |
Metadata (metadata) | Repository | Read | Always included |
GitHub accepts a fine-grained token without any of these permissions.
Without Contents, Pull requests, Issues, Discussions, Members, Projects, or Issue types, the corresponding sync stays empty, as the table describes.
GitHub does not offer Checks to fine-grained tokens (community discussion).
A token without it can be refused a private repository's check state.
There, only check_suite and status webhook deliveries record the check state.
Classic tokens and the GitHub App also read it with each sync.
GitHub App mode
- Create the App at Settings → Developer settings → GitHub Apps → New GitHub App.
For an organization, use
https://github.com/organizations/<org>/settings/apps. - Use
external-app-icon-1024.pngas its display image. - Set Homepage URL to a page that describes this deployment. See brand identity and instance routing.
- Select Webhook → Active.
- Set Webhook URL to
https://<APP_HOSTNAME>/webhooks/github. - Set Secret to your
.envvalue ofWEBHOOK_SECRET, byte-identical. - Set Where can this GitHub App be installed to Any account. A public personally owned app can be installed on organizations. Only on this account restricts it to its owner. Installation and repository selection remain required.
- Select Identifying and authorizing users → Request user authorization (OAuth) during installation.
- Set Callback URL to
https://<APP_HOSTNAME>/api/oauth/callback/github. GitHub then returns to Hephaestus after installation and hides Setup URL.
Connecting an installation to a workspace
The App requests user authorization during installation.
When a workspace admin connects GitHub, Hephaestus exchanges GitHub's authorization for a short-lived token of the person who installed the App.
It connects the installation only if that person can access it and owns its account.
This means an organization owner or the user themselves.
Hephaestus uses the token once and never stores it.
GitHub documents that the return URL's installation_id can be forged.
Hephaestus never trusts that id alone. These conditions also apply:
- One workspace per installation. A connected installation cannot connect to another workspace until the first workspace disconnects it. This applies even while the connection is suspended. A refusal names the other workspace only to its workspace admins.
- Disconnect uninstalls only what the workspace holds. A disconnect removes the installation from GitHub only when the connection recorded it.
- Installed from GitHub or the workspace wizard. The return has no workspace to connect. Hephaestus sends the person to its home page. The installation webhook creates or updates the workspace as before.
- Installation requests. A member without permission to install the App sends a request to the account's owners instead. After an owner approves it, connect again.
Workspace attachment
An existing GitHub installation stays bound to its workspace after an account rename. A new or reinstalled installation can reuse a matching-name workspace only if that workspace already recorded the same immutable GitHub organization identity. A PAT connection with a stored token remains in PAT mode.
Personal-account workspaces cannot establish that match from an account name alone. The same applies to older organization workspaces that have not synchronized their organization identity. The installation gets a separate workspace with an available slug. The existing workspace and its access remain unchanged.
Before a migration from PAT mode:
- Synchronize the organization's identity.
- When ready to switch, remove the PAT credential.
A name match never transfers ownership or private conversations. If an old synchronized organization still holds a reused GitHub name, refresh its identity before you retry installation. Hephaestus rejects conflicting identities rather than merging them.
Permission model
Core permissions support synchronization and feedback delivery. Provisioned reads precede named features to combine approval rounds. They do not enable those features. Held permissions remain ungranted until their release conditions are met.
Pre-provisioning trades broader access for fewer approval rounds. This differs from GitHub's default advice to request only necessary permissions and events. It applies only to permissions. A subscription follows its consumer, as the next section explains.
- Limit the repositories on which you install the App.
- Omit any provisioned read you prefer to approve when its feature arrives.
- Never pre-provision a write.
Every permission or event change must update this page in the same pull request. The tables, their manifest, and the consent-screen description must agree.
- Apply the configuration to each environment.
- Verify installation approvals.
The manifest template contains the same set in machine-readable form. It and the tables stay identical. The tables explain each grant.
Core contract — required
“Write” in GitHub's manifest includes read access. All unlisted permissions remain No access.
| Permission (manifest key) | Scope | Level | Why |
|---|---|---|---|
Metadata (metadata) | Repository | Read | Mandatory repository identity and metadata |
Contents (contents) | Repository | Read | Push and commit sync. Repository checkout for practice reviews. No code writes. |
Pull requests (pull_requests) | Repository | Read & write | PR and review sync. Delivery of review feedback, inline notes, and approvals. |
Issues (issues) | Repository | Read & write | Issue sync. Delivery of feedback as issue or PR comments. |
Discussions (discussions) | Repository | Read | Discussion sync, not discussion delivery |
Members (members) | Organization | Read | Organization membership, teams, and member events |
Projects (organization_projects) | Organization | Read | Projects v2 sync and project event access, not classic repository projects. |
Issue types (issue_types) | Organization | Read | Native issue-type sync, and the issue-type change that opens a practice review |
Checks (checks) | Repository | Read | Check state and check runs of a pull request's head. Read with the pull request and when a suite completes. |
Commit statuses (statuses) | Repository | Read | The same state and runs from commit statuses, including non-Actions CI. status events keep them current. |
Provisioned — read access ahead of features
| Permission (manifest key) | Scope | Level | Why / roadmap |
|---|---|---|---|
Actions (actions) | Repository | Read | Workflow, run, and job signals. No workflow dispatch or cancellation: #1403, #1338 |
Deployments (deployments) | Repository | Read | Delivery cadence, outcomes, and approval history: #1676 |
Merge queues (merge_queues) | Repository | Read | Queue usage and ejection signals, never queue control: #1677 |
Dependabot alerts (vulnerability_alerts) | Repository | Read | Vulnerability-response times and dismissal practices: #1675 |
Secret scanning alerts (secret_scanning_alerts) | Repository | Read | Leaked-credential response timing: #1675 |
Code scanning alerts (security_events) | Repository | Read | Static-analysis triage practices: #1675 |
A provisioned permission grants access, not a subscription. Its events stay unsubscribed until a feature reads them. Security-alert event payloads are sensitive even before a consumer exists. If you subscribe regardless:
- Restrict access to delivery logs and queue storage.
- Never copy a raw payload or secret value into application logs, support tickets, or practice context.
Held — do not grant
The manifest grants none of these at the level shown or above.
| Permission (manifest key) | Scope | Level | Release trigger / why it is held |
|---|---|---|---|
Checks (checks) | Repository | Read & write | Only with non-blocking annotation delivery (#1678). Requires an installation approval round. Read is core. |
Administration (administration) | Repository | Read | Held until GitHub offers a narrower branch-protection grant. Broad administration access is not justified for that signal. |
Issue fields (issue_fields) | Organization | Read | Custom issue fields have no reader. Issue-type sync uses Issue types above. Revoke it where an existing registration grants it. |
Copilot metrics (organization_copilot_metrics) | Organization | Read | Privacy assessment first (#1679). Provision only if approved. |
Event subscriptions
Subscribe only to the events below. This is the complete set Hephaestus consumes. GitHub's event reference lists each webhook type's permissions and availability.
A subscription without a consumer still has a cost.
The webhook role stores every delivery in its queue before a consumer check.
The bounded queue removes the oldest delivery first.
A high-volume event family can remove pull-request and review deliveries that the queue needs to hold.
For example, workflow_job alone can outnumber pull_request a hundredfold.
Those deliveries then wait for hourly production reconciliation to fetch them again. See webhook ingestion operations. Provision the permission now, because permission increases need an approval round. Add the subscription in the release with the feature that reads it.
| Event | Rationale |
|---|---|
push | Refresh commits and repository activity. |
pull_request | Synchronize PR lifecycle and changes. |
pull_request_review | Synchronize submitted and updated reviews. |
pull_request_review_comment | Synchronize inline review comments. |
pull_request_review_thread | Track review-thread resolution, and when it happened. |
check_suite | Keep the head's check state and runs current. Start reviews when checks finish. |
status | The same for commit statuses. |
issues | Synchronize issue lifecycle and content. |
issue_comment | Synchronize issue and PR conversation comments. |
label | Keep label metadata current. |
milestone | Keep milestone metadata current. |
member | Track repository collaborator changes. |
repository | Track repository metadata and lifecycle. |
discussion | Synchronize discussions. |
discussion_comment | Synchronize discussion comments. |
sub_issues | Update parent/child issue relationships. |
issue_dependencies | Update blocking/blocked-by relationships. |
organization | Track organization changes. |
team | Synchronize teams. |
membership | Synchronize team membership. |
projects_v2 | Synchronize organization project metadata where delivered. |
projects_v2_item | Synchronize project items and field values where delivered. |
projects_v2_status_update | Synchronize project status updates where delivered. |
GitHub delivers installation and installation_repositories automatically.
Do not add them to the manifest's default_events.
The server also handles account renames through installation_target when delivered.
Projects v2: GitHub's event reference lists organization-webhook availability for the three project events.
Discussions and Projects sync are off by default (HEPHAESTUS_SYNC_DISCUSSIONS_ENABLED=false and HEPHAESTUS_SYNC_PROJECTS_ENABLED=false).
- Change a project item.
- Verify that the App delivers the
projects_v2_itemevent. - If App delivery is unavailable, add an organization webhook subscribed only to those three events.
Projects read remains required for API synchronization.
Deliberately not subscribed
Hephaestus subscribes to no GitHub event absent from the table above. These include three groups:
| Not subscribed | Why |
|---|---|
The provisioned families — check_run, workflow_run, workflow_job, deployment, deployment_status, deployment_review, merge_group, merge_queue_entry, release, commit_comment, create, delete, dependabot_alert, secret_scanning_alert, code_scanning_alert | Their permissions are provisioned. Their consumers do not yet exist. Subscribe with the feature, not before it. check_run is not needed: the suite event reports its runs' conclusion. |
deployment_protection_rule, exemption_request_push_ruleset | Each requests an approval decision from Hephaestus. Hephaestus observes work and never gates it. exemption_request_push_ruleset also needs Administration read, which is held, so it stays unsubscribed. |
deploy_key, team_add | Access management, not practice context. Team and membership sync use the events above. |
GitHub App manifest template
This reference uses GitHub's native manifest format. Hephaestus does not implement GitHub's manifest registration flow.
- Replace the app name for each environment.
- Replace
<APP_HOSTNAME>for each environment. - Unless you have tools for that registration flow, use the manual setup above.
{
"name": "your-hephaestus",
"url": "https://<APP_HOSTNAME>",
"description": "Reads repository and organization data, including CI, deployment, merge-queue and security-alert signals. Can write pull-request reviews and issue/PR comments for practice feedback. Never modifies your code.",
"public": true,
"hook_attributes": {
"url": "https://<APP_HOSTNAME>/webhooks/github",
"active": true
},
"callback_urls": ["https://<APP_HOSTNAME>/api/oauth/callback/github"],
"request_oauth_on_install": true,
"default_permissions": {
"metadata": "read",
"contents": "read",
"pull_requests": "write",
"issues": "write",
"discussions": "read",
"members": "read",
"organization_projects": "read",
"issue_types": "read",
"checks": "read",
"statuses": "read",
"actions": "read",
"deployments": "read",
"merge_queues": "read",
"vulnerability_alerts": "read",
"secret_scanning_alerts": "read",
"security_events": "read"
},
"default_events": [
"push",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"pull_request_review_thread",
"check_suite",
"status",
"issues",
"issue_comment",
"label",
"milestone",
"member",
"repository",
"discussion",
"discussion_comment",
"sub_issues",
"issue_dependencies",
"organization",
"team",
"membership",
"projects_v2",
"projects_v2_item",
"projects_v2_status_update"
]
}
The GitHub App reads repositories with installation tokens.
Its callback URL and client credentials only confirm who installed it.
Login uses a separate OAuth App.
Only the OAuth App's credentials belong in GH_OAUTH_CLIENT_ID / GH_OAUTH_CLIENT_SECRET.
Only the GitHub App's credentials belong in GH_APP_CLIENT_ID / GH_APP_CLIENT_SECRET.
Install and configure
On an existing instance, replacing WEBHOOK_SECRET breaks other configured webhooks.
Set the new App's webhook secret to the existing WEBHOOK_SECRET.
On a new instance, the manifest-generated secret can become WEBHOOK_SECRET.
-
Generate a private key under App settings → Private keys.
-
Download the
.pem. Alternatively, use the key from manifest registration. -
Install the App on your organization, for all repositories or only those to monitor.
-
Set these values in
.env:GH_APP_ID=123456# Multi-line PEM: quote the whole value; Docker Compose .env preserves the newlines.GH_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----"# The workspace-creation wizard sends operators to this URL to install the App.GH_APP_INSTALLATION_URL=https://github.com/apps/<your-app-slug>/installations/new# App settings → General → Client ID; generate a client secret there too.GH_APP_CLIENT_ID=Iv23...GH_APP_CLIENT_SECRET=... -
Restart with the new environment:
docker compose --env-file .env --env-file release-lock.env up -d
GitHub webhooks — manual setup (PAT mode)
PAT connections require a manual webhook for live updates.
Prefer one organization webhook.
Repository webhooks cannot deliver organization, team, or membership.
App-only events are unavailable here.
A subscription grants the PAT no API access.
- Open
https://github.com/organizations/<org>/settings/hooks. - Select Add webhook.
- Set Payload URL to
https://<APP_HOSTNAME>/webhooks/github. - Set Content type to
application/json. - Set Secret to your
WEBHOOK_SECRET. The receiver verifies it asX-Hub-Signature-256HMAC. - Under Events, select "Let me select individual events".
- Select the listed events that GitHub offers for this webhook type.
Include the three
projects_v2*events for organization project boards.
Operational mechanics
Permission changes and approval rounds
Permission increases take effect per installation, after its account owner approves them under organization → Settings → GitHub Apps. Until approval, that installation keeps its old grants. New installations approve the current set during installation. Reductions and event changes apply immediately without approval. A new subscription cannot bypass a permission increase that awaits approval.
- Combine permission increases into approval rounds.
- Before you enable dependent features, verify each installation's approval.
- After a permission reduction, recheck event availability.
An App from an earlier manifest needs manual updates under Permissions & events:
- Subscribe to
check_suiteandstatus. These need no approval round. - If the App predates them, grant Checks and Commit statuses read. These need an approval round per installation.
Until an installation grants them, the head's check state reaches Hephaestus only through scheduled sync, which reads it with the pull request.
Brand identity, instance routing, and consent
A homepage URL identifies the deployment. Any page that describes it is suitable. GitHub never delivers anything there. The webhook URL and every OAuth callback must use the instance hostname.
Use the exact callback procedure for login providers. GitHub deliveries do not follow redirects. A redirect loses every event even if the URL works in a browser.
- Keep TLS verification enabled.
- Route
/webhooks/githubto the webhook runtime role.
Each environment needs its own GitHub App because an App has one webhook URL.
- Use separate keys and secrets for each environment.
- Use a
-stagingname suffix for staging. - Register a separate login OAuth App per environment, with that instance's callback and credentials.
Use the manifest description for consent text. Disclose reads and writes, including provisioned security-alert access. The exact claim “Never modifies your code” refers to Contents read, not an absence of review or comment writes. Describe provisioned signals as access, not available features.
Ownership transfer
Installations normally stay in place after a transfer. The app ID, client credentials, and private keys stay unchanged. GitHub warns when a transfer would uninstall the app.
- Before transfer, check Where can this app be installed → Any account.
- Use the app registration's Advanced → Transfer ownership, not deletion and recreation.
- Before you proceed, review any warning.
- After transfer, check Where can this app be installed → Any account again.
- Verify installations, repository selections, and delivery.
- Review app managers and access to the secret store.
- If the previous owner must lose access, rotate keys and secrets separately.
Transfer does not revoke credential copies.
Failed deliveries and redelivery
For a GitHub App:
- Open its registration, not the organization's installation page.
- Open Advanced → Recent deliveries.
- Select a delivery GUID to inspect its request and response.
- Correct the cause.
- Select Redeliver.
For a manual webhook, use Settings → Webhooks → webhook → Recent Deliveries in the organization or repository instead. Hephaestus redelivers failed GitHub App deliveries of the past two hours by itself. Webhook ingestion operations describes the limits. Do not rely on scheduled polling to recover event-only history, such as CI state at merge or security-response timestamps.
| Symptom | Check / action |
|---|---|
| No delivery entry | Confirm installation, repository selection, subscription, and accepted permissions. Confirm event availability for this webhook type. Check GitHub Status. |
3xx | Replace the webhook URL that redirects with the direct instance URL. |
401 on a real event | Check that the app or manual webhook secret matches WEBHOOK_SECRET. Proxies must preserve the body and signature header. |
Timeout, TLS error, or 404 | Check public DNS, certificate chain, reverse-proxy routing, and webhook-role health. |
5xx | Inspect webhook-role and NATS health (webhook ingestion operations). Repair the failure before redelivery. |
2xx, but no application update | Check server consumption and workspace repository selection. Check the repository's sync error under Administration → Integrations → GitHub → Manage repositories. Events without a feature consumer are acknowledged and discarded without updates. |
Follow GitHub's delivery troubleshooting guide.
Verify it works
The wrong-secret test changes GitHub's secret and rejects real events.
Run it only on a dedicated test App, never a shared staging App.
Leave the receiver's WEBHOOK_SECRET unchanged.
-
Compare each environment's registration with the permission tables, including held grants.
-
Confirm each installation's approved permissions.
-
Remove grants or subscriptions absent from the tables.
-
Open a test PR in a monitored repository.
-
Update it.
-
Verify
2xxdelivery. -
Verify the updated state in the correct workspace without a wait for scheduled sync.
The initial
pingreturns200even with a wrong secret. It tests reachability, not authentication. For failures, use delivery triage and:docker compose --env-file .env --env-file release-lock.env logs -f webhook-server -
For Projects v2, change a project item.
-
Verify delivery and synchronization.
-
Only if App delivery is unavailable, add the narrowly scoped organization webhook.
-
On the dedicated test App, change the secret in GitHub's App settings only.
-
Trigger a new real event.
-
Confirm rejection.
-
Restore the matching secret.
-
Select Redeliver for the rejected event.
-
Verify acceptance and application state.
-
Sign in through the instance hostname.
-
Verify that the OAuth callback returns to that instance, not the product homepage or another environment.