Skip to content
CommonLayer
Documentation menu
On this page

The CommonLayer GitHub App

How an operator creates and configures the GitHub App that connects maintainers' repositories to the platform: its permissions and events, the URLs GitHub must call, the settings and which process holds which secret, and the optional stewardship organization. The model is described in ADR-0046 and plan 13.

Not verified: no GitHub App has been created for this platform and nothing here has run against github.com. The settings below are what the code expects, taken from the configuration and the callback and webhook handlers, and tested against an in-memory GitHub, wiremock and local repositories. Expect to adjust details on the first real installation, and record what you find.

What the App is for

A maintainer installs the App on the account or organization that owns their repository. The repository stays theirs. Through the App the platform:

  • verifies that the person onboarding a repository administers it;
  • reads files for the onboarding scan and the guardrail checks (nothing from the repository is executed);
  • takes source backups (a git bundle of every branch and tag) on every release and weekly;
  • invites the maintainers the project appointed as collaborators and removes the access it granted when an assignment ends, never anyone it did not add;
  • observes pushes, pull requests, releases and workflow runs.

It never pushes to a maintainer's repository.

Creating the App

GitHub has no command and no API call that creates a GitHub App outright, so gh alone cannot do it. The closest is GitHub's app manifest flow: a page posts a manifest to GitHub, you confirm the name there, and GitHub returns a one-time code that is exchanged for the App's id, client id and secret, webhook secret and private key.

With the helper script

scripts/ops/github-app-create.py does all of it except your confirmation:

scripts/ops/github-app-create.py \
  --portal-url https://commonlayer.example \
  --api-url https://api.commonlayer.example \
  --org your-organization

It builds the manifest with the settings of the next section, serves a local page (http://127.0.0.1:8765/) with one button that posts it to GitHub, receives the redirect, exchanges the code and writes three files to var/github-app/, readable by you only: <slug>.env (the settings), <slug>.private-key.pem and <slug>.json (the whole answer). It prints no secret. Without --org the App is created under your own account. --print-manifest prints the manifest and stops.

For an API on localhost the script configures no webhook, because GitHub cannot reach it: connecting a repository and scanning work, but pushes, transfers and removals are not reported until the App has a reachable webhook URL (--webhook-url with a tunnel, or later in the App's settings). The exchange is GitHub's public POST /app-manifests/{code}/conversions; by hand it is gh api -X POST /app-manifests/<code>/conversions, within one hour.

The script was tested against a local stand-in for GitHub only.

By hand

Create it under the account or organization of the operating entity (GitHub → Settings → Developer settings → GitHub Apps → New GitHub App).

SettingValue
Homepage URLthe portal's public URL
Callback URL<API base>/api/v1/github/app/callback
Request user authorization (OAuth) during installationon. The callback needs the code to ask GitHub which installations the person can access; without it nothing is bound
Expire user authorization tokenson. The token is used once in the callback and never stored
Setup URLnot used (GitHub sends the person to the callback URL when authorization during installation is on)
Webhook URL<API base>/webhooks/github (outside /api/v1)
Webhook secreta random value; the same value goes into github.webhook_secret
Where can this App be installedany account, if maintainers outside your organization are to connect repositories

Repository permissions (the managed access level):

PermissionAccessUsed for
Administrationread and writeadding and removing the collaborators CommonLayer manages; reading collaborators and branch protection. No other administrative call is made; each one is audited
Contentsreadmanifest, Dockerfiles, workflows, charts; the source backups
Metadatareadrepository identity, transfers, renames
Actionsreadobserving workflow runs
Pull requestsreadobserving contributions
Issuesread and writeoptional: the GitHub issue notification channel

A maintainer who does not grant Administration gets the observed level: the repository is scanned and backed up, maintainers are not managed, and the project cannot be approved while projects.require_managed_repository is on.

Subscribe to these events: push, pull request, pull request review, release, workflow run, repository. GitHub sends installation events to every App. Other events are stored as ignored.

After creating the App, generate a private key and a client secret, and note the App id, the slug (the name in the App's public URL) and the client id.

Using the credentials

On Kubernetes the values go into the platform's Secret under the keys the chart names (secrets.keys.githubAppPrivateKey, githubAppClientSecret, githubWebhookSecret) and the App id, slug and client id into the configuration.

In local development everything goes into .env. Remove the line COMMONLAYER__GITHUB__PROVIDER=development from it, then append the settings the helper wrote and restart:

grep -v '^#' var/github-app/<slug>.env >> .env
make restart-backend

GET /api/v1/github/app then answers "configured": true, "provider": "github".

The settings name the private key by its file (COMMONLAYER__GITHUB__APP_PRIVATE_KEY_PATH, setting github.app_private_key_path), because a key of several lines cannot be written into .env, which the Makefile also reads. The api and the worker read the file when they start; an unreadable or empty file stops them with github.app_private_key_path: cannot read <path>: <reason> or github.app_private_key_path: <path> is empty (the content is never printed). Giving both the path and the content (COMMONLAYER__GITHUB__APP_PRIVATE_KEY) is refused.

The platform refuses to start with an App and no webhook secret (github.webhook_secret is required with a GitHub App). When GitHub configured no webhook and therefore issued no secret, the helper script generates one and writes it into <slug>.env with a note; enter the same value in the App's settings when you give the App a webhook URL.

Checked locally with made-up App values and a generated key: with these settings the backend's configuration is accepted and the App is reported as configured. Installing and the callback have not been run against GitHub.

Settings

SettingProcessNotes
github.providerallgithub (default). development is refused outside development and test
github.app_id, github.app_slug, github.app_client_idallnot secret
github.app_private_key or github.app_private_key_pathapi, workerat most one of the two; the path names a PEM file read at start by the api and the worker (the scheduler never reads it). The api holds the key because the repository picker asks GitHub directly (threat model, O16)
github.app_client_secretapithe callback's code exchange
github.webhook_secretapiverifies webhook signatures
github.git_pathworkergit for the source backups; the worker image carries it at /usr/bin/git
github.backup_bucketapi, workerbucket of the bundles and manifests; versioned, like the artifact bucket
github.backup_max_bytes, github.backup_timeout_secs, github.backup_keep_scheduledworkerlimits and retention; release, manual and succession backups are always kept
github.backup_downloads_publicapioff: a signed-in person may download a backup of a public project; on: anyone
github.collaborator_permission, github.organization_collaborator_permissionworkerthe role invited maintainers get (push; maintain in organizations)
github.revoke_access_on_endworkerremove the access CommonLayer granted when an assignment ends
github.stewardship.organization, github.stewardship.tokenworker (token)optional, see below

In the Helm chart these are secrets.keys.githubAppPrivateKey, githubAppClientSecret, githubWebhookSecret, githubStewardshipToken, config.stewardshipOrganization and objectStorage.backupBucket; the chart passes each secret only to the processes in the table.

Runtime settings (administration → settings, audited): projects.require_managed_repository (default on), github.guardrails_required (the required rules; an unknown rule id makes every scan end in error), github.backup_downloads_public.

Egress: the api and the worker need api.github.com. The api also needs github.com for the callback's code exchange, and the worker needs it over HTTPS for the mirror clones. The worker's git accepts no other host and follows no redirects.

The stewardship organization (optional)

When a repository's owner leaves and does not transfer it, the project continues from a backup in a new repository. By default the successor creates that repository in their own account and pushes the bundle. If you want the platform to be able to host such a repository itself, create a separate GitHub organization for it and a token limited to that organization that can create repositories and push to them, and set github.stewardship.organization and github.stewardship.token.

This is the only place the platform writes to a repository, and the organization is the operator's own. Without these settings a stewardship-hosted restore is refused with github.stewardship_not_configured and the successor-hosted way remains.

Checking an installation

  1. Sign in to the portal as a maintainer and start the onboarding; GitHub's installation page for the App should open.
  2. After installing, the browser returns to the portal with the installation listed. github.installation.bound appears in the audit log; a refusal is github.installation.bind_refused with the reason.
  3. The repository picker lists the repositories of the installation and marks those the person administers.
  4. Starting an onboarding queues a scan; the worker's log shows it and the report appears.
  5. POST /projects/{slug}/source-backups on a connected project queues a backup; commonlayer_repository_backups_total counts it and the backup appears in the list with its size and ref count.

Alerts for this area: RepositoryReleaseNotBackedUp, RepositoryGuardrailsFailing, RepositorySuccessionOverdue (runbooks).

Development

Local development and the test suites use the development provider (github.provider = "development"), which stands in for GitHub with repositories built from the checkout and a portal page in place of GitHub's installation page. It is described in the repository's README. It exists so the flow can be used and tested without an App; it proves nothing about github.com.