Compare commits

..

No commits in common. "master" and "v0.1.0-prealpha.3" have entirely different histories.

20 changed files with 185 additions and 502 deletions

View File

@ -1,5 +1,4 @@
version: ---
!cargo
changelog: changelog:
enable: true enable: true
git-log: true git-log: true
@ -15,16 +14,15 @@ uses:
api: api:
github: github:
type: github type: github
repository: autocrate endpoint: https://github.com
auth: auth:
user: PlexSheep user: PlexSheep
pass: pass:
!env TOKEN_GH env: TOKEN_GH
cscherr: cscherr:
type: forgejo type: forgejo
endpoint: https://git.cscherr.de endpoint: https://git.cscherr.de
repository: autocrate
auth: auth:
user: PlexSheep user: PlexSheep
pass: pass:
!env TOKEN_CSCHERR env: TOKEN_CSCHERR

View File

@ -31,13 +31,13 @@ jobs:
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
cat ~/.cargo/config.toml cat ~/.cargo/config.toml
- name: cargo clippy check - name: cargo clippy check
run: cargo clippy --all-features --all-targets --workspace run: cargo clippy --all-features --all-targets
- name: cargo clippy fix - name: cargo clippy fix
run: cargo clippy --fix --all-features --all-targets --workspace run: cargo clippy --fix --all-features --all-targets
- name: cargo fmt - name: cargo fmt
run: cargo fmt --all run: cargo fmt --all
- name: cargo test - name: cargo test
run: cargo test --all-features --all-targets --workspace run: cargo test --all-features --all-targets
- name: commit back to repository - name: commit back to repository
uses: https://github.com/stefanzweifel/git-auto-commit-action@v5 uses: https://github.com/stefanzweifel/git-auto-commit-action@v5
with: with:

View File

@ -32,13 +32,13 @@ jobs:
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
cat ~/.cargo/config.toml cat ~/.cargo/config.toml
- name: cargo clippy check - name: cargo clippy check
run: cargo clippy --all-features --all-targets --workspace run: cargo clippy --all-features --all-targets
- name: cargo clippy fix - name: cargo clippy fix
run: cargo clippy --fix --all-features --all-targets --workspace run: cargo clippy --fix --all-features --all-targets
- name: cargo fmt - name: cargo fmt
run: cargo fmt --all run: cargo fmt --all
- name: cargo test - name: cargo test
run: cargo test --all-features --all-targets --workspace run: cargo test --all-features --all-targets
- name: commit back to repository - name: commit back to repository
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v5
with: with:

1
.gitignore vendored
View File

@ -19,4 +19,3 @@ Cargo.lock
# Added by cargo # Added by cargo
/target /target
.env

View File

@ -1,6 +1,6 @@
[package] [package]
name = "autocrate" name = "autocrate"
version = "0.1.0-prealpha.5" version = "0.1.0-prealpha.3"
edition = "2021" edition = "2021"
publish = true publish = true
authors = ["Christoph J. Scherr <software@cscherr.de>"] authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -25,14 +25,10 @@ async-trait = "0.1.77"
# cargo = "0.76.0" # cargo = "0.76.0"
clap = { version = "4.4.18", features = ["derive", "help"] } clap = { version = "4.4.18", features = ["derive", "help"] }
clap-verbosity-flag = "2.1.2" clap-verbosity-flag = "2.1.2"
forgejo-api = "0.1.0"
futures = "0.3.30"
git2 = "0.18.1" git2 = "0.18.1"
libpt = { version = "0.4.2", features = ["log"] } libpt = { version = "0.3.11", features = ["log"] }
octocrab = "0.38.0"
reqwest = "0.11.24" reqwest = "0.11.24"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.116"
serde_yaml = "0.9.30" serde_yaml = "0.9.30"
tempfile = "3.9.0" tempfile = "3.9.0"
thiserror = "1.0.56" thiserror = "1.0.56"

View File

@ -8,65 +8,47 @@
![logo](data/media/autocrate.jpeg) ![logo](data/media/autocrate.jpeg)
**Disclaimer**: I've generated the Readme and logo with the help of so called AI
tools and modified them afterwards.
Autocrate simplifies the creation and maintenance of releases for your Rust Autocrate simplifies the creation and maintenance of releases for your Rust
projects hosted on fancy git servers. By providing functionalities projects hosted on Gitea servers. By providing essential functionalities
like creating releases uploading artifacts, publishing crates, and managing changelogs, like uploading artifacts, publishing crates, and managing changelogs,
Autocrate tries to streamline the release process. Although initially built for Forgejo, Autocrate streamlines the release process, allowing developers to focus on
I plan to extend support to other platforms such as GitHub and GitLab. their work. Although initially built for Gitea, we plan to extend support
for additional platforms such as GitHub and GitLab.
Autocrate can then be used in CI/CD, or in projects without
continuous integration to release software.
The software is built in Rust, and offers integration for Rust Projects with Cargo.
In the future, using other tools and specifying custom scripts will become possible.
* [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate) * [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate)
* [Codeberg Mirror](https://codeberg.org/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)
* [docs.rs](https://docs.rs/crate/autocrate/) * [docs.rs](https://docs.rs/crate/autocrate/)
Take a look at the [scripts](./scripts) directory! [publish.sh](scripts/publish.sh) Take a look at the [scripts](./scripts) directory! [publish.sh](scripts/publish.sh)
and [release.sh](scripts/release.sh) are what I'm trying to get rid of. and [release.sh](scripts/release.sh) are exactly what I'm trying to get rid of.
## Features ## Features
* Create and update releases on your Git platform * Create and update releases on your Gitea server
* Publish crates to crates.io or other repositories * Publish crates to Cargo.rs
* Upload artifacts, including binaries and signatures alongside your releases or other repositories directly from your Rust projects
* Generate changelogs and release notes * Upload artifacts, including documentation and binaries, alongside your releases
* Configure with a simple yaml file * Generate and maintain changelogs and release notes.
### Upcoming Features ### Upcoming Features
Autocrate is still in pre-alpha, so the features listed above are still being My goal is to continuously enhance Autocrate to better serve the developer
worked on. For the future, the following Features are planned: community. Some planned improvements include supporting other popular hosting
platforms, enabling even greater flexibility and convenience.
* Support for platforms other than Forgejo
* Custom artifact build scripts
* Version bumping
* Interactive and scriptable CLI interface
* Publish a cargo workspace (that depends on it's own crates)
## Getting Started ## Getting Started
Before getting started with Autocrate, make sure you have the necessary Before getting started with Autocrate, make sure you have the necessary
prerequisites covered: prerequisites covered:
You can use `autocrate init` to set your workspace up with a basic * **A Rust Environment**: Install the latest stable Rust compiler and
`.autocrate.yaml`. associated tools via the official website: <https://www.rust-lang.org/>
* **Access to a Gitea Server** (such as [git.cscherr.de](https://git.cscherr.de)
* **Access to a supported Git Server** (such as [git.cscherr.de](https://git.cscherr.de) and [codeberg.org](https://codeberg.org) (strictly speaking, uses a Gitea fork)
and [codeberg.org](https://codeberg.org))
### Pre-requisites
* Git
#### If you want to compile it yourself
Install Rust, the officially recommended way is through [rustup.rs](https://rustup.rs/).
Your distribution may offer a Rust distribution in your package manager as an alternative
### Installing ### Installing
@ -140,11 +122,7 @@ with itself, so you can take a look at this repositories
## Using Autocrate ## Using Autocrate
After you have your workspace with a `.autocrate.yaml` file, you can: TBD
* `autocrapte release` to create a release on your git server(s), optionally publishing too
* `autocrate publish` to publish your crate to the specified registries(s) (default is crates.io)
* `autocrate changelog` to generate a changelog since the last tag
## Licensing ## Licensing
@ -155,23 +133,13 @@ License. Please refer to [`LICENSE`](./LICENSE) for complete licensing details.
## Project status ## Project status
The project has started recently and is currently in pre-alpha. Many features The project has started recently and is currently in pre-alpha.
are still missing or experimental
## Contributing ## Contributing
I'd be very happy to get contributions! Although the master repository is on I'd be very happy to get contributions! Although the master repository is on
my self hosted git server, you're free to create issues, PRs and so on my self hosted git server, you're free to create issues, PRs and so on on
GitHub. If enough activity comes around, moving to GitHub Codeberg might be a GitHub. If enough activity comes around, moving to GitHub might be a good idea.
good idea.
If you have any questions, use issues and discussions tabs or write me an email If you have any questions, use issues and discussions tabs or write me an email
to [software@cscherr.de](mailto:software@cscherr.de) to [software@cscherr.de](mailto:software@cscherr.de)
## Security
If you find a security issue with this repository, it would be best if you sent
me a mail to [software@cscherr.de](mailto:software@cscherr.de) or reported it on
GitHub.
See [`SECURITY`](SECURITY.md).

View File

@ -1,14 +0,0 @@
# Security Policy
## Supported Versions
Only the latest release is currently supported.
## Reporting a Vulnerability
It would be best if you reported any found security vulnerabilities on the GitHub mirror.
If you want to send something encrypted, use [this](https://static.cscherr.de/keys/software@cscherr.de.asc) key.
You can always reach me on [software@cscherr.de](mailto:software@cscherr.de), but sadly, I'm struggeling with PGP
encryption in my mail client. I use protonmail to host my EMail services, so if you send me something from protonmail,
it will be end to end encrypted.

View File

@ -17,7 +17,6 @@ impl Changelog {
Ok(Changelog { git_log }) Ok(Changelog { git_log })
} }
// TODO: use libgit2 instead of the cli interface
fn make_git_log(cfg: &Config) -> Result<Option<String>> { fn make_git_log(cfg: &Config) -> Result<Option<String>> {
if !cfg.yaml.changelog.enable { if !cfg.yaml.changelog.enable {
return Ok(None); return Ok(None);
@ -36,6 +35,7 @@ impl Changelog {
return Err(ChangelogError::GitBadStatus(out.status, buf).into()); return Err(ChangelogError::GitBadStatus(out.status, buf).into());
} }
dbg!(&buf);
Ok(Some(buf)) Ok(Some(buf))
} }

View File

@ -62,11 +62,9 @@ pub enum Commands {
message: Option<Vec<String>>, message: Option<Vec<String>>,
/// generate and add a changelog /// generate and add a changelog
#[arg(short, long)]
changelog: bool, changelog: bool,
/// publish after releasing /// publish after releasing
#[arg(short, long)]
publish: bool, publish: bool,
}, },
/// Publish to a package registry /// Publish to a package registry
@ -110,10 +108,10 @@ impl Cli {
} }
}; };
if cli.meta { if cli.meta {
Logger::build(None, Some(ll), true).expect("could not initialize Logger"); Logger::init(None, Some(ll), true).expect("could not initialize Logger");
} else { } else {
// less verbose version // less verbose version
Logger::build_mini(Some(ll)).expect("could not initialize Logger"); Logger::init_mini(Some(ll)).expect("could not initialize Logger");
} }
cli cli
} }

View File

@ -1,11 +1,8 @@
use std::str::FromStr; use std::{collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf};
use std::{
collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf, process::Command,
};
use git2; use git2;
use libpt::log::{debug, error, trace}; use libpt::log::{debug, error, trace};
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use url::Url; use url::Url;
use crate::error::*; use crate::error::*;
@ -18,7 +15,7 @@ pub trait YamlConfigSection: Debug + Clone + for<'a> Deserialize<'a> {
fn check(&self) -> Result<()>; fn check(&self) -> Result<()>;
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Changelog { pub struct Changelog {
pub enable: bool, pub enable: bool,
#[serde(alias = "git-log")] #[serde(alias = "git-log")]
@ -30,7 +27,7 @@ impl YamlConfigSection for Changelog {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
pub struct UseCargo { pub struct UseCargo {
pub publish: bool, pub publish: bool,
pub registries: Vec<String>, pub registries: Vec<String>,
@ -41,7 +38,7 @@ impl YamlConfigSection for UseCargo {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Uses { pub struct Uses {
cargo: UseCargo, cargo: UseCargo,
} }
@ -52,17 +49,13 @@ impl YamlConfigSection for Uses {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Pass { pub enum Pass {
/// pass specified as plainext /// pass specified as plainext
#[serde(alias = "pass_text")]
Text(String), Text(String),
/// pass to be loaded from an env var /// pass to be loaded from an env var
#[serde(alias = "pass_env")]
Env(String), Env(String),
/// pass to be loaded from a file /// pass to be loaded from a file
#[serde(alias = "pass_file")]
File(PathBuf), File(PathBuf),
} }
impl Pass { impl Pass {
@ -95,7 +88,7 @@ impl YamlConfigSection for Pass {
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
pub struct ApiAuth { pub struct ApiAuth {
pub user: String, pub user: String,
pub pass: Pass, pub pass: Pass,
@ -107,45 +100,20 @@ impl YamlConfigSection for ApiAuth {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Api { pub struct Api {
#[serde(alias = "type")]
pub server_type: ApiType, pub server_type: ApiType,
/// May be left empty if the [ApiType] is [Github](ApiType::Github). pub endpoint: Url,
pub endpoint: Option<Url>,
/// May be left empty if the Api does not need auth or the auth is part of the /// May be left empty if the Api does not need auth or the auth is part of the
/// [endpoint](Api::endpoint) [Url]. /// [endpoint](Api::endpoint) [Url].
pub auth: Option<ApiAuth>, pub auth: Option<ApiAuth>,
/// Name of the repository on the Git server, as git itself has no concept of repository name
pub repository: String,
}
impl std::fmt::Display for Api {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.server_type {
ApiType::Github => write!(
f,
"{}",
self.server_type
.default_endpoint()
.expect("no default endpoint set for github")
),
_ => write!(f, "{}", self.endpoint.clone().expect("no endpoint set")),
}
}
} }
impl YamlConfigSection for Api { impl YamlConfigSection for Api {
fn check(&self) -> Result<()> { fn check(&self) -> Result<()> {
self.server_type.check()?; self.server_type.check()?;
if self.server_type != ApiType::Github { match self.endpoint.socket_addrs(|| None) {
if self.auth.is_none() { Ok(_) => (),
return Err(ConfigError::NoEndpointSet.into()); Err(err) => return Err(err.into()),
}
match self.endpoint.clone().unwrap().socket_addrs(|| None) {
Ok(_) => (),
Err(err) => return Err(err.into()),
}
} else if let Some(_url) = &self.endpoint {
return Err(ConfigError::EndpointSetButNotNeeded.into());
} }
if self.auth.is_some() { if self.auth.is_some() {
self.auth.clone().unwrap().check()?; self.auth.clone().unwrap().check()?;
@ -154,7 +122,7 @@ impl YamlConfigSection for Api {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize)]
pub enum ApiType { pub enum ApiType {
#[serde(alias = "gitea")] #[serde(alias = "gitea")]
Gitea, Gitea,
@ -165,97 +133,22 @@ pub enum ApiType {
#[serde(alias = "forgejo")] #[serde(alias = "forgejo")]
Forgejo, Forgejo,
} }
impl ApiType {
pub fn default_endpoint(&self) -> Option<Url> {
match self {
Self::Github => Some(Url::from_str("https://github.com").unwrap()),
_ => None,
}
}
}
impl YamlConfigSection for ApiType { impl YamlConfigSection for ApiType {
fn check(&self) -> Result<()> { fn check(&self) -> Result<()> {
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Version {
Text(String),
Cmd(String),
Cargo,
}
impl Version {
pub fn get_version(&self) -> String {
// TODO: Error handling
match self {
Self::Text(ver) => ver.clone(),
Self::Cmd(shell_command) => {
match Command::new("/bin/bash")
.arg("-c")
.arg(shell_command)
.output()
{
Ok(output) => {
// TODO: check status
String::from_utf8(output.stdout).unwrap()
}
Err(err) => {
panic!("{err:?}");
}
}
}
Self::Cargo => {
match Command::new("/bin/bash")
.arg("-c")
.arg(r#"cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1'"#)
.output()
{
Ok(output) => {
// TODO: check status
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
Err(err) => {
panic!("{err:?}");
}
}
}
}
}
}
impl YamlConfigSection for Version {
fn check(&self) -> Result<()> {
match self {
Self::Text(_) => (),
Self::Cmd(_cmd) => {
// TODO: get the version with a command
todo!("verion from cmd not implemented")
}
Self::Cargo => {
// TODO: get the version as specified in a Cargo.toml
todo!("verion from cargo not implemented")
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct YamlConfig { pub struct YamlConfig {
pub changelog: Changelog, pub changelog: Changelog,
pub uses: Uses, pub uses: Uses,
pub api: HashMap<String, Api>, pub api: HashMap<String, Api>,
pub version: Version,
} }
impl YamlConfigSection for YamlConfig { impl YamlConfigSection for YamlConfig {
fn check(&self) -> Result<()> { fn check(&self) -> Result<()> {
self.changelog.check()?; self.changelog.check()?;
self.uses.check()?; self.uses.check()?;
self.version.check()?;
for api in self.api.values() { for api in self.api.values() {
api.check()?; api.check()?;
} }
@ -273,10 +166,10 @@ impl YamlConfig {
} }
} }
#[derive(Clone)]
pub struct Config { pub struct Config {
pub yaml: YamlConfig, pub yaml: YamlConfig,
pub cli: Cli, pub cli: Cli,
pub repo: git2::Repository,
pub path: PathBuf, pub path: PathBuf,
} }
@ -333,6 +226,7 @@ impl Config {
Ok(Config { Ok(Config {
yaml, yaml,
repo,
path, path,
cli: cli.clone(), cli: cli.clone(),
}) })

View File

@ -32,10 +32,6 @@ pub enum ServerApiError {
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error(transparent)] #[error(transparent)]
ReqwestErr(#[from] reqwest::Error), ReqwestErr(#[from] reqwest::Error),
#[error(transparent)]
ForgejoApiError(#[from] forgejo_api::ForgejoError),
#[error(transparent)]
GithubApiError(#[from] octocrab::Error),
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -65,8 +61,4 @@ pub enum ConfigError {
EnvNotSet(String), EnvNotSet(String),
#[error("Bad value for environment variable: {0}")] #[error("Bad value for environment variable: {0}")]
BadEnv(#[from] VarError), BadEnv(#[from] VarError),
#[error("An endpoint was set for an ApiType that does not require one")]
EndpointSetButNotNeeded,
#[error("No endpoint was set for an ApiType that requires one")]
NoEndpointSet,
} }

View File

@ -1,66 +0,0 @@
use std::process::Command;
use git2;
use libpt::log::error;
use crate::error::ConfigError;
use crate::{config::Config, error::Result};
pub(crate) fn get_repo() -> Result<git2::Repository> {
let repo = match git2::Repository::open_from_env() {
Ok(repo) => repo,
Err(_err) => {
let err = ConfigError::GitRepoNotFound.into();
error!("{err}");
return Err(err);
}
};
Ok(repo)
}
pub async fn tag<'repo>(
repo: &'repo mut git2::Repository,
cfg: &Config,
) -> Result<git2::Tag<'repo>> {
// TODO: error handling
// TODO: allow force
// TODO: allow setting a message
// TODO: maybe using git as cmd is fancier?
let target = repo
.find_object(
repo.head().unwrap().target().unwrap(),
Some(git2::ObjectType::Commit),
)
.unwrap();
let tagger = repo.signature().expect("could not get signature");
let message = String::new();
let force = true;
let tag = repo
.tag(
&cfg.yaml.version.get_version(),
// "importantversion",
&target,
&tagger,
&message,
force,
)
.unwrap();
let tag: git2::Tag = repo.find_tag(tag).unwrap();
Ok(tag)
}
pub async fn push(_cfg: &Config) -> Result<()> {
// TODO: error handling
// TODO: maybe using git as lib is fancier?
Command::new("git").arg("push").status().unwrap();
Ok(())
}
pub async fn get_commit_sig<'repo>(repo: &'repo git2::Repository) -> Result<String> {
// TODO: error handling
// TODO: maybe using git as cmd is fancier?
let target = repo
.find_commit(repo.head().unwrap().target().unwrap())
.unwrap();
Ok(target.id().to_string())
}

View File

@ -1,7 +1,6 @@
pub mod changelog; pub mod changelog;
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod git;
pub mod publish; pub mod publish;
pub mod release; pub mod release;
pub mod serverapi; pub mod serverapi;

View File

@ -1,5 +1,3 @@
use std::error::Error as _;
use autocrate::{ use autocrate::{
changelog::*, changelog::*,
config::{ config::{
@ -9,39 +7,24 @@ use autocrate::{
error::*, error::*,
publish::publish, publish::publish,
release::release, release::release,
serverapi::ApiCollection, serverapi::init_servers,
}; };
use libpt::log::{debug, error};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cli = Cli::cli_parse(); let cli = Cli::cli_parse();
let cfg = Config::load(&cli)?; let cfg = Config::load(&cli)?;
let status: Option<Error> = match cli.command { match cli.command {
Commands::Changelog { .. } => { Commands::Changelog { .. } => {
let chlog = Changelog::build(&cfg); println!("{}", Changelog::build(&cfg)?);
if chlog.is_ok() {
println!("{}", chlog.unwrap());
None
} else {
Some(chlog.unwrap_err())
}
} }
Commands::Release { .. } => { Commands::Release { .. } => {
// TODO: check if repo is dirty and create a commit with a given option let mut apis = init_servers(&cfg).await?;
let mut apis = ApiCollection::build(&cfg).await?; release(&cfg, &mut apis).await?;
match release(&cfg, &mut apis).await {
Ok(_) => None,
Err(err) => Some(err),
}
} }
Commands::Publish { .. } => { Commands::Publish { .. } => {
// TODO: check if repo is dirty and create a commit with a given option publish(&cfg).await?;
match publish(&cfg).await {
Ok(_) => None,
Err(err) => Some(err),
}
} }
Commands::Version {} => { Commands::Version {} => {
// TODO: version bump // TODO: version bump
@ -54,9 +37,5 @@ async fn main() -> Result<()> {
todo!() todo!()
} }
}; };
if let Some(err) = status {
error!("{err}");
debug!("{:#?}", err.source());
}
Ok(()) Ok(())
} }

View File

@ -1,14 +1,5 @@
use crate::{ use crate::{config::Config, error::*, serverapi::ApiCollection};
config::Config,
error::*,
git::{self, get_commit_sig, push, tag},
serverapi::ApiCollection,
};
use futures::{self, stream::FuturesUnordered, StreamExt};
use libpt::log::info;
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct ReleaseContext { pub struct ReleaseContext {
pub draft: bool, pub draft: bool,
pub prerelease: bool, pub prerelease: bool,
@ -19,43 +10,13 @@ pub struct ReleaseContext {
pub commit_sig: String, pub commit_sig: String,
} }
pub async fn release(cfg: &Config, apis: &mut ApiCollection) -> Result<()> { pub async fn release(cfg: &Config, _apis: &mut ApiCollection) -> Result<()> {
// TODO: Error handling // TODO: git tag
let _changelog = crate::changelog::Changelog::build(cfg)?.to_string(); // TODO: push to each server
let mut repo = git::get_repo()?;
let tag = tag(&mut repo, cfg).await?.name().unwrap().to_string();
let commit_sig = get_commit_sig(&repo).await?;
push(cfg).await?; // we assume that we only need to push the current branch to the singular
// remote, expecting that the repositories are somehow mirrored
// TODO: push to multiple remotes?
let mut results = FuturesUnordered::new(); // TODO: release to each server
for api in apis.iter_mut() { tag(cfg).await?;
// TODO: check that auth exists todo!();
let specific_rc = ReleaseContext {
draft: true,
prerelease: true,
username: api
.get_inner()
.clone()
.auth
.expect("no auth but trying to publish")
.user,
repository: api.get_inner().repository.clone(),
text: crate::changelog::Changelog::build(cfg)?.to_string(),
tag: tag.clone(),
commit_sig: commit_sig.clone(),
};
info!("pushing release for {}", api.get_inner());
results.push(api.push_release(specific_rc));
}
// wait for the release requests to finish
while let Some(result) = results.next().await {
if result.is_err() {
return Err(result.unwrap_err());
}
}
// TODO: check that the release is made // TODO: check that the release is made
// TODO: generate artifacts // TODO: generate artifacts
@ -64,3 +25,7 @@ pub async fn release(cfg: &Config, apis: &mut ApiCollection) -> Result<()> {
Ok(()) Ok(())
} }
async fn tag(_cfg: &Config) -> Result<()> {
todo!()
}

View File

@ -4,54 +4,80 @@ use crate::{
serverapi::{PublishContext, ReleaseContext, ServerApi}, serverapi::{PublishContext, ReleaseContext, ServerApi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use forgejo_api; use reqwest::{
header::{HeaderMap, HeaderValue},
Client, Url,
};
pub struct Forgejo { pub struct Forgejo {
api: Api, cfg: Api,
cfg: Config, client: Client,
api_wrapper: forgejo_api::Forgejo,
} }
impl Forgejo { impl Forgejo {
pub async fn build(api: &Api, cfg: &Config) -> Result<Self> { pub async fn build(api: &Api) -> Result<Self> {
let api_wrapper: forgejo_api::Forgejo = forgejo_api::Forgejo::new( let mut headers: HeaderMap = HeaderMap::new();
forgejo_api::Auth::Token(&api.auth.clone().unwrap().pass.get_pass()?), // may be left empty if we only do reads from publically accessible urls
api.endpoint.clone().unwrap(), if api.auth.is_some() {
) let _ = headers.insert(
.map_err(ServerApiError::from)?; "Authorization",
HeaderValue::from_str(api.auth.clone().unwrap().pass.get_pass()?.as_str())
.map_err(ServerApiError::from)?,
);
}
let client = super::client_builder()
.default_headers(headers)
.build()
.map_err(ServerApiError::from)?;
Ok(Self { Ok(Self {
api: api.clone(), cfg: api.clone(),
cfg: cfg.clone(), client,
api_wrapper,
}) })
} }
} }
#[async_trait] #[async_trait]
impl ServerApi for Forgejo { impl ServerApi for Forgejo {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()> { async fn init(&mut self, _cfg: &Config) -> Result<()> {
let body: forgejo_api::structs::CreateReleaseOption = todo!()
forgejo_api::structs::CreateReleaseOption { }
body: Some(rc.text), async fn push_release(&mut self, rc: &ReleaseContext) -> Result<()> {
draft: Some(rc.draft), let raw_url = format!(
name: Some(rc.tag.clone()), "{}/api/v1/repos/{}/{}/releases",
prerelease: Some(rc.prerelease), self.cfg.endpoint, rc.username, rc.repository
tag_name: rc.tag, );
target_commitish: Some(rc.commit_sig), let url = Url::parse(&raw_url).map_err(ServerApiError::from)?;
}; let body = format!(
self.api_wrapper r#"
.repo_create_release(&rc.username, &rc.repository, body) {{
"body": "{}",
"draft": {},
"name": "{}",
"prerelease": {},
"tag_name": "{}",
"target_commitish": "{}"
}}
"#,
rc.text, rc.draft, rc.tag, rc.prerelease, rc.tag, rc.commit_sig
);
let request = self
.client
.post(url)
.body(body)
.build()
.map_err(ServerApiError::from)?;
let _response = self
.client
.execute(request)
.await .await
.map_err(ServerApiError::from)?; .map_err(ServerApiError::from)?;
Ok(()) Ok(())
} }
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> { async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> { async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
todo!() todo!()
} }
fn get_inner(&self) -> &Api {
&self.api
}
} }

View File

@ -6,27 +6,27 @@ use crate::{
error::*, error::*,
}; };
pub struct Gitea { pub struct Gitea {
api: Api, cfg: Api,
} }
#[async_trait] #[async_trait]
impl ServerApi for Gitea { impl ServerApi for Gitea {
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> { async fn init(&mut self, _cfg: &Config) -> Result<()> {
todo!() todo!()
} }
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> { async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> { async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
fn get_inner(&self) -> &Api { async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
&self.api todo!()
} }
} }
impl Gitea { impl Gitea {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> { pub async fn build(api: &Api) -> Result<Self> {
Ok(Self { api: api.clone() }) Ok(Self { cfg: api.clone() })
} }
} }

View File

@ -1,5 +1,4 @@
use async_trait::async_trait; use async_trait::async_trait;
use octocrab;
use super::{PublishContext, ReleaseContext, ServerApi}; use super::{PublishContext, ReleaseContext, ServerApi};
use crate::{ use crate::{
@ -7,41 +6,27 @@ use crate::{
error::*, error::*,
}; };
pub struct Github { pub struct Github {
api: Api, cfg: Api,
}
impl Github {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
Ok(Self {
api: api.to_owned(),
})
}
} }
#[async_trait] #[async_trait]
impl ServerApi for Github { impl ServerApi for Github {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()> { async fn init(&mut self, _cfg: &Config) -> Result<()> {
let _response = octocrab::instance()
.repos(rc.username, rc.repository)
.releases()
.create(&rc.tag)
.target_commitish(&rc.commit_sig)
.name(&rc.tag)
.body(&rc.text)
.draft(rc.draft)
.prerelease(rc.prerelease)
.send()
.await
.map_err(ServerApiError::from)?;
Ok(())
}
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!() todo!()
} }
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> { async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
fn get_inner(&self) -> &Api { async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
&self.api todo!()
}
async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
todo!()
}
}
impl Github {
pub async fn build(api: &Api) -> Result<Self> {
Ok(Self { cfg: api.clone() })
} }
} }

View File

@ -6,27 +6,27 @@ use crate::{
error::*, error::*,
}; };
pub struct Gitlab { pub struct Gitlab {
api: Api, cfg: Api,
} }
#[async_trait] #[async_trait]
impl ServerApi for Gitlab { impl ServerApi for Gitlab {
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> { async fn init(&mut self, _cfg: &Config) -> Result<()> {
todo!() todo!()
} }
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> { async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> { async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
todo!() todo!()
} }
fn get_inner(&self) -> &Api { async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
&self.api todo!()
} }
} }
impl Gitlab { impl Gitlab {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> { pub async fn build(api: &Api) -> Result<Self> {
Ok(Self { api: api.clone() }) Ok(Self { cfg: api.clone() })
} }
} }

View File

@ -1,10 +1,8 @@
use std::ops::{Deref, DerefMut};
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::ClientBuilder; use reqwest::ClientBuilder;
use crate::{ use crate::{
config::{self, ApiType, Config}, config::{ApiType, Config},
error::*, error::*,
publish::PublishContext, publish::PublishContext,
release::ReleaseContext, release::ReleaseContext,
@ -20,77 +18,43 @@ use github::*;
use gitlab::*; use gitlab::*;
pub static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); pub static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
pub type ApiCollection = Vec<Box<dyn ServerApi>>;
// NOTE: in stable rust, traits can normally not contain async methods, // NOTE: in stable rust, traits can normally not contain async methods,
// see [here](https://stackoverflow.com/questions/65921581/how-can-i-define-an-async-method-in-a-trait). // see [here](https://stackoverflow.com/questions/65921581/how-can-i-define-an-async-method-in-a-trait).
// The `async_trait` crate can be used to work around this limitation. // The `async_trait` crate can be used to work around this limitation.
#[async_trait] #[async_trait]
pub trait ServerApi { pub trait ServerApi {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()>; async fn init(&mut self, cfg: &Config) -> Result<()>;
async fn push_release_artifact(&mut self, rc: ReleaseContext) -> Result<()>; async fn push_release(&mut self, rc: &ReleaseContext) -> Result<()>;
async fn push_pkg(&mut self, pc: PublishContext) -> Result<()>; async fn push_release_artifact(&mut self, rc: &ReleaseContext) -> Result<()>;
fn get_inner(&self) -> &config::Api; async fn push_pkg(&mut self, pc: &PublishContext) -> Result<()>;
}
pub(crate) type ApiCollectionInner = Vec<Box<dyn ServerApi>>;
pub struct ApiCollection {
collection: ApiCollectionInner,
}
impl ApiCollection {
pub async fn build(cfg: &Config) -> Result<Self> {
let mut collection: ApiCollectionInner = ApiCollectionInner::new();
for api in &cfg.yaml.api {
match api.1.server_type {
ApiType::Gitea => {
collection.push(Box::new(Gitea::build(api.1, cfg).await?));
}
ApiType::Gitlab => {
collection.push(Box::new(Gitlab::build(api.1, cfg).await?));
}
ApiType::Github => {
collection.push(Box::new(Github::build(api.1, cfg).await?));
}
ApiType::Forgejo => {
collection.push(Box::new(Forgejo::build(api.1, cfg).await?));
}
}
}
Ok(ApiCollection { collection })
}
pub fn collection(&self) -> &ApiCollectionInner {
self.collection.as_ref()
}
pub fn collection_mut(&mut self) -> &mut ApiCollectionInner {
self.collection.as_mut()
}
} }
pub fn client_builder() -> ClientBuilder { pub fn client_builder() -> ClientBuilder {
ClientBuilder::new().user_agent(USER_AGENT) ClientBuilder::new().user_agent(USER_AGENT)
} }
// trait iimplementations for easy use of ApiCollection follow pub async fn init_servers(cfg: &Config) -> Result<ApiCollection> {
impl IntoIterator for ApiCollection { let mut collection: ApiCollection = ApiCollection::new();
fn into_iter(self) -> Self::IntoIter { for api in &cfg.yaml.api {
self.collection.into_iter() match api.1.server_type {
ApiType::Gitea => {
collection.push(Box::new(Gitea::build(api.1).await?));
}
ApiType::Gitlab => {
collection.push(Box::new(Gitlab::build(api.1).await?));
}
ApiType::Github => {
collection.push(Box::new(Github::build(api.1).await?));
}
ApiType::Forgejo => {
collection.push(Box::new(Forgejo::build(api.1).await?));
}
}
} }
type Item = Box<dyn ServerApi>; for api in collection.iter_mut() {
type IntoIter = std::vec::IntoIter<Self::Item>; api.init(cfg).await?;
}
impl Deref for ApiCollection {
type Target = [Box<dyn ServerApi>];
fn deref(&self) -> &Self::Target {
&self.collection[..]
}
}
impl DerefMut for ApiCollection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.collection[..]
} }
Ok(collection)
} }