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 bundleof 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).
| Setting | Value |
|---|---|
| Homepage URL | the portal's public URL |
| Callback URL | <API base>/api/v1/github/app/callback |
| Request user authorization (OAuth) during installation | on. The callback needs the code to ask GitHub which installations the person can access; without it nothing is bound |
| Expire user authorization tokens | on. The token is used once in the callback and never stored |
| Setup URL | not 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 secret | a random value; the same value goes into github.webhook_secret |
| Where can this App be installed | any account, if maintainers outside your organization are to connect repositories |
Repository permissions (the managed access level):
| Permission | Access | Used for |
|---|---|---|
| Administration | read and write | adding and removing the collaborators CommonLayer manages; reading collaborators and branch protection. No other administrative call is made; each one is audited |
| Contents | read | manifest, Dockerfiles, workflows, charts; the source backups |
| Metadata | read | repository identity, transfers, renames |
| Actions | read | observing workflow runs |
| Pull requests | read | observing contributions |
| Issues | read and write | optional: 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
| Setting | Process | Notes |
|---|---|---|
github.provider | all | github (default). development is refused outside development and test |
github.app_id, github.app_slug, github.app_client_id | all | not secret |
github.app_private_key or github.app_private_key_path | api, worker | at 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_secret | api | the callback's code exchange |
github.webhook_secret | api | verifies webhook signatures |
github.git_path | worker | git for the source backups; the worker image carries it at /usr/bin/git |
github.backup_bucket | api, worker | bucket of the bundles and manifests; versioned, like the artifact bucket |
github.backup_max_bytes, github.backup_timeout_secs, github.backup_keep_scheduled | worker | limits and retention; release, manual and succession backups are always kept |
github.backup_downloads_public | api | off: a signed-in person may download a backup of a public project; on: anyone |
github.collaborator_permission, github.organization_collaborator_permission | worker | the role invited maintainers get (push; maintain in organizations) |
github.revoke_access_on_end | worker | remove the access CommonLayer granted when an assignment ends |
github.stewardship.organization, github.stewardship.token | worker (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
- Sign in to the portal as a maintainer and start the onboarding; GitHub's installation page for the App should open.
- After installing, the browser returns to the portal with the installation listed.
github.installation.boundappears in the audit log; a refusal isgithub.installation.bind_refusedwith the reason. - The repository picker lists the repositories of the installation and marks those the person administers.
- Starting an onboarding queues a scan; the worker's log shows it and the report appears.
POST /projects/{slug}/source-backupson a connected project queues a backup;commonlayer_repository_backups_totalcounts 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.