generated from PlexSheep/rs-base
Compare commits
32 Commits
v0.1.0-pre
...
master
Author | SHA1 | Date |
---|---|---|
Christoph J. Scherr | 872338c83f | |
Christoph J. Scherr | 411ded1395 | |
PlexSheep | 97b36966d5 | |
Christoph J. Scherr | e7ab89f050 | |
Christoph J. Scherr | ca3d1b7e82 | |
PlexSheep | fcd6b6a4d4 | |
Christoph J. Scherr | 56d6a50142 | |
Christoph J. Scherr | ff9306047e | |
Christoph J. Scherr | 181954cbce | |
Christoph J. Scherr | 5bb4072e24 | |
PlexSheep | 9aa9c8e07b | |
PlexSheep | e767752338 | |
Christoph J. Scherr | e80b68f1c4 | |
Christoph J. Scherr | e66550f2ef | |
PlexSheep | abe2e25071 | |
Christoph J. Scherr | bf2b9c6d08 | |
Christoph J. Scherr | 378b269fa0 | |
Christoph J. Scherr | 1bc34011c1 | |
Christoph J. Scherr | 0e4d694b35 | |
Christoph J. Scherr | 926f72b87e | |
Christoph J. Scherr | 51a50cd13f | |
PlexSheep | 90312a457b | |
Christoph J. Scherr | 2f8442bf74 | |
Christoph J. Scherr | 0c2f270336 | |
Christoph J. Scherr | 9839b8b034 | |
PlexSheep | 23c51ac1da | |
Christoph J. Scherr | 7d4a3986a9 | |
Christoph J. Scherr | 2c26a65a65 | |
Christoph J. Scherr | 3e9b58da62 | |
Christoph J. Scherr | 6e9183af20 | |
Christoph J. Scherr | 6e5582cbe6 | |
Christoph J. Scherr | 6f70057aba |
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
version:
|
||||
!cargo
|
||||
changelog:
|
||||
enable: true
|
||||
git-log: true
|
||||
|
@ -14,15 +15,16 @@ uses:
|
|||
api:
|
||||
github:
|
||||
type: github
|
||||
endpoint: https://github.com
|
||||
repository: autocrate
|
||||
auth:
|
||||
user: PlexSheep
|
||||
pass:
|
||||
env: TOKEN_GH
|
||||
!env TOKEN_GH
|
||||
cscherr:
|
||||
type: forgejo
|
||||
endpoint: https://git.cscherr.de
|
||||
repository: autocrate
|
||||
auth:
|
||||
user: PlexSheep
|
||||
pass:
|
||||
env: TOKEN_CSCHERR
|
||||
!env TOKEN_CSCHERR
|
||||
|
|
|
@ -31,13 +31,13 @@ jobs:
|
|||
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
|
||||
cat ~/.cargo/config.toml
|
||||
- name: cargo clippy check
|
||||
run: cargo clippy --all-features --all-targets
|
||||
run: cargo clippy --all-features --all-targets --workspace
|
||||
- name: cargo clippy fix
|
||||
run: cargo clippy --fix --all-features --all-targets
|
||||
run: cargo clippy --fix --all-features --all-targets --workspace
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all
|
||||
- name: cargo test
|
||||
run: cargo test --all-features --all-targets
|
||||
run: cargo test --all-features --all-targets --workspace
|
||||
- name: commit back to repository
|
||||
uses: https://github.com/stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
|
|
|
@ -32,13 +32,13 @@ jobs:
|
|||
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
|
||||
cat ~/.cargo/config.toml
|
||||
- name: cargo clippy check
|
||||
run: cargo clippy --all-features --all-targets
|
||||
run: cargo clippy --all-features --all-targets --workspace
|
||||
- name: cargo clippy fix
|
||||
run: cargo clippy --fix --all-features --all-targets
|
||||
run: cargo clippy --fix --all-features --all-targets --workspace
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all
|
||||
- name: cargo test
|
||||
run: cargo test --all-features --all-targets
|
||||
run: cargo test --all-features --all-targets --workspace
|
||||
- name: commit back to repository
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
|
|
|
@ -19,3 +19,4 @@ Cargo.lock
|
|||
# Added by cargo
|
||||
|
||||
/target
|
||||
.env
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "autocrate"
|
||||
version = "0.1.0-prealpha.3"
|
||||
version = "0.1.0-prealpha.5"
|
||||
edition = "2021"
|
||||
publish = true
|
||||
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
||||
|
@ -25,10 +25,14 @@ async-trait = "0.1.77"
|
|||
# cargo = "0.76.0"
|
||||
clap = { version = "4.4.18", features = ["derive", "help"] }
|
||||
clap-verbosity-flag = "2.1.2"
|
||||
forgejo-api = "0.1.0"
|
||||
futures = "0.3.30"
|
||||
git2 = "0.18.1"
|
||||
libpt = { version = "0.3.11", features = ["log"] }
|
||||
libpt = { version = "0.4.2", features = ["log"] }
|
||||
octocrab = "0.38.0"
|
||||
reqwest = "0.11.24"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
serde_yaml = "0.9.30"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.56"
|
||||
|
|
82
README.md
82
README.md
|
@ -8,47 +8,65 @@
|
|||
|
||||
![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
|
||||
projects hosted on Gitea servers. By providing essential functionalities
|
||||
like uploading artifacts, publishing crates, and managing changelogs,
|
||||
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.
|
||||
projects hosted on fancy git servers. By providing functionalities
|
||||
like creating releases uploading artifacts, publishing crates, and managing changelogs,
|
||||
Autocrate tries to streamline the release process. Although initially built for Forgejo,
|
||||
I plan to extend support to other 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)
|
||||
* [Codeberg Mirror](https://codeberg.org/PlexSheep/autocrate)
|
||||
* [GitHub Mirror](https://github.com/PlexSheep/Autocrate)
|
||||
* [crates.io](https://crates.io/crates/autocrate)
|
||||
* [docs.rs](https://docs.rs/crate/autocrate/)
|
||||
|
||||
Take a look at the [scripts](./scripts) directory! [publish.sh](scripts/publish.sh)
|
||||
and [release.sh](scripts/release.sh) are exactly what I'm trying to get rid of.
|
||||
and [release.sh](scripts/release.sh) are what I'm trying to get rid of.
|
||||
|
||||
## Features
|
||||
|
||||
* Create and update releases on your Gitea server
|
||||
* Publish crates to Cargo.rs
|
||||
or other repositories directly from your Rust projects
|
||||
* Upload artifacts, including documentation and binaries, alongside your releases
|
||||
* Generate and maintain changelogs and release notes.
|
||||
* Create and update releases on your Git platform
|
||||
* Publish crates to crates.io or other repositories
|
||||
* Upload artifacts, including binaries and signatures alongside your releases
|
||||
* Generate changelogs and release notes
|
||||
* Configure with a simple yaml file
|
||||
|
||||
### Upcoming Features
|
||||
|
||||
My goal is to continuously enhance Autocrate to better serve the developer
|
||||
community. Some planned improvements include supporting other popular hosting
|
||||
platforms, enabling even greater flexibility and convenience.
|
||||
Autocrate is still in pre-alpha, so the features listed above are still being
|
||||
worked on. For the future, the following Features are planned:
|
||||
|
||||
* 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
|
||||
|
||||
Before getting started with Autocrate, make sure you have the necessary
|
||||
prerequisites covered:
|
||||
|
||||
* **A Rust Environment**: Install the latest stable Rust compiler and
|
||||
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)
|
||||
and [codeberg.org](https://codeberg.org) (strictly speaking, uses a Gitea fork)
|
||||
You can use `autocrate init` to set your workspace up with a basic
|
||||
`.autocrate.yaml`.
|
||||
|
||||
* **Access to a supported Git Server** (such as [git.cscherr.de](https://git.cscherr.de)
|
||||
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
|
||||
|
||||
|
@ -122,7 +140,11 @@ with itself, so you can take a look at this repositories
|
|||
|
||||
## Using Autocrate
|
||||
|
||||
TBD
|
||||
After you have your workspace with a `.autocrate.yaml` file, you can:
|
||||
|
||||
* `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
|
||||
|
||||
|
@ -133,13 +155,23 @@ License. Please refer to [`LICENSE`](./LICENSE) for complete licensing details.
|
|||
|
||||
## Project status
|
||||
|
||||
The project has started recently and is currently in pre-alpha.
|
||||
The project has started recently and is currently in pre-alpha. Many features
|
||||
are still missing or experimental
|
||||
|
||||
## Contributing
|
||||
|
||||
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 on
|
||||
GitHub. If enough activity comes around, moving to GitHub might be a good idea.
|
||||
my self hosted git server, you're free to create issues, PRs and so on
|
||||
GitHub. If enough activity comes around, moving to GitHub Codeberg might be a
|
||||
good idea.
|
||||
|
||||
If you have any questions, use issues and discussions tabs or write me an email
|
||||
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).
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# 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.
|
|
@ -17,6 +17,7 @@ impl Changelog {
|
|||
Ok(Changelog { git_log })
|
||||
}
|
||||
|
||||
// TODO: use libgit2 instead of the cli interface
|
||||
fn make_git_log(cfg: &Config) -> Result<Option<String>> {
|
||||
if !cfg.yaml.changelog.enable {
|
||||
return Ok(None);
|
||||
|
@ -35,7 +36,6 @@ impl Changelog {
|
|||
return Err(ChangelogError::GitBadStatus(out.status, buf).into());
|
||||
}
|
||||
|
||||
dbg!(&buf);
|
||||
Ok(Some(buf))
|
||||
}
|
||||
|
||||
|
|
|
@ -62,9 +62,11 @@ pub enum Commands {
|
|||
message: Option<Vec<String>>,
|
||||
|
||||
/// generate and add a changelog
|
||||
#[arg(short, long)]
|
||||
changelog: bool,
|
||||
|
||||
/// publish after releasing
|
||||
#[arg(short, long)]
|
||||
publish: bool,
|
||||
},
|
||||
/// Publish to a package registry
|
||||
|
@ -108,10 +110,10 @@ impl Cli {
|
|||
}
|
||||
};
|
||||
if cli.meta {
|
||||
Logger::init(None, Some(ll), true).expect("could not initialize Logger");
|
||||
Logger::build(None, Some(ll), true).expect("could not initialize Logger");
|
||||
} else {
|
||||
// less verbose version
|
||||
Logger::init_mini(Some(ll)).expect("could not initialize Logger");
|
||||
Logger::build_mini(Some(ll)).expect("could not initialize Logger");
|
||||
}
|
||||
cli
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use std::{collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::{
|
||||
collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf, process::Command,
|
||||
};
|
||||
|
||||
use git2;
|
||||
use libpt::log::{debug, error, trace};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::error::*;
|
||||
|
@ -15,7 +18,7 @@ pub trait YamlConfigSection: Debug + Clone + for<'a> Deserialize<'a> {
|
|||
fn check(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Changelog {
|
||||
pub enable: bool,
|
||||
#[serde(alias = "git-log")]
|
||||
|
@ -27,7 +30,7 @@ impl YamlConfigSection for Changelog {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UseCargo {
|
||||
pub publish: bool,
|
||||
pub registries: Vec<String>,
|
||||
|
@ -38,7 +41,7 @@ impl YamlConfigSection for UseCargo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Uses {
|
||||
cargo: UseCargo,
|
||||
}
|
||||
|
@ -49,13 +52,17 @@ impl YamlConfigSection for Uses {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Pass {
|
||||
/// pass specified as plainext
|
||||
#[serde(alias = "pass_text")]
|
||||
Text(String),
|
||||
/// pass to be loaded from an env var
|
||||
#[serde(alias = "pass_env")]
|
||||
Env(String),
|
||||
/// pass to be loaded from a file
|
||||
#[serde(alias = "pass_file")]
|
||||
File(PathBuf),
|
||||
}
|
||||
impl Pass {
|
||||
|
@ -88,7 +95,7 @@ impl YamlConfigSection for Pass {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ApiAuth {
|
||||
pub user: String,
|
||||
pub pass: Pass,
|
||||
|
@ -100,20 +107,45 @@ impl YamlConfigSection for ApiAuth {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Api {
|
||||
#[serde(alias = "type")]
|
||||
pub server_type: ApiType,
|
||||
pub endpoint: Url,
|
||||
/// May be left empty if the [ApiType] is [Github](ApiType::Github).
|
||||
pub endpoint: Option<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>,
|
||||
/// 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 {
|
||||
fn check(&self) -> Result<()> {
|
||||
self.server_type.check()?;
|
||||
match self.endpoint.socket_addrs(|| None) {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
if self.server_type != ApiType::Github {
|
||||
if self.auth.is_none() {
|
||||
return Err(ConfigError::NoEndpointSet.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() {
|
||||
self.auth.clone().unwrap().check()?;
|
||||
|
@ -122,7 +154,7 @@ impl YamlConfigSection for Api {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum ApiType {
|
||||
#[serde(alias = "gitea")]
|
||||
Gitea,
|
||||
|
@ -133,22 +165,97 @@ pub enum ApiType {
|
|||
#[serde(alias = "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 {
|
||||
fn check(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[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 changelog: Changelog,
|
||||
pub uses: Uses,
|
||||
pub api: HashMap<String, Api>,
|
||||
pub version: Version,
|
||||
}
|
||||
impl YamlConfigSection for YamlConfig {
|
||||
fn check(&self) -> Result<()> {
|
||||
self.changelog.check()?;
|
||||
self.uses.check()?;
|
||||
self.version.check()?;
|
||||
for api in self.api.values() {
|
||||
api.check()?;
|
||||
}
|
||||
|
@ -166,10 +273,10 @@ impl YamlConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub yaml: YamlConfig,
|
||||
pub cli: Cli,
|
||||
pub repo: git2::Repository,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -226,7 +333,6 @@ impl Config {
|
|||
|
||||
Ok(Config {
|
||||
yaml,
|
||||
repo,
|
||||
path,
|
||||
cli: cli.clone(),
|
||||
})
|
||||
|
|
|
@ -32,6 +32,10 @@ pub enum ServerApiError {
|
|||
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
|
||||
#[error(transparent)]
|
||||
ReqwestErr(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
ForgejoApiError(#[from] forgejo_api::ForgejoError),
|
||||
#[error(transparent)]
|
||||
GithubApiError(#[from] octocrab::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -61,4 +65,8 @@ pub enum ConfigError {
|
|||
EnvNotSet(String),
|
||||
#[error("Bad value for environment variable: {0}")]
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
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())
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod changelog;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod git;
|
||||
pub mod publish;
|
||||
pub mod release;
|
||||
pub mod serverapi;
|
||||
|
|
33
src/main.rs
33
src/main.rs
|
@ -1,3 +1,5 @@
|
|||
use std::error::Error as _;
|
||||
|
||||
use autocrate::{
|
||||
changelog::*,
|
||||
config::{
|
||||
|
@ -7,24 +9,39 @@ use autocrate::{
|
|||
error::*,
|
||||
publish::publish,
|
||||
release::release,
|
||||
serverapi::init_servers,
|
||||
serverapi::ApiCollection,
|
||||
};
|
||||
use libpt::log::{debug, error};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::cli_parse();
|
||||
let cfg = Config::load(&cli)?;
|
||||
|
||||
match cli.command {
|
||||
let status: Option<Error> = match cli.command {
|
||||
Commands::Changelog { .. } => {
|
||||
println!("{}", Changelog::build(&cfg)?);
|
||||
let chlog = Changelog::build(&cfg);
|
||||
if chlog.is_ok() {
|
||||
println!("{}", chlog.unwrap());
|
||||
None
|
||||
} else {
|
||||
Some(chlog.unwrap_err())
|
||||
}
|
||||
}
|
||||
Commands::Release { .. } => {
|
||||
let mut apis = init_servers(&cfg).await?;
|
||||
release(&cfg, &mut apis).await?;
|
||||
// TODO: check if repo is dirty and create a commit with a given option
|
||||
let mut apis = ApiCollection::build(&cfg).await?;
|
||||
match release(&cfg, &mut apis).await {
|
||||
Ok(_) => None,
|
||||
Err(err) => Some(err),
|
||||
}
|
||||
}
|
||||
Commands::Publish { .. } => {
|
||||
publish(&cfg).await?;
|
||||
// TODO: check if repo is dirty and create a commit with a given option
|
||||
match publish(&cfg).await {
|
||||
Ok(_) => None,
|
||||
Err(err) => Some(err),
|
||||
}
|
||||
}
|
||||
Commands::Version {} => {
|
||||
// TODO: version bump
|
||||
|
@ -37,5 +54,9 @@ async fn main() -> Result<()> {
|
|||
todo!()
|
||||
}
|
||||
};
|
||||
if let Some(err) = status {
|
||||
error!("{err}");
|
||||
debug!("{:#?}", err.source());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
use crate::{config::Config, error::*, serverapi::ApiCollection};
|
||||
use crate::{
|
||||
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 draft: bool,
|
||||
pub prerelease: bool,
|
||||
|
@ -10,13 +19,43 @@ pub struct ReleaseContext {
|
|||
pub commit_sig: String,
|
||||
}
|
||||
|
||||
pub async fn release(cfg: &Config, _apis: &mut ApiCollection) -> Result<()> {
|
||||
// TODO: git tag
|
||||
// TODO: push to each server
|
||||
pub async fn release(cfg: &Config, apis: &mut ApiCollection) -> Result<()> {
|
||||
// TODO: Error handling
|
||||
let _changelog = crate::changelog::Changelog::build(cfg)?.to_string();
|
||||
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?
|
||||
|
||||
// TODO: release to each server
|
||||
tag(cfg).await?;
|
||||
todo!();
|
||||
let mut results = FuturesUnordered::new();
|
||||
for api in apis.iter_mut() {
|
||||
// TODO: check that auth exists
|
||||
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: generate artifacts
|
||||
|
@ -25,7 +64,3 @@ pub async fn release(cfg: &Config, _apis: &mut ApiCollection) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tag(_cfg: &Config) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -4,80 +4,54 @@ use crate::{
|
|||
serverapi::{PublishContext, ReleaseContext, ServerApi},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, Url,
|
||||
};
|
||||
use forgejo_api;
|
||||
|
||||
pub struct Forgejo {
|
||||
cfg: Api,
|
||||
client: Client,
|
||||
api: Api,
|
||||
cfg: Config,
|
||||
api_wrapper: forgejo_api::Forgejo,
|
||||
}
|
||||
|
||||
impl Forgejo {
|
||||
pub async fn build(api: &Api) -> Result<Self> {
|
||||
let mut headers: HeaderMap = HeaderMap::new();
|
||||
// may be left empty if we only do reads from publically accessible urls
|
||||
if api.auth.is_some() {
|
||||
let _ = headers.insert(
|
||||
"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)?;
|
||||
pub async fn build(api: &Api, cfg: &Config) -> Result<Self> {
|
||||
let api_wrapper: forgejo_api::Forgejo = forgejo_api::Forgejo::new(
|
||||
forgejo_api::Auth::Token(&api.auth.clone().unwrap().pass.get_pass()?),
|
||||
api.endpoint.clone().unwrap(),
|
||||
)
|
||||
.map_err(ServerApiError::from)?;
|
||||
Ok(Self {
|
||||
cfg: api.clone(),
|
||||
client,
|
||||
api: api.clone(),
|
||||
cfg: cfg.clone(),
|
||||
api_wrapper,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerApi for Forgejo {
|
||||
async fn init(&mut self, _cfg: &Config) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release(&mut self, rc: &ReleaseContext) -> Result<()> {
|
||||
let raw_url = format!(
|
||||
"{}/api/v1/repos/{}/{}/releases",
|
||||
self.cfg.endpoint, rc.username, rc.repository
|
||||
);
|
||||
let url = Url::parse(&raw_url).map_err(ServerApiError::from)?;
|
||||
let body = format!(
|
||||
r#"
|
||||
{{
|
||||
"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)
|
||||
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()> {
|
||||
let body: forgejo_api::structs::CreateReleaseOption =
|
||||
forgejo_api::structs::CreateReleaseOption {
|
||||
body: Some(rc.text),
|
||||
draft: Some(rc.draft),
|
||||
name: Some(rc.tag.clone()),
|
||||
prerelease: Some(rc.prerelease),
|
||||
tag_name: rc.tag,
|
||||
target_commitish: Some(rc.commit_sig),
|
||||
};
|
||||
self.api_wrapper
|
||||
.repo_create_release(&rc.username, &rc.repository, body)
|
||||
.await
|
||||
.map_err(ServerApiError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
|
||||
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
fn get_inner(&self) -> &Api {
|
||||
&self.api
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,27 @@ use crate::{
|
|||
error::*,
|
||||
};
|
||||
pub struct Gitea {
|
||||
cfg: Api,
|
||||
api: Api,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerApi for Gitea {
|
||||
async fn init(&mut self, _cfg: &Config) -> Result<()> {
|
||||
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
fn get_inner(&self) -> &Api {
|
||||
&self.api
|
||||
}
|
||||
}
|
||||
|
||||
impl Gitea {
|
||||
pub async fn build(api: &Api) -> Result<Self> {
|
||||
Ok(Self { cfg: api.clone() })
|
||||
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
|
||||
Ok(Self { api: api.clone() })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use async_trait::async_trait;
|
||||
use octocrab;
|
||||
|
||||
use super::{PublishContext, ReleaseContext, ServerApi};
|
||||
use crate::{
|
||||
|
@ -6,27 +7,41 @@ use crate::{
|
|||
error::*,
|
||||
};
|
||||
pub struct Github {
|
||||
cfg: Api,
|
||||
api: Api,
|
||||
}
|
||||
|
||||
impl Github {
|
||||
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
|
||||
Ok(Self {
|
||||
api: api.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerApi for Github {
|
||||
async fn init(&mut self, _cfg: &Config) -> Result<()> {
|
||||
async fn push_release(&mut self, rc: ReleaseContext) -> 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!()
|
||||
}
|
||||
async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
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() })
|
||||
fn get_inner(&self) -> &Api {
|
||||
&self.api
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,27 @@ use crate::{
|
|||
error::*,
|
||||
};
|
||||
pub struct Gitlab {
|
||||
cfg: Api,
|
||||
api: Api,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerApi for Gitlab {
|
||||
async fn init(&mut self, _cfg: &Config) -> Result<()> {
|
||||
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_release_artifact(&mut self, _rc: &ReleaseContext) -> Result<()> {
|
||||
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn push_pkg(&mut self, _pc: &PublishContext) -> Result<()> {
|
||||
todo!()
|
||||
fn get_inner(&self) -> &Api {
|
||||
&self.api
|
||||
}
|
||||
}
|
||||
|
||||
impl Gitlab {
|
||||
pub async fn build(api: &Api) -> Result<Self> {
|
||||
Ok(Self { cfg: api.clone() })
|
||||
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
|
||||
Ok(Self { api: api.clone() })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::ClientBuilder;
|
||||
|
||||
use crate::{
|
||||
config::{ApiType, Config},
|
||||
config::{self, ApiType, Config},
|
||||
error::*,
|
||||
publish::PublishContext,
|
||||
release::ReleaseContext,
|
||||
|
@ -18,43 +20,77 @@ use github::*;
|
|||
use gitlab::*;
|
||||
|
||||
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,
|
||||
// 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.
|
||||
#[async_trait]
|
||||
pub trait ServerApi {
|
||||
async fn init(&mut self, cfg: &Config) -> Result<()>;
|
||||
async fn push_release(&mut self, rc: &ReleaseContext) -> Result<()>;
|
||||
async fn push_release_artifact(&mut self, rc: &ReleaseContext) -> Result<()>;
|
||||
async fn push_pkg(&mut self, pc: &PublishContext) -> Result<()>;
|
||||
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()>;
|
||||
async fn push_release_artifact(&mut self, rc: ReleaseContext) -> Result<()>;
|
||||
async fn push_pkg(&mut self, pc: PublishContext) -> Result<()>;
|
||||
fn get_inner(&self) -> &config::Api;
|
||||
}
|
||||
|
||||
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 {
|
||||
ClientBuilder::new().user_agent(USER_AGENT)
|
||||
}
|
||||
|
||||
pub async fn init_servers(cfg: &Config) -> Result<ApiCollection> {
|
||||
let mut collection: ApiCollection = ApiCollection::new();
|
||||
for api in &cfg.yaml.api {
|
||||
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?));
|
||||
}
|
||||
}
|
||||
// trait iimplementations for easy use of ApiCollection follow
|
||||
impl IntoIterator for ApiCollection {
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.collection.into_iter()
|
||||
}
|
||||
type Item = Box<dyn ServerApi>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
}
|
||||
|
||||
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[..]
|
||||
}
|
||||
for api in collection.iter_mut() {
|
||||
api.init(cfg).await?;
|
||||
}
|
||||
Ok(collection)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue