2
.env
2
.env
@@ -1,2 +1,2 @@
|
||||
RUST_LOG=notmad=debug,info
|
||||
RUST_LOG=notmad=debug,nodrift=debug,forge=trace,info
|
||||
FE_CONFIG_DIR=./templates/fe/configs/
|
||||
|
||||
1305
Cargo.lock
generated
1305
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,3 +19,5 @@ tokio-util = "0.7.18"
|
||||
async-trait = "0.1.89"
|
||||
regex = "1.12.3"
|
||||
toml = "0.9.11"
|
||||
nodrift = "0.3.5"
|
||||
octocrab = "0.49.5"
|
||||
|
||||
@@ -2,6 +2,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ForgeConfig {
|
||||
#[serde(flatten)]
|
||||
pub forge_type: ForgeConfigType,
|
||||
@@ -19,12 +20,14 @@ pub struct ForgeConfig {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Policies {
|
||||
#[serde(default)]
|
||||
pub squash_merge_only: PolicyOption,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PolicyOption {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
@@ -38,14 +41,17 @@ fn allow_all() -> Vec<ForgeRegex> {
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub enum ForgeSchedule {
|
||||
Cron {},
|
||||
Interval {},
|
||||
Once {},
|
||||
#[serde(rename = "cron")]
|
||||
Cron(String),
|
||||
#[serde(rename = "interval")]
|
||||
Interval(String),
|
||||
#[serde(rename = "once")]
|
||||
Once(bool),
|
||||
}
|
||||
|
||||
impl Default for ForgeSchedule {
|
||||
fn default() -> Self {
|
||||
Self::Interval {}
|
||||
Self::Interval("60".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +60,17 @@ pub struct ForgeRegex {
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl From<ForgeRegex> for Regex {
|
||||
fn from(value: ForgeRegex) -> Self {
|
||||
value.regex
|
||||
}
|
||||
}
|
||||
impl From<&ForgeRegex> for Regex {
|
||||
fn from(value: &ForgeRegex) -> Self {
|
||||
value.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ForgeRegex {
|
||||
type Target = Regex;
|
||||
|
||||
@@ -83,8 +100,19 @@ impl<'de> Deserialize<'de> for ForgeRegex {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
// #[serde(tag = "type")]
|
||||
pub enum ForgeConfigType {
|
||||
#[serde(rename = "github")]
|
||||
GitHub {},
|
||||
GitHub {
|
||||
credentials: GitHubCredentials,
|
||||
organisation: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum GitHubCredentials {
|
||||
#[serde(rename = "token")]
|
||||
Token(String),
|
||||
#[serde(rename = "token_env")]
|
||||
TokenEnv(String),
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use std::path::Path;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use nodrift::Drifter;
|
||||
use notmad::{Component, MadError};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{State, forge_config::ForgeConfig};
|
||||
use crate::{
|
||||
State,
|
||||
forge_config::{ForgeConfig, ForgeConfigType, ForgeSchedule, GitHubCredentials},
|
||||
forges,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForgeService {
|
||||
name: String,
|
||||
config: ForgeConfig,
|
||||
@@ -18,7 +25,71 @@ impl Component for ForgeService {
|
||||
}
|
||||
|
||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||
cancellation_token.cancelled().await;
|
||||
let job_cancel = match &self.config.schedule {
|
||||
ForgeSchedule::Cron(cron) => nodrift::schedule_drifter_cron(cron, self.clone())?,
|
||||
ForgeSchedule::Interval(interval) => nodrift::schedule_drifter(
|
||||
Duration::from_mins(
|
||||
interval
|
||||
.parse::<u64>()
|
||||
.context("interval is not a minute duration")?,
|
||||
),
|
||||
self.clone(),
|
||||
),
|
||||
ForgeSchedule::Once(enabled) => {
|
||||
if *enabled {
|
||||
self.execute(cancellation_token.child_token()).await?;
|
||||
}
|
||||
cancellation_token.clone()
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => { job_cancel.cancel() },
|
||||
_ = job_cancel.cancelled() => {},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl nodrift::Drifter for ForgeService {
|
||||
async fn execute(&self, _token: CancellationToken) -> anyhow::Result<()> {
|
||||
tracing::info!("running schedule");
|
||||
|
||||
match &self.config.forge_type {
|
||||
ForgeConfigType::GitHub {
|
||||
credentials,
|
||||
organisation,
|
||||
} => {
|
||||
let creds = match credentials.clone() {
|
||||
GitHubCredentials::Token(token) => {
|
||||
forges::github::GitHubCredentials::Token(token)
|
||||
}
|
||||
GitHubCredentials::TokenEnv(key) => {
|
||||
let token =
|
||||
std::env::var(key).context("failed to lookup github token env")?;
|
||||
|
||||
forges::github::GitHubCredentials::Token(token)
|
||||
}
|
||||
};
|
||||
|
||||
let client = forges::github::GitHub::new(creds);
|
||||
|
||||
// 1. get repositories matching allow / deny list
|
||||
|
||||
client
|
||||
.get_repositories(
|
||||
organisation,
|
||||
self.config.allow.iter().map(|a| a.into()).collect(),
|
||||
self.config.deny.iter().map(|a| a.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. Get updates for each
|
||||
// 3. Update repositories
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
1
crates/forge-enforce/src/forges.rs
Normal file
1
crates/forge-enforce/src/forges.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod github;
|
||||
100
crates/forge-enforce/src/forges/github.rs
Normal file
100
crates/forge-enforce/src/forges/github.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use anyhow::Context;
|
||||
use octocrab::{Octocrab, models::Repository};
|
||||
use regex::Regex;
|
||||
|
||||
pub struct GitHub {
|
||||
credentials: GitHubCredentials,
|
||||
client: Octocrab,
|
||||
}
|
||||
|
||||
pub enum GitHubCredentials {
|
||||
Token(String),
|
||||
}
|
||||
|
||||
impl GitHub {
|
||||
pub fn new(credentials: GitHubCredentials) -> Self {
|
||||
let builder = Octocrab::builder();
|
||||
|
||||
let builder = match &credentials {
|
||||
GitHubCredentials::Token(token) => builder.personal_token(token.clone()),
|
||||
};
|
||||
|
||||
let client = builder
|
||||
.build()
|
||||
.expect("to be able to create octocrab client");
|
||||
|
||||
Self {
|
||||
credentials,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_repositories(
|
||||
&self,
|
||||
organisation: &str,
|
||||
allow: Vec<Regex>,
|
||||
deny: Vec<Regex>,
|
||||
) -> anyhow::Result<Vec<Repository>> {
|
||||
tracing::debug!(organisation, "fetching github repositories");
|
||||
|
||||
let mut current_page = self
|
||||
.client
|
||||
.orgs(organisation)
|
||||
.list_repos()
|
||||
.per_page(100)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to get github repositories")?;
|
||||
|
||||
let mut repos = current_page.take_items();
|
||||
let new_repos = self
|
||||
.client
|
||||
.all_pages(current_page)
|
||||
.await
|
||||
.context("failed to get github pages")?;
|
||||
repos.extend(new_repos);
|
||||
|
||||
let mut allowed_count = 0usize;
|
||||
let mut denied_count = 0usize;
|
||||
let fetched_count = repos.len();
|
||||
|
||||
let allowed_repos = repos
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
for a in &allow {
|
||||
if a.is_match(&r.name) {
|
||||
tracing::trace!(name = r.name, rule = a.to_string(), "allowed repository");
|
||||
|
||||
allowed_count += 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
.filter(|r| {
|
||||
for d in &deny {
|
||||
if d.is_match(&r.name) {
|
||||
tracing::trace!(name = r.name, rule = d.to_string(), "denied repository");
|
||||
|
||||
denied_count += 1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::debug!(
|
||||
organisation,
|
||||
fetched_count,
|
||||
allowed_count,
|
||||
denied_count,
|
||||
total_count = allowed_repos.len(),
|
||||
"github repositories"
|
||||
);
|
||||
|
||||
Ok(allowed_repos)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod state;
|
||||
pub use state::*;
|
||||
|
||||
@@ -8,6 +10,8 @@ pub mod servehttp;
|
||||
mod forge_config;
|
||||
mod forge_services;
|
||||
|
||||
mod forges;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
type = "github"
|
||||
deny = [".*"]
|
||||
# schedule.cron = "0 * * * * *"
|
||||
schedule.once = true
|
||||
|
||||
allow = ["^canopy-.*$"]
|
||||
deny = ["^infrastructure-.*$", "^canopy-data-gateway$"]
|
||||
|
||||
[github]
|
||||
credentials.token_env = "GITHUB_ACCESS_TOKEN"
|
||||
organisation = "understory-io"
|
||||
|
||||
[policies]
|
||||
squash_merge_only.enabled = true
|
||||
|
||||
Reference in New Issue
Block a user