github_workflows_update/
workflow.rs1use anyhow::{Result, anyhow};
8use futures::future::join_all;
9use serde_yml::Value;
10use std::collections::HashMap;
11use std::collections::HashSet;
12use std::io;
13use std::path;
14use tokio::io::AsyncReadExt;
15use tokio::io::AsyncWriteExt;
16use tracing::Level;
17use tracing::event;
18use tracing::instrument;
19
20use crate::proxy;
21use crate::resource::Resource;
22use crate::version::Version;
23
24#[derive(Debug)]
25pub struct Workflow {
26 pub filename: path::PathBuf,
28 pub contents: String,
30 pub uses: HashSet<(Resource, Version)>,
33 pub latest: HashMap<Resource, Version>,
36}
37
38impl Workflow {
39 #[instrument(level="debug", fields(filename = ?filename.as_ref().display()))]
40 pub async fn new(filename: impl AsRef<path::Path>) -> Result<Workflow> {
41 let filename = filename.as_ref();
42 let mut file = tokio::fs::File::open(filename).await?;
43 let mut contents = String::new();
44 file.read_to_string(&mut contents).await?;
45 let uses = buf_parse(contents.as_bytes())?;
46 Ok(Workflow {
47 filename: filename.to_owned(),
48 contents,
49 uses,
50 latest: Default::default(),
51 })
52 }
53
54 #[instrument(level = "debug")]
55 pub async fn fetch_latest_versions(&mut self, proxy_server: &proxy::Server) {
56 let tasks = self
57 .uses
58 .iter()
59 .map(|rv| (rv, proxy_server.new_client()))
60 .map(|((resource, current_version), proxy_client)| async move {
61 proxy_client
62 .fetch_latest_version(resource, current_version)
63 .await
64 });
65 self.latest = join_all(tasks)
66 .await
67 .into_iter()
68 .flatten()
69 .collect::<HashMap<_, _>>();
70 }
71
72 #[instrument(level = "debug")]
73 pub async fn update_file(&self) -> Result<bool> {
74 let mut contents = self.contents.clone();
75 for (resource, current_version) in &self.uses {
76 if let Some(latest_version) = self.latest.get(resource) {
77 let current_line = resource.versioned_string(current_version);
78 let latest_line = resource.versioned_string(latest_version);
79 contents = contents.replace(¤t_line, &latest_line);
80 }
81 }
82 let updated = contents != self.contents;
83 if updated {
84 let mut file = tokio::fs::File::create(&self.filename).await?;
85 file.write_all(contents.as_bytes()).await?;
86 }
87 Ok(updated)
88 }
89}
90
91#[instrument(level = "debug", skip(r))]
92fn buf_parse(r: impl io::BufRead) -> Result<HashSet<(Resource, Version)>> {
93 let data: serde_yml::Mapping = serde_yml::from_reader(r)?;
94 let jobs = data
95 .get(Value::String("jobs".into()))
96 .ok_or_else(|| anyhow!("jobs entry not found"))?
97 .as_mapping()
98 .ok_or_else(|| anyhow!("invalid type for jobs entry"))?;
99 let mut ret = HashSet::default();
100 for (_, job) in jobs {
101 if let Some(uses) = job.get(Value::String("uses".into())) {
102 let reference = uses
103 .as_str()
104 .ok_or_else(|| anyhow!("invalid type for uses entry"))?;
105 if let Ok((resource, version)) = Resource::parse(reference) {
106 event!(
107 Level::INFO,
108 resource = %resource,
109 version = %version,
110 "parsed entity"
111 );
112 ret.insert((resource, version));
113 } else {
114 event!(
115 Level::WARN,
116 reference = reference,
117 "unable to parse resource"
118 );
119 }
120 }
121 if let Some(steps) = job.get(Value::String("steps".into())) {
122 let steps = steps
123 .as_sequence()
124 .ok_or_else(|| anyhow!("invalid type for steps entry"))?;
125 for step in steps {
126 if let Some(uses) = step.get(Value::String("uses".into())) {
127 let reference = uses
128 .as_str()
129 .ok_or_else(|| anyhow!("invalid type for uses entry"))?;
130 if let Ok((resource, version)) = Resource::parse(reference) {
131 event!(
132 Level::INFO,
133 resource = %resource,
134 version = %version,
135 "parsed entity"
136 );
137 ret.insert((resource, version));
138 } else {
139 event!(
140 Level::WARN,
141 reference = reference,
142 "unable to parse resource"
143 );
144 }
145 }
146 }
147 }
148 }
149 Ok(ret)
150}
151
152#[test]
153fn test_parse() -> Result<()> {
154 let s = r"
155---
156name: test
157jobs:
158 omnilint:
159 runs-on: ubuntu-latest
160 steps:
161 - uses: actions/checkout@v2
162 - uses: docker://lpenz/omnilint:0.4
163 - run: ls
164 rust:
165 uses: lpenz/ghworkflow-rust/.github/workflows/rust.yml@v0.4
166";
167 buf_parse(s.as_bytes())?;
168 Ok(())
169}