github_workflow_update/
workflow.rs1use anyhow::{anyhow, Result};
11use futures::future::join_all;
12use regex::Regex;
13use serde_yaml::Value;
14use std::collections::HashSet;
15use std::io;
16use std::path;
17use tokio::io::AsyncReadExt;
18use tokio::io::AsyncWriteExt;
19use tracing::event;
20use tracing::instrument;
21use tracing::Level;
22
23use crate::entity::Entity;
24use crate::resolver;
25use crate::version::Version;
26
27#[derive(Debug)]
28pub struct Workflow {
29 pub filename: path::PathBuf,
30 pub contents: String,
31 pub entities: HashSet<Entity>,
32}
33
34impl Workflow {
35 #[instrument(level="debug", fields(filename = ?filename.as_ref().display()))]
36 pub async fn new(filename: impl AsRef<path::Path>) -> Result<Workflow> {
37 let filename = filename.as_ref();
38 let mut file = tokio::fs::File::open(filename).await?;
39 let mut contents = String::new();
40 file.read_to_string(&mut contents).await?;
41 let entities = buf_parse(contents.as_bytes())?;
42 Ok(Workflow {
43 filename: filename.to_owned(),
44 contents,
45 entities,
46 })
47 }
48
49 #[instrument(level = "debug")]
50 pub async fn resolve_entities(&mut self, resolver: &resolver::Server) {
51 let entities = std::mem::take(&mut self.entities);
52 let resolve_entity_tasks = entities
53 .into_iter()
54 .map(|e| (e, resolver.new_client()))
55 .map(|(e, resolver_client)| async move { resolver_client.resolve_entity(e).await });
56 self.entities = join_all(resolve_entity_tasks)
57 .await
58 .into_iter()
59 .collect::<HashSet<_>>();
60 }
61
62 #[instrument(level = "debug")]
63 pub async fn update_file(&self) -> Result<bool> {
64 let mut contents = self.contents.clone();
65 for entity in &self.entities {
66 if let Some(updated_line) = &entity.updated_line {
67 contents = contents.replace(&entity.line, updated_line);
68 }
69 }
70 let updated = contents != self.contents;
71 if updated {
72 let mut file = tokio::fs::File::create(&self.filename).await?;
73 file.write_all(contents.as_bytes()).await?;
74 }
75 Ok(updated)
76 }
77}
78
79#[instrument(level = "debug")]
80fn reference_parse_version(
81 re_docker: &Regex,
82 re_github: &Regex,
83 reference: &str,
84) -> Option<(String, Version)> {
85 if let Some(m) = re_docker.captures(reference) {
86 return Some((
87 m.name("resource").unwrap().as_str().into(),
88 Version::new(m.name("version").unwrap().as_str())?,
89 ));
90 }
91 if let Some(m) = re_github.captures(reference) {
92 return Some((
93 format!("github://{}", m.name("userrepo").unwrap().as_str()),
94 Version::new(m.name("version").unwrap().as_str())?,
95 ));
96 }
97 None
98}
99
100#[instrument(level = "debug", skip(r))]
101fn buf_parse(r: impl io::BufRead) -> Result<HashSet<Entity>> {
102 let data: serde_yaml::Mapping = serde_yaml::from_reader(r)?;
103 let jobs = data
104 .get(&Value::String("jobs".into()))
105 .ok_or_else(|| anyhow!("jobs entry not found"))?
106 .as_mapping()
107 .ok_or_else(|| anyhow!("invalid type for jobs entry"))?;
108 let mut ret = HashSet::default();
109 let re_docker = Regex::new(r"^(?P<resource>docker://[^:]+):(?P<version>[^:]+)$").unwrap();
110 let re_github = Regex::new(r"^(?P<userrepo>[^/]+/[^@]+)@(?P<version>[^@]+)$").unwrap();
111 for (_, job) in jobs {
112 if let Some(steps) = job.get(&Value::String("steps".into())) {
113 let steps = steps
114 .as_sequence()
115 .ok_or_else(|| anyhow!("invalid type for steps entry"))?;
116 for step in steps {
117 if let Some(uses) = step.get(&Value::String("uses".into())) {
118 let reference = uses
119 .as_str()
120 .ok_or_else(|| anyhow!("invalid type for uses entry"))?;
121 if let Some((resource, version)) =
122 reference_parse_version(&re_docker, &re_github, reference)
123 {
124 let entity = Entity {
125 line: reference.into(),
126 resource,
127 version,
128 ..Default::default()
129 };
130 event!(Level::INFO, reference = reference, "parsed entity");
131 ret.insert(entity);
132 } else {
133 event!(Level::WARN, reference = reference, "entity not parsed");
134 }
135 }
136 }
137 }
138 }
139 Ok(ret)
140}
141
142#[test]
143fn test_parse() -> Result<()> {
144 let s = r"
145---
146name: test
147jobs:
148 omnilint:
149 runs-on: ubuntu-latest
150 steps:
151 - uses: actions/checkout@v2
152 - uses: docker://lpenz/omnilint:0.4
153 - run: ls
154";
155 buf_parse(s.as_bytes())?;
156 Ok(())
157}