Initial commit
This commit is contained in:
commit
e857889721
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Check.toml
|
1199
Cargo.lock
generated
Normal file
1199
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "release-checker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.5.11"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-error = { version = "0.1.2", features = ["traced-error"] }
|
||||
tracing-subscriber = { version = "0.2", features = ["env-filter"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml = "0.5.8"
|
309
src/main.rs
Normal file
309
src/main.rs
Normal file
|
@ -0,0 +1,309 @@
|
|||
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 {
|
||||
#[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: String) -> Self {
|
||||
if self.version == version_string {
|
||||
Revision {
|
||||
revision: self.revision + 1,
|
||||
..self
|
||||
}
|
||||
} else {
|
||||
Revision {
|
||||
version: version_string,
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Err(RevisionError::Format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Format,
|
||||
Revision(ParseIntError),
|
||||
}
|
||||
impl Display for RevisionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Format => write!(f, "Invalid revision format"),
|
||||
Self::Revision(e) => write!(f, "Failed to parse revision number, {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for RevisionError {}
|
||||
|
||||
#[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(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=1",
|
||||
config.domain, config.owner, config.repo
|
||||
);
|
||||
let tags_response = client.get(tags_url).send().await?;
|
||||
|
||||
let mut tags: Vec<GiteaTagResponse> = tags_response.json().await?;
|
||||
|
||||
let revision = if let Some(tag) = tags.pop() {
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn check_project(project: ProjectConfig) -> color_eyre::eyre::Result<String> {
|
||||
match project {
|
||||
ProjectConfig::Git {
|
||||
repository,
|
||||
language,
|
||||
} => check_git_project(repository, language).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn run() -> color_eyre::eyre::Result<()> {
|
||||
let config = tokio::fs::read_to_string("./Check.toml").await?;
|
||||
let config: CheckConfig = toml::from_str(&config)?;
|
||||
|
||||
let client = build_client(&std::env::var("GITEA_TOKEN")?)?;
|
||||
|
||||
let previous_revision = get_previous_revision(&client, &config.gitea).await?;
|
||||
|
||||
let version_string = check_project(config.project).await?;
|
||||
|
||||
let revision = previous_revision.next(version_string);
|
||||
tracing::info!("New revision: {}", revision);
|
||||
|
||||
create_tag(&client, &config.gitea, &revision).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> color_eyre::eyre::Result<()> {
|
||||
init_tracing()?;
|
||||
color_eyre::install()?;
|
||||
|
||||
run().await
|
||||
}
|
Loading…
Reference in a new issue