• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lpenz / github-workflows-update / 17780379842

16 Sep 2025 09:58PM UTC coverage: 50.572% (+5.7%) from 44.828%
17780379842

push

github

lpenz
Cargo.*: increment version to 0.3.26

221 of 437 relevant lines covered (50.57%)

1.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.22
/src/workflow.rs
1
// Copyright (C) 2022 Leandro Lisboa Penz <lpenz@lpenz.org>
2
// This file is subject to the terms and conditions defined in
3
// file 'LICENSE', which is part of this source code package.
4

5
//! Workflow file parsing, into [`Workflow`] type.
6

7
use anyhow::{Result, anyhow};
8
use futures::future::join_all;
9
use serde_norway::Value;
10
use std::collections::HashMap;
11
use std::collections::HashSet;
12
use std::io;
13
use std::path;
14
use tokio::io::AsyncReadExt;
15
use tokio::io::AsyncWriteExt;
16
use tracing::Level;
17
use tracing::event;
18
use tracing::instrument;
19

20
use crate::proxy;
21
use crate::resource::Resource;
22
use crate::version::Version;
23

24
#[derive(Debug)]
25
pub struct Workflow {
26
    /// The name of the workflow file.
27
    pub filename: path::PathBuf,
28
    /// Contents of the workflow file as a `String`.
29
    pub contents: String,
30
    /// Set with all [`Resource`]s that the workflow `uses` along with the
31
    /// current versions.
32
    pub uses: HashSet<(Resource, Version)>,
33
    /// The latest version of each [`Resource`] as fetched from the
34
    /// upstream docker or github repository.
35
    pub latest: HashMap<Resource, Version>,
36
}
37

38
impl 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(&current_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))]
92
fn buf_parse(r: impl io::BufRead) -> Result<HashSet<(Resource, Version)>> {
1✔
93
    let data: serde_norway::Mapping = serde_norway::from_reader(r)?;
1✔
94
    let jobs = data
1✔
95
        .get(Value::String("jobs".into()))
1✔
96
        .ok_or_else(|| anyhow!("jobs entry not found"))?
1✔
97
        .as_mapping()
1✔
98
        .ok_or_else(|| anyhow!("invalid type for jobs entry"))?;
1✔
99
    let mut ret = HashSet::default();
1✔
100
    for (_, job) in jobs {
3✔
101
        if let Some(uses) = job.get(Value::String("uses".into())) {
2✔
102
            let reference = uses
1✔
103
                .as_str()
1✔
104
                .ok_or_else(|| anyhow!("invalid type for uses entry"))?;
1✔
105
            if let Ok((resource, version)) = Resource::parse(reference) {
1✔
106
                event!(
1✔
107
                    Level::INFO,
1✔
108
                    resource = %resource,
109
                    version = %version,
110
                    "parsed entity"
×
111
                );
112
                ret.insert((resource, version));
1✔
113
            } else {
114
                event!(
×
115
                    Level::WARN,
×
116
                    reference = reference,
117
                    "unable to parse resource"
×
118
                );
119
            }
120
        }
1✔
121
        if let Some(steps) = job.get(Value::String("steps".into())) {
2✔
122
            let steps = steps
1✔
123
                .as_sequence()
1✔
124
                .ok_or_else(|| anyhow!("invalid type for steps entry"))?;
1✔
125
            for step in steps {
4✔
126
                if let Some(uses) = step.get(Value::String("uses".into())) {
3✔
127
                    let reference = uses
2✔
128
                        .as_str()
2✔
129
                        .ok_or_else(|| anyhow!("invalid type for uses entry"))?;
2✔
130
                    if let Ok((resource, version)) = Resource::parse(reference) {
2✔
131
                        event!(
2✔
132
                            Level::INFO,
2✔
133
                            resource = %resource,
134
                            version = %version,
135
                            "parsed entity"
×
136
                        );
137
                        ret.insert((resource, version));
2✔
138
                    } else {
139
                        event!(
×
140
                            Level::WARN,
×
141
                            reference = reference,
142
                            "unable to parse resource"
×
143
                        );
144
                    }
145
                }
1✔
146
            }
147
        }
1✔
148
    }
149
    Ok(ret)
1✔
150
}
1✔
151

152
#[test]
153
fn test_parse() -> Result<()> {
1✔
154
    let s = r"
1✔
155
---
1✔
156
name: test
1✔
157
jobs:
1✔
158
  omnilint:
1✔
159
    runs-on: ubuntu-latest
1✔
160
    steps:
1✔
161
      - uses: actions/checkout@v2
1✔
162
      - uses: docker://lpenz/omnilint:0.4
1✔
163
      - run: ls
1✔
164
  rust:
1✔
165
    uses: lpenz/ghworkflow-rust/.github/workflows/rust.yml@v0.4
1✔
166
";
1✔
167
    buf_parse(s.as_bytes())?;
1✔
168
    Ok(())
1✔
169
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc