# Tickborg — GitHub Integration ## Overview Tickborg communicates with GitHub through the **GitHub App** model. A custom fork of the `hubcaps` crate provides the Rust API client. Integration covers webhook reception, commit statuses, check runs, issue/PR manipulation, and comment posting. --- ## GitHub App Authentication ### `GithubAppVendingMachine` ```rust // config.rs pub struct GithubAppVendingMachine { conf: GithubAppConfig, current_token: Option, token_expiry: Option, } ``` Handles two-stage GitHub App auth: 1. **JWT**: Signed with the App's private RSA key, valid for up to 10 minutes. 2. **Installation token**: Obtained with the JWT, valid for ~1 hour. ### Token Lifecycle ```rust impl GithubAppVendingMachine { pub fn new(conf: GithubAppConfig) -> Self { GithubAppVendingMachine { conf, current_token: None, token_expiry: None, } } fn is_token_fresh(&self) -> bool { match self.token_expiry { Some(exp) => Instant::now() < exp, None => false, } } pub async fn get_token(&mut self) -> Result { if self.is_token_fresh() { return Ok(self.current_token.clone().unwrap()); } // Generate a fresh JWT let jwt = self.make_jwt()?; // Exchange JWT for installation token let client = hubcaps::Github::new( "tickborg".to_owned(), hubcaps::Credentials::Jwt(hubcaps::JwtToken::new(jwt)), )?; let installation = client.app() .find_repo_installation(&self.conf.owner, &self.conf.repo) .await?; let token_result = client.app() .create_installation_token(installation.id) .await?; self.current_token = Some(token_result.token.clone()); // Expire tokens 5 minutes early to avoid edge cases self.token_expiry = Some( Instant::now() + Duration::from_secs(55 * 60) - Duration::from_secs(5 * 60) ); Ok(token_result.token) } pub async fn github(&mut self) -> Result { let token = self.get_token().await?; Ok(hubcaps::Github::new( "tickborg".to_owned(), hubcaps::Credentials::Token(token), )?) } } ``` ### JWT Generation ```rust fn make_jwt(&self) -> Result { let now = SystemTime::now() .duration_since(UNIX_EPOCH).unwrap() .as_secs() as i64; let payload = json!({ "iat": now - 60, // 1 minute in the past (clock skew) "exp": now + (10 * 60), // 10 minutes from now "iss": self.conf.app_id, }); let key = EncodingKey::from_rsa_pem( &std::fs::read(&self.conf.private_key_file)? )?; encode(&Header::new(Algorithm::RS256), &payload, &key) .map_err(|e| format!("JWT encoding error: {}", e)) } ``` ### `GithubAppConfig` ```rust #[derive(Deserialize, Debug)] pub struct GithubAppConfig { pub app_id: u64, pub private_key_file: PathBuf, pub owner: String, pub repo: String, pub installation_id: Option, } ``` --- ## GitHub App Configuration The `GithubAppConfig` is nested under the top-level `Config`: ```json { "github_app": { "app_id": 12345, "private_key_file": "/etc/tickborg/private-key.pem", "owner": "project-tick", "repo": "Project-Tick", "installation_id": 67890 } } ``` --- ## Commit Statuses ### `CommitStatus` ```rust // commitstatus.rs pub struct CommitStatus { api: hubcaps::statuses::Statuses, sha: String, context: String, description: String, url: Option, } ``` ### State Machine ```rust impl CommitStatus { pub fn new( statuses: hubcaps::statuses::Statuses, sha: String, context: String, description: String, url: Option, ) -> Self; pub async fn set_url(&mut self, url: Option); pub async fn set_with_description( &mut self, description: &str, state: hubcaps::statuses::State, ) -> Result<(), CommitStatusError> { self.description = description.to_owned(); self.send_status(state).await } pub async fn set( &mut self, state: hubcaps::statuses::State, ) -> Result<(), CommitStatusError>; async fn send_status( &self, state: hubcaps::statuses::State, ) -> Result<(), CommitStatusError> { let options = hubcaps::statuses::StatusOptions::builder(state) .description(&self.description) .context(&self.context); let options = match &self.url { Some(u) => options.target_url(u).build(), None => options.build(), }; self.api.create(&self.sha, &options) .await .map_err(|e| CommitStatusError::from(e))?; Ok(()) } } ``` ### Error Classification ```rust #[derive(Debug)] pub enum CommitStatusError { ExpiredCreds(String), // GitHub App token expired MissingSha(String), // Commit was force-pushed away InternalError(String), // 5xx from GitHub API Error(String), // Other errors } ``` Error mapping from HTTP response: | HTTP Status | CommitStatusError Variant | Worker Action | |------------|--------------------------|---------------| | 401 | `ExpiredCreds` | `NackRequeue` (retry) | | 422 ("No commit found") | `MissingSha` | `Ack` (skip) | | 500-599 | `InternalError` | `NackRequeue` (retry) | | Other | `Error` | `Ack` + add error label | --- ## Check Runs ### `job_to_check()` Creates a Check Run when a build job is started: ```rust pub async fn job_to_check( github: &hubcaps::Github, repo_full_name: &str, job: &BuildJob, runner_identity: &str, ) -> Result<(), String> { let (owner, repo) = parse_repo_name(repo_full_name); let checks = github.repo(owner, repo).check_runs(); checks.create(&hubcaps::checks::CheckRunOptions { name: format!("build-{}-{}", job.project, job.system), head_sha: job.pr.head_sha.clone(), status: Some(hubcaps::checks::CheckRunStatus::InProgress), external_id: Some(format!("{runner_identity}")), started_at: Some(Utc::now()), output: Some(hubcaps::checks::Output { title: format!("Building {} on {}", job.project, job.system), summary: format!("Runner: {runner_identity}"), text: None, annotations: vec![], }), ..Default::default() }).await.map_err(|e| format!("Failed to create check run: {e}"))?; Ok(()) } ``` ### `result_to_check()` Updates a Check Run when a build completes: ```rust pub async fn result_to_check( github: &hubcaps::Github, repo_full_name: &str, result: &BuildResult, ) -> Result<(), String> { let (owner, repo) = parse_repo_name(repo_full_name); let checks = github.repo(owner, repo).check_runs(); let conclusion = match &result.status { BuildStatus::Success => hubcaps::checks::Conclusion::Success, BuildStatus::Failure => hubcaps::checks::Conclusion::Failure, BuildStatus::TimedOut => hubcaps::checks::Conclusion::TimedOut, BuildStatus::Skipped => hubcaps::checks::Conclusion::Skipped, BuildStatus::UnexpectedError { .. } => hubcaps::checks::Conclusion::Failure, }; // Find and update the existing check run // ... } ``` --- ## GitHub Event Types (ghevent) ### Common Types ```rust // ghevent/common.rs #[derive(Deserialize, Debug)] pub struct GenericWebhook { pub repository: Repository, } #[derive(Deserialize, Debug)] pub struct Repository { pub owner: User, pub name: String, pub full_name: String, pub clone_url: String, } #[derive(Deserialize, Debug)] pub struct User { pub login: String, pub id: u64, } #[derive(Deserialize, Debug)] pub struct Comment { pub id: u64, pub body: String, pub user: User, } #[derive(Deserialize, Debug)] pub struct Issue { pub number: u64, pub title: String, pub state: String, pub user: User, pub labels: Vec