Initial commit

This commit is contained in:
Aode (lion) 2021-12-13 20:51:41 -06:00
commit e857889721
4 changed files with 1526 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Check.toml

1199
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View 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
View 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
}