Compare commits

..

15 commits

Author SHA1 Message Date
499bcdd4c1
important changes
All checks were successful
cargo devel CI / cargo CI (push) Successful in 3m51s
2024-01-28 01:42:28 +01:00
6701e86c38
changelog until last tag
All checks were successful
cargo devel CI / cargo CI (push) Successful in 4m14s
2024-01-28 01:33:43 +01:00
15d11edac7
we got basic git log
All checks were successful
cargo devel CI / cargo CI (push) Successful in 4m17s
2024-01-28 01:19:46 +01:00
PlexSheep
d31befe195 automatic cargo CI changes 2024-01-27 22:50:10 +00:00
0c603f9e56
remove message from changelog, add skeletons for release and publish
All checks were successful
cargo devel CI / cargo CI (push) Successful in 4m15s
2024-01-27 23:46:00 +01:00
PlexSheep
e62a0f3534 automatic cargo CI changes 2024-01-27 22:31:06 +00:00
0d2ac7e163
a basic changelog interface
All checks were successful
cargo devel CI / cargo CI (push) Successful in 4m21s
2024-01-27 23:26:49 +01:00
e75f072cb0
link to own .autocrate.yaml
All checks were successful
cargo devel CI / cargo CI (push) Successful in 2m0s
2024-01-25 22:51:22 +01:00
PlexSheep
28343a98f3 automatic cargo CI changes 2024-01-25 21:37:52 +00:00
0fc325ff72
rename git_log to git-log again
All checks were successful
cargo devel CI / cargo CI (push) Successful in 2m12s
2024-01-25 22:35:40 +01:00
7f1c4d3888
checks for the yaml config
Some checks failed
cargo devel CI / cargo CI (push) Has been cancelled
2024-01-25 22:33:48 +01:00
f9daac2f40
we can load a yaml config and have error handling for it
All checks were successful
cargo devel CI / cargo CI (push) Successful in 2m16s
2024-01-25 22:08:37 +01:00
a2c699bb98
get going with some configs
All checks were successful
cargo devel CI / cargo CI (push) Successful in 2m20s
2024-01-24 23:04:01 +01:00
78c9dd474f
add lib and clap to cargo.toml 2024-01-24 22:13:34 +01:00
cdf77655f7
release script update 2024-01-24 22:13:20 +01:00
10 changed files with 512 additions and 14 deletions

26
.autocrate.yaml Normal file
View 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

View file

@ -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"

View file

@ -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
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

View file

@ -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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
pub mod changelog;
pub mod config;
pub mod error;

View file

@ -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!()
}
}
}