github_workflow_update/updater/
github.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
5use async_trait::async_trait;
6use reqwest::header::USER_AGENT;
7use tracing::instrument;
8
9use crate::entity::Entity;
10use crate::error::Error;
11use crate::error::Result;
12use crate::updater;
13use crate::version::Version;
14
15#[derive(Debug)]
16pub struct Github {
17    re_ref: regex::Regex,
18}
19
20impl Default for Github {
21    fn default() -> Github {
22        Github {
23            re_ref: regex::Regex::new(r"^refs/tags/(?P<version>.+)$").unwrap(),
24        }
25    }
26}
27
28#[async_trait]
29impl updater::Updater for Github {
30    fn url(&self, resource: &str) -> Option<String> {
31        url(resource)
32    }
33
34    async fn get_versions(&self, url: &str) -> Result<Vec<Version>> {
35        get_versions(self, url).await
36    }
37
38    fn updated_line(&self, entity: &Entity) -> Option<String> {
39        let path = entity.resource.strip_prefix("github://")?;
40        entity.latest.as_ref().map(|v| format!("{}@{}", path, v))
41    }
42}
43
44#[instrument(level = "debug")]
45pub fn url(resource: &str) -> Option<String> {
46    resource.strip_prefix("github://").map(|path| {
47        format!(
48            "https://api.github.com/repos/{}/git/matching-refs/tags",
49            path
50        )
51    })
52}
53
54#[instrument(level = "debug")]
55async fn get_json(url: &str) -> Result<serde_json::Value> {
56    let client = reqwest::Client::new();
57    let mut builder = client.get(url);
58    builder = builder.header(USER_AGENT, "reqwest");
59    builder = builder.header("Accept", "application/vnd.github.v3+json");
60    if let Ok(token) = std::env::var("PERSONAL_TOKEN") {
61        builder = builder.header("Authorization", format!("token {}", token));
62    }
63    let response = builder.send().await?;
64    if !response.status().is_success() {
65        return Err(Error::HttpError(url.into(), response.status()));
66    }
67    Ok(response.json::<serde_json::Value>().await?)
68}
69
70#[instrument(level = "debug")]
71fn parse_versions(github: &Github, data: serde_json::Value) -> Result<Vec<Version>> {
72    data.as_array()
73        .ok_or_else(|| Error::JsonParsing("invalid type for layer object list".into()))?
74        .iter()
75        .map(|tag_obj| {
76            tag_obj
77                .as_object()
78                .ok_or_else(|| Error::JsonParsing("invalid type for tag object".into()))?
79                .get("ref")
80                .ok_or_else(|| Error::JsonParsing("ref field not found in tag object".into()))
81                .map(|ref_value| {
82                    let version_str = ref_value.as_str().ok_or_else(|| {
83                        Error::JsonParsing("invalid type for ref field in tag object".into())
84                    })?;
85                    let m = github.re_ref.captures(version_str).ok_or_else(|| {
86                        Error::JsonParsing(format!(
87                            "could not match github ref {} to tag regex",
88                            version_str
89                        ))
90                    })?;
91                    let version_str = m.name("version").unwrap().as_str();
92                    Version::new(version_str)
93                        .ok_or_else(|| Error::VersionParsing(version_str.into()))
94                })?
95        })
96        .collect::<Result<Vec<Version>>>()
97}
98
99#[instrument(level = "debug")]
100pub async fn get_versions(github: &Github, url: &str) -> Result<Vec<Version>> {
101    let data = get_json(url).await?;
102    let versions = parse_versions(github, data)?;
103    Ok(versions)
104}
105
106#[test]
107fn test_docker_parse_versions() -> Result<()> {
108    let json_str = r#"
109[
110  {
111    "ref": "refs/tags/v0.1",
112    "node_id": "REF_kwDOHcsoLq5yZWZzL3RhZ3MvdjAuMQ",
113    "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/refs/tags/v0.1",
114    "object": {
115      "sha": "ca550057e88e5885030e756b90bd040ad7840cee",
116      "type": "commit",
117      "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/commits/ca550057e88e5885030e756b90bd040ad7840cee"
118    }
119  },
120  {
121    "ref": "refs/tags/0.2",
122    "node_id": "REF_kwDOHcsoLq5yZWZzL3RhZ3MvdjAuMg",
123    "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/refs/tags/v0.2",
124    "object": {
125      "sha": "2b80e7d13e4b1738a17887b4d66143433267cea6",
126      "type": "commit",
127      "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/commits/2b80e7d13e4b1738a17887b4d66143433267cea6"
128    }
129  },
130  {
131    "ref": "refs/tags/latest",
132    "node_id": "REF_kwDOHcsoLq5yZWZzL3RhZ3MvdjAuMw",
133    "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/refs/tags/v0.3",
134    "object": {
135      "sha": "c7d367f5f10a2605aa43a540f9f88177d5fa12ac",
136      "type": "commit",
137      "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/commits/c7d367f5f10a2605aa43a540f9f88177d5fa12ac"
138    }
139  },
140  {
141    "ref": "refs/tags/v0.4",
142    "node_id": "REF_kwDOHcsoLq5yZWZzL3RhZ3MvdjAuNA",
143    "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/refs/tags/v0.4",
144    "object": {
145      "sha": "04bb04c23563d3302fe6ca0c2b832e9e67c47d58",
146      "type": "commit",
147      "url": "https://api.github.com/repos/lpenz/ghworkflow-rust/git/commits/04bb04c23563d3302fe6ca0c2b832e9e67c47d58"
148    }
149  }
150]
151"#;
152    let json_value: serde_json::Value = serde_json::from_str(json_str)?;
153    let gh = Github::default();
154    let mut versions = parse_versions(&gh, json_value)?
155        .into_iter()
156        .collect::<Vec<_>>();
157    versions.sort();
158    let versions = versions
159        .into_iter()
160        .map(|v| format!("{}", v))
161        .collect::<Vec<_>>();
162    assert_eq!(versions, ["latest", "v0.1", "0.2", "v0.4"]);
163    Ok(())
164}