Inital
This commit is contained in:
commit
1db4778dc4
8 changed files with 2410 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/.dev-environment.json
|
||||||
|
/target/
|
||||||
2108
Cargo.lock
generated
Normal file
2108
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "fafo-taskbot"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["rahix <rahix@rahix.de>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
env_logger = { version = "0.11.9", default-features = false, features = ["auto-color", "color", "humantime"] }
|
||||||
|
forgejo-api = "0.9.1"
|
||||||
|
log = "0.4.29"
|
||||||
|
url = "2.5.8"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
|
time = "0.3.41"
|
||||||
93
src/ci_meta.rs
Normal file
93
src/ci_meta.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CiEventMeta {
|
||||||
|
pub action: Option<IssueAction>,
|
||||||
|
pub issue: Option<IssueMeta>,
|
||||||
|
pub repository: Option<RepoMeta>,
|
||||||
|
|
||||||
|
// Only for development environments, may be used as fallback for `$GITHUB_TOKEN`
|
||||||
|
#[serde(rename = "dev-token")]
|
||||||
|
pub dev_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum IssueAction {
|
||||||
|
Opened,
|
||||||
|
Reopened,
|
||||||
|
Closed,
|
||||||
|
Assigned,
|
||||||
|
Unassigned,
|
||||||
|
Edited,
|
||||||
|
#[serde(rename = "label_updated")]
|
||||||
|
LabelUpdated,
|
||||||
|
Labeled,
|
||||||
|
#[serde(rename = "label_cleared")]
|
||||||
|
LabelCleared,
|
||||||
|
Unlabeled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct IssueMeta {
|
||||||
|
pub number: u64,
|
||||||
|
// pub repository: RepoMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RepoMeta {
|
||||||
|
pub name: String,
|
||||||
|
pub owner: OwnerMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct OwnerMeta {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ci_meta_or_fallback() -> anyhow::Result<CiEventMeta> {
|
||||||
|
use std::io::Read as _;
|
||||||
|
|
||||||
|
let path = if let Some(p) = std::env::var_os("GITHUB_EVENT_PATH") {
|
||||||
|
PathBuf::from(p)
|
||||||
|
} else {
|
||||||
|
dev_environment_path()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut f = std::fs::File::open(&path).context("Could not open GITHUB_EVENT_PATH file")?;
|
||||||
|
let mut s = String::new();
|
||||||
|
f.read_to_string(&mut s)?;
|
||||||
|
log::info!("Event Metadata: \n{s}");
|
||||||
|
|
||||||
|
let f = std::fs::File::open(&path).context("Could not open GITHUB_EVENT_PATH file")?;
|
||||||
|
let meta: CiEventMeta = serde_json::de::from_reader(f).context("Failed to parse")?;
|
||||||
|
|
||||||
|
Ok(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dev_environment_path() -> anyhow::Result<PathBuf> {
|
||||||
|
log::warn!("Not running in CI, using .dev-environment.json metadata instead.");
|
||||||
|
let p = PathBuf::from(".dev-environment.json");
|
||||||
|
if !p.exists() {
|
||||||
|
log::error!(
|
||||||
|
"{}",
|
||||||
|
r#"When not running in CI environment,
|
||||||
|
metadata has to be substituded using a `.dev-environment.json` file.
|
||||||
|
|
||||||
|
Example content:
|
||||||
|
|
||||||
|
{
|
||||||
|
"repository": {
|
||||||
|
"owner": "rahix",
|
||||||
|
"name": "arbeitsschutz"
|
||||||
|
},
|
||||||
|
"dev-token": "<your development access token>"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
anyhow::bail!("Missing development drop-in CI metadata")
|
||||||
|
}
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
41
src/collect.rs
Normal file
41
src/collect.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::task;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
|
||||||
|
let issues = list_all_issues(ctx).await?;
|
||||||
|
let issues: Vec<_> = issues
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
format!(
|
||||||
|
"#{} — {}",
|
||||||
|
i.number.unwrap_or(-1),
|
||||||
|
i.title.as_deref().unwrap_or("")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
dbg!(issues);
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<forgejo_api::structs::Issue>> {
|
||||||
|
let issues = ctx
|
||||||
|
.forgejo
|
||||||
|
.issue_list_issues(
|
||||||
|
&ctx.owner,
|
||||||
|
&ctx.repo,
|
||||||
|
forgejo_api::structs::IssueListIssuesQuery {
|
||||||
|
// We also want the closed issues
|
||||||
|
state: Some(forgejo_api::structs::IssueListIssuesQueryState::All),
|
||||||
|
// Only issues
|
||||||
|
r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed fetching issue list"))?;
|
||||||
|
|
||||||
|
Ok(issues)
|
||||||
|
}
|
||||||
120
src/context.rs
Normal file
120
src/context.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
/// API Accessor object
|
||||||
|
pub forgejo: forgejo_api::Forgejo,
|
||||||
|
/// Repository Owner
|
||||||
|
pub owner: String,
|
||||||
|
/// Repository Name
|
||||||
|
pub repo: String,
|
||||||
|
/// URL of the repository page
|
||||||
|
pub repo_url: url::Url,
|
||||||
|
/// URL of the repository with authentication information attached
|
||||||
|
pub repo_auth_url: url::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Context {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Context")
|
||||||
|
.field("owner", &self.owner)
|
||||||
|
.field("repo", &self.repo)
|
||||||
|
.field("repo_url", &self.repo_url.to_string())
|
||||||
|
.field("repo_auth_url", &"<redacted>")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new_from_env_or_dev_fallback() -> anyhow::Result<Self> {
|
||||||
|
if std::env::var("CI").is_ok() {
|
||||||
|
Self::new_from_env()
|
||||||
|
} else {
|
||||||
|
log::info!("No CI=1 in environment, using development fallback environment.");
|
||||||
|
Self::new_from_dev_fallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_env() -> anyhow::Result<Self> {
|
||||||
|
let token = get_env_value("GITHUB_TOKEN")?;
|
||||||
|
let server_url = get_env_value("GITHUB_SERVER_URL")?;
|
||||||
|
let repo_with_owner = get_env_value("GITHUB_REPOSITORY")?;
|
||||||
|
|
||||||
|
Self::new(&token, &server_url, &repo_with_owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_dev_fallback() -> anyhow::Result<Self> {
|
||||||
|
let mut f = std::fs::File::open(&dev_environment_path()?)
|
||||||
|
.context("Failed to open dev environment config")?;
|
||||||
|
|
||||||
|
let config: DevEnvironment =
|
||||||
|
serde_json::de::from_reader(f).context("Failed to parse dev environment")?;
|
||||||
|
|
||||||
|
Self::new(&config.dev_token, &config.server_url, &config.repo_with_owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(token: &str, server_url: &str, repo_with_owner: &str) -> anyhow::Result<Self> {
|
||||||
|
let (owner, repo) = repo_with_owner.rsplit_once("/").with_context(|| {
|
||||||
|
format!("Could not split repo slug {repo_with_owner:?} into owner and repo")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let server_url = url::Url::parse(&server_url).context("Failed parsing server URL")?;
|
||||||
|
|
||||||
|
let repo_url = server_url
|
||||||
|
.join(&repo_with_owner)
|
||||||
|
.context("Failed to build server + repo URL")?;
|
||||||
|
let repo_auth_url = {
|
||||||
|
let mut repo_auth_url = repo_url.clone();
|
||||||
|
repo_auth_url.set_username("forgejo-actions").unwrap();
|
||||||
|
repo_auth_url.set_password(Some(token)).unwrap();
|
||||||
|
repo_auth_url
|
||||||
|
};
|
||||||
|
|
||||||
|
let forgejo = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), server_url)
|
||||||
|
.context("Could not create Forgejo API access object")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
forgejo,
|
||||||
|
owner: owner.to_owned(),
|
||||||
|
repo: repo.to_owned(),
|
||||||
|
repo_url,
|
||||||
|
repo_auth_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_env_value(key: &str) -> anyhow::Result<String> {
|
||||||
|
std::env::var(key).with_context(|| format!("Missing ${key} environment variable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
struct DevEnvironment {
|
||||||
|
pub dev_token: String,
|
||||||
|
pub server_url: String,
|
||||||
|
pub repo_with_owner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dev_environment_path() -> anyhow::Result<PathBuf> {
|
||||||
|
log::warn!("Not running in CI, using .dev-environment.json metadata instead.");
|
||||||
|
let p = PathBuf::from(".dev-environment.json");
|
||||||
|
if !p.exists() {
|
||||||
|
log::error!(
|
||||||
|
"{}",
|
||||||
|
r#"When not running in CI environment,
|
||||||
|
metadata has to be substituded using a `.dev-environment.json` file.
|
||||||
|
|
||||||
|
Example content:
|
||||||
|
|
||||||
|
{
|
||||||
|
"dev-token": "<your development access token>",
|
||||||
|
"server-url": "https://git.fa-fo.de",
|
||||||
|
"repo-with-owner": "rahix/TaskBot"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
anyhow::bail!("Missing development environment config.")
|
||||||
|
}
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
23
src/main.rs
Normal file
23
src/main.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
mod task;
|
||||||
|
mod collect;
|
||||||
|
mod ci_meta;
|
||||||
|
mod context;
|
||||||
|
|
||||||
|
use context::Context;
|
||||||
|
|
||||||
|
async fn run() -> anyhow::Result<()> {
|
||||||
|
let ctx = Context::new_from_env_or_dev_fallback()?;
|
||||||
|
log::info!("Context: \n{ctx:#?}");
|
||||||
|
|
||||||
|
collect::collect_tasks(&ctx).await?;
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
run().await
|
||||||
|
}
|
||||||
5
src/task.rs
Normal file
5
src/task.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Task {
|
||||||
|
issue_number: u64,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue