This guide will walk you through integrating the github_app library into your Rust application to manage GitHub App integrations with GitOps support.
- Prerequisites
- Installation
- GitHub App Setup
- Quick Start
- Configuration
- Authentication
- Webhook Handling
- GitOps Operations
- Complete Example
- Error Handling
- Best Practices
- Troubleshooting
Before using this library, you need:
- Rust: Version 1.70 or higher
- GitHub App: A registered GitHub App with:
- App ID
- Installation ID
- Private key (
.pemfile) - Webhook secret
- Repository access permissions
- Git: Installed on your system (for GitOps functionality)
Add the library to your Cargo.toml:
[dependencies]
github_app = { path = "../github_app" } # or from git/crates.io when published
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }- Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App
- Fill in:
- App name: Your app name
- Homepage URL: Your app URL
- Webhook URL:
https://your-domain.com/github/webhook - Webhook secret: Generate a secure random string
- Set permissions:
- Repository permissions:
- Contents: Read & write (for GitOps)
- Metadata: Read-only
- Repository permissions:
- Subscribe to events:
- Push
- (Any other events you need)
- Click "Create GitHub App"
- App ID: Found on your app's settings page
- Private Key: Click "Generate a private key" and download the
.pemfile - Installation ID: Install the app to your repository/organization, then get the installation ID from the URL:
https://github.com/settings/installations/{installation_id} - Webhook Secret: The secret you set during creation
Here's a minimal example to get you started:
use github_app::{GitHubAppConfig, GitHubTokenProvider, WebhookVerifier, GitHubGitOps};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create configuration
let config = GitHubAppConfig::new(
123456, // Your App ID
789012, // Your Installation ID
std::fs::read_to_string("path/to/private-key.pem")?, // Private key
std::env::var("GITHUB_WEBHOOK_SECRET")?, // Webhook secret
"owner/repo".to_string(), // Repository
"main".to_string(), // Branch
PathBuf::from("/tmp/my-repo-clone"), // Clone path
"manifests/**/*.yaml".to_string(), // Manifest glob pattern
);
config.validate()?;
// 2. Initialize components
let token_provider = GitHubTokenProvider::new(config.clone());
let webhook_verifier = WebhookVerifier::new(&config);
let gitops = GitHubGitOps::new(config.clone(), GitHubTokenProvider::new(config.clone()));
// 3. Initialize GitOps (clone repo)
gitops.initialize().await?;
println!("GitHub App integration ready!");
Ok(())
}Store sensitive data in environment variables:
export GITHUB_APP_ID=123456
export GITHUB_INSTALLATION_ID=789012
export GITHUB_PRIVATE_KEY_PATH=/path/to/private-key.pem
export GITHUB_WEBHOOK_SECRET=your-webhook-secret
export GITHUB_REPO=owner/repo
export GITHUB_BRANCH=main
export GIT_CLONE_PATH=/tmp/repo
export MANIFEST_GLOB="manifests/**/*.yaml"Then load them in your code:
use github_app::GitHubAppConfig;
use std::path::PathBuf;
fn load_config() -> Result<GitHubAppConfig, Box<dyn std::error::Error>> {
let config = GitHubAppConfig::new(
std::env::var("GITHUB_APP_ID")?.parse()?,
std::env::var("GITHUB_INSTALLATION_ID")?.parse()?,
std::fs::read_to_string(std::env::var("GITHUB_PRIVATE_KEY_PATH")?)?,
std::env::var("GITHUB_WEBHOOK_SECRET")?,
std::env::var("GITHUB_REPO")?,
std::env::var("GITHUB_BRANCH")?,
PathBuf::from(std::env::var("GIT_CLONE_PATH")?),
std::env::var("MANIFEST_GLOB")?,
);
config.validate()?;
Ok(config)
}Always validate your configuration:
match config.validate() {
Ok(_) => println!("Configuration is valid"),
Err(e) => eprintln!("Configuration error: {}", e),
}The library handles GitHub App authentication automatically using JWT and installation tokens.
use github_app::GitHubTokenProvider;
let token_provider = GitHubTokenProvider::new(config);
// Get a token (automatically cached and refreshed)
let token = token_provider.get_token().await?;
println!("Installation token: {}", token);- JWT Creation: Creates a JWT signed with your private key (valid for 10 minutes)
- Token Request: Uses the JWT to request an installation access token from GitHub
- Caching: Caches the token until 5 minutes before expiry
- Auto-Refresh: Automatically refreshes the token when needed
use reqwest::Client;
let token = token_provider.get_token().await?;
let client = Client::new();
let response = client
.get("https://api.github.com/repos/owner/repo")
.header("Authorization", format!("token {}", token))
.header("User-Agent", "my-app")
.send()
.await?;
println!("Status: {}", response.status());Here's an example using Axum (popular Rust web framework):
use axum::{
extract::{State, Json},
http::{StatusCode, HeaderMap},
response::IntoResponse,
routing::post,
Router,
};
use github_app::{WebhookVerifier, WebhookEvent};
use std::sync::Arc;
struct AppState {
verifier: WebhookVerifier,
}
async fn webhook_handler(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> impl IntoResponse {
// Extract GitHub headers
let event_type = match headers.get("X-GitHub-Event") {
Some(v) => v.to_str().unwrap_or(""),
None => return (StatusCode::BAD_REQUEST, "Missing X-GitHub-Event header"),
};
let signature = match headers.get("X-Hub-Signature-256") {
Some(v) => v.to_str().unwrap_or(""),
None => return (StatusCode::BAD_REQUEST, "Missing X-Hub-Signature-256 header"),
};
// Verify and parse event
match state.verifier.parse_event(event_type, signature, &body) {
Ok(event) => {
// Handle the event
if let Err(e) = handle_github_event(event).await {
eprintln!("Error handling event: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Error processing event");
}
(StatusCode::OK, "Event processed")
}
Err(github_app::GitHubError::InvalidSignature) => {
(StatusCode::UNAUTHORIZED, "Invalid signature")
}
Err(e) => {
eprintln!("Error parsing webhook: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Error parsing webhook")
}
}
}
async fn handle_github_event(event: WebhookEvent) -> Result<(), Box<dyn std::error::Error>> {
match event {
WebhookEvent::Push(push_event) => {
println!("Push event received!");
println!(" Repository: {}", push_event.repository.full_name);
println!(" Ref: {}", push_event.git_ref);
if let Some(branch) = push_event.branch() {
println!(" Branch: {}", branch);
}
if let Some(sha) = push_event.commit_sha() {
println!(" Commit SHA: {}", sha);
}
}
WebhookEvent::Ping => {
println!("Ping received - webhook is working!");
}
WebhookEvent::Unknown(event_type) => {
println!("Unknown event type: {}", event_type);
}
}
Ok(())
}
#[tokio::main]
async fn main() {
let config = load_config().unwrap();
let verifier = WebhookVerifier::new(&config);
let state = Arc::new(AppState { verifier });
let app = Router::new()
.route("/github/webhook", post(webhook_handler))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Webhook server running on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}If you need to verify signatures separately:
use github_app::WebhookVerifier;
let verifier = WebhookVerifier::new(&config);
match verifier.verify_signature(signature, body) {
Ok(_) => println!("Signature is valid!"),
Err(_) => println!("Invalid signature - webhook may be spoofed!"),
}use github_app::{GitHubGitOps, GitHubTokenProvider};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct MyManifest {
name: String,
version: String,
replicas: u32,
}
async fn gitops_example(config: GitHubAppConfig) -> Result<(), Box<dyn std::error::Error>> {
let token_provider = GitHubTokenProvider::new(config.clone());
let gitops = GitHubGitOps::new(config, token_provider);
// 1. Initialize (clone repository if not exists)
println!("Initializing repository...");
gitops.initialize().await?;
// 2. Sync (pull latest changes)
println!("Syncing repository...");
gitops.sync().await?;
// 3. Load manifests
println!("Loading manifests...");
let manifests: Vec<MyManifest> = gitops.load_all_manifests()?;
// 4. Process manifests
for manifest in manifests {
println!("Processing: {} v{} (replicas: {})",
manifest.name, manifest.version, manifest.replicas);
}
Ok(())
}Typical pattern: sync repo when push events occur on the main branch:
use github_app::{WebhookEvent, GitHubGitOps};
async fn handle_webhook_with_gitops(
event: WebhookEvent,
gitops: &GitHubGitOps,
config: &GitHubAppConfig,
) -> Result<(), github_app::GitHubError> {
match event {
WebhookEvent::Push(push_event) => {
// Only sync if push is to the tracked branch
if push_event.branch() == Some(config.branch.clone()) {
println!("Push to {} - syncing repository", config.branch);
gitops.sync().await?;
// Reload and process manifests
let manifests: Vec<MyManifest> = gitops.load_all_manifests()?;
process_manifests(manifests).await?;
}
}
_ => {}
}
Ok(())
}Define your own manifest structures:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct DeploymentManifest {
api_version: String,
kind: String,
metadata: Metadata,
spec: DeploymentSpec,
}
#[derive(Debug, Deserialize)]
struct Metadata {
name: String,
namespace: Option<String>,
}
#[derive(Debug, Deserialize)]
struct DeploymentSpec {
replicas: u32,
image: String,
port: u16,
}
// Load your custom manifests
let deployments: Vec<DeploymentManifest> = gitops.load_all_manifests()?;match gitops.sync().await {
Ok(_) => println!("Repository synced successfully"),
Err(github_app::GitHubError::Git(msg)) => {
eprintln!("Git error: {}", msg);
// Handle git-specific errors (e.g., merge conflicts)
}
Err(github_app::GitHubError::Http(e)) => {
eprintln!("Network error: {}", e);
// Handle network issues
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}Here's a full working example of a GitOps broker:
use github_app::{
GitHubAppConfig, GitHubTokenProvider, WebhookVerifier,
GitHubGitOps, WebhookEvent, GitHubError
};
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Deserialize)]
struct AppManifest {
name: String,
version: String,
enabled: bool,
}
struct GitOpsBroker {
config: GitHubAppConfig,
token_provider: Arc<GitHubTokenProvider>,
webhook_verifier: WebhookVerifier,
gitops: Arc<GitHubGitOps>,
state: Arc<RwLock<BrokerState>>,
}
struct BrokerState {
manifests: Vec<AppManifest>,
last_sync: Option<std::time::SystemTime>,
}
impl GitOpsBroker {
async fn new(config: GitHubAppConfig) -> Result<Self, GitHubError> {
config.validate()?;
let token_provider = Arc::new(GitHubTokenProvider::new(config.clone()));
let webhook_verifier = WebhookVerifier::new(&config);
let gitops = Arc::new(GitHubGitOps::new(
config.clone(),
GitHubTokenProvider::new(config.clone())
));
// Initialize repository
println!("Initializing repository...");
gitops.initialize().await?;
let state = Arc::new(RwLock::new(BrokerState {
manifests: Vec::new(),
last_sync: None,
}));
let broker = Self {
config,
token_provider,
webhook_verifier,
gitops,
state,
};
// Initial sync
broker.sync_and_reconcile().await?;
Ok(broker)
}
async fn handle_webhook(
&self,
event_type: &str,
signature: &str,
body: &[u8],
) -> Result<(), GitHubError> {
// Verify and parse webhook
let event = self.webhook_verifier.parse_event(event_type, signature, body)?;
match event {
WebhookEvent::Push(push_event) => {
println!("Push event: {} to {}",
push_event.commit_sha().unwrap_or_default(),
push_event.branch().unwrap_or_default());
// Only sync if push is to our tracked branch
if push_event.branch() == Some(self.config.branch.clone()) {
self.sync_and_reconcile().await?;
}
}
WebhookEvent::Ping => {
println!("Ping received - webhook configured correctly");
}
WebhookEvent::Unknown(event_type) => {
println!("Ignoring unknown event: {}", event_type);
}
}
Ok(())
}
async fn sync_and_reconcile(&self) -> Result<(), GitHubError> {
println!("Syncing repository...");
self.gitops.sync().await?;
println!("Loading manifests...");
let manifests: Vec<AppManifest> = self.gitops.load_all_manifests()?;
println!("Loaded {} manifests", manifests.len());
// Update state
let mut state = self.state.write().await;
state.manifests = manifests;
state.last_sync = Some(std::time::SystemTime::now());
// Reconcile (apply changes)
self.reconcile(&state.manifests).await?;
Ok(())
}
async fn reconcile(&self, manifests: &[AppManifest]) -> Result<(), GitHubError> {
println!("Reconciling {} manifests...", manifests.len());
for manifest in manifests {
if manifest.enabled {
println!(" ✓ {} v{} - enabled", manifest.name, manifest.version);
// Deploy or update application
} else {
println!(" ✗ {} v{} - disabled", manifest.name, manifest.version);
// Remove or disable application
}
}
Ok(())
}
async fn get_state(&self) -> BrokerState {
self.state.read().await.clone()
}
}
// Make BrokerState cloneable
impl Clone for BrokerState {
fn clone(&self) -> Self {
Self {
manifests: self.manifests.clone(),
last_sync: self.last_sync,
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load configuration
let config = GitHubAppConfig::new(
std::env::var("GITHUB_APP_ID")?.parse()?,
std::env::var("GITHUB_INSTALLATION_ID")?.parse()?,
std::fs::read_to_string(std::env::var("GITHUB_PRIVATE_KEY_PATH")?)?,
std::env::var("GITHUB_WEBHOOK_SECRET")?,
std::env::var("GITHUB_REPO")?,
std::env::var("GITHUB_BRANCH")?,
std::path::PathBuf::from(std::env::var("GIT_CLONE_PATH")?),
std::env::var("MANIFEST_GLOB")?,
);
// Initialize broker
let broker = Arc::new(GitOpsBroker::new(config).await?);
println!("GitOps Broker initialized successfully!");
// Start webhook server (example with Axum)
// ... (see webhook handling section)
Ok(())
}The library uses a unified GitHubError type for all errors:
use github_app::GitHubError;
async fn handle_operation() -> Result<(), GitHubError> {
// Your code here
Ok(())
}
// Pattern match on specific errors
match handle_operation().await {
Ok(_) => println!("Success!"),
Err(GitHubError::InvalidSignature) => {
eprintln!("Webhook signature verification failed");
}
Err(GitHubError::Git(msg)) => {
eprintln!("Git operation failed: {}", msg);
}
Err(GitHubError::Http(e)) => {
eprintln!("HTTP request failed: {}", e);
}
Err(GitHubError::Yaml(e)) => {
eprintln!("YAML parsing failed: {}", e);
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}- Never commit private keys or secrets to version control
- Use environment variables or secret management systems
- Validate webhook signatures before processing events
- Use HTTPS for webhook endpoints in production
- Let the library handle token caching - don't cache tokens yourself
- Share one
GitHubTokenProviderinstance across your application - The library automatically refreshes tokens before expiry
- Call
initialize()once at startup - Call
sync()when you receive webhook push events - Handle git errors gracefully (network issues, merge conflicts)
- Use specific manifest types with proper validation
- Always handle
GitHubError::InvalidSignaturefor webhooks (return 401) - Log all errors for debugging
- Implement retry logic for transient network errors
- Share instances across async tasks using
Arc - Use
tokio::sync::RwLockfor shared state - Process webhook events asynchronously
- Consider rate limiting for GitOps sync operations
Cause: Webhook secret mismatch or request tampering
Solution:
- Verify your webhook secret matches GitHub App settings
- Ensure you're passing the raw request body (not parsed JSON)
- Check header name:
X-Hub-Signature-256(notX-Hub-Signature)
// Correct: use raw bytes
let body = request.body().as_bytes();
verifier.parse_event(event_type, signature, body)?;
// Incorrect: don't parse first
let parsed = serde_json::from_slice(body)?; // ❌ Don't do thisCause: Invalid private key or incorrect App ID
Solution:
- Verify private key format (should be PEM format)
- Check App ID is correct
- Ensure private key file is readable
# Private key should look like this:
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
...
-----END RSA PRIVATE KEY-----Cause: Network issues, authentication problems, or local repo corruption
Solution:
- Check network connectivity to GitHub
- Verify installation token permissions
- Delete clone directory and re-initialize
- Check git is installed:
which git
Cause: Invalid YAML in manifest files
Solution:
- Validate YAML syntax:
yamllint manifests/ - Ensure manifest structure matches your struct
- Check for required fields
#[derive(Debug, Deserialize)]
struct Manifest {
name: String, // Required
version: String, // Required
description: Option<String>, // Optional
}If GitOps operations are slow:
- Reduce manifest glob scope: Use specific patterns like
manifests/*.yamlinstead of**/*.yaml - Limit sync frequency: Don't sync on every webhook event
- Use shallow clones: Modify
gitops.rsto use--depth 1
- GitHub Apps Documentation
- GitHub Webhooks Documentation
- Rust Tokio Documentation
- Serde Documentation
For issues, questions, or contributions, please open an issue in the repository.
MIT