_private/qwestly-docs/Engineering/qwestly-bot.md

Qwestly Bot (GitHub App)

qwestly is a GitHub App that lets developers run gh CLI commands as a bot identity — PR reviews, comments, and issue interactions appear posted by qwestly[bot] instead of a personal user.

Why

  • Automated PR reviews from CI/CD (GitHub Actions) can use the bot
  • Local PR reviews from AI tools can appear under the bot instead of your personal account, keeping human vs. automated contributions distinct
  • Shared bot identity — any team member with the env vars can post as the bot

App details

Field Value
App name qwestly
App ID 3884618
Owner @Qwestly
Settings https://github.com/organizations/Qwestly/settings/apps/qwestly
Installed on Qwestly org (all repos)

Permissions

Permission Level
Pull requests Read & Write
Issues Read & Write

Added at App permissions.

Activity

In the past 2 days (2026-05-28 to 2026-05-29), qwestly[bot] opened 23 PRs across 5 repos:

Repo PRs
candidate #514–#530, #532–#534 (19)
candidate-catalog #47
qwestly-ui #27
public-site #65
qwestly-hire #105

Local usage

The repo ships a wrapper script at scripts/gh-as-qwestly/gh-as-qwestly that handles the full auth flow (JWT → installation token → gh command).

Prerequisites

You need three environment variables:

export GH_QWESTLY_APP_ID=3884618
export GH_QWESTLY_INSTALLATION_ID=136085062
export GH_QWESTLY_KEY_FILE=_private/qwestly.2026-05-27.private-key.pem

The private key is stored in _private/ (gitignored). Ask a team member with access to the app settings if you need it.

Examples

# Approve a PR
./scripts/gh-as-qwestly/gh-as-qwestly pr review 42 --approve --body "LGTM 🚀"

# Leave inline review comments
./scripts/gh-as-qwestly/gh-as-qwestly pr review 42 --comment --body "Consider extracting this."

# Comment on a PR
./scripts/gh-as-qwestly/gh-as-qwestly pr comment 42 --body "Reviewed by qwestly."

# Any gh command works
./scripts/gh-as-qwestly/gh-as-qwestly api repos/Qwestly/candidate/pulls --jq '.[].title'

Convenience alias

alias gh-bot='./scripts/gh-as-qwestly/gh-as-qwestly'

How auth works

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ 1. Generate JWT  │ ──▶ │ 2. Exchange for   │ ──▶ │ 3. Run gh with  │
│    (app private   │     │    installation   │     │    installation  │
│     key, RS256)   │     │    token (1h TTL) │     │    token         │
└─────────────────┘     └──────────────────┘     └─────────────────┘
  1. A JWT is signed with the app's private key (RS256, 10-min expiry)
  2. The JWT is exchanged for an installation access token via POST /app/installations/{id}/access_tokens
  3. gh runs with GH_TOKEN set to the installation token — all API calls appear as qwestly[bot]

Installation tokens expire after 1 hour. The wrapper script generates a fresh token each time, so this is transparent.

Setup reference (for app admins)

The scripts live at:

scripts/gh-as-qwestly/
├── README.md           # Detailed setup and troubleshooting
├── generate-jwt.sh     # Creates a JWT signed with the app's private key
└── gh-as-qwestly       # Orchestrator: JWT → installation token → gh

Creating the app from scratch

  1. Go to GitHub Settings → Developer settings → GitHub Apps
  2. Create a new app with:
    • Repository permissions: Pull Requests (Read & Write), Issues (Read & Write)
    • Webhook: Unchecked (not needed)
    • Installation: Any account
  3. Transfer ownership to the Qwestly org (Settings → Advanced → Transfer)
  4. Generate a private key and store it securely
  5. Install on the Qwestly org
  6. Note the App ID and Installation ID for env vars

Regenerating the private key

  1. Go to App Settings → General → Private keys
  2. Click Generate a private key
  3. Store the .pem file in _private/ and update GH_QWESTLY_KEY_FILE
  4. Revoke the old key after confirming the new one works