Skip to content

Commit 5ac03f9

Browse files
committed
github, jobsh: split some of the basic variety code into a reusable crate
1 parent caa66dd commit 5ac03f9

File tree

23 files changed

+1303
-991
lines changed

23 files changed

+1303
-991
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ members = [
1717
"github/hooktypes",
1818
"github/server",
1919
"github/testdata",
20+
"jobsh",
2021
"server",
2122
"sse",
2223
"types",

common/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use rand::distributions::Alphanumeric;
1313
use rand::{thread_rng, Rng};
1414
use regex::Regex;
1515
use rusty_ulid::Ulid;
16-
use serde::Deserialize;
16+
use serde::{Deserialize, Serialize};
1717
use slog::{o, Drain, Logger};
1818

1919
pub fn read_toml<P: AsRef<Path>, T>(n: P) -> Result<T>
@@ -207,3 +207,10 @@ pub fn looks_like_a_ulid(s: &str) -> bool {
207207
pub fn true_if_missing() -> bool {
208208
true
209209
}
210+
211+
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
212+
#[serde(untagged)]
213+
pub enum StringOrBool {
214+
String(String),
215+
Bool(bool),
216+
}

github/database/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ doctest = false
1111
buildomat-common = { path = "../../common" }
1212
buildomat-database = { path = "../../database" }
1313
buildomat-github-hooktypes = { path = "../hooktypes" }
14+
buildomat-jobsh = { path = "../../jobsh" }
1415

1516
anyhow = { workspace = true }
1617
chrono = { workspace = true }

github/database/src/tables/check_run.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ pub struct JobFileDepend {
187187
pub config: serde_json::Value,
188188
}
189189

190+
impl From<buildomat_jobsh::jobfile::JobFileDepend> for JobFileDepend {
191+
fn from(value: buildomat_jobsh::jobfile::JobFileDepend) -> Self {
192+
let buildomat_jobsh::jobfile::JobFileDepend { job, config } = value;
193+
194+
Self { job, config }
195+
}
196+
}
197+
190198
pub struct CheckRunDependency(JobFileDepend);
191199

192200
impl CheckRunDependency {

github/database/src/tables/check_suite.rs

Lines changed: 25 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Copyright 2024 Oxide Computer Company
33
*/
44

5+
use buildomat_jobsh::jobfile;
6+
57
use super::check_run::{CheckRunVariety, JobFileDepend};
68
use super::sublude::*;
79

@@ -38,114 +40,30 @@ pub struct JobFile {
3840
pub dependencies: HashMap<String, JobFileDepend>,
3941
}
4042

41-
#[derive(Deserialize)]
42-
struct FrontMatter {
43-
name: String,
44-
variety: CheckRunVariety,
45-
#[serde(default = "true_if_missing")]
46-
enable: bool,
47-
#[serde(default)]
48-
dependencies: HashMap<String, FrontMatterDepend>,
49-
#[serde(flatten)]
50-
extra: toml::Value,
51-
}
52-
53-
#[derive(Deserialize)]
54-
struct FrontMatterDepend {
55-
job: String,
56-
#[serde(flatten)]
57-
extra: toml::Value,
58-
}
59-
60-
impl JobFile {
61-
/**
62-
* Lift the TOML frontmatter out of a job file and parse the global values.
63-
* Returns None if the file is valid, but not enabled.
64-
*/
65-
pub fn parse_content_at_path(
66-
content: &str,
67-
path: &str,
68-
) -> Result<Option<JobFile>> {
69-
let mut lines = content.lines();
70-
71-
if let Some(shebang) = lines.next() {
72-
/*
73-
* For now, we accept any script and assume it is effectively
74-
* bourne-compatible, at least with respect to comments.
75-
*/
76-
if !shebang.starts_with("#!") {
77-
bail!("{:?} must have an interpreter line", path);
78-
}
79-
};
80-
81-
/*
82-
* Extract lines after the interpreter line that begin with "#:". Treat
83-
* this as a TOML block wrapped in something that bourne shells will
84-
* ignore as a comment. Allow the use of regular comments interspersed
85-
* with TOML lines, as long as there are no blank lines.
86-
*/
87-
let frontmatter = lines
88-
.by_ref()
89-
.take_while(|l| l.starts_with('#'))
90-
.filter(|l| l.starts_with("#:"))
91-
.map(|l| l.trim_start_matches("#:"))
92-
.collect::<Vec<_>>()
93-
.join("\n");
94-
95-
/*
96-
* Parse the front matter as TOML:
97-
*/
98-
let toml =
99-
toml::from_str::<FrontMatter>(&frontmatter).map_err(|e| {
100-
anyhow!("TOML front matter in {path:?}: {}", e.message())
101-
})?;
102-
103-
if !toml.enable {
104-
/*
105-
* Skip job files that have been marked as disabled.
106-
*/
107-
return Ok(None);
43+
impl From<jobfile::JobFile> for JobFile {
44+
fn from(value: jobfile::JobFile) -> Self {
45+
let jobfile::JobFile {
46+
path,
47+
name,
48+
variety,
49+
config,
50+
content,
51+
dependencies,
52+
} = value;
53+
54+
Self {
55+
path,
56+
name,
57+
variety: match variety {
58+
jobfile::Variety::Basic => CheckRunVariety::Basic,
59+
},
60+
config,
61+
content,
62+
dependencies: dependencies
63+
.into_iter()
64+
.map(|(a, b)| (a.into(), b.into()))
65+
.collect(),
10866
}
109-
110-
/*
111-
* Rule out some common mispellings of "enable", before we get
112-
* all the way into variety processing:
113-
*/
114-
if toml.extra.get("enabled").is_some()
115-
|| toml.extra.get("disable").is_some()
116-
|| toml.extra.get("disabled").is_some()
117-
{
118-
bail!(
119-
"TOML front matter in {path:?}: \
120-
use \"enable\" to disable a job"
121-
);
122-
}
123-
124-
Ok(Some(JobFile {
125-
path: path.to_string(),
126-
name: toml.name.to_string(),
127-
variety: toml.variety,
128-
/*
129-
* The use of the flattened "extra" member here is critical, as it
130-
* allows varieties to use "deny_unknown_fields" on their subset of
131-
* the frontmatter because we have subtracted the global parts here.
132-
*/
133-
config: serde_json::to_value(&toml.extra)?,
134-
content: content.to_string(),
135-
dependencies: toml
136-
.dependencies
137-
.iter()
138-
.map(|(name, dep)| {
139-
Ok((
140-
name.to_string(),
141-
JobFileDepend {
142-
job: dep.job.to_string(),
143-
config: serde_json::to_value(&dep.extra)?,
144-
},
145-
))
146-
})
147-
.collect::<Result<_>>()?,
148-
}))
14967
}
15068
}
15169

github/database/src/tables/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ mod sublude {
77
pub use std::str::FromStr;
88

99
pub use crate::itypes::*;
10+
#[allow(unused_imports)]
1011
pub use anyhow::{anyhow, bail, Result};
12+
#[allow(unused_imports)]
1113
pub use buildomat_common::*;
1214
pub use buildomat_database::{
1315
rusqlite, sqlite_json_new_type, sqlite_sql_enum, FromRow,

github/server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ buildomat-download = { path = "../../download" }
1212
buildomat-github-client = { path = "../client" }
1313
buildomat-github-database = { path = "../database" }
1414
buildomat-github-hooktypes = { path = "../hooktypes" }
15+
buildomat-jobsh = { path = "../../jobsh" }
1516
buildomat-sse = { path = "../../sse" }
1617

1718
ansi-to-html = { workspace = true }

github/server/src/http.rs

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use buildomat_client::prelude::*;
77
use buildomat_common::*;
88
use buildomat_download::RequestContextEx;
99
use buildomat_github_database::types::*;
10-
use buildomat_sse::{HeaderMapEx, ServerSentEvents};
10+
use buildomat_sse::HeaderMapEx;
1111
use chrono::prelude::*;
1212
use dropshot::{
1313
endpoint, ConfigDropshot, HttpError, HttpResponseOk, RequestContext,
@@ -309,7 +309,7 @@ async fn details(
309309
* The <style> tag needs to appear inside the <head>:
310310
*/
311311
out += "<style>\n";
312-
out += &app.templates.load("variety/basic/style.css")?;
312+
out += &app.templates.load("variety/basic/www/style.css")?;
313313
out += "</style>\n";
314314
}
315315
out += "</head>\n";
@@ -381,41 +381,6 @@ async fn details_live(
381381
let path = path.into_inner();
382382
let query = query.into_inner();
383383

384-
/*
385-
* The "Last-Event-ID" header will be sent by a browser when reconnecting,
386-
* with the "id" field of the last event it saw in the previous stream. The
387-
* event stream for this endpoint is a mixture of job events, which have a
388-
* well-defined sequence number, and status change events, which do not.
389-
*
390-
* We include in each ID value the sequence number of the most recently sent
391-
* job event, so that we can always seek to the right point in the events
392-
* for the job. This may lead to more than one event with the same sequence
393-
* number, but that doesn't appear to be a problem in practice.
394-
*
395-
* Note that this value must take precedence over the query parameter, as a
396-
* resumed stream from the browser will, each time it reconnects, include
397-
* the original query string we gave to the EventSource. It will only
398-
* include the header on subsequent retries once it has seen at least one
399-
* event.
400-
*/
401-
let mut minseq = None;
402-
if let Some(lei) = rc.request.headers().last_event_id() {
403-
if let Some(num) = lei.strip_prefix("seq-") {
404-
if let Ok(lei_seq) = num.parse::<u32>() {
405-
if let Some(lei_seq) = lei_seq.checked_add(1) {
406-
/*
407-
* Resume the event stream from this earlier point.
408-
*/
409-
minseq = Some(lei_seq);
410-
}
411-
}
412-
}
413-
}
414-
415-
if minseq.is_none() {
416-
minseq = query.minseq;
417-
}
418-
419384
let load = match path.load(&rc) {
420385
Ok(Some(load)) => load,
421386
Ok(None) => return html_404(false),
@@ -425,28 +390,26 @@ async fn details_live(
425390
}
426391
};
427392

428-
let mut sse = ServerSentEvents::default();
429-
let response = match sse.to_response() {
430-
Ok(response) => response,
431-
Err(e) => {
432-
error!(rc.log, "details live: sse: {e}");
433-
return html_500(&rc.request_id, false);
434-
}
435-
};
436-
437-
let ok = match load.cr.variety {
393+
let res = match load.cr.variety {
438394
CheckRunVariety::Basic => {
439-
variety::basic::live(app, &load.cs, &load.cr, minseq, sse).await
395+
variety::basic::live(
396+
app,
397+
&load.cs,
398+
&load.cr,
399+
rc.request.headers().last_event_id(),
400+
query.minseq,
401+
)
402+
.await
440403
}
441404
/*
442405
* No other variety exposes live details:
443406
*/
444-
_ => Ok(false),
407+
_ => Ok(None),
445408
};
446409

447-
match ok {
448-
Ok(true) => Ok(response),
449-
Ok(false) => html_404(false),
410+
match res {
411+
Ok(Some(res)) => Ok(res),
412+
Ok(None) => html_404(false),
450413
Err(e) => {
451414
error!(rc.log, "details live: {e}");
452415
html_500(&rc.request_id, false)

0 commit comments

Comments
 (0)