mirror of
https://github.com/Xevion/banner.git
synced 2025-12-10 10:06:38 -06:00
feat: modernize build tooling and add CI/CD workflow
Switch to Bun for 2-5x faster frontend builds, implement cargo-chef for reliable Rust dependency caching, and add Biome for fast code formatting. Build system improvements: - Replace pnpm with Bun for frontend package management - Add cargo-chef to Dockerfile for better Rust build layer caching - Update all commands to use bun instead of pnpm Developer experience: - Add comprehensive Justfile commands (format, format-check, db) - Implement automated PostgreSQL Docker setup with random port allocation - Add stricter checks (--deny warnings on clippy, --all-features flag) Code quality: - Add Biome formatter for 10-100x faster TypeScript/JavaScript formatting - Add GitHub Actions CI/CD workflow for automated checks - Update .dockerignore with comprehensive exclusions - Format all code with cargo fmt (Rust) and Biome (TypeScript) All changes maintain backward compatibility and can be tested incrementally.
This commit is contained in:
@@ -124,7 +124,7 @@ impl App {
|
||||
/// Setup bot service if enabled
|
||||
pub async fn setup_bot_service(&mut self) -> Result<(), anyhow::Error> {
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
|
||||
// Create shutdown channel for status update task
|
||||
let (status_shutdown_tx, status_shutdown_rx) = broadcast::channel(1);
|
||||
|
||||
@@ -32,7 +32,10 @@ pub fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Re
|
||||
if !path.is_empty() && path != "." {
|
||||
err_msg.push_str(&format!("for path '{}'\n", path));
|
||||
}
|
||||
err_msg.push_str(&format!("({}) at line {} column {}\n\n", type_info, line, column));
|
||||
err_msg.push_str(&format!(
|
||||
"({}) at line {} column {}\n\n",
|
||||
type_info, line, column
|
||||
));
|
||||
err_msg.push_str(&context);
|
||||
|
||||
err_msg
|
||||
@@ -86,10 +89,10 @@ fn parse_type_mismatch(error_msg: &str) -> String {
|
||||
}
|
||||
|
||||
// Try to parse "expected X at line Y" format
|
||||
if error_msg.starts_with("expected ") {
|
||||
if let Some(expected_part) = error_msg.split(" at line ").next() {
|
||||
return expected_part.to_string();
|
||||
}
|
||||
if error_msg.starts_with("expected ")
|
||||
&& let Some(expected_part) = error_msg.split(" at line ").next()
|
||||
{
|
||||
return expected_part.to_string();
|
||||
}
|
||||
|
||||
// Fallback: return original message without location info
|
||||
|
||||
@@ -316,9 +316,11 @@ impl SessionPool {
|
||||
return Err(anyhow::anyhow!("Failed to get cookies"));
|
||||
}
|
||||
|
||||
let jsessionid = cookies.get("JSESSIONID")
|
||||
let jsessionid = cookies
|
||||
.get("JSESSIONID")
|
||||
.ok_or_else(|| anyhow::anyhow!("JSESSIONID cookie missing after validation"))?;
|
||||
let ssb_cookie = cookies.get("SSB_COOKIE")
|
||||
let ssb_cookie = cookies
|
||||
.get("SSB_COOKIE")
|
||||
.ok_or_else(|| anyhow::anyhow!("SSB_COOKIE cookie missing after validation"))?;
|
||||
let cookie_header = format!("JSESSIONID={}; SSB_COOKIE={}", jsessionid, ssb_cookie);
|
||||
|
||||
@@ -437,15 +439,23 @@ impl SessionPool {
|
||||
|
||||
let redirect: RedirectResponse = response.json().await?;
|
||||
|
||||
let base_url_path = self.base_url.parse::<Url>()
|
||||
let base_url_path = self
|
||||
.base_url
|
||||
.parse::<Url>()
|
||||
.context("Failed to parse base URL")?
|
||||
.path()
|
||||
.to_string();
|
||||
let non_overlap_redirect = redirect.fwd_url.strip_prefix(&base_url_path)
|
||||
.ok_or_else(|| anyhow::anyhow!(
|
||||
"Redirect URL '{}' does not start with expected prefix '{}'",
|
||||
redirect.fwd_url, base_url_path
|
||||
))?;
|
||||
let non_overlap_redirect =
|
||||
redirect
|
||||
.fwd_url
|
||||
.strip_prefix(&base_url_path)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Redirect URL '{}' does not start with expected prefix '{}'",
|
||||
redirect.fwd_url,
|
||||
base_url_path
|
||||
)
|
||||
})?;
|
||||
|
||||
// Follow the redirect
|
||||
let redirect_url = format!("{}{}", self.base_url, non_overlap_redirect);
|
||||
|
||||
@@ -54,10 +54,7 @@ pub async fn batch_upsert_courses(courses: &[Course], db_pool: &PgPool) -> Resul
|
||||
|
||||
let subjects: Vec<&str> = courses.iter().map(|c| c.subject.as_str()).collect();
|
||||
|
||||
let course_numbers: Vec<&str> = courses
|
||||
.iter()
|
||||
.map(|c| c.course_number.as_str())
|
||||
.collect();
|
||||
let course_numbers: Vec<&str> = courses.iter().map(|c| c.course_number.as_str()).collect();
|
||||
|
||||
let titles: Vec<&str> = courses.iter().map(|c| c.course_title.as_str()).collect();
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ impl ScraperService {
|
||||
"Spawned worker tasks"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -104,7 +103,10 @@ impl Service for ScraperService {
|
||||
let results = futures::future::join_all(all_handles).await;
|
||||
let failed = results.iter().filter(|r| r.is_err()).count();
|
||||
if failed > 0 {
|
||||
warn!(failed_count = failed, "Some scraper tasks panicked during shutdown");
|
||||
warn!(
|
||||
failed_count = failed,
|
||||
"Some scraper tasks panicked during shutdown"
|
||||
);
|
||||
return Err(anyhow::anyhow!("{} task(s) panicked", failed));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time;
|
||||
use tracing::{debug, error, info, trace, warn, Instrument};
|
||||
use tracing::{Instrument, debug, error, info, trace, warn};
|
||||
|
||||
/// A single worker instance.
|
||||
///
|
||||
@@ -73,7 +73,8 @@ impl Worker {
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Handle the job processing result
|
||||
self.handle_job_result(job_id, retry_count, max_retries, process_result, duration).await;
|
||||
self.handle_job_result(job_id, retry_count, max_retries, process_result, duration)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ impl Worker {
|
||||
"UPDATE scrape_jobs
|
||||
SET locked_at = NULL, retry_count = retry_count + 1
|
||||
WHERE id = $1
|
||||
RETURNING CASE WHEN retry_count + 1 < $2 THEN retry_count + 1 ELSE NULL END"
|
||||
RETURNING CASE WHEN retry_count + 1 < $2 THEN retry_count + 1 ELSE NULL END",
|
||||
)
|
||||
.bind(job_id)
|
||||
.bind(max_retries)
|
||||
@@ -170,7 +171,10 @@ impl Worker {
|
||||
|
||||
/// Handle shutdown signal received during job processing
|
||||
async fn handle_shutdown_during_processing(&self, job_id: i32) {
|
||||
info!(worker_id = self.id, job_id, "Shutdown received during job processing");
|
||||
info!(
|
||||
worker_id = self.id,
|
||||
job_id, "Shutdown received during job processing"
|
||||
);
|
||||
|
||||
if let Err(e) = self.unlock_job(job_id).await {
|
||||
warn!(
|
||||
@@ -187,7 +191,14 @@ impl Worker {
|
||||
}
|
||||
|
||||
/// Handle the result of job processing
|
||||
async fn handle_job_result(&self, job_id: i32, retry_count: i32, max_retries: i32, result: Result<(), JobError>, duration: std::time::Duration) {
|
||||
async fn handle_job_result(
|
||||
&self,
|
||||
job_id: i32,
|
||||
retry_count: i32,
|
||||
max_retries: i32,
|
||||
result: Result<(), JobError>,
|
||||
duration: std::time::Duration,
|
||||
) {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
@@ -201,7 +212,8 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
Err(JobError::Recoverable(e)) => {
|
||||
self.handle_recoverable_error(job_id, retry_count, max_retries, e, duration).await;
|
||||
self.handle_recoverable_error(job_id, retry_count, max_retries, e, duration)
|
||||
.await;
|
||||
}
|
||||
Err(JobError::Unrecoverable(e)) => {
|
||||
error!(
|
||||
@@ -219,7 +231,14 @@ impl Worker {
|
||||
}
|
||||
|
||||
/// Handle recoverable errors by logging appropriately and unlocking the job
|
||||
async fn handle_recoverable_error(&self, job_id: i32, retry_count: i32, max_retries: i32, e: anyhow::Error, duration: std::time::Duration) {
|
||||
async fn handle_recoverable_error(
|
||||
&self,
|
||||
job_id: i32,
|
||||
retry_count: i32,
|
||||
max_retries: i32,
|
||||
e: anyhow::Error,
|
||||
duration: std::time::Duration,
|
||||
) {
|
||||
let next_attempt = retry_count.saturating_add(1);
|
||||
let remaining_retries = max_retries.saturating_sub(next_attempt);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use serenity::Client;
|
||||
use serenity::all::{ActivityData, ClientBuilder, GatewayIntents};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
@@ -91,7 +91,11 @@ impl BotService {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
|
||||
// Start status update task with shutdown support
|
||||
let handle = Self::start_status_update_task(ctx.clone(), app_state.clone(), status_shutdown_rx);
|
||||
let handle = Self::start_status_update_task(
|
||||
ctx.clone(),
|
||||
app_state.clone(),
|
||||
status_shutdown_rx,
|
||||
);
|
||||
*status_task_handle.lock().await = Some(handle);
|
||||
|
||||
Ok(Data { app_state })
|
||||
|
||||
@@ -62,7 +62,8 @@ impl ServiceManager {
|
||||
});
|
||||
|
||||
// Store abort handle for shutdown control
|
||||
self.service_handles.insert(name.clone(), handle.abort_handle());
|
||||
self.service_handles
|
||||
.insert(name.clone(), handle.abort_handle());
|
||||
debug!(service = name, id = ?handle.id(), "service spawned");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user