Skip to content

Commit 0bdc651

Browse files
committed
github: handle missing repository in pull request delivery
1 parent 7abf37c commit 0bdc651

File tree

4 files changed

+81
-43
lines changed

4 files changed

+81
-43
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/hooktypes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ license = "MPL-2.0"
88
doctest = false
99

1010
[dependencies]
11+
anyhow = { workspace = true }
1112
serde = { workspace = true }

github/hooktypes/src/lib.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/*
2-
* Copyright 2023 Oxide Computer Company
2+
* Copyright 2025 Oxide Computer Company
33
*/
44

5+
use anyhow::{bail, Result};
56
use serde::Deserialize;
67

78
#[derive(Deserialize, Debug)]
@@ -13,10 +14,38 @@ pub struct Payload {
1314
pub installation: Option<Installation>,
1415
pub check_suite: Option<CheckSuite>,
1516
pub check_run: Option<CheckRun>,
16-
pub pull_request: Option<PullRequest>,
17+
pull_request: Option<PullRequest>,
1718
pub requested_action: Option<RequestedAction>,
1819
}
1920

21+
impl Payload {
22+
pub fn pull_request(&self) -> Result<&PullRequest> {
23+
let mut problems = Vec::new();
24+
25+
if let Some(pr) = self.pull_request.as_ref() {
26+
/*
27+
* Unfortunately, GitHub has been known to occasionally omit the
28+
* repository object in web hooks that get delivered to us. If that
29+
* occurs, just drop the delivery.
30+
*/
31+
if pr.base.repo.is_none() {
32+
problems.push("missing pull_request.base.repo");
33+
}
34+
if pr.head.repo.is_none() {
35+
problems.push("missing pull_request.head.repo");
36+
}
37+
38+
if problems.is_empty() {
39+
return Ok(pr);
40+
}
41+
} else {
42+
problems.push("missing pull request information");
43+
}
44+
45+
bail!("{}", problems.join(", "));
46+
}
47+
}
48+
2049
#[derive(Deserialize, Debug)]
2150
pub struct User {
2251
pub login: String,
@@ -90,7 +119,7 @@ pub struct PullRequestCommit {
90119
pub ref_: String,
91120
pub sha: String,
92121
pub user: User,
93-
pub repo: Repository,
122+
pub repo: Option<Repository>,
94123
}
95124

96125
impl PullRequest {
@@ -99,6 +128,15 @@ impl PullRequest {
99128
}
100129
}
101130

131+
impl PullRequestCommit {
132+
pub fn repo(&self) -> &Repository {
133+
/*
134+
* This is validated in the pull_request() routine on the Payload.
135+
*/
136+
self.repo.as_ref().unwrap()
137+
}
138+
}
139+
102140
#[derive(Deserialize, Debug)]
103141
#[serde(rename_all = "snake_case")]
104142
pub enum PullRequestState {

github/server/src/main.rs

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Oxide Computer Company
2+
* Copyright 2025 Oxide Computer Company
33
*/
44

55
#![allow(clippy::vec_init_then_push)]
@@ -497,49 +497,47 @@ async fn process_deliveries(app: &Arc<App>) -> Result<()> {
497497
continue;
498498
};
499499

500-
let pr = if let Some(pr) = &payload.pull_request {
501-
info!(
502-
log,
503-
"del {}: pull request from {}/{} against {}/{}",
504-
del.seq,
505-
&pr.head.repo.owner.login,
506-
&pr.head.repo.name,
507-
&pr.base.repo.owner.login,
508-
&pr.base.repo.name
509-
);
510-
511-
if pr.base.repo.id != repo.id {
512-
warn!(
513-
log,
514-
"delivery {}: base repo {} != hook repo {}",
515-
del.seq,
516-
pr.base.repo.id,
517-
repo.id,
518-
);
500+
let pr = match payload.pull_request() {
501+
Ok(pr) => pr,
502+
Err(e) => {
503+
error!(log, "delivery {}: {e}", del.seq);
519504
app.db.delivery_ack(del.seq, ack)?;
520505
continue;
521506
}
507+
};
522508

523-
/*
524-
* Even though we do not technically need it, it may assist
525-
* with debugging to store the mapping from ID to owner/name
526-
* for the foreign repository.
527-
*/
528-
app.db.store_repository(
529-
pr.head.repo.id,
530-
&pr.head.repo.owner.login,
531-
&pr.head.repo.name,
532-
)?;
509+
info!(
510+
log,
511+
"del {}: pull request from {}/{} against {}/{}",
512+
del.seq,
513+
&pr.head.repo().owner.login,
514+
&pr.head.repo().name,
515+
&pr.base.repo().owner.login,
516+
&pr.base.repo().name
517+
);
533518

534-
pr
535-
} else {
536-
error!(
519+
if pr.base.repo().id != repo.id {
520+
warn!(
537521
log,
538-
"delivery {} missing pull request information", del.seq
522+
"delivery {}: base repo {} != hook repo {}",
523+
del.seq,
524+
pr.base.repo().id,
525+
repo.id,
539526
);
540527
app.db.delivery_ack(del.seq, ack)?;
541528
continue;
542-
};
529+
}
530+
531+
/*
532+
* Even though we do not technically need it, it may assist
533+
* with debugging to store the mapping from ID to owner/name
534+
* for the foreign repository.
535+
*/
536+
app.db.store_repository(
537+
pr.head.repo().id,
538+
&pr.head.repo().owner.login,
539+
&pr.head.repo().name,
540+
)?;
543541

544542
let gh = app.install_client(instid);
545543

@@ -551,8 +549,8 @@ async fn process_deliveries(app: &Arc<App>) -> Result<()> {
551549
let suites = gh
552550
.checks()
553551
.list_suites_for_ref(
554-
&pr.base.repo.owner.login,
555-
&pr.base.repo.name,
552+
&pr.base.repo().owner.login,
553+
&pr.base.repo().name,
556554
&pr.head.sha,
557555
app.config.id as i64,
558556
"",
@@ -593,8 +591,8 @@ async fn process_deliveries(app: &Arc<App>) -> Result<()> {
593591
let res = gh
594592
.checks()
595593
.create_suite(
596-
&pr.base.repo.owner.login,
597-
&pr.base.repo.name,
594+
&pr.base.repo().owner.login,
595+
&pr.base.repo().name,
598596
&ChecksCreateSuiteRequest {
599597
head_sha: pr.head.sha.to_string(),
600598
},
@@ -616,7 +614,7 @@ async fn process_deliveries(app: &Arc<App>) -> Result<()> {
616614
* or created.
617615
*/
618616
let mut cs = app.db.ensure_check_suite(
619-
pr.base.repo.id,
617+
pr.base.repo().id,
620618
instid,
621619
suite_id,
622620
&pr.head.sha,

0 commit comments

Comments
 (0)