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] [package]
name = "autocrate" name = "autocrate"
version = "0.1.0-prealpha.0" version = "0.1.0-prealpha.1"
edition = "2021" edition = "2021"
publish = true publish = true
authors = ["Christoph J. Scherr <software@cscherr.de>"] authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -20,6 +20,22 @@ keywords = [
[dependencies] [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"] } libpt = { version = "0.3.11", features = ["log"] }
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_yaml = "0.9.30" 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 their work. Although initially built for Gitea, we plan to extend support
for additional platforms such as GitHub and GitLab. for additional platforms such as GitHub and GitLab.
* [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate) * [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate)
* [GitHub Mirror](https://github.com/PlexSheep/Autocrate) * [GitHub Mirror](https://github.com/PlexSheep/Autocrate)
* [crates.io](https://crates.io/crates/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 | | `api.NAME.auth` | `pass` | a string | A secret for authentication o the server, probably a token |
An example `.autocrate.yaml` could look like this: An example `.autocrate.yaml` could look like this:
```yaml ```yaml
changelog: changelog:
- enable: true enable: true
- git-log: true git-log: true
uses: uses:
- cargo: cargo:
- publish = true publish: true
# tokens are loaded from ~/.cargo/config.toml # tokens are loaded from ~/.cargo/config.toml
- registries: registries:
- crates.io - default
- example.com - cscherr
api: api:
github: github:
user: myUserName type: github
endpoint: https://github.com
auth:
user: PlexSheep
pass: token_superimportantsecret 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 ## Using Autocrate
TBD TBD

View file

@ -2,7 +2,7 @@
TOKEN=$(cat ~/.git-credentials | grep 'git.cscherr.de' | grep -P '(?:)[^:]*(?=@)' -o) TOKEN=$(cat ~/.git-credentials | grep 'git.cscherr.de' | grep -P '(?:)[^:]*(?=@)' -o)
NEW_VERSION=$(cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1') NEW_VERSION=$(cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1')
GIT_COMMIT_SHA=$(git rev-parse HEAD) GIT_COMMIT_SHA=$(git rev-parse HEAD)
REPO=autocrate REPO=${PWD##*/} # name of cwd
BODY=" BODY="
$(git log $(git describe --tags --abbrev=0)..HEAD --pretty="- %s" --oneline --decorate) $(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() { use autocrate::{
println!("Hello, world!"); 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!()
}
}
} }