App manifest with the submission form#

Use this form to register an app with the project’s required permissions and events, when you set the credentials in the environment yourself. Registering the app on the running site (Register the GitHub App on the site) is easier. You need a GitHub account allowed to register the app, a browser, and a checkout with uv installed to save credentials. For the other setup flows, return to Set up the GitHub App.

1. Prepare the site#

From the checkout:

make dev
test -f .env || cp .env.development.example .env
mkdir -p data

Set your app name and site address in .env. Use the same values in the form:

SITE_NAME=your-unique-development-app-name
SITE_URL=http://localhost:5000

Choose a distinct app name of at most 34 characters. For a deployed site, use its public HTTPS address; see Run the site with Docker. Start make run in another terminal so the site can receive GitHub’s registration return.

2. Submit the manifest#

Fill in the form and press Create app on GitHub. GitHub opens in a new tab, where you review the name and confirm Create GitHub App. Leave the organization empty for your personal account, or enter its GitHub login.

All URLs of the app are derived from your site URL: callback, installation, registration return and the webhook, which is always the site URL plus /github/webhook/. GitHub cannot deliver webhooks to localhost; to test them locally, make the site public with Tailscale Funnel as in Test the GitHub App locally with Tailscale and enter that address as site URL.

The site that make run serves. For deployment, use its public HTTPS URL. GitHub sends the webhooks to this address plus /github/webhook/.

Enter an organization login, such as PyCalendar, to register the app there.

GitHub opens in a new tab to confirm registration. Leave this tab open until you finish the code exchange.

Review the generated manifest

  

3. Save the return URL and registration state#

  1. GitHub returns the new tab to your site with code and state in the URL. Copy that whole return URL. The homepage currently does not exchange the code automatically.

  2. Return to this documentation tab and click Save registration state. Save the downloaded github-app-state.txt as data/github-app-state.txt in your checkout. Keep it unchanged and do not replace a state file from another registration that is still in progress.

If this tab is lost before you save the state, use a fresh registration attempt; the code-exchange check requires the state of the attempt you submitted. The form sends configuration to GitHub; it does not install the app or save credentials to your checkout.

4. Exchange the code and configure credentials#

Keep the saved state file at data/github-app-state.txt. The following command checks it against the return URL before exchanging the code.

Within one hour of starting registration, run the next command. Paste the return URL at its hidden prompt. It checks the saved state, exchanges the code with GitHub and writes the private key and a settings fragment without printing their contents:

uv run python - <<'PY'
import getpass
import hmac
import os
from pathlib import Path
from urllib.parse import parse_qs, quote, urlsplit

import httpx

for filename in ("data/github-app.pem", "data/github-app-settings.env"):
    if Path(filename).exists():
        raise SystemExit("Credential files already exist; choose fresh filenames before conversion.")
returned = getpass.getpass("Paste the full manifest return URL: ")
parameters = parse_qs(urlsplit(returned).query)
expected = Path("data/github-app-state.txt").read_text()
states = parameters.get("state", [])
codes = parameters.get("code", [])
if len(states) != 1 or not hmac.compare_digest(states[0], expected):
    raise SystemExit("The return state does not match this registration attempt.")
if len(codes) != 1:
    raise SystemExit("The manifest return URL must contain one code.")
response = httpx.post(
    "https://api.github.com/app-manifests/" + quote(codes[0], safe="") + "/conversions",
    headers={"Accept": "application/vnd.github+json"},
    timeout=20,
)
if response.status_code != 201:
    raise SystemExit("GitHub rejected the conversion; check the code and one-hour deadline.")
credentials = response.json()
pem = credentials["pem"]
values = {
    "GITHUB_APP_ID": credentials["id"],
    "GITHUB_APP_SLUG": credentials["slug"],
    "GITHUB_CLIENT_ID": credentials["client_id"],
    "GITHUB_SECRET": credentials["client_secret"],
    "GITHUB_WEBHOOK_SECRET": credentials["webhook_secret"],
    "GITHUB_APP_PRIVATE_KEY_FILE": "data/github-app.pem",
}
os.umask(0o077)
with Path("data/github-app.pem").open("x") as output:
    output.write(pem)
with Path("data/github-app-settings.env").open("x") as output:
    output.write("".join(f"{key}={value}\n" for key, value in values.items()))
print("Saved data/github-app.pem and data/github-app-settings.env.")
PY

Open data/github-app-settings.env in your editor and copy its six settings into .env, replacing any existing values for those keys. These files contain credentials; data/ and .env are excluded from version control. Keep the generated private key rather than generating another one on GitHub.

For Docker, mount the key as /app/data/github-app.pem and change GITHUB_APP_PRIVATE_KEY_FILE to that container path. The response fields map directly to the six environment settings shown in the example; GITHUB_SECRET means the client secret, not the webhook secret or key.

5. Restart, sign in and install the app#

Stop the development server and run make run again so it reads the new credentials. For Docker, restart the service with its updated environment. There should be no ai_pycal.W001 warning about missing GitHub App settings.

Open http://localhost:5000/accounts/login/, sign in with GitHub, open Setup and press Install the GitHub App. Select the repositories to include. Registration creates the app; installation grants it access to repositories. Continue with Set up a repository to add .github/agreements.yml and activate a repository. Open a test PR and inspect the app’s recent webhook deliveries on GitHub if it is not checked.

This form follows GitHub’s manifest registration examples.