treewide: format with nightly rustfmt; auto-fix Clippy lints
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If4fd0511087dbaa65afc56a34d7c2f166a6a6964
This commit is contained in:
parent
fe45fff3f3
commit
3a03cf7b3e
26 changed files with 222 additions and 161 deletions
|
|
@ -36,7 +36,12 @@ fn expand_path(path: &str) -> String {
|
|||
if let Some(end) = result[start..].find('}') {
|
||||
let var_name = &result[start + 2..start + end];
|
||||
let replacement = std::env::var(var_name).unwrap_or_default();
|
||||
result = format!("{}{}{}", &result[..start], replacement, &result[start + end + 1..]);
|
||||
result = format!(
|
||||
"{}{}{}",
|
||||
&result[..start],
|
||||
replacement,
|
||||
&result[start + end + 1..]
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -112,22 +117,24 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
|
||||
for decl_jobset in &decl_project.jobsets {
|
||||
// Parse state string to JobsetState enum
|
||||
let state = decl_jobset.state.as_ref().map(|s| match s.as_str() {
|
||||
"disabled" => JobsetState::Disabled,
|
||||
"enabled" => JobsetState::Enabled,
|
||||
"one_shot" => JobsetState::OneShot,
|
||||
"one_at_a_time" => JobsetState::OneAtATime,
|
||||
_ => JobsetState::Enabled, // Default to enabled for unknown values
|
||||
let state = decl_jobset.state.as_ref().map(|s| {
|
||||
match s.as_str() {
|
||||
"disabled" => JobsetState::Disabled,
|
||||
"enabled" => JobsetState::Enabled,
|
||||
"one_shot" => JobsetState::OneShot,
|
||||
"one_at_a_time" => JobsetState::OneAtATime,
|
||||
_ => JobsetState::Enabled, // Default to enabled for unknown values
|
||||
}
|
||||
});
|
||||
|
||||
let jobset = repo::jobsets::upsert(pool, CreateJobset {
|
||||
project_id: project.id,
|
||||
name: decl_jobset.name.clone(),
|
||||
nix_expression: decl_jobset.nix_expression.clone(),
|
||||
enabled: Some(decl_jobset.enabled),
|
||||
flake_mode: Some(decl_jobset.flake_mode),
|
||||
check_interval: Some(decl_jobset.check_interval),
|
||||
branch: decl_jobset.branch.clone(),
|
||||
project_id: project.id,
|
||||
name: decl_jobset.name.clone(),
|
||||
nix_expression: decl_jobset.nix_expression.clone(),
|
||||
enabled: Some(decl_jobset.enabled),
|
||||
flake_mode: Some(decl_jobset.flake_mode),
|
||||
check_interval: Some(decl_jobset.check_interval),
|
||||
branch: decl_jobset.branch.clone(),
|
||||
scheduling_shares: Some(decl_jobset.scheduling_shares),
|
||||
state,
|
||||
})
|
||||
|
|
@ -141,8 +148,12 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
|
||||
// Sync jobset inputs
|
||||
if !decl_jobset.inputs.is_empty() {
|
||||
repo::jobset_inputs::sync_for_jobset(pool, jobset.id, &decl_jobset.inputs)
|
||||
.await?;
|
||||
repo::jobset_inputs::sync_for_jobset(
|
||||
pool,
|
||||
jobset.id,
|
||||
&decl_jobset.inputs,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
project = %project.name,
|
||||
jobset = %jobset.name,
|
||||
|
|
@ -192,9 +203,12 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
|
||||
// Sync channels
|
||||
if !decl_project.channels.is_empty() {
|
||||
repo::channels::sync_for_project(pool, project.id, &decl_project.channels, |name| {
|
||||
jobset_map.get(name).copied()
|
||||
})
|
||||
repo::channels::sync_for_project(
|
||||
pool,
|
||||
project.id,
|
||||
&decl_project.channels,
|
||||
|name| jobset_map.get(name).copied(),
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
project = %project.name,
|
||||
|
|
@ -202,7 +216,6 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
"Synced declarative channels"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Upsert API keys
|
||||
|
|
@ -251,11 +264,11 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
if let Some(user) = existing {
|
||||
// Update existing user
|
||||
let update = crate::models::UpdateUser {
|
||||
email: Some(decl_user.email.clone()),
|
||||
full_name: decl_user.full_name.clone(),
|
||||
email: Some(decl_user.email.clone()),
|
||||
full_name: decl_user.full_name.clone(),
|
||||
password,
|
||||
role: Some(decl_user.role.clone()),
|
||||
enabled: Some(decl_user.enabled),
|
||||
role: Some(decl_user.role.clone()),
|
||||
enabled: Some(decl_user.enabled),
|
||||
public_dashboard: None,
|
||||
};
|
||||
if let Err(e) = repo::users::update(pool, user.id, &update).await {
|
||||
|
|
@ -287,12 +300,12 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
// Set enabled status if false (users are enabled by default)
|
||||
if !decl_user.enabled
|
||||
&& let Err(e) = repo::users::set_enabled(pool, user.id, false).await
|
||||
{
|
||||
tracing::warn!(
|
||||
username = %user.username,
|
||||
"Failed to disable declarative user: {e}"
|
||||
);
|
||||
}
|
||||
{
|
||||
tracing::warn!(
|
||||
username = %user.username,
|
||||
"Failed to disable declarative user: {e}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
|
|
@ -313,8 +326,8 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
if !config.remote_builders.is_empty() {
|
||||
repo::remote_builders::sync_all(pool, &config.remote_builders).await?;
|
||||
tracing::info!(
|
||||
builders = config.remote_builders.len(),
|
||||
"Synced declarative remote builders"
|
||||
builders = config.remote_builders.len(),
|
||||
"Synced declarative remote builders"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -332,7 +345,9 @@ pub async fn run(pool: &PgPool, config: &DeclarativeConfig) -> Result<()> {
|
|||
}
|
||||
|
||||
// Get project by name (already exists from earlier upsert)
|
||||
if let Ok(project) = repo::projects::get_by_name(pool, &decl_project.name).await {
|
||||
if let Ok(project) =
|
||||
repo::projects::get_by_name(pool, &decl_project.name).await
|
||||
{
|
||||
repo::project_members::sync_for_project(
|
||||
pool,
|
||||
project.id,
|
||||
|
|
|
|||
|
|
@ -220,7 +220,8 @@ pub struct DeclarativeProject {
|
|||
/// Declarative notification configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeclarativeNotification {
|
||||
/// Notification type: github_status, email, gitlab_status, gitea_status, run_command
|
||||
/// Notification type: github_status, email, gitlab_status, gitea_status,
|
||||
/// run_command
|
||||
pub notification_type: String,
|
||||
/// Type-specific configuration (JSON object)
|
||||
pub config: serde_json::Value,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub enum CiError {
|
|||
}
|
||||
|
||||
impl CiError {
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_disk_full(&self) -> bool {
|
||||
let msg = self.to_string().to_lowercase();
|
||||
msg.contains("no space left on device")
|
||||
|
|
@ -186,19 +186,19 @@ pub struct DiskSpaceInfo {
|
|||
|
||||
impl DiskSpaceInfo {
|
||||
/// Check if disk space is critically low (less than 1GB available)
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_critical(&self) -> bool {
|
||||
self.available_gb < 1.0
|
||||
}
|
||||
|
||||
/// Check if disk space is low (less than 5GB available)
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_low(&self) -> bool {
|
||||
self.available_gb < 5.0
|
||||
}
|
||||
|
||||
/// Get a human-readable summary
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"Total: {:.1}GB, Free: {:.1}GB ({:.1}%), Available: {:.1}GB",
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ impl LogStorage {
|
|||
}
|
||||
|
||||
/// Returns the filesystem path where a build's log should be stored
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn log_path(&self, build_id: &Uuid) -> PathBuf {
|
||||
self.log_dir.join(format!("{build_id}.log"))
|
||||
}
|
||||
|
||||
/// Returns the filesystem path for an active (in-progress) build log
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn log_path_for_active(&self, build_id: &Uuid) -> PathBuf {
|
||||
self.log_dir.join(format!("{build_id}.active.log"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,13 +77,13 @@ pub enum JobsetState {
|
|||
|
||||
impl JobsetState {
|
||||
/// Returns true if this jobset state allows evaluation.
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub const fn is_evaluable(&self) -> bool {
|
||||
matches!(self, Self::Enabled | Self::OneShot | Self::OneAtATime)
|
||||
}
|
||||
|
||||
/// Returns the database string representation of this state.
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Disabled => "disabled",
|
||||
|
|
@ -339,12 +339,12 @@ pub struct PaginationParams {
|
|||
}
|
||||
|
||||
impl PaginationParams {
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn limit(&self) -> i64 {
|
||||
self.limit.unwrap_or(50).clamp(1, 200)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn offset(&self) -> i64 {
|
||||
self.offset.unwrap_or(0).max(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,28 +95,25 @@ pub async fn probe_flake(
|
|||
base_ref
|
||||
};
|
||||
|
||||
let output =
|
||||
tokio::time::timeout(std::time::Duration::from_mins(1), async {
|
||||
tokio::process::Command::new("nix")
|
||||
.args([
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"flake",
|
||||
"show",
|
||||
"--json",
|
||||
"--no-write-lock-file",
|
||||
&flake_ref,
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
CiError::Timeout("Flake probe timed out after 60s".to_string())
|
||||
})?
|
||||
.map_err(|e| {
|
||||
CiError::NixEval(format!("Failed to run nix flake show: {e}"))
|
||||
})?;
|
||||
let output = tokio::time::timeout(std::time::Duration::from_mins(1), async {
|
||||
tokio::process::Command::new("nix")
|
||||
.args([
|
||||
"--extra-experimental-features",
|
||||
"nix-command flakes",
|
||||
"flake",
|
||||
"show",
|
||||
"--json",
|
||||
"--no-write-lock-file",
|
||||
&flake_ref,
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| CiError::Timeout("Flake probe timed out after 60s".to_string()))?
|
||||
.map_err(|e| {
|
||||
CiError::NixEval(format!("Failed to run nix flake show: {e}"))
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,9 @@ async fn set_github_status(
|
|||
build: &Build,
|
||||
) {
|
||||
// Parse owner/repo from URL
|
||||
let (owner, repo) = if let Some(v) = parse_github_repo(repo_url) { v } else {
|
||||
let (owner, repo) = if let Some(v) = parse_github_repo(repo_url) {
|
||||
v
|
||||
} else {
|
||||
warn!("Cannot parse GitHub owner/repo from {repo_url}");
|
||||
return;
|
||||
};
|
||||
|
|
@ -142,7 +144,9 @@ async fn set_gitea_status(
|
|||
build: &Build,
|
||||
) {
|
||||
// Parse owner/repo from URL (try to extract from the gitea URL)
|
||||
let (owner, repo) = if let Some(v) = parse_gitea_repo(repo_url, base_url) { v } else {
|
||||
let (owner, repo) = if let Some(v) = parse_gitea_repo(repo_url, base_url) {
|
||||
v
|
||||
} else {
|
||||
warn!("Cannot parse Gitea owner/repo from {repo_url}");
|
||||
return;
|
||||
};
|
||||
|
|
@ -191,7 +195,9 @@ async fn set_gitlab_status(
|
|||
build: &Build,
|
||||
) {
|
||||
// Parse project path from URL
|
||||
let project_path = if let Some(p) = parse_gitlab_project(repo_url, base_url) { p } else {
|
||||
let project_path = if let Some(p) = parse_gitlab_project(repo_url, base_url) {
|
||||
p
|
||||
} else {
|
||||
warn!("Cannot parse GitLab project from {repo_url}");
|
||||
return;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ pub async fn upsert(
|
|||
) -> Result<Channel> {
|
||||
sqlx::query_as::<_, Channel>(
|
||||
"INSERT INTO channels (project_id, name, jobset_id) VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET jobset_id = EXCLUDED.jobset_id \
|
||||
RETURNING *",
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET jobset_id = \
|
||||
EXCLUDED.jobset_id RETURNING *",
|
||||
)
|
||||
.bind(project_id)
|
||||
.bind(name)
|
||||
|
|
@ -119,12 +119,14 @@ pub async fn sync_for_project(
|
|||
let names: Vec<&str> = channels.iter().map(|c| c.name.as_str()).collect();
|
||||
|
||||
// Delete channels not in declarative config
|
||||
sqlx::query("DELETE FROM channels WHERE project_id = $1 AND name != ALL($2::text[])")
|
||||
.bind(project_id)
|
||||
.bind(&names)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(CiError::Database)?;
|
||||
sqlx::query(
|
||||
"DELETE FROM channels WHERE project_id = $1 AND name != ALL($2::text[])",
|
||||
)
|
||||
.bind(project_id)
|
||||
.bind(&names)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(CiError::Database)?;
|
||||
|
||||
// Upsert each channel
|
||||
for channel in channels {
|
||||
|
|
|
|||
|
|
@ -73,12 +73,9 @@ pub async fn upsert(
|
|||
) -> Result<JobsetInput> {
|
||||
sqlx::query_as::<_, JobsetInput>(
|
||||
"INSERT INTO jobset_inputs (jobset_id, name, input_type, value, revision) \
|
||||
VALUES ($1, $2, $3, $4, $5) \
|
||||
ON CONFLICT (jobset_id, name) DO UPDATE SET \
|
||||
input_type = EXCLUDED.input_type, \
|
||||
value = EXCLUDED.value, \
|
||||
revision = EXCLUDED.revision \
|
||||
RETURNING *",
|
||||
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (jobset_id, name) DO UPDATE SET \
|
||||
input_type = EXCLUDED.input_type, value = EXCLUDED.value, revision = \
|
||||
EXCLUDED.revision RETURNING *",
|
||||
)
|
||||
.bind(jobset_id)
|
||||
.bind(name)
|
||||
|
|
@ -102,7 +99,8 @@ pub async fn sync_for_jobset(
|
|||
|
||||
// Delete inputs not in declarative config
|
||||
sqlx::query(
|
||||
"DELETE FROM jobset_inputs WHERE jobset_id = $1 AND name != ALL($2::text[])",
|
||||
"DELETE FROM jobset_inputs WHERE jobset_id = $1 AND name != \
|
||||
ALL($2::text[])",
|
||||
)
|
||||
.bind(jobset_id)
|
||||
.bind(&names)
|
||||
|
|
|
|||
|
|
@ -231,8 +231,8 @@ pub async fn has_running_builds(
|
|||
}
|
||||
|
||||
/// List jobsets that are due for evaluation based on their `check_interval`.
|
||||
/// Returns jobsets where `last_checked_at` is NULL or older than `check_interval`
|
||||
/// seconds.
|
||||
/// Returns jobsets where `last_checked_at` is NULL or older than
|
||||
/// `check_interval` seconds.
|
||||
pub async fn list_due_for_eval(
|
||||
pool: &PgPool,
|
||||
limit: i64,
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ pub async fn upsert(
|
|||
) -> Result<NotificationConfig> {
|
||||
sqlx::query_as::<_, NotificationConfig>(
|
||||
"INSERT INTO notification_configs (project_id, notification_type, config, \
|
||||
enabled) VALUES ($1, $2, $3, $4) ON CONFLICT (project_id, notification_type) \
|
||||
DO UPDATE SET config = EXCLUDED.config, enabled = EXCLUDED.enabled \
|
||||
RETURNING *",
|
||||
enabled) VALUES ($1, $2, $3, $4) ON CONFLICT (project_id, \
|
||||
notification_type) DO UPDATE SET config = EXCLUDED.config, enabled = \
|
||||
EXCLUDED.enabled RETURNING *",
|
||||
)
|
||||
.bind(project_id)
|
||||
.bind(notification_type)
|
||||
|
|
|
|||
|
|
@ -141,9 +141,7 @@ pub async fn delete(pool: &PgPool, id: Uuid) -> Result<()> {
|
|||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(CiError::NotFound(format!(
|
||||
"Project member {id} not found"
|
||||
)));
|
||||
return Err(CiError::NotFound(format!("Project member {id} not found")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -199,8 +197,8 @@ pub async fn upsert(
|
|||
.map_err(|e| CiError::Validation(e.to_string()))?;
|
||||
|
||||
sqlx::query_as::<_, ProjectMember>(
|
||||
"INSERT INTO project_members (project_id, user_id, role) VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (project_id, user_id) DO UPDATE SET role = EXCLUDED.role \
|
||||
"INSERT INTO project_members (project_id, user_id, role) VALUES ($1, $2, \
|
||||
$3) ON CONFLICT (project_id, user_id) DO UPDATE SET role = EXCLUDED.role \
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(project_id)
|
||||
|
|
|
|||
|
|
@ -152,9 +152,9 @@ pub async fn upsert(
|
|||
sqlx::query_as::<_, RemoteBuilder>(
|
||||
"INSERT INTO remote_builders (name, ssh_uri, systems, max_jobs, \
|
||||
speed_factor, supported_features, mandatory_features, enabled, \
|
||||
public_host_key, ssh_key_file) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, \
|
||||
$10) ON CONFLICT (name) DO UPDATE SET ssh_uri = EXCLUDED.ssh_uri, systems = \
|
||||
EXCLUDED.systems, max_jobs = EXCLUDED.max_jobs, speed_factor = \
|
||||
public_host_key, ssh_key_file) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, \
|
||||
$9, $10) ON CONFLICT (name) DO UPDATE SET ssh_uri = EXCLUDED.ssh_uri, \
|
||||
systems = EXCLUDED.systems, max_jobs = EXCLUDED.max_jobs, speed_factor = \
|
||||
EXCLUDED.speed_factor, supported_features = EXCLUDED.supported_features, \
|
||||
mandatory_features = EXCLUDED.mandatory_features, enabled = \
|
||||
EXCLUDED.enabled, public_host_key = COALESCE(EXCLUDED.public_host_key, \
|
||||
|
|
|
|||
|
|
@ -364,7 +364,8 @@ async fn search_evaluations(
|
|||
}
|
||||
}
|
||||
|
||||
// Get count - simple count (full filter support would require building query differently)
|
||||
// Get count - simple count (full filter support would require building query
|
||||
// differently)
|
||||
let (total,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM evaluations")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -94,10 +94,10 @@ pub async fn upsert(
|
|||
enabled: bool,
|
||||
) -> Result<WebhookConfig> {
|
||||
sqlx::query_as::<_, WebhookConfig>(
|
||||
"INSERT INTO webhook_configs (project_id, forge_type, secret_hash, enabled) \
|
||||
VALUES ($1, $2, $3, $4) ON CONFLICT (project_id, forge_type) DO UPDATE SET \
|
||||
secret_hash = COALESCE(EXCLUDED.secret_hash, webhook_configs.secret_hash), \
|
||||
enabled = EXCLUDED.enabled RETURNING *",
|
||||
"INSERT INTO webhook_configs (project_id, forge_type, secret_hash, \
|
||||
enabled) VALUES ($1, $2, $3, $4) ON CONFLICT (project_id, forge_type) DO \
|
||||
UPDATE SET secret_hash = COALESCE(EXCLUDED.secret_hash, \
|
||||
webhook_configs.secret_hash), enabled = EXCLUDED.enabled RETURNING *",
|
||||
)
|
||||
.bind(project_id)
|
||||
.bind(forge_type)
|
||||
|
|
@ -117,7 +117,8 @@ pub async fn sync_for_project(
|
|||
resolve_secret: impl Fn(&DeclarativeWebhook) -> Option<String>,
|
||||
) -> Result<()> {
|
||||
// Get forge types from declarative config
|
||||
let types: Vec<&str> = webhooks.iter().map(|w| w.forge_type.as_str()).collect();
|
||||
let types: Vec<&str> =
|
||||
webhooks.iter().map(|w| w.forge_type.as_str()).collect();
|
||||
|
||||
// Delete webhook configs not in declarative config
|
||||
sqlx::query(
|
||||
|
|
|
|||
|
|
@ -49,19 +49,19 @@ pub const VALID_PROJECT_ROLES: &[&str] = &[
|
|||
];
|
||||
|
||||
/// Check if a global role is valid
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_valid_role(role: &str) -> bool {
|
||||
VALID_ROLES.contains(&role)
|
||||
}
|
||||
|
||||
/// Check if a project role is valid
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_valid_project_role(role: &str) -> bool {
|
||||
VALID_PROJECT_ROLES.contains(&role)
|
||||
}
|
||||
|
||||
/// Get the highest project role (for permission checks)
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn project_role_level(role: &str) -> i32 {
|
||||
match role {
|
||||
PROJECT_ROLE_ADMIN => 3,
|
||||
|
|
@ -73,7 +73,7 @@ pub fn project_role_level(role: &str) -> i32 {
|
|||
|
||||
/// Check if user has required project permission
|
||||
/// Higher level roles automatically have lower level permissions
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn has_project_permission(user_role: &str, required: &str) -> bool {
|
||||
let user_level = project_role_level(user_role);
|
||||
let required_level = project_role_level(required);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ use regex::Regex;
|
|||
|
||||
/// Validate that a path is a valid nix store path.
|
||||
/// Rejects path traversal, overly long paths, and non-store paths.
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_valid_store_path(path: &str) -> bool {
|
||||
path.starts_with("/nix/store/") && !path.contains("..") && path.len() < 512
|
||||
}
|
||||
|
||||
/// Validate that a string is a valid nix store hash (32 lowercase alphanumeric
|
||||
/// chars).
|
||||
#[must_use]
|
||||
#[must_use]
|
||||
pub fn is_valid_nix_hash(hash: &str) -> bool {
|
||||
hash.len() == 32
|
||||
&& hash
|
||||
|
|
@ -62,7 +62,8 @@ fn validate_repository_url(url: &str) -> Result<(), String> {
|
|||
}
|
||||
if !VALID_REPO_PREFIXES.iter().any(|p| url.starts_with(p)) {
|
||||
return Err(
|
||||
"repository_url must start with https://, http://, git://, ssh://, or file://"
|
||||
"repository_url must start with https://, http://, git://, ssh://, or \
|
||||
file://"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
|
@ -146,7 +147,19 @@ fn validate_forge_type(forge_type: &str) -> Result<(), String> {
|
|||
|
||||
// --- Implementations ---
|
||||
|
||||
use crate::models::{CreateProject, UpdateProject, CreateJobset, UpdateJobset, CreateEvaluation, CreateBuild, CreateChannel, UpdateChannel, CreateRemoteBuilder, UpdateRemoteBuilder, CreateWebhookConfig};
|
||||
use crate::models::{
|
||||
CreateBuild,
|
||||
CreateChannel,
|
||||
CreateEvaluation,
|
||||
CreateJobset,
|
||||
CreateProject,
|
||||
CreateRemoteBuilder,
|
||||
CreateWebhookConfig,
|
||||
UpdateChannel,
|
||||
UpdateJobset,
|
||||
UpdateProject,
|
||||
UpdateRemoteBuilder,
|
||||
};
|
||||
|
||||
impl Validate for CreateProject {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue