generated from PlexSheep/rs-base
Compare commits
15 commits
2baacdf739
...
499bcdd4c1
Author | SHA1 | Date | |
---|---|---|---|
499bcdd4c1 | |||
6701e86c38 | |||
15d11edac7 | |||
|
d31befe195 | ||
0c603f9e56 | |||
|
e62a0f3534 | ||
0d2ac7e163 | |||
e75f072cb0 | |||
|
28343a98f3 | ||
0fc325ff72 | |||
7f1c4d3888 | |||
f9daac2f40 | |||
a2c699bb98 | |||
78c9dd474f | |||
cdf77655f7 |
10 changed files with 512 additions and 14 deletions
26
.autocrate.yaml
Normal file
26
.autocrate.yaml
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
changelog:
|
||||
enable: true
|
||||
git-log: true
|
||||
|
||||
uses:
|
||||
cargo:
|
||||
publish: true
|
||||
# tokens are loaded from ~/.cargo/config.toml
|
||||
registries:
|
||||
- crates.io
|
||||
- cscherr
|
||||
|
||||
api:
|
||||
github:
|
||||
type: github
|
||||
endpoint: https://github.com
|
||||
auth:
|
||||
user: myUserName
|
||||
pass: token_superimportantsecret
|
||||
myserv:
|
||||
type: gitea
|
||||
endpoint: https://git.cscherr.de
|
||||
auth:
|
||||
user: myUserName
|
||||
pass: importantsecrettoken
|
18
Cargo.toml
18
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "autocrate"
|
||||
version = "0.1.0-prealpha.0"
|
||||
version = "0.1.0-prealpha.1"
|
||||
edition = "2021"
|
||||
publish = true
|
||||
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
||||
|
@ -20,6 +20,22 @@ keywords = [
|
|||
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
cargo = "0.76.0"
|
||||
clap = { version = "4.4.18", features = ["derive", "help"] }
|
||||
clap-verbosity-flag = "2.1.2"
|
||||
git2 = "0.18.1"
|
||||
libpt = { version = "0.3.11", features = ["log"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_yaml = "0.9.30"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.56"
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
|
||||
[[bin]]
|
||||
name = "autocrate"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "autocrate"
|
||||
path = "src/lib.rs"
|
||||
|
|
33
README.md
33
README.md
|
@ -18,7 +18,6 @@ Autocrate streamlines the release process, allowing developers to focus on
|
|||
their work. Although initially built for Gitea, we plan to extend support
|
||||
for additional platforms such as GitHub and GitLab.
|
||||
|
||||
|
||||
* [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate)
|
||||
* [GitHub Mirror](https://github.com/PlexSheep/Autocrate)
|
||||
* [crates.io](https://crates.io/crates/autocrate)
|
||||
|
@ -83,25 +82,39 @@ repository. It should contain the following parameters (replace the placeholders
|
|||
| `api.NAME.auth` | `pass` | a string | A secret for authentication o the server, probably a token |
|
||||
|
||||
An example `.autocrate.yaml` could look like this:
|
||||
|
||||
```yaml
|
||||
changelog:
|
||||
- enable: true
|
||||
- git-log: true
|
||||
enable: true
|
||||
git-log: true
|
||||
|
||||
uses:
|
||||
- cargo:
|
||||
- publish = true
|
||||
cargo:
|
||||
publish: true
|
||||
# tokens are loaded from ~/.cargo/config.toml
|
||||
- registries:
|
||||
- crates.io
|
||||
- example.com
|
||||
registries:
|
||||
- default
|
||||
- cscherr
|
||||
|
||||
api:
|
||||
github:
|
||||
user: myUserName
|
||||
pass: token_superimportantsecret
|
||||
type: github
|
||||
endpoint: https://github.com
|
||||
auth:
|
||||
user: PlexSheep
|
||||
pass: token_superimportantsecret
|
||||
cscherr:
|
||||
type: gitea
|
||||
endpoint: https://git.cscherr.de
|
||||
auth:
|
||||
user: PlexSheep
|
||||
pass: Bearer importantsecrettoken
|
||||
```
|
||||
|
||||
After Autocrate has been bootstrapped, you it will be released and published
|
||||
with itself, so you can take a look at this repositories
|
||||
[`.autocrate.yaml`](./.autocrate.yaml).
|
||||
|
||||
## Using Autocrate
|
||||
|
||||
TBD
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
TOKEN=$(cat ~/.git-credentials | grep 'git.cscherr.de' | grep -P '(?:)[^:]*(?=@)' -o)
|
||||
NEW_VERSION=$(cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1')
|
||||
GIT_COMMIT_SHA=$(git rev-parse HEAD)
|
||||
REPO=autocrate
|
||||
REPO=${PWD##*/} # name of cwd
|
||||
BODY="
|
||||
$(git log $(git describe --tags --abbrev=0)..HEAD --pretty="- %s" --oneline --decorate)
|
||||
"
|
||||
|
|
66
src/changelog/mod.rs
Normal file
66
src/changelog/mod.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::{fmt::Display, process::Command};
|
||||
|
||||
use crate::{config::Config, error::*};
|
||||
|
||||
/// Represents a changelog that is currently under construction.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Changelog {
|
||||
git_log: Option<String>,
|
||||
}
|
||||
|
||||
impl Changelog {
|
||||
pub fn build(cfg: &Config) -> Result<Self> {
|
||||
if !cfg.yaml.changelog.enable {
|
||||
return Err(ChangelogError::IsDisabledButUsed.into());
|
||||
}
|
||||
let git_log = Self::make_git_log(cfg)?;
|
||||
Ok(Changelog { git_log })
|
||||
}
|
||||
|
||||
fn make_git_log(cfg: &Config) -> Result<Option<String>> {
|
||||
if !cfg.yaml.changelog.enable {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("log")
|
||||
.arg(format!("{}..HEAD", Self::get_last_tag()?,))
|
||||
.arg("--oneline");
|
||||
let out = cmd.output()?;
|
||||
// FIXME: this does not catch fancy colors, those are from the shell as it seems? I don't
|
||||
// get it.
|
||||
let buf = String::from_utf8(out.stdout).map_err(|err| ChangelogError::GitUTF8Error(err))?;
|
||||
if !out.status.success() {
|
||||
// TODO: get the stderr for error reporting
|
||||
// TODO: Make the error more understandable for the user
|
||||
return Err(ChangelogError::GitBadStatus(out.status, buf).into());
|
||||
}
|
||||
|
||||
dbg!(&buf);
|
||||
Ok(Some(buf))
|
||||
}
|
||||
|
||||
fn get_last_tag() -> Result<String> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("describe").arg("--tags").arg("--abbrev=0");
|
||||
let out = cmd.output()?;
|
||||
let buf = String::from_utf8(out.stdout).map_err(|err| ChangelogError::GitUTF8Error(err))?;
|
||||
if !out.status.success() {
|
||||
// TODO: get the stderr for error reporting
|
||||
// TODO: Make the error more understandable for the user
|
||||
return Err(ChangelogError::GitBadStatus(out.status, buf).into());
|
||||
}
|
||||
let buf = buf.replace("\n", "");
|
||||
return Ok(buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Changelog {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut full: String = String::new();
|
||||
full += "Changelog";
|
||||
if self.git_log.is_some() {
|
||||
full += format!("\n\n{}", self.git_log.clone().unwrap()).as_str();
|
||||
}
|
||||
write!(f, "{full}")
|
||||
}
|
||||
}
|
100
src/config/cli.rs
Normal file
100
src/config/cli.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use libpt::log::{Level, Logger};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
version,
|
||||
about,
|
||||
long_about,
|
||||
help_template = r#"{about-section}
|
||||
{usage-heading} {usage}
|
||||
{all-args}{tab}
|
||||
|
||||
autocrate: {version}
|
||||
Author: {author-with-newline}
|
||||
"#
|
||||
)]
|
||||
/// Release Manager for Your Projects on Gitea, GitHub, and GitLab.
|
||||
pub struct Cli {
|
||||
// clap_verbosity_flag seems to make this a global option implicitly
|
||||
/// set a verbosity, multiple allowed (f.e. -vvv)
|
||||
#[command(flatten)]
|
||||
pub verbose: Verbosity<InfoLevel>,
|
||||
|
||||
/// show additional logging meta data
|
||||
#[arg(long)]
|
||||
pub meta: bool,
|
||||
|
||||
/// the subcommands are part of this enum
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Commands {
|
||||
Changelog {},
|
||||
Release {
|
||||
// FIXME: allow taking a message like this:
|
||||
// `autocrate changelog -m arg1 arg2 arg3`
|
||||
// -> msg="arg1 arg2 arg3"
|
||||
// Instead of only
|
||||
// `autocrate changelog -m "arg1 arg2 arg3"`
|
||||
// -> msg="arg1 arg2 arg3"
|
||||
//
|
||||
// TODO:
|
||||
// Perhaps open the $EDITOR of the user if
|
||||
// no message is provided, like git does
|
||||
//
|
||||
// TODO:
|
||||
// find a way to make this a global option but only usable with specific subcommands
|
||||
#[arg(short, long)]
|
||||
message: Option<Vec<String>>,
|
||||
},
|
||||
Publish {
|
||||
// see Commands::Release { message }
|
||||
#[arg(short, long)]
|
||||
message: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for Commands {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Changelog { .. } => "Changelog",
|
||||
Self::Release { .. } => "Release",
|
||||
Self::Publish { .. } => "Publish",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn cli_parse() -> Self {
|
||||
let cli = Self::parse();
|
||||
let ll: Level = match cli.verbose.log_level().unwrap().as_str() {
|
||||
"TRACE" => Level::TRACE,
|
||||
"DEBUG" => Level::DEBUG,
|
||||
"INFO" => Level::INFO,
|
||||
"WARN" => Level::WARN,
|
||||
"ERROR" => Level::ERROR,
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
if cli.meta {
|
||||
Logger::init(None, Some(ll), true).expect("could not initialize Logger");
|
||||
} else {
|
||||
// less verbose version
|
||||
Logger::init_mini(Some(ll)).expect("could not initialize Logger");
|
||||
}
|
||||
return cli;
|
||||
}
|
||||
}
|
203
src/config/mod.rs
Normal file
203
src/config/mod.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
use std::{collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf};
|
||||
|
||||
use git2;
|
||||
use libpt::log::{debug, error, trace};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
pub mod cli;
|
||||
use cli::Cli;
|
||||
|
||||
pub trait YamlConfigSection: Debug + Clone + for<'a> Deserialize<'a> {
|
||||
fn check(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Changelog {
|
||||
pub enable: bool,
|
||||
#[serde(alias = "git-log")]
|
||||
pub git_log: bool,
|
||||
}
|
||||
impl YamlConfigSection for Changelog {
|
||||
fn check(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct UseCargo {
|
||||
pub publish: bool,
|
||||
pub registries: Vec<String>,
|
||||
}
|
||||
impl YamlConfigSection for UseCargo {
|
||||
fn check(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Uses {
|
||||
cargo: UseCargo,
|
||||
}
|
||||
impl YamlConfigSection for Uses {
|
||||
fn check(&self) -> Result<()> {
|
||||
self.cargo.check()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ApiAuth {
|
||||
pub user: String,
|
||||
pub pass: Option<String>,
|
||||
pub pass_file: Option<PathBuf>,
|
||||
}
|
||||
impl YamlConfigSection for ApiAuth {
|
||||
fn check(&self) -> Result<()> {
|
||||
if self.pass.is_some() && self.pass_file.is_some() {
|
||||
let err = ConfigError::YamlApiAuthBothPass(self.clone()).into();
|
||||
error!("{err}");
|
||||
return Err(err);
|
||||
}
|
||||
if self.pass_file.is_some() {
|
||||
let file = self.pass_file.clone().unwrap();
|
||||
if !file.exists() {
|
||||
return Err(ConfigError::PassFileDoesNotExist(file).into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Api {
|
||||
pub r#type: ApiType,
|
||||
pub endpoint: Url,
|
||||
/// May be left empty if the Api does not need auth or the auth is part of the
|
||||
/// [endpoint](Api::endpoint) [Url].
|
||||
pub auth: Option<ApiAuth>,
|
||||
}
|
||||
impl YamlConfigSection for Api {
|
||||
fn check(&self) -> Result<()> {
|
||||
self.r#type.check()?;
|
||||
match self.endpoint.socket_addrs(|| None) {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
if self.auth.is_some() {
|
||||
self.auth.clone().unwrap().check()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub enum ApiType {
|
||||
#[serde(alias = "gitea")]
|
||||
Gitea,
|
||||
#[serde(alias = "gitlab")]
|
||||
Gitlab,
|
||||
#[serde(alias = "github", alias = "GitHub")]
|
||||
Github,
|
||||
}
|
||||
impl YamlConfigSection for ApiType {
|
||||
fn check(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct YamlConfig {
|
||||
pub changelog: Changelog,
|
||||
pub uses: Uses,
|
||||
pub api: HashMap<String, Api>,
|
||||
}
|
||||
impl YamlConfigSection for YamlConfig {
|
||||
fn check(&self) -> Result<()> {
|
||||
self.changelog.check()?;
|
||||
self.uses.check()?;
|
||||
for api in self.api.values() {
|
||||
api.check()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl YamlConfig {
|
||||
/// check if the built configuration is valid
|
||||
pub fn check(&self) -> Result<()> {
|
||||
for api in &self.api {
|
||||
api.1.check()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub yaml: YamlConfig,
|
||||
pub cli: Cli,
|
||||
pub repo: git2::Repository,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Debug for Config {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
format!(
|
||||
"Config {{yaml: {:?}, repo_path: {:?}}}",
|
||||
self.yaml, self.path
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(cli: Cli) -> Result<Self> {
|
||||
let repo = match git2::Repository::open_from_env() {
|
||||
Ok(repo) => repo,
|
||||
Err(_err) => {
|
||||
let err = ConfigError::GitRepoNotFound.into();
|
||||
error!("{err}");
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let mut path = repo.path().to_path_buf();
|
||||
path.pop(); // we want the real root, not the `.git` dir
|
||||
|
||||
let yaml_file_name = if path.join(".autocrate.yaml").exists() {
|
||||
".autocrate.yaml"
|
||||
} else if path.join(".autocrate.yml").exists() {
|
||||
".autocrate.yml"
|
||||
} else {
|
||||
let err = ConfigError::NoYamlFile.into();
|
||||
error!("{err}");
|
||||
return Err(err);
|
||||
};
|
||||
let yaml_file_path = path.join(yaml_file_name);
|
||||
// we can be sure it exists from the checks above
|
||||
assert!(yaml_file_path.exists());
|
||||
if !yaml_file_path.is_file() {
|
||||
let err = ConfigError::YamlFileIsNotFile.into();
|
||||
error!("{err}");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let yaml_rd = BufReader::new(File::open(yaml_file_path)?);
|
||||
debug!("reading yaml config and building data structure");
|
||||
let yaml: YamlConfig = serde_yaml::from_reader(yaml_rd)?;
|
||||
trace!("load config:\n{:#?}", yaml);
|
||||
yaml.check()?;
|
||||
debug!("built and checked yaml config");
|
||||
|
||||
Ok(Config {
|
||||
yaml,
|
||||
repo,
|
||||
path,
|
||||
cli,
|
||||
})
|
||||
}
|
||||
}
|
48
src/error.rs
Normal file
48
src/error.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use std::{path::PathBuf, process::ExitStatus, string::FromUtf8Error};
|
||||
|
||||
use anyhow;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::ApiAuth;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Bad IO operation
|
||||
#[error("Bad IO operation")]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error("Bad configuration file")]
|
||||
Config(#[from] ConfigError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
#[error("Yaml error")]
|
||||
SerdeYaml(#[from] serde_yaml::Error),
|
||||
#[error("Could not generate the changelog")]
|
||||
ChangelogError(#[from] ChangelogError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ChangelogError {
|
||||
#[error("changelog has 'enabled = false' in the yaml config")]
|
||||
IsDisabledButUsed,
|
||||
#[error("error while using `git log`, is git installed?")]
|
||||
GitCommandError,
|
||||
#[error("error while using `git log`, could not format stdout with utf8")]
|
||||
GitUTF8Error(#[from] FromUtf8Error),
|
||||
#[error("git exited with status {0}: {1}")]
|
||||
GitBadStatus(ExitStatus, String),
|
||||
}
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("could not find git repository")]
|
||||
GitRepoNotFound,
|
||||
#[error("no \".autocrate.yaml\" or \".autocrate.yml\" found in repository root")]
|
||||
NoYamlFile,
|
||||
#[error("the autocrate config file is not a regular file (is it a directory?)")]
|
||||
YamlFileIsNotFile,
|
||||
#[error("api {0:?} provides both a `pass` and a `pass_file`")]
|
||||
YamlApiAuthBothPass(ApiAuth),
|
||||
#[error("password provided as file, but does not exist: {0}")]
|
||||
PassFileDoesNotExist(PathBuf),
|
||||
}
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod changelog;
|
||||
pub mod config;
|
||||
pub mod error;
|
27
src/main.rs
27
src/main.rs
|
@ -1,3 +1,26 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use autocrate::{
|
||||
changelog::*,
|
||||
config::{
|
||||
cli::{Cli, Commands},
|
||||
Config,
|
||||
},
|
||||
error::*,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::cli_parse();
|
||||
let cfg = Config::load(cli.clone())?;
|
||||
|
||||
match cli.command {
|
||||
Commands::Changelog { .. } => {
|
||||
println!("{}", Changelog::build(&cfg)?.to_string());
|
||||
Ok(())
|
||||
}
|
||||
Commands::Release { .. } => {
|
||||
todo!()
|
||||
}
|
||||
Commands::Publish { .. } => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue