Split main into multiple files
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
9ef11baff4
commit
78ec51ec12
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -906,7 +906,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
|||
|
||||
[[package]]
|
||||
name = "release-checker"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"regex",
|
||||
|
|
71
src/alpine.rs
Normal file
71
src/alpine.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AlpineError {
|
||||
NoPackage,
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AlpineError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoPackage => write!(f, "No package found for query"),
|
||||
Self::VersionMismatch => write!(f, "Multiple versions detected for architectures"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AlpineError {}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn check_alpine_package(
|
||||
package: String,
|
||||
branch: String,
|
||||
architectures: Vec<String>,
|
||||
) -> color_eyre::eyre::Result<Option<String>> {
|
||||
let alpine_client = reqwest::Client::builder()
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
let url = format!(
|
||||
"https://pkgs.alpinelinux.org/packages?name={}&branch={}",
|
||||
package, branch
|
||||
);
|
||||
|
||||
let response = alpine_client.get(url).send().await?;
|
||||
|
||||
let text = response.text().await?;
|
||||
|
||||
let document = scraper::Html::parse_document(&text);
|
||||
|
||||
let tr_selector = scraper::Selector::parse("tr").expect("'tr' is valid selector");
|
||||
let version_selector = scraper::Selector::parse("td.version strong a")
|
||||
.expect("'td.version strong a' is valid selector");
|
||||
let arch_selector =
|
||||
scraper::Selector::parse("td.arch a").expect("'td.arch a' is valid selector");
|
||||
|
||||
let mut versions = HashSet::new();
|
||||
|
||||
for row in document.select(&tr_selector) {
|
||||
if let Some(arch) = row.select(&arch_selector).next() {
|
||||
if architectures
|
||||
.iter()
|
||||
.any(|architecture| architecture == arch.inner_html().trim())
|
||||
{
|
||||
if let Some(version) = row.select(&version_selector).next() {
|
||||
versions.insert(version.inner_html().trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if versions.len() > 1 {
|
||||
return Err(AlpineError::VersionMismatch.into());
|
||||
}
|
||||
|
||||
if versions.is_empty() {
|
||||
return Err(AlpineError::NoPackage.into());
|
||||
}
|
||||
|
||||
Ok(versions.into_iter().next())
|
||||
}
|
53
src/config.rs
Normal file
53
src/config.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) struct CheckConfig {
|
||||
pub(crate) project: ProjectConfig,
|
||||
pub(crate) gitea: GiteaConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "kind")]
|
||||
pub(crate) enum ProjectConfig {
|
||||
None,
|
||||
|
||||
#[serde(rename = "dockerhub")]
|
||||
DockerHub {
|
||||
namespace: String,
|
||||
repository: String,
|
||||
regex: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "alpine")]
|
||||
Alpine {
|
||||
package: String,
|
||||
branch: String,
|
||||
architectures: Vec<String>,
|
||||
},
|
||||
|
||||
#[serde(rename = "git")]
|
||||
Git {
|
||||
repository: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
language: LanguageConfig,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "language")]
|
||||
pub(crate) enum LanguageConfig {
|
||||
#[serde(rename = "rust")]
|
||||
Rust { config: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) struct GiteaConfig {
|
||||
pub(crate) domain: String,
|
||||
pub(crate) owner: String,
|
||||
pub(crate) repo: String,
|
||||
}
|
50
src/dockerhub.rs
Normal file
50
src/dockerhub.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
#[derive(Debug)]
|
||||
enum DockerError {
|
||||
NoTag,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DockerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoTag => write!(f, "No tag found for query"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DockerError {}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn check_dockerhub_image(
|
||||
namespace: String,
|
||||
repository: String,
|
||||
regex: String,
|
||||
) -> color_eyre::eyre::Result<String> {
|
||||
let regex = regex::Regex::new(®ex)?;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TagResult {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TagsResponse {
|
||||
results: Vec<TagResult>,
|
||||
}
|
||||
|
||||
let images_url = format!("https://registry.hub.docker.com/v2/repositories/{}/{}/tags?currently_tagged=true,ordering=last_updated,page_size=30,status=active", namespace, repository);
|
||||
let tags: TagsResponse = client.get(images_url).send().await?.json().await?;
|
||||
|
||||
for result in tags.results {
|
||||
if let Some(matched) = regex.find(&result.name) {
|
||||
if matched.as_str() == result.name {
|
||||
return Ok(result.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(DockerError::NoTag.into())
|
||||
}
|
63
src/git.rs
Normal file
63
src/git.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::config::LanguageConfig;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct CargoToml {
|
||||
package: CargoPackage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct CargoPackage {
|
||||
version: String,
|
||||
}
|
||||
|
||||
struct RemoveOnDrop<'a>(&'a str);
|
||||
|
||||
impl<'a> Drop for RemoveOnDrop<'a> {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GitError {
|
||||
Clone,
|
||||
}
|
||||
impl std::fmt::Display for GitError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Clone => write!(f, "Failed to clone repository"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for GitError {}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn check_git_project(
|
||||
repository: String,
|
||||
language: LanguageConfig,
|
||||
) -> color_eyre::eyre::Result<String> {
|
||||
static CHECK_REPO: &str = "checked-project";
|
||||
let _remover = RemoveOnDrop(CHECK_REPO);
|
||||
|
||||
let output = tokio::process::Command::new("git")
|
||||
.args(["clone", &repository, CHECK_REPO])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GitError::Clone.into());
|
||||
}
|
||||
|
||||
match language {
|
||||
LanguageConfig::Rust { config } => {
|
||||
let config_path = PathBuf::from(CHECK_REPO).join(config);
|
||||
|
||||
let config_string = tokio::fs::read_to_string(&config_path).await?;
|
||||
|
||||
let rust_config: CargoToml = toml::from_str(&config_string)?;
|
||||
|
||||
Ok(format!("v{}", rust_config.package.version))
|
||||
}
|
||||
}
|
||||
}
|
97
src/gitea.rs
Normal file
97
src/gitea.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use crate::{config::GiteaConfig, revision::Revision};
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, StatusCode,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
struct GiteaTag {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct GiteaTagResponse {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TagError {
|
||||
Status(StatusCode),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TagError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Status(code) => {
|
||||
write!(f, "Failed to create tag, invalid response status: {}", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TagError {}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) fn build_client(gitea_token: &str) -> color_eyre::eyre::Result<Client> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("token {}", gitea_token))?,
|
||||
);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(client))]
|
||||
pub(crate) async fn get_previous_revision(
|
||||
client: &Client,
|
||||
config: &GiteaConfig,
|
||||
) -> color_eyre::eyre::Result<Revision> {
|
||||
let tags_url = format!(
|
||||
"https://{}/api/v1/repos/{}/{}/tags?page=1&limit=4",
|
||||
config.domain, config.owner, config.repo
|
||||
);
|
||||
let tags_response = client.get(tags_url).send().await?;
|
||||
|
||||
let tags: Vec<GiteaTagResponse> = tags_response.json().await?;
|
||||
|
||||
let revision = if let Some(tag) = tags.get(0) {
|
||||
tracing::info!("tag: {}", tag.name);
|
||||
tag.name.parse()?
|
||||
} else {
|
||||
Revision::default()
|
||||
};
|
||||
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(client))]
|
||||
pub(crate) async fn create_tag(
|
||||
client: &Client,
|
||||
config: &GiteaConfig,
|
||||
revision: &Revision,
|
||||
) -> color_eyre::eyre::Result<()> {
|
||||
let tag_url = format!(
|
||||
"https://{}/api/v1/repos/{}/{}/tags",
|
||||
config.domain, config.owner, config.repo
|
||||
);
|
||||
|
||||
let response = client
|
||||
.post(tag_url)
|
||||
.json(&GiteaTag {
|
||||
tag_name: revision.to_string(),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(TagError::Status(response.status()).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
17
src/init_tracing.rs
Normal file
17
src/init_tracing.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use tracing::subscriber::set_global_default;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::{EnvFilter, Registry};
|
||||
|
||||
pub(crate) fn init_tracing() -> color_eyre::eyre::Result<()> {
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
let format_layer = tracing_subscriber::fmt::layer().pretty();
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(env_filter)
|
||||
.with(format_layer)
|
||||
.with(ErrorLayer::default());
|
||||
|
||||
set_global_default(subscriber)?;
|
||||
Ok(())
|
||||
}
|
430
src/main.rs
430
src/main.rs
|
@ -1,419 +1,17 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::num::ParseIntError;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use tracing::subscriber::set_global_default;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::{EnvFilter, Registry};
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct CheckConfig {
|
||||
project: ProjectConfig,
|
||||
gitea: GiteaConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "kind")]
|
||||
enum ProjectConfig {
|
||||
None,
|
||||
|
||||
#[serde(rename = "dockerhub")]
|
||||
DockerHub {
|
||||
namespace: String,
|
||||
repository: String,
|
||||
regex: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "alpine")]
|
||||
Alpine {
|
||||
package: String,
|
||||
branch: String,
|
||||
architectures: Vec<String>,
|
||||
},
|
||||
|
||||
#[serde(rename = "git")]
|
||||
Git {
|
||||
repository: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
language: LanguageConfig,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "language")]
|
||||
enum LanguageConfig {
|
||||
#[serde(rename = "rust")]
|
||||
Rust { config: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct GiteaConfig {
|
||||
domain: String,
|
||||
owner: String,
|
||||
repo: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct CargoToml {
|
||||
package: CargoPackage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct CargoPackage {
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Revision {
|
||||
version: String,
|
||||
revision: usize,
|
||||
}
|
||||
|
||||
impl Revision {
|
||||
fn next(self, version_string: Option<String>) -> Self {
|
||||
if let Some(version_string) = version_string {
|
||||
if self.version == version_string {
|
||||
Revision {
|
||||
revision: self.revision + 1,
|
||||
..self
|
||||
}
|
||||
} else {
|
||||
Revision {
|
||||
version: version_string,
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Revision {
|
||||
revision: self.revision + 1,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Revision {
|
||||
type Err = RevisionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some((version, revision)) = s.rsplit_once('-') {
|
||||
let revision = revision
|
||||
.trim_start_matches('r')
|
||||
.parse()
|
||||
.map_err(RevisionError::Revision)?;
|
||||
|
||||
Ok(Revision {
|
||||
version: version.to_owned(),
|
||||
revision,
|
||||
})
|
||||
} else {
|
||||
Ok(Revision {
|
||||
version: s.to_string(),
|
||||
revision: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Revision {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}-r{}", self.version, self.revision)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
struct GiteaTag {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct GiteaTagResponse {
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct RemoveOnDrop<'a>(&'a str);
|
||||
|
||||
impl<'a> Drop for RemoveOnDrop<'a> {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GitError {
|
||||
Clone,
|
||||
}
|
||||
impl Display for GitError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Clone => write!(f, "Failed to clone repository"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for GitError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TagError {
|
||||
Status(StatusCode),
|
||||
}
|
||||
impl Display for TagError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Status(code) => {
|
||||
write!(f, "Failed to create tag, invalid response status: {}", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for TagError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RevisionError {
|
||||
Revision(ParseIntError),
|
||||
}
|
||||
impl Display for RevisionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Revision(e) => write!(f, "Failed to parse revision number, {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for RevisionError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AlpineError {
|
||||
NoPackage,
|
||||
VersionMismatch,
|
||||
}
|
||||
impl Display for AlpineError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoPackage => write!(f, "No package found for query"),
|
||||
Self::VersionMismatch => write!(f, "Multiple versions detected for architectures"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for AlpineError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DockerError {
|
||||
NoTag,
|
||||
}
|
||||
impl Display for DockerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoTag => write!(f, "No tag found for query"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for DockerError {}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn check_git_project(
|
||||
repository: String,
|
||||
language: LanguageConfig,
|
||||
) -> color_eyre::eyre::Result<String> {
|
||||
static CHECK_REPO: &str = "checked-project";
|
||||
let _remover = RemoveOnDrop(CHECK_REPO);
|
||||
|
||||
let output = tokio::process::Command::new("git")
|
||||
.args(["clone", &repository, CHECK_REPO])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GitError::Clone.into());
|
||||
}
|
||||
|
||||
match language {
|
||||
LanguageConfig::Rust { config } => {
|
||||
let config_path = PathBuf::from(CHECK_REPO).join(config);
|
||||
|
||||
let config_string = tokio::fs::read_to_string(&config_path).await?;
|
||||
|
||||
let rust_config: CargoToml = toml::from_str(&config_string)?;
|
||||
|
||||
Ok(format!("v{}", rust_config.package.version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn check_alpine_package(
|
||||
package: String,
|
||||
branch: String,
|
||||
architectures: Vec<String>,
|
||||
) -> color_eyre::eyre::Result<Option<String>> {
|
||||
let alpine_client = reqwest::Client::builder()
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
let url = format!(
|
||||
"https://pkgs.alpinelinux.org/packages?name={}&branch={}",
|
||||
package, branch
|
||||
);
|
||||
|
||||
let response = alpine_client.get(url).send().await?;
|
||||
|
||||
let text = response.text().await?;
|
||||
|
||||
let document = scraper::Html::parse_document(&text);
|
||||
|
||||
let tr_selector = scraper::Selector::parse("tr").expect("'tr' is valid selector");
|
||||
let version_selector = scraper::Selector::parse("td.version strong a")
|
||||
.expect("'td.version strong a' is valid selector");
|
||||
let arch_selector =
|
||||
scraper::Selector::parse("td.arch a").expect("'td.arch a' is valid selector");
|
||||
|
||||
let mut versions = HashSet::new();
|
||||
|
||||
for row in document.select(&tr_selector) {
|
||||
if let Some(arch) = row.select(&arch_selector).next() {
|
||||
if architectures
|
||||
.iter()
|
||||
.any(|architecture| architecture == arch.inner_html().trim())
|
||||
{
|
||||
if let Some(version) = row.select(&version_selector).next() {
|
||||
versions.insert(version.inner_html().trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if versions.len() > 1 {
|
||||
return Err(AlpineError::VersionMismatch.into());
|
||||
}
|
||||
|
||||
if versions.is_empty() {
|
||||
return Err(AlpineError::NoPackage.into());
|
||||
}
|
||||
|
||||
Ok(versions.into_iter().next())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn check_dockerhub_image(
|
||||
namespace: String,
|
||||
repository: String,
|
||||
regex: String,
|
||||
) -> color_eyre::eyre::Result<String> {
|
||||
let regex = regex::Regex::new(®ex)?;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TagResult {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TagsResponse {
|
||||
results: Vec<TagResult>,
|
||||
}
|
||||
|
||||
let images_url = format!("https://registry.hub.docker.com/v2/repositories/{}/{}/tags?currently_tagged=true,ordering=last_updated,page_size=30,status=active", namespace, repository);
|
||||
let tags: TagsResponse = client.get(images_url).send().await?.json().await?;
|
||||
|
||||
for result in tags.results {
|
||||
if let Some(matched) = regex.find(&result.name) {
|
||||
if matched.as_str() == result.name {
|
||||
return Ok(result.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(DockerError::NoTag.into())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn build_client(gitea_token: &str) -> color_eyre::eyre::Result<Client> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("token {}", gitea_token))?,
|
||||
);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.user_agent("release-checker (+https://git.asonix.dog/asonix/release-checker)")
|
||||
.build()?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(client))]
|
||||
async fn get_previous_revision(
|
||||
client: &Client,
|
||||
config: &GiteaConfig,
|
||||
) -> color_eyre::eyre::Result<Revision> {
|
||||
let tags_url = format!(
|
||||
"https://{}/api/v1/repos/{}/{}/tags?page=1&limit=4",
|
||||
config.domain, config.owner, config.repo
|
||||
);
|
||||
let tags_response = client.get(tags_url).send().await?;
|
||||
|
||||
let tags: Vec<GiteaTagResponse> = tags_response.json().await?;
|
||||
|
||||
let revision = if let Some(tag) = tags.get(0) {
|
||||
tracing::info!("tag: {}", tag.name);
|
||||
tag.name.parse()?
|
||||
} else {
|
||||
Revision::default()
|
||||
};
|
||||
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(client))]
|
||||
async fn create_tag(
|
||||
client: &Client,
|
||||
config: &GiteaConfig,
|
||||
revision: &Revision,
|
||||
) -> color_eyre::eyre::Result<()> {
|
||||
let tag_url = format!(
|
||||
"https://{}/api/v1/repos/{}/{}/tags",
|
||||
config.domain, config.owner, config.repo
|
||||
);
|
||||
|
||||
let response = client
|
||||
.post(tag_url)
|
||||
.json(&GiteaTag {
|
||||
tag_name: revision.to_string(),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(TagError::Status(response.status()).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_tracing() -> color_eyre::eyre::Result<()> {
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
let format_layer = tracing_subscriber::fmt::layer().pretty();
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(env_filter)
|
||||
.with(format_layer)
|
||||
.with(ErrorLayer::default());
|
||||
|
||||
set_global_default(subscriber)?;
|
||||
Ok(())
|
||||
}
|
||||
mod alpine;
|
||||
mod config;
|
||||
mod dockerhub;
|
||||
mod git;
|
||||
mod gitea;
|
||||
mod init_tracing;
|
||||
mod revision;
|
||||
|
||||
use alpine::check_alpine_package;
|
||||
use config::{CheckConfig, ProjectConfig};
|
||||
use dockerhub::check_dockerhub_image;
|
||||
use git::check_git_project;
|
||||
use gitea::{build_client, create_tag, get_previous_revision};
|
||||
use init_tracing::init_tracing;
|
||||
|
||||
async fn check_project(project: ProjectConfig) -> color_eyre::eyre::Result<Option<String>> {
|
||||
match project {
|
||||
|
|
74
src/revision.rs
Normal file
74
src/revision.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::{num::ParseIntError, str::FromStr};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct Revision {
|
||||
version: String,
|
||||
revision: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RevisionError {
|
||||
Revision(ParseIntError),
|
||||
}
|
||||
|
||||
impl Revision {
|
||||
pub(crate) fn next(self, version_string: Option<String>) -> Self {
|
||||
if let Some(version_string) = version_string {
|
||||
if self.version == version_string {
|
||||
Revision {
|
||||
revision: self.revision + 1,
|
||||
..self
|
||||
}
|
||||
} else {
|
||||
Revision {
|
||||
version: version_string,
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Revision {
|
||||
revision: self.revision + 1,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Revision {
|
||||
type Err = RevisionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some((version, revision)) = s.rsplit_once('-') {
|
||||
let revision = revision
|
||||
.trim_start_matches('r')
|
||||
.parse()
|
||||
.map_err(RevisionError::Revision)?;
|
||||
|
||||
Ok(Revision {
|
||||
version: version.to_owned(),
|
||||
revision,
|
||||
})
|
||||
} else {
|
||||
Ok(Revision {
|
||||
version: s.to_string(),
|
||||
revision: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Revision {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}-r{}", self.version, self.revision)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RevisionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Revision(e) => write!(f, "Failed to parse revision number, {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RevisionError {}
|
Loading…
Reference in a new issue