or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

actions.mdapi-client.mdapps.mdexceptions.mdhttp-implementations.mdindex.mdrouting.mdsansio.md

apps.mddocs/

0

# GitHub Apps

1

2

Support for GitHub Apps authentication including JWT creation and installation access token retrieval for app-based integrations. GitHub Apps provide a more secure and scalable way to integrate with GitHub compared to OAuth tokens.

3

4

## Capabilities

5

6

### JWT Token Generation

7

8

Create JSON Web Tokens (JWT) for GitHub App authentication.

9

10

```python { .api }

11

def get_jwt(

12

*,

13

app_id: str,

14

private_key: str,

15

expiration: int = 10 * 60

16

) -> str:

17

"""

18

Construct the JWT (JSON Web Token) for GitHub App authentication.

19

20

Parameters:

21

- app_id: GitHub App ID (numeric string)

22

- private_key: RSA private key in PEM format

23

- expiration: Token expiration time in seconds (default: 600, max: 600)

24

25

Returns:

26

- JWT bearer token string

27

28

Note: JWT tokens are used to authenticate as the GitHub App itself,

29

not as an installation of the app.

30

"""

31

```

32

33

### Installation Access Tokens

34

35

Obtain installation access tokens for GitHub App installations.

36

37

```python { .api }

38

async def get_installation_access_token(

39

gh: GitHubAPI,

40

*,

41

installation_id: str,

42

app_id: str,

43

private_key: str

44

) -> Dict[str, Any]:

45

"""

46

Obtain a GitHub App's installation access token.

47

48

Parameters:

49

- gh: GitHubAPI instance for making the request

50

- installation_id: Installation ID (numeric string)

51

- app_id: GitHub App ID (numeric string)

52

- private_key: RSA private key in PEM format

53

54

Returns:

55

- Dictionary containing:

56

- "token": Installation access token string

57

- "expires_at": ISO 8601 expiration datetime string

58

59

Note: Installation access tokens allow acting on behalf of the

60

app installation with permissions granted to the app.

61

"""

62

```

63

64

## Usage Examples

65

66

### Basic GitHub App Authentication

67

68

```python

69

import asyncio

70

from gidgethub.aiohttp import GitHubAPI

71

from gidgethub.apps import get_jwt, get_installation_access_token

72

import aiohttp

73

74

async def github_app_example():

75

# GitHub App credentials

76

app_id = "12345"

77

private_key = """-----BEGIN RSA PRIVATE KEY-----

78

MIIEpAIBAAKCAQEA...

79

-----END RSA PRIVATE KEY-----"""

80

installation_id = "67890"

81

82

async with aiohttp.ClientSession() as session:

83

gh = GitHubAPI(session, "my-app/1.0")

84

85

# Get installation access token

86

token_response = await get_installation_access_token(

87

gh,

88

installation_id=installation_id,

89

app_id=app_id,

90

private_key=private_key

91

)

92

93

access_token = token_response["token"]

94

expires_at = token_response["expires_at"]

95

print(f"Token expires at: {expires_at}")

96

97

# Create new client with installation token

98

gh_installed = GitHubAPI(session, "my-app/1.0", oauth_token=access_token)

99

100

# Now you can act on behalf of the installation

101

repos = []

102

async for repo in gh_installed.getiter("/installation/repositories"):

103

repos.append(repo["name"])

104

105

print(f"App has access to {len(repos)} repositories")

106

107

asyncio.run(github_app_example())

108

```

109

110

### JWT Token Usage

111

112

```python

113

import asyncio

114

from gidgethub.aiohttp import GitHubAPI

115

from gidgethub.apps import get_jwt

116

import aiohttp

117

118

async def jwt_example():

119

app_id = "12345"

120

private_key = """-----BEGIN RSA PRIVATE KEY-----

121

MIIEpAIBAAKCAQEA...

122

-----END RSA PRIVATE KEY-----"""

123

124

# Generate JWT token

125

jwt_token = get_jwt(app_id=app_id, private_key=private_key)

126

127

async with aiohttp.ClientSession() as session:

128

gh = GitHubAPI(session, "my-app/1.0")

129

130

# Use JWT to authenticate as the app

131

app_info = await gh.getitem("/app", jwt=jwt_token)

132

print(f"App name: {app_info['name']}")

133

print(f"App owner: {app_info['owner']['login']}")

134

135

# Get app installations

136

installations = []

137

async for installation in gh.getiter("/app/installations", jwt=jwt_token):

138

installations.append({

139

"id": installation["id"],

140

"account": installation["account"]["login"],

141

"permissions": installation["permissions"]

142

})

143

144

print(f"App has {len(installations)} installations")

145

146

asyncio.run(jwt_example())

147

```

148

149

### Complete GitHub App Workflow

150

151

```python

152

import asyncio

153

from gidgethub.aiohttp import GitHubAPI

154

from gidgethub.apps import get_jwt, get_installation_access_token

155

import aiohttp

156

157

class GitHubAppClient:

158

def __init__(self, app_id: str, private_key: str):

159

self.app_id = app_id

160

self.private_key = private_key

161

self._jwt_token = None

162

self._installation_tokens = {}

163

164

def _get_jwt(self):

165

"""Get or refresh JWT token."""

166

# In production, you'd want to cache and refresh JWT tokens

167

return get_jwt(app_id=self.app_id, private_key=self.private_key)

168

169

async def get_installation_client(self, session, installation_id: str):

170

"""Get a GitHubAPI client for a specific installation."""

171

gh = GitHubAPI(session, "my-app/1.0")

172

173

# Get installation access token

174

token_response = await get_installation_access_token(

175

gh,

176

installation_id=installation_id,

177

app_id=self.app_id,

178

private_key=self.private_key

179

)

180

181

# Return client with installation token

182

return GitHubAPI(

183

session,

184

"my-app/1.0",

185

oauth_token=token_response["token"]

186

)

187

188

async def get_app_client(self, session):

189

"""Get a GitHubAPI client authenticated as the app."""

190

jwt_token = self._get_jwt()

191

return GitHubAPI(session, "my-app/1.0", jwt=jwt_token)

192

193

async def app_workflow_example():

194

app_client = GitHubAppClient(

195

app_id="12345",

196

private_key=open("app-private-key.pem").read()

197

)

198

199

async with aiohttp.ClientSession() as session:

200

# Get app-level client

201

app_gh = await app_client.get_app_client(session)

202

203

# List all installations

204

installations = []

205

async for installation in app_gh.getiter("/app/installations"):

206

installations.append(installation)

207

208

# Process each installation

209

for installation in installations:

210

installation_id = str(installation["id"])

211

account_name = installation["account"]["login"]

212

213

print(f"Processing installation for {account_name}")

214

215

# Get installation-specific client

216

install_gh = await app_client.get_installation_client(

217

session, installation_id

218

)

219

220

# List repositories for this installation

221

async for repo in install_gh.getiter("/installation/repositories"):

222

print(f" Repository: {repo['full_name']}")

223

224

# Example: Create an issue

225

await install_gh.post(

226

f"/repos/{repo['full_name']}/issues",

227

data={

228

"title": "Automated issue from GitHub App",

229

"body": "This issue was created by our GitHub App!"

230

}

231

)

232

233

# asyncio.run(app_workflow_example())

234

```

235

236

### Webhook Event Handling with Apps

237

238

```python

239

from gidgethub.routing import Router

240

from gidgethub.apps import get_installation_access_token

241

import aiohttp

242

243

router = Router()

244

245

@router.register("installation", action="created")

246

async def handle_new_installation(event):

247

"""Handle new app installation."""

248

installation = event.data["installation"]

249

print(f"New installation: {installation['account']['login']}")

250

251

# You might want to store installation info in a database

252

# or perform initial setup tasks

253

254

@router.register("pull_request", action="opened")

255

async def auto_review_pr(event):

256

"""Automatically review pull requests."""

257

installation_id = str(event.data["installation"]["id"])

258

repo_name = event.data["repository"]["full_name"]

259

pr_number = event.data["pull_request"]["number"]

260

261

# Get installation token

262

async with aiohttp.ClientSession() as session:

263

gh = GitHubAPI(session, "pr-reviewer-app/1.0")

264

265

token_response = await get_installation_access_token(

266

gh,

267

installation_id=installation_id,

268

app_id="your_app_id",

269

private_key="your_private_key"

270

)

271

272

# Create installation client

273

install_gh = GitHubAPI(

274

session,

275

"pr-reviewer-app/1.0",

276

oauth_token=token_response["token"]

277

)

278

279

# Add review comment

280

await install_gh.post(

281

f"/repos/{repo_name}/pulls/{pr_number}/reviews",

282

data={

283

"body": "Thanks for the PR! Our automated review is complete.",

284

"event": "COMMENT"

285

}

286

)

287

```

288

289

### Error Handling

290

291

```python

292

import asyncio

293

from gidgethub.aiohttp import GitHubAPI

294

from gidgethub.apps import get_installation_access_token

295

from gidgethub import HTTPException

296

import aiohttp

297

import jwt

298

299

async def robust_app_auth():

300

async with aiohttp.ClientSession() as session:

301

gh = GitHubAPI(session, "my-app/1.0")

302

303

try:

304

token_response = await get_installation_access_token(

305

gh,

306

installation_id="12345",

307

app_id="67890",

308

private_key="invalid_key"

309

)

310

except HTTPException as exc:

311

if exc.status_code == 401:

312

print("Authentication failed - check app ID and private key")

313

elif exc.status_code == 404:

314

print("Installation not found - check installation ID")

315

else:

316

print(f"HTTP error: {exc.status_code}")

317

except jwt.InvalidTokenError:

318

print("Invalid JWT token - check private key format")

319

except Exception as exc:

320

print(f"Unexpected error: {exc}")

321

322

asyncio.run(robust_app_auth())

323

```

324

325

### Private Key Management

326

327

```python

328

import os

329

from pathlib import Path

330

331

def load_private_key():

332

"""Load private key from various sources."""

333

334

# Option 1: Environment variable

335

if "GITHUB_PRIVATE_KEY" in os.environ:

336

return os.environ["GITHUB_PRIVATE_KEY"]

337

338

# Option 2: File path from environment

339

if "GITHUB_PRIVATE_KEY_PATH" in os.environ:

340

key_path = Path(os.environ["GITHUB_PRIVATE_KEY_PATH"])

341

return key_path.read_text()

342

343

# Option 3: Default file location

344

default_path = Path("github-app-key.pem")

345

if default_path.exists():

346

return default_path.read_text()

347

348

raise ValueError("GitHub App private key not found")

349

350

# Usage

351

try:

352

private_key = load_private_key()

353

app_client = GitHubAppClient("12345", private_key)

354

except ValueError as exc:

355

print(f"Configuration error: {exc}")

356

```

357

358

## GitHub App Permissions

359

360

When creating a GitHub App, you need to configure permissions for what the app can access:

361

362

### Repository Permissions

363

- **Contents**: Read/write repository files

364

- **Issues**: Create and manage issues

365

- **Pull requests**: Create and manage PRs

366

- **Metadata**: Access repository metadata

367

- **Deployments**: Create deployments

368

- **Statuses**: Create commit statuses

369

- **Checks**: Create check runs

370

371

### Organization Permissions

372

- **Members**: Access organization membership

373

- **Administration**: Manage organization settings

374

375

### Account Permissions

376

- **Email addresses**: Access user email addresses

377

- **Profile**: Access user profile information

378

379

## Types

380

381

```python { .api }

382

from typing import Any, Dict

383

from gidgethub.abc import GitHubAPI

384

385

# JWT payload structure (internal)

386

JWTPayload = Dict[str, Any] # {"iat": int, "exp": int, "iss": str}

387

388

# Installation access token response

389

InstallationTokenResponse = Dict[str, Any] # {"token": str, "expires_at": str}

390

```