Compare commits

..

No commits in common. "499bcdd4c1ca2ff5d48f0910b6f9d358b89b25bb" and "2baacdf7391f5b28c6a69fd1c5ffce3d16f47c7a" have entirely different histories.

10 changed files with 14 additions and 512 deletions

View file

@ -1,26 +0,0 @@
---
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.1" version = "0.1.0-prealpha.0"
edition = "2021" edition = "2021"
publish = true publish = true
authors = ["Christoph J. Scherr <software@cscherr.de>"] authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -20,22 +20,6 @@ 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,6 +18,7 @@ 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)
@ -82,39 +83,25 @@ 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:
- default - crates.io
- cscherr - example.com
api: api:
github: github:
type: github user: myUserName
endpoint: https://github.com pass: token_superimportantsecret
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 ## 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=${PWD##*/} # name of cwd REPO=autocrate
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)
" "

View file

@ -1,66 +0,0 @@
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}")
}
}

View file

@ -1,100 +0,0 @@
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;
}
}

View file

@ -1,203 +0,0 @@
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,
})
}
}

View file

@ -1,48 +0,0 @@
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),
}

View file

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

View file

@ -1,26 +1,3 @@
use autocrate::{ fn main() {
changelog::*, println!("Hello, world!");
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!()
}
}
} }