Compare commits

...

10 Commits

Author SHA1 Message Date
3c790efb28
feat: moved to gitea workflows for CI/CD
All checks were successful
Build Rust binary in release mode / build (push) Successful in 46s
Signed-off-by: Louis Vallat <contact@louis-vallat.dev>
2024-09-08 13:46:04 +02:00
c7da323add
Added color for 'created' pipelines
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-04 18:34:28 +02:00
a1d8d1f0ff
Order latest commits by id instead of by updated_at, as it could screw up the ordering for getting the latest pipeline
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-04 17:24:07 +02:00
32ef9027fd
Added a check on pipeline retry/creation in case the user doesn't have sufficient rights
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 22:49:07 +02:00
be11082b07
Updated project name and version in Cargo.toml
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 10:15:16 +02:00
de1e93a64f
Added show project name on launchpad functionnality and updated the README accordingly
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 10:11:12 +02:00
5ad765fcae
Added launchpad mini in project limitations
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 09:47:07 +02:00
1487f82a5b
Added refresh timer delay in project's limitations
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 09:40:08 +02:00
9e82b3102c
Added some logging capability in project using env_logger
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 09:39:38 +02:00
2447a01145
Renamed key to token in config file and added config.json.example
Signed-off-by: Louis Vallat <louis@louis-vallat.xyz>
2022-06-03 08:53:29 +02:00
9 changed files with 165 additions and 60 deletions

View File

@ -0,0 +1,16 @@
name: "Build Rust binary in release mode"
on: push
jobs:
build:
name: "build"
runs-on: rust-bookworm
steps:
- name: Install build dependencies
run: |
apt-get update
apt-get install -y libasound2-dev
- name: Check out repository code
uses: actions/checkout@v4
- name: Build binary
run: cargo build --release

View File

@ -1,13 +0,0 @@
default:
image: rust
stages:
- build
before_script:
- apt update && apt install -y libasound2-dev
build:
stage: build
script:
- cargo build --release

View File

@ -1,6 +1,6 @@
[package] [package]
name = "gitlab-rust" name = "gitlabci-launchpad-controller"
version = "0.1.0" version = "1.0.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -18,6 +18,30 @@ I used mainly two libraries for this project:
These two libraries are maintained at the moment this README is being written. These two libraries are maintained at the moment this README is being written.
## Using
### Open associated project
Clicking on a tile opens the corresponding web page for the project linked to
this tile.
### Retry/create pipeline for project
Clicking on the `A` button will engage the `restart` mode. As long as it is
lit up, the mode is engaged.
If you click on a tile while the `A` button is lit up, it will retry or create a
new pipeline for this project and for this ref. You can disengage the mode by
pressing again the `A` button.
### Show project name on launchpad
Clicking on the `B` button will engage the `show text` mode. As long as it is
lit up, the mode is engaged.
If you click on a tile while the `B` button is lit up, it will show the project
name. You can disengage the mode by pressing again the `B` button.
## Building ## Building
As any other `cargo` project, it can be built with a simple command: As any other `cargo` project, it can be built with a simple command:
@ -41,12 +65,6 @@ in the current working directory. An example configuration file is provided,
The abs\_x and abs\_y coordinate are defined using the top-left grid tile as The abs\_x and abs\_y coordinate are defined using the top-left grid tile as
the origin. the origin.
Clicking on a tile opens the corresponding web page for the project linked to
this tile. Clicking on the `A` button will light it up, it is the restart button.
If you click on a tile with the `A` button lit up, it will retry or create a
new pipeline for this project and for this ref. You can disengage the restart
mode by pressing again the `A` button.
## Limitations ## Limitations
This project has some limitations right now, and some of them will be fixed: This project has some limitations right now, and some of them will be fixed:
@ -54,4 +72,6 @@ This project has some limitations right now, and some of them will be fixed:
- the program is able to talk to one and only one gitlab API right now - the program is able to talk to one and only one gitlab API right now
- the configuration file has to be in the current working directory - the configuration file has to be in the current working directory
- only one page is allowed, but 8 could be leveraged later using the 8 selectors - only one page is allowed, but 8 could be leveraged later using the 8 selectors
- the refresh delay is fixed for all threads and is at 2 seconds
- the launchpad mini is the only launchpad that can be used with this project

12
config.json.example Normal file
View File

@ -0,0 +1,12 @@
{
"host": "gitlab host",
"token": "gitlab token",
"projects": [
{
"name": "lovallat/lovallat",
"ref_": "master",
"abs_x": 0,
"abs_y": 0
}
]
}

View File

@ -1,18 +1,17 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use log::{trace, warn};
use std::path::Path; use std::path::Path;
const CONFIG_FILENAME: &str = "./config.json"; const CONFIG_FILENAME: &str = "./config.json";
// TODO REMOVE PUB AND REPLACE BY FN #[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Deserialize, Serialize)]
pub struct Config { pub struct Config {
pub host: String, pub host: String,
pub key: String, pub token: String,
pub projects: Vec<Project> pub projects: Vec<Project>
} }
// TODO REMOVE PUB AND REPLACE BY FN
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
@ -22,15 +21,21 @@ pub struct Project {
} }
pub fn get_config() -> Option<Config> { pub fn get_config() -> Option<Config> {
trace!("Trying to read config file at location '{}'.", CONFIG_FILENAME);
if Path::exists(Path::new(CONFIG_FILENAME)) { if Path::exists(Path::new(CONFIG_FILENAME)) {
return serde_json::from_str( return serde_json::from_str(
std::fs::read_to_string(CONFIG_FILENAME).unwrap().as_str()) std::fs::read_to_string(CONFIG_FILENAME)
.unwrap(); .expect("Cannot read configuration file.").as_str())
.expect("Cannot deserialize configuration file.");
} }
warn!("Configuration file doesn't exist at location '{}'.", CONFIG_FILENAME);
return None; return None;
} }
pub fn save_config(config: &Config) { pub fn save_config(config: &Config) {
trace!("Saving configuration file with content {:?}.", config);
std::fs::write(Path::new(CONFIG_FILENAME), std::fs::write(Path::new(CONFIG_FILENAME),
serde_json::to_string_pretty(config).unwrap()).unwrap(); serde_json::to_string_pretty(config)
.expect("Could not serialize the configuration class to JSON."))
.expect("Could not write serialized configuration file.");
} }

View File

@ -2,7 +2,7 @@ use std::{thread::{sleep, self, JoinHandle}, time::Duration};
use launchy::{launchpad_mini::{Button, Color, Output, DoubleBufferingBehavior}, OutputDevice}; use launchy::{launchpad_mini::{Button, Color, Output, DoubleBufferingBehavior}, OutputDevice};
use serde::Deserialize; use serde::Deserialize;
use log::info; use log::{warn, trace, debug, error};
use gitlab::{Gitlab, api::{projects::pipelines::{Pipelines, PipelineOrderBy}, Query}}; use gitlab::{Gitlab, api::{projects::pipelines::{Pipelines, PipelineOrderBy}, Query}};
use crate::config_manager::{Config, Project}; use crate::config_manager::{Config, Project};
@ -15,6 +15,7 @@ pub struct Pipeline {
status: String status: String
} }
#[derive(Debug)]
enum PipelineStatus { enum PipelineStatus {
Created, Created,
WaitingForResource, WaitingForResource,
@ -31,6 +32,7 @@ enum PipelineStatus {
impl PipelineStatus { impl PipelineStatus {
fn from(s: &str) -> Option<PipelineStatus> { fn from(s: &str) -> Option<PipelineStatus> {
trace!("Trying to convert '{}' to PipelineStatus.", s);
return match s { return match s {
"created" => Some(PipelineStatus::Created), "created" => Some(PipelineStatus::Created),
"waiting_for_resource" => Some(PipelineStatus::WaitingForResource), "waiting_for_resource" => Some(PipelineStatus::WaitingForResource),
@ -43,29 +45,37 @@ impl PipelineStatus {
"skipped" => Some(PipelineStatus::Skipped), "skipped" => Some(PipelineStatus::Skipped),
"manual" => Some(PipelineStatus::Manual), "manual" => Some(PipelineStatus::Manual),
"scheduled" => Some(PipelineStatus::Scheduled), "scheduled" => Some(PipelineStatus::Scheduled),
_ => None _ => { warn!("Could not find correspondance for status '{}'.", s); None }
} }
} }
fn get_color(&self) -> (Color, bool) { fn get_color(&self) -> (Color, bool) {
trace!("Getting color for PipelineStatus '{:?}'.", self);
return match self { return match self {
PipelineStatus::Pending => (Color::YELLOW, false), PipelineStatus::Pending => (Color::YELLOW, false),
PipelineStatus::Running => (Color::AMBER, true), PipelineStatus::Running => (Color::AMBER, true),
PipelineStatus::Success => (Color::GREEN, false), PipelineStatus::Success => (Color::GREEN, false),
PipelineStatus::Failed => (Color::RED, false), PipelineStatus::Failed => (Color::RED, false),
_ => (Color::OFF, false) PipelineStatus::Created => (Color::ORANGE, false),
_ => { warn!("Unknown color for pipeline status {:?}.", self);
(Color::OFF, false) }
}; };
} }
} }
pub fn refresh_on_timer(client: Gitlab, project: Project) { pub fn refresh_on_timer(client: Gitlab, project: Project) {
trace!("Starting a refresh on timer task for project {:?}.", project);
let mut output = Output::guess().unwrap(); let mut output = Output::guess().unwrap();
loop { loop {
let pipeline = get_latest_pipelines(&client, &project); let pipeline = get_latest_pipelines(&client, &project);
if pipeline.is_none() { return; } if pipeline.is_none() {
error!("Project {:?} has no existing pipeline. Ignoring.", project);
return;
}
let c = PipelineStatus::from(pipeline.unwrap().status.as_str()) let c = PipelineStatus::from(pipeline.unwrap().status.as_str())
.unwrap(); .unwrap();
debug!("Project {:?} has a pipeline status of {:?}, updating.", project, c);
output.set_button(Button::GridButton { x: project.abs_x, y: project.abs_y }, output.set_button(Button::GridButton { x: project.abs_x, y: project.abs_y },
c.get_color().0, c.get_color().0,
if c.get_color().1 { DoubleBufferingBehavior::Clear } if c.get_color().1 { DoubleBufferingBehavior::Clear }
@ -75,42 +85,65 @@ pub fn refresh_on_timer(client: Gitlab, project: Project) {
} }
pub fn load_from_config(config: Config) -> Vec<JoinHandle<()>> { pub fn load_from_config(config: Config) -> Vec<JoinHandle<()>> {
info!("Loading gitlab manager from configuration."); trace!("Loading gitlab manager from configuration.");
let mut threads = vec![]; let mut threads = vec![];
for p in config.projects.iter() { for p in config.projects.iter() {
let client = Gitlab::new(config.host.as_str(), config.key.as_str()).unwrap(); let client = Gitlab::new(config.host.as_str(), config.token.as_str()).unwrap();
let project = p.clone(); let project = p.clone();
debug!("Spawning refresh thread for project {:?}.", project);
threads.push(thread::spawn(move|| refresh_on_timer(client, project))); threads.push(thread::spawn(move|| refresh_on_timer(client, project)));
} }
debug!("Spawned {} threads for the refreshes.", threads.len());
return threads; return threads;
} }
pub fn get_latest_pipelines(client: &Gitlab, project: &Project) -> Option<Pipeline> { pub fn get_latest_pipelines(client: &Gitlab, project: &Project) -> Option<Pipeline> {
trace!("Getting latest pipeline for project {:?}.", project);
let endpoint = Pipelines::builder() let endpoint = Pipelines::builder()
.project(project.name.as_str()).ref_(project.ref_.as_str()) .project(project.name.as_str()).ref_(project.ref_.as_str())
.order_by(PipelineOrderBy::UpdatedAt).build().unwrap(); .order_by(PipelineOrderBy::Id).build().unwrap();
let pipelines: Vec<Pipeline> = endpoint.query(client).unwrap_or(vec![]); let pipelines: Vec<Pipeline> = endpoint.query(client).unwrap_or(vec![]);
return if pipelines.is_empty() { None } else { if pipelines.is_empty() {
Some(pipelines.first().unwrap().to_owned()) }; warn!("No pipeline found for project {:?}.", project);
return None;
} else {
let pipeline = pipelines.first().unwrap().to_owned();
debug!("Found {:?} as latest pipeline for project {:?}.",
pipeline, project);
return Some(pipeline);
}
} }
pub fn retry_pipeline(host: &str, token: &str, project: &Project) { pub fn retry_pipeline(host: &str, token: &str, project: &Project) {
trace!("Retrying pipeline for project {:?}.", project);
let client = Gitlab::new(host, token).unwrap(); let client = Gitlab::new(host, token).unwrap();
let pipeline = get_latest_pipelines(&client, &project); let pipeline = get_latest_pipelines(&client, &project);
if pipeline.is_none() { return; } if pipeline.is_none() {
let _res: Pipeline; warn!("{:?} has no pipeline, ignoring.", project);
return;
}
let res: Option<Pipeline>;
if pipeline.clone().unwrap().status == "success" { if pipeline.clone().unwrap().status == "success" {
debug!("Latest pipeline for {:?} has a 'success' status, creating another one.",
project);
let endpoint = gitlab::api::projects::pipelines::CreatePipeline::builder() let endpoint = gitlab::api::projects::pipelines::CreatePipeline::builder()
.project(project.name.clone()) .project(project.name.clone())
.ref_(project.ref_.clone()) .ref_(project.ref_.clone())
.build().unwrap(); .build().unwrap();
_res = endpoint.query(&client).unwrap(); res = endpoint.query(&client).ok();
} else { } else {
debug!("Latest pipeline status for {:?} wasn't a success, retrying.",
project);
let endpoint = gitlab::api::projects::pipelines::RetryPipeline::builder() let endpoint = gitlab::api::projects::pipelines::RetryPipeline::builder()
.project(project.name.clone()) .project(project.name.clone())
.pipeline(pipeline.unwrap().id) .pipeline(pipeline.unwrap().id)
.build().unwrap(); .build().unwrap();
_res = endpoint.query(&client).unwrap(); res = endpoint.query(&client).ok();
}
if res.is_none() {
error!("Could not retry/create a new pipeline for project {:?}. Ignoring.", project);
} else {
debug!("API answer was {:?}.", res.unwrap());
} }
} }

View File

@ -1,42 +1,67 @@
use launchy::{OutputDevice, InputDevice, launchpad_mini::{Output, Input, Buffer, Button, Color, DoubleBuffering, DoubleBufferingBehavior, Message}, MsgPollingWrapper}; use launchy::{OutputDevice, InputDevice, launchpad_mini::{Output, Input, Buffer, Button, Color, DoubleBuffering, DoubleBufferingBehavior, Message}, MsgPollingWrapper};
use log::{trace, debug, warn};
use crate::{config_manager::Config, gitlab_controller::retry_pipeline}; use crate::{config_manager::Config, gitlab_controller::retry_pipeline};
const RESTART_BUTTON: Button = Button::GridButton { x: 8, y: 0 }; const RESTART_BUTTON: Button = Button::GridButton { x: 8, y: 0 }; // A BUTTON
const SHOW_TEXT_BUTTON: Button = Button::GridButton { x: 8, y: 1 }; // B BUTTON
fn engage_restart(output: &mut Output) { fn set_restart_light(output: &mut Output, status: bool) {
output.set_button(RESTART_BUTTON, Color::DIM_GREEN, DoubleBufferingBehavior::Copy).unwrap(); trace!("{} restart mode.", if status { "Engaged" } else { "Disengaged" });
output.set_button(RESTART_BUTTON,
if status { Color::DIM_GREEN } else { Color::OFF },
DoubleBufferingBehavior::Copy).unwrap();
} }
fn disengage_restart(output: &mut Output) { fn set_show_text_light(output: &mut Output, status: bool) {
output.set_button(RESTART_BUTTON, Color::OFF, DoubleBufferingBehavior::Copy).unwrap(); trace!("{} show text mode.", if status { "Engaged" } else { "Disengaged" });
output.set_button(SHOW_TEXT_BUTTON,
if status { Color::DIM_GREEN } else { Color::OFF },
DoubleBufferingBehavior::Copy).unwrap();
} }
fn running_thread(config: Config) { fn running_thread(config: Config) {
trace!("Starting a thread dedicated to the Launchpad input.");
let mut output = Output::guess().unwrap(); let mut output = Output::guess().unwrap();
let input = Input::guess_polling().unwrap(); let input = Input::guess_polling().unwrap();
let mut restart = false; let (mut restart, mut show) = (false, false);
for msg in input.iter() { for msg in input.iter() {
debug!("Got message from Launchpad: {:?}.", msg);
if let Message::Release { button } = msg { if let Message::Release { button } = msg {
if button == RESTART_BUTTON { if button == RESTART_BUTTON {
if !restart { debug!("Restart button has been pressed.");
restart = true; restart = !restart;
engage_restart(&mut output); set_restart_light(&mut output, restart);
} else { } else if button == SHOW_TEXT_BUTTON {
restart = false; debug!("Show text button has been pressed.");
disengage_restart(&mut output); show = !show;
} set_show_text_light(&mut output, show);
} else if button.abs_x() != 8 { } else if button.abs_x() != 8 {
debug!("A project tile has been pressed.");
let project = config.projects.iter() let project = config.projects.iter()
.find(|p| p.abs_x == button.abs_x() && p.abs_y + 1 == button.abs_y()); .find(|p| p.abs_x == button.abs_x() && p.abs_y + 1 == button.abs_y());
if project.is_none() { continue; } if project.is_none() {
warn!("The tile {:?} has no project associated. Ignoring.", button);
continue;
}
let project = project.unwrap(); let project = project.unwrap();
debug!("Tile {:?} has an associated project {:?}.", button, project);
if restart { if restart {
debug!("Restart mode is engaged, attempting to restart pipeline for project {:?}.",
project);
restart = false; restart = false;
retry_pipeline(config.host.as_str(), config.key.as_str(), project); retry_pipeline(config.host.as_str(), config.token.as_str(), project);
disengage_restart(&mut output); set_restart_light(&mut output, restart);
} else if show {
debug!("Showing project name on launchpad for project {:?}.", project);
show = false;
set_show_text_light(&mut output, show);
output.scroll_text(project.name.as_bytes(), Color::RED, false).unwrap();
} else { } else {
open::that_in_background(format!("https://{}/{}", config.host, project.name)); let url = format!("https://{}/{}", config.host, project.name);
debug!("Restart mode isn't engaged. Trying to open project {:?} in browser with URL {}.",
project, url);
open::that_in_background(url);
} }
} }
} }
@ -44,13 +69,17 @@ fn running_thread(config: Config) {
} }
pub fn init(config: Config) { pub fn init(config: Config) {
trace!("Initializing launchpad.");
let mut output = launchy::launchpad_mini::Output::guess().unwrap(); let mut output = launchy::launchpad_mini::Output::guess().unwrap();
debug!("Resetting launchpad state.");
output.reset().unwrap(); output.reset().unwrap();
debug!("Setting up the double buffering behavior for the launchpad.");
output.control_double_buffering(DoubleBuffering { output.control_double_buffering(DoubleBuffering {
copy: false, copy: false,
flash: true, flash: true,
edited_buffer: Buffer::A, edited_buffer: Buffer::A,
displayed_buffer: Buffer::A displayed_buffer: Buffer::A
}).unwrap(); }).unwrap();
debug!("Spawning a thread for handling to launchpad I/O.");
std::thread::spawn(move || running_thread(config)); std::thread::spawn(move || running_thread(config));
} }

View File

@ -1,4 +1,4 @@
use log::error; use log::{info, error, trace};
use crate::config_manager::Config; use crate::config_manager::Config;
use crate::config_manager::Project; use crate::config_manager::Project;
@ -9,11 +9,13 @@ mod gitlab_controller;
fn main() { fn main() {
env_logger::init(); env_logger::init();
trace!("Starting main application.");
let conf = config_manager::get_config(); let conf = config_manager::get_config();
if conf.is_none() { if conf.is_none() {
info!("Configuration file cannot be loaded, saving a default one.");
config_manager::save_config(&Config { config_manager::save_config(&Config {
host: "gitlab-host".to_string(), host: "gitlab-host".to_string(),
key: "your-gitlab-key".to_string(), token: "your-gitlab-key".to_string(),
projects: vec![Project { projects: vec![Project {
name: "example".to_string(), name: "example".to_string(),
ref_: "master".to_string(), ref_: "master".to_string(),
@ -27,6 +29,7 @@ fn main() {
let conf = conf.unwrap(); let conf = conf.unwrap();
launchpad_controller::init(conf.clone()); launchpad_controller::init(conf.clone());
let threads = gitlab_controller::load_from_config(conf); let threads = gitlab_controller::load_from_config(conf);
trace!("Joining timer threads.");
for t in threads { for t in threads {
t.join().unwrap(); t.join().unwrap();
} }