From c3d16d741941cb3b3980041d81df27e22ae4a1c4 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Thu, 12 Mar 2020 13:22:04 +0100 Subject: [PATCH] Add AppVeyor collector Closes #3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 11 + schemas/v0.10.json | 724 ++++++++++++++++++ src/api/endpoints.rs | 6 +- src/builds.rs | 1 + src/config.rs | 68 +- src/config/expansions.rs | 39 + src/config/validation.rs | 41 +- src/providers.rs | 1 + src/providers/collectors.rs | 1 + src/providers/collectors/appveyor/client.rs | 131 ++++ src/providers/collectors/appveyor/mod.rs | 171 +++++ .../collectors/appveyor/test_data/builds.json | 343 +++++++++ .../collectors/appveyor/validation.rs | 129 ++++ src/providers/collectors/github/mod.rs | 1 - src/utils/date.rs | 7 + src/utils/http.rs | 4 + web/package-lock.json | 2 +- web/package.json | 2 +- web/src/assets/appveyor.svg | 6 + 21 files changed, 1635 insertions(+), 57 deletions(-) create mode 100644 schemas/v0.10.json create mode 100644 src/providers/collectors/appveyor/client.rs create mode 100644 src/providers/collectors/appveyor/mod.rs create mode 100644 src/providers/collectors/appveyor/test_data/builds.json create mode 100644 src/providers/collectors/appveyor/validation.rs create mode 100644 web/src/assets/appveyor.svg diff --git a/Cargo.lock b/Cargo.lock index d647339..0606b83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,7 +704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "duck" -version = "0.9.0" +version = "0.10.0" dependencies = [ "actix-cors 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-files 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index df6f212..1f59a09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "duck" -version = "0.9.0" +version = "0.10.0" authors = ["Patrik Svensson "] edition = "2018" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 3e439fb..802e6db 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ other systems such as build servers. * [Azure DevOps](https://azure.microsoft.com/en-us/services/devops) * [GitHub Actions](https://github.com/features/actions) * [Octopus Deploy](https://octopus.com/) +* [AppVeyor](https://www.appveyor.com/) ### Observers @@ -171,6 +172,16 @@ Below is an example configuration that specifies multiple collectors and observe } ] } + }, + { + "appveyor": { + "id": "appveyor", + "credentials": { + "bearer": "${APPVEYOR_BEARER_TOKEN}" + }, + "account": "myaccount", + "project": "myproject-slug" + } } ], "observers": [ diff --git a/schemas/v0.10.json b/schemas/v0.10.json new file mode 100644 index 0000000..7fd14a5 --- /dev/null +++ b/schemas/v0.10.json @@ -0,0 +1,724 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Configuration", + "type": "object", + "required": [ + "collectors" + ], + "properties": { + "collectors": { + "title": "Collectors", + "type": "array", + "items": { + "$ref": "#/definitions/CollectorConfiguration" + } + }, + "interval": { + "title": "Update interval", + "description": "The update interval in seconds", + "default": null, + "allOf": [ + { + "$ref": "#/definitions/Interval" + } + ] + }, + "observers": { + "title": "Observers", + "default": null, + "type": "array", + "items": { + "$ref": "#/definitions/ObserverConfiguration" + } + }, + "title": { + "title": "Duck frontend title", + "description": "The title that is displayed in the UI", + "default": null, + "type": "string" + }, + "views": { + "title": "Views", + "type": "array", + "items": { + "$ref": "#/definitions/ViewConfiguration" + } + } + }, + "definitions": { + "AppVeyorAuth": { + "anyOf": [ + { + "type": "object", + "required": [ + "bearer" + ], + "properties": { + "bearer": { + "type": "string" + } + } + } + ] + }, + "AppVeyorConfiguration": { + "type": "object", + "required": [ + "account", + "credentials", + "id", + "project" + ], + "properties": { + "account": { + "title": "The AppVeyor account", + "type": "string" + }, + "count": { + "title": "The number of builds to retrieve", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "credentials": { + "title": "The TeamCity credentials", + "allOf": [ + { + "$ref": "#/definitions/AppVeyorAuth" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The AppVeyor collector ID", + "type": "string" + }, + "project": { + "title": "The AppVeyor project", + "type": "string" + } + } + }, + "AzureDevOpsConfiguration": { + "type": "object", + "required": [ + "branches", + "credentials", + "definitions", + "id", + "organization", + "project" + ], + "properties": { + "branches": { + "title": "The branches to include", + "type": "array", + "items": { + "type": "string" + } + }, + "credentials": { + "title": "The Azure DevOps credentials", + "allOf": [ + { + "$ref": "#/definitions/AzureDevOpsCredentials" + } + ] + }, + "definitions": { + "title": "The build definitions to include", + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The Azure DevOps collector ID", + "type": "string" + }, + "organization": { + "title": "The Azure DevOps organization", + "type": "string" + }, + "project": { + "title": "The Azure DevOps project", + "type": "string" + } + } + }, + "AzureDevOpsCredentials": { + "anyOf": [ + { + "enum": [ + "anonymous" + ] + }, + { + "title": "Personal access token", + "description": "Authenticate using a personal access token (PAT)", + "type": "object", + "required": [ + "pat" + ], + "properties": { + "pat": { + "type": "string" + } + } + } + ] + }, + "CollectorConfiguration": { + "anyOf": [ + { + "title": "TeamCity collector", + "description": "Gets builds from TeamCity", + "type": "object", + "required": [ + "teamcity" + ], + "properties": { + "teamcity": { + "$ref": "#/definitions/TeamCityConfiguration" + } + } + }, + { + "title": "Azure DevOps collector", + "description": "Gets builds from Azure DevOps", + "type": "object", + "required": [ + "azure" + ], + "properties": { + "azure": { + "$ref": "#/definitions/AzureDevOpsConfiguration" + } + } + }, + { + "title": "GitHub collector", + "description": "Gets builds from GitHub Actions", + "type": "object", + "required": [ + "github" + ], + "properties": { + "github": { + "$ref": "#/definitions/GitHubConfiguration" + } + } + }, + { + "title": "Octopus Deploy collector", + "description": "Gets deployments from Octopus Deploy", + "type": "object", + "required": [ + "octopus" + ], + "properties": { + "octopus": { + "$ref": "#/definitions/OctopusDeployConfiguration" + } + } + }, + { + "title": "AppVeyor collector", + "description": "Gets builds from AppVeyor", + "type": "object", + "required": [ + "appveyor" + ], + "properties": { + "appveyor": { + "$ref": "#/definitions/AppVeyorConfiguration" + } + } + } + ] + }, + "GitHubConfiguration": { + "type": "object", + "required": [ + "credentials", + "id", + "owner", + "repository", + "workflow" + ], + "properties": { + "credentials": { + "title": "The GitHub credentials", + "allOf": [ + { + "$ref": "#/definitions/GitHubCredentials" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The GitHub collector ID", + "type": "string" + }, + "owner": { + "title": "The GitHub owner", + "type": "string" + }, + "repository": { + "title": "The GitHub repository", + "type": "string" + }, + "workflow": { + "title": "The GitHub Actions workflow", + "type": "string" + } + } + }, + "GitHubCredentials": { + "anyOf": [ + { + "title": "Basic authentication", + "description": "Authenticate using basic authentication", + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "title": "The password to use", + "type": "string" + }, + "username": { + "title": "The username to use", + "type": "string" + } + } + } + } + } + ] + }, + "HueConfiguration": { + "type": "object", + "required": [ + "hubUrl", + "id", + "lights", + "username" + ], + "properties": { + "brightness": { + "title": "The brightness of the lamps", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "collectors": { + "title": "The collectors to include events from", + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "hubUrl": { + "title": "The Philips Hue hub URL", + "type": "string" + }, + "id": { + "title": "The Philips Hue collector ID", + "type": "string" + }, + "lights": { + "title": "The lights that should be controlled by this observer", + "type": "array", + "items": { + "type": "string" + } + }, + "username": { + "title": "The Philips Hue username", + "type": "string" + } + } + }, + "Interval": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "MattermostConfiguration": { + "type": "object", + "required": [ + "credentials", + "id" + ], + "properties": { + "channel": { + "title": "The Mattermost channel to send messages to", + "default": null, + "type": "string" + }, + "collectors": { + "title": "The collectors to include events from", + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "credentials": { + "title": "The Mattermost credentials", + "allOf": [ + { + "$ref": "#/definitions/MattermostCredentials" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "type": "string" + } + } + }, + "MattermostCredentials": { + "anyOf": [ + { + "title": "Webhook", + "description": "Send messages directly to a webhook", + "type": "object", + "required": [ + "webhook" + ], + "properties": { + "webhook": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } + } + ] + }, + "ObserverConfiguration": { + "anyOf": [ + { + "title": "Philips Hue observer", + "type": "object", + "required": [ + "hue" + ], + "properties": { + "hue": { + "$ref": "#/definitions/HueConfiguration" + } + } + }, + { + "title": "Slack observer", + "type": "object", + "required": [ + "slack" + ], + "properties": { + "slack": { + "$ref": "#/definitions/SlackConfiguration" + } + } + }, + { + "title": "Mattermost observer", + "type": "object", + "required": [ + "mattermost" + ], + "properties": { + "mattermost": { + "$ref": "#/definitions/MattermostConfiguration" + } + } + } + ] + }, + "OctopusDeployConfiguration": { + "type": "object", + "required": [ + "credentials", + "id", + "projects", + "serverUrl" + ], + "properties": { + "credentials": { + "title": "The Octopus Deploy credentials", + "allOf": [ + { + "$ref": "#/definitions/OctopusDeployCredentials" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The Octopus Deploy collector ID", + "type": "string" + }, + "projects": { + "title": "The Octopus Deploy projects to include", + "type": "array", + "items": { + "$ref": "#/definitions/OctopusDeployProject" + } + }, + "serverUrl": { + "title": "The Octopus Deploy server URL", + "type": "string" + } + } + }, + "OctopusDeployCredentials": { + "anyOf": [ + { + "title": "API Key", + "description": "Authenticate using an API key", + "type": "object", + "required": [ + "apiKey" + ], + "properties": { + "apiKey": { + "type": "string" + } + } + } + ] + }, + "OctopusDeployProject": { + "type": "object", + "required": [ + "environments", + "projectId" + ], + "properties": { + "environments": { + "title": "The Octopus Deploy environment IDs within the project", + "type": "array", + "items": { + "type": "string" + } + }, + "projectId": { + "title": "The Octopus Deploy project ID", + "type": "string" + } + } + }, + "SlackConfiguration": { + "type": "object", + "required": [ + "credentials", + "id" + ], + "properties": { + "channel": { + "title": "The Slack channel to send messages to", + "default": null, + "type": "string" + }, + "collectors": { + "title": "The collectors to include events from", + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "credentials": { + "title": "The Slack credentials", + "allOf": [ + { + "$ref": "#/definitions/SlackCredentials" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The Slack collector ID", + "type": "string" + } + } + }, + "SlackCredentials": { + "anyOf": [ + { + "title": "Webhook", + "description": "Send messages directly to a webhook", + "type": "object", + "required": [ + "webhook" + ], + "properties": { + "webhook": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } + } + ] + }, + "TeamCityAuth": { + "anyOf": [ + { + "enum": [ + "guest" + ] + }, + { + "title": "Basic authentication", + "description": "Authenticate using basic authentication", + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "title": "The password to use", + "type": "string" + }, + "username": { + "title": "The username to use", + "type": "string" + } + } + } + } + } + ] + }, + "TeamCityConfiguration": { + "type": "object", + "required": [ + "builds", + "credentials", + "id", + "serverUrl" + ], + "properties": { + "builds": { + "title": "The TeamCity builds definitions to include", + "type": "array", + "items": { + "type": "string" + } + }, + "credentials": { + "title": "The TeamCity credentials", + "allOf": [ + { + "$ref": "#/definitions/TeamCityAuth" + } + ] + }, + "enabled": { + "title": "Determines whether or not this collector is enabled", + "default": null, + "type": "boolean" + }, + "id": { + "title": "The TeamCity collector ID", + "type": "string" + }, + "serverUrl": { + "title": "The TeamCity server URL", + "type": "string" + } + } + }, + "ViewConfiguration": { + "type": "object", + "required": [ + "collectors", + "id", + "name" + ], + "properties": { + "collectors": { + "title": "Included collectors", + "description": "The collectors included in this view", + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "title": "View ID", + "description": "The ID of the view", + "type": "string" + }, + "name": { + "title": "View name", + "description": "the name of the view", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 927189b..1c5a15a 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -14,7 +14,11 @@ use super::models::{BuildViewModel, ServerInfoModel, ViewInfoModel}; pub async fn server_info(state: web::Data>) -> HttpResponse { let info = ServerInfoModel { title: &state.title[..], - started: state.started.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs(), + started: state + .started + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), version: VERSION, views: state .views diff --git a/src/builds.rs b/src/builds.rs index f743aa5..88a6d7f 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -132,6 +132,7 @@ pub enum BuildProvider { AzureDevOps, GitHub, OctopusDeploy, + AppVeyor, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/config.rs b/src/config.rs index caa37b2..a5b8fa4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,10 @@ use crate::DuckResult; mod expansions; mod validation; +pub trait Validate { + fn validate(&self) -> DuckResult<()>; +} + #[derive(Serialize, Deserialize, JsonSchema, Clone)] pub struct Configuration { /// # Update interval @@ -29,10 +33,6 @@ pub struct Configuration { pub observers: Option>, } -pub trait Validate { - fn validate(&self) -> DuckResult<()>; -} - impl Configuration { pub fn from_file(variables: &impl VariableProvider, path: PathBuf) -> DuckResult { let expander = &Expander::new(variables); @@ -90,6 +90,7 @@ impl Configuration { CollectorConfiguration::Azure(c) => c.id.clone(), CollectorConfiguration::OctopusDeploy(c) => c.id.clone(), CollectorConfiguration::GitHub(c) => c.id.clone(), + CollectorConfiguration::AppVeyor(c) => c.id.clone(), }) .collect(); // Get all observer id:s @@ -109,6 +110,14 @@ impl Configuration { } } +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +pub struct Interval(pub u32); +impl Default for Interval { + fn default() -> Self { + Interval(15) + } +} + #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] pub struct ViewConfiguration { /// # View ID @@ -122,14 +131,6 @@ pub struct ViewConfiguration { pub collectors: Vec, } -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] -pub struct Interval(pub u32); -impl Default for Interval { - fn default() -> Self { - Interval(15) - } -} - #[derive(Serialize, Deserialize, JsonSchema, Clone)] pub enum CollectorConfiguration { /// # TeamCity collector @@ -148,6 +149,10 @@ pub enum CollectorConfiguration { /// Gets deployments from Octopus Deploy #[serde(rename = "octopus")] OctopusDeploy(OctopusDeployConfiguration), + /// # AppVeyor collector + /// Gets builds from AppVeyor + #[serde(rename = "appveyor")] + AppVeyor(AppVeyorConfiguration), } impl CollectorConfiguration { @@ -157,6 +162,7 @@ impl CollectorConfiguration { CollectorConfiguration::Azure(c) => &c.id, CollectorConfiguration::GitHub(c) => &c.id, CollectorConfiguration::OctopusDeploy(c) => &c.id, + CollectorConfiguration::AppVeyor(c) => &c.id, } } @@ -166,6 +172,7 @@ impl CollectorConfiguration { CollectorConfiguration::Azure(c) => c.enabled, CollectorConfiguration::GitHub(c) => c.enabled, CollectorConfiguration::OctopusDeploy(c) => c.enabled, + CollectorConfiguration::AppVeyor(c) => c.enabled, } { return enabled; } @@ -180,6 +187,7 @@ impl Validate for CollectorConfiguration { CollectorConfiguration::Azure(c) => c.validate(), CollectorConfiguration::GitHub(c) => c.validate(), CollectorConfiguration::OctopusDeploy(c) => c.validate(), + CollectorConfiguration::AppVeyor(c) => c.validate(), } } } @@ -236,6 +244,42 @@ impl Validate for ObserverConfiguration { } } +/////////////////////////////////////////////////////////// +// AppVeyor + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub struct AppVeyorConfiguration { + /// # The AppVeyor collector ID + pub id: String, + /// # Determines whether or not this collector is enabled + #[serde(default)] + pub enabled: Option, + /// # The TeamCity credentials + pub credentials: AppVeyorCredentials, + /// # The AppVeyor account + pub account: String, + /// # The AppVeyor project + pub project: String, + /// # The number of builds to retrieve + #[serde(default)] + pub count: Option, +} + +impl AppVeyorConfiguration { + pub fn get_count(&self) -> u16 { + match self.count { + None => 1, + Some(count) => std::cmp::max(1, count), + } + } +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +pub enum AppVeyorCredentials { + #[serde(rename = "bearer")] + Bearer(String), +} + /////////////////////////////////////////////////////////// // TeamCity diff --git a/src/config/expansions.rs b/src/config/expansions.rs index 52389b8..abdc2f2 100644 --- a/src/config/expansions.rs +++ b/src/config/expansions.rs @@ -65,6 +65,17 @@ mod tests { } ] } + }, + { + "appveyor": { + "id": "${APPVEYOR_ID}", + "credentials": { + "bearer": "${APPVEYOR_BEARER_TOKEN}" + }, + "account": "${APPVEYOR_ACCOUNT}", + "project": "${APPVEYOR_PROJECT}", + "count": ${APPVEYOR_COUNT} + } } ], "observers": [ @@ -148,6 +159,11 @@ mod tests { variables.add("OCTOPUS_PROJECT_PREFIX", "Projects"); variables.add("OCTOPUS_ENVIRONMENT_PREFIX", "Environments"); variables.add("OCTOPUS_API_KEY", "SECRET-API-KEY"); + variables.add("APPVEYOR_ID", "appveyor"); + variables.add("APPVEYOR_BEARER_TOKEN", "SECRET-APPVEYOR-TOKEN"); + variables.add("APPVEYOR_ACCOUNT", "patriksvensson"); + variables.add("APPVEYOR_PROJECT", "spectre-commandline"); + variables.add("APPVEYOR_COUNT", "4"); variables.add("HUE_ID", "hue"); variables.add("HUE_BRIGHTNESS", "128"); variables.add("HUE_HOST", "192.168.1.155"); @@ -230,6 +246,21 @@ mod tests { assert_eq!("Environments-2", octopus.projects[0].environments[1]); } + #[test] + fn should_expand_appveyor_configuration() { + // Given, When + let config = read_config!(CONFIGURATION); + + // Then + let appveyor = find_config!(config.collectors, CollectorConfiguration::AppVeyor); + + assert_eq!("appveyor", appveyor.id); + assert_eq!("patriksvensson", appveyor.account); + assert_eq!("spectre-commandline", appveyor.project); + assert_eq!("SECRET-APPVEYOR-TOKEN", appveyor.get_bearer_token()); + assert_eq!(4, appveyor.get_count()); + } + #[test] fn should_expand_hue_configuration() { // Given, When @@ -318,6 +349,14 @@ mod utilities { } } + impl AppVeyorConfiguration { + pub fn get_bearer_token(&self) -> &str { + match &self.credentials { + AppVeyorCredentials::Bearer(token) => token, + } + } + } + impl SlackConfiguration { pub fn get_webhook_url(&self) -> &str { match &self.credentials { diff --git a/src/config/validation.rs b/src/config/validation.rs index 3ac7276..c42cdd8 100644 --- a/src/config/validation.rs +++ b/src/config/validation.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use log::warn; -use super::{CollectorConfiguration, Configuration, Validate}; +use super::{Configuration, Validate}; use crate::DuckResult; impl Validate for Configuration { @@ -74,44 +74,7 @@ fn validate_collector_references(configuration: &Configuration) -> DuckResult<() // Build a list of all collectors and whether or not they are enabled. let mut collectors: HashMap = HashMap::new(); for collector in configuration.collectors.iter() { - match collector { - CollectorConfiguration::TeamCity(c) => { - collectors.insert( - c.id.clone(), - match c.enabled { - None => true, - Some(enabled) => enabled, - }, - ); - } - CollectorConfiguration::Azure(c) => { - collectors.insert( - c.id.clone(), - match c.enabled { - None => true, - Some(enabled) => enabled, - }, - ); - } - CollectorConfiguration::GitHub(c) => { - collectors.insert( - c.id.clone(), - match c.enabled { - None => true, - Some(enabled) => enabled, - }, - ); - } - CollectorConfiguration::OctopusDeploy(c) => { - collectors.insert( - c.id.clone(), - match c.enabled { - None => true, - Some(enabled) => enabled, - }, - ); - } - } + collectors.insert(collector.get_id().to_string(), collector.is_enabled()); } // Validate referenced collectors. diff --git a/src/providers.rs b/src/providers.rs index b744611..2746bb2 100644 --- a/src/providers.rs +++ b/src/providers.rs @@ -31,6 +31,7 @@ fn get_collector_loader(config: &CollectorConfiguration) -> Box<&dyn CollectorLo CollectorConfiguration::Azure(config) => Box::new(config), CollectorConfiguration::GitHub(config) => Box::new(config), CollectorConfiguration::OctopusDeploy(config) => Box::new(config), + CollectorConfiguration::AppVeyor(config) => Box::new(config), } } diff --git a/src/providers/collectors.rs b/src/providers/collectors.rs index c6a34af..a860bc8 100644 --- a/src/providers/collectors.rs +++ b/src/providers/collectors.rs @@ -5,6 +5,7 @@ use waithandle::EventWaitHandle; use crate::builds::{Build, BuildProvider}; use crate::DuckResult; +mod appveyor; mod azure; mod github; mod octopus; diff --git a/src/providers/collectors/appveyor/client.rs b/src/providers/collectors/appveyor/client.rs new file mode 100644 index 0000000..44f9850 --- /dev/null +++ b/src/providers/collectors/appveyor/client.rs @@ -0,0 +1,131 @@ +use log::{trace, warn}; + +use crate::builds::BuildStatus; +use crate::config::{AppVeyorCredentials, AppVeyorConfiguration}; +use crate::utils::date; +use crate::utils::http::*; +use crate::DuckResult; + +pub struct AppVeyorClient { + credentials: AppVeyorCredentials, +} + +impl AppVeyorClient { + pub fn new(config: &AppVeyorConfiguration) -> Self { + Self { + credentials: config.credentials.clone(), + } + } + + pub fn get_builds( + &self, + client: &impl HttpClient, + account: &str, + project: &str, + count: u16, + ) -> DuckResult { + let url = format!( + "https://ci.appveyor.com/api/projects/{account}/{project}/history?recordsNumber={count}", + account = account, + project = project, + count = count + ); + + trace!("Sending request to: {}", url); + let mut builder = HttpRequestBuilder::get(&url); + builder.add_header("Content-Type", "application/json"); + builder.add_header("Accept", "application/json"); + + self.credentials.authenticate(&mut builder); + let mut response = client.send(&builder)?; + + trace!("Received response: {}", response.status()); + if !response.status().is_success() { + return Err(format_err!( + "Received non 200 HTTP status code. ({})", + response.status() + )); + } + + // Get the response body. + let body = response.body()?; + // Deserialize and return the value. + Ok(serde_json::from_str(&body[..])?) + } +} + +impl AppVeyorCredentials { + fn authenticate<'a>(&self, builder: &'a mut HttpRequestBuilder) { + match self { + AppVeyorCredentials::Bearer(token) => { + builder.bearer(token); + } + } + } +} + +#[derive(Deserialize, Debug)] +pub struct AppVeyorResponse { + pub project: AppVeyorProject, + pub builds: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct AppVeyorProject { + #[serde(alias = "accountId")] + pub account_id: u64, + #[serde(alias = "accountName")] + pub account_name: String, + #[serde(alias = "projectId")] + pub project_id: u64, + #[serde(alias = "name")] + pub project_name: String, + #[serde(alias = "repositoryName")] + pub repository_name: String, +} + +#[derive(Deserialize, Debug)] +pub struct AppVeyorBuild { + #[serde(alias = "buildId")] + pub build_id: u64, + #[serde(alias = "buildNumber")] + pub build_number: u64, + pub branch: String, + pub status: String, + pub created: String, + pub started: Option, + pub finished: Option, +} + +impl AppVeyorBuild { + pub fn get_status(&self) -> BuildStatus { + match &self.status[..] { + "success" => BuildStatus::Success, + "queued" => BuildStatus::Queued, + "starting" => BuildStatus::Queued, + "running" => BuildStatus::Running, + "failed" => BuildStatus::Failed, + "cancelled" => BuildStatus::Canceled, + status => { + warn!("Unknown build status: {}", status); + BuildStatus::Unknown + } + } + } + + pub fn get_started_timestamp(&self) -> DuckResult { + let started = match &self.started { + Some(started) => started, + None => &self.created, + }; + date::to_timestamp(&started[..], date::APPVEYOR_FORMAT) + } + + pub fn get_finished_timestamp(&self) -> DuckResult> { + if let Some(finished) = &self.finished { + let ts = date::to_timestamp(&finished[..], date::APPVEYOR_FORMAT)?; + return Ok(Some(ts)); + } + Ok(None) + } +} diff --git a/src/providers/collectors/appveyor/mod.rs b/src/providers/collectors/appveyor/mod.rs new file mode 100644 index 0000000..9ef9367 --- /dev/null +++ b/src/providers/collectors/appveyor/mod.rs @@ -0,0 +1,171 @@ +use std::sync::Arc; + +use waithandle::EventWaitHandle; + +use crate::builds::{Build, BuildBuilder, BuildProvider}; +use crate::config::AppVeyorConfiguration; +use crate::providers::collectors::{Collector, CollectorInfo, CollectorLoader}; +use crate::utils::http::*; +use crate::DuckResult; + +use self::client::*; + +mod client; +mod validation; + +impl CollectorLoader for AppVeyorConfiguration { + fn load(&self) -> DuckResult> { + Ok(Box::new(AppVeyorCollector::::new(self))) + } +} + +pub struct AppVeyorCollector { + info: CollectorInfo, + http: T, + client: AppVeyorClient, + account: String, + project: String, + count: u16, +} + +impl AppVeyorCollector { + pub fn new(config: &AppVeyorConfiguration) -> Self { + AppVeyorCollector:: { + http: Default::default(), + client: AppVeyorClient::new(config), + account: config.account.clone(), + project: config.project.clone(), + count: config.get_count(), + info: CollectorInfo { + id: config.id.clone(), + enabled: match config.enabled { + Option::None => true, + Option::Some(e) => e, + }, + provider: BuildProvider::AppVeyor, + }, + } + } + + #[cfg(test)] + pub fn get_client(&self) -> &T { + &self.http + } +} + +impl Collector for AppVeyorCollector { + fn info(&self) -> &CollectorInfo { + &self.info + } + + fn collect( + &self, + _handle: Arc, + callback: &mut dyn FnMut(Build), + ) -> DuckResult<()> { + let result = + self.client + .get_builds(&self.http, &self.account, &self.project, self.count)?; + + for (count, build) in result.builds.iter().enumerate() { + // Got enough? + if count >= self.count as usize { + break; + } + + callback( + BuildBuilder::new() + .build_id(build.build_id.to_string()) + .provider(BuildProvider::AppVeyor) + .origin(format!( + "https://ci.appveyor.com/project/{account}/{project}", + account = self.account, + project = self.project + )) + .collector(&self.info.id) + .project_id(&result.project.project_id.to_string()) + .project_name(&result.project.repository_name) + .definition_id(&result.project.account_id.to_string()) + .definition_name(&result.project.account_name) + .build_number(&build.build_number.to_string()) + .status(build.get_status()) + .url(format!( + "https://ci.appveyor.com/project/{account}/{project}/builds/{id}", + account = self.account, + project = self.project, + id = build.build_id + )) + .started_at(build.get_started_timestamp()?) + .finished_at(build.get_finished_timestamp()?) + .branch(&build.branch) + .build() + .unwrap(), + ); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builds::BuildStatus; + use crate::config::*; + use crate::utils::http::{HttpMethod, MockHttpClient, MockHttpResponseBuilder}; + use reqwest::StatusCode; + + #[test] + fn should_get_correct_data() { + // Given + let appveyor = AppVeyorCollector::::new(&AppVeyorConfiguration { + id: "appveyor".to_owned(), + enabled: Some(true), + account: "patriksvensson".to_owned(), + project: "spectre-commandline".to_owned(), + credentials: AppVeyorCredentials::Bearer("SECRET".to_owned()), + count: Option::None, + }); + + let client = appveyor.get_client(); + client.add_response( + MockHttpResponseBuilder::new( + HttpMethod::Get, + "https://ci.appveyor.com/api/projects/patriksvensson/spectre-commandline/history?recordsNumber=1" + ) + .returns_status(StatusCode::OK) + .returns_body(include_str!("test_data/builds.json")) + ); + + // When + let mut result = Vec::::new(); + appveyor + .collect( + Arc::new(waithandle::EventWaitHandle::new()), + &mut |build: Build| { + // Store the results + result.push(build); + }, + ) + .unwrap(); + + // Then + assert_eq!(1, result.len()); + assert_eq!("31395671", result[0].build_id); + assert_eq!(BuildProvider::AppVeyor, result[0].provider); + assert_eq!("appveyor", result[0].collector); + assert_eq!("408686", result[0].project_id); + assert_eq!("spectresystems/spectre.cli", result[0].project_name); + assert_eq!("12349", result[0].definition_id); + assert_eq!("patriksvensson", result[0].definition_name); + assert_eq!("202", result[0].build_number); + assert_eq!(BuildStatus::Success, result[0].status); + assert_eq!("master", result[0].branch); + assert_eq!( + "https://ci.appveyor.com/project/patriksvensson/spectre-commandline/builds/31395671", + result[0].url + ); + assert_eq!(1583929960, result[0].started_at); + assert_eq!(1583930062, result[0].finished_at.unwrap()); + } +} diff --git a/src/providers/collectors/appveyor/test_data/builds.json b/src/providers/collectors/appveyor/test_data/builds.json new file mode 100644 index 0000000..22a734d --- /dev/null +++ b/src/providers/collectors/appveyor/test_data/builds.json @@ -0,0 +1,343 @@ +{ + "project": { + "projectId": 408686, + "accountId": 12349, + "accountName": "patriksvensson", + "builds": [], + "name": "spectre.cli", + "slug": "spectre-commandline", + "repositoryType": "gitHub", + "repositoryScm": "git", + "repositoryName": "spectresystems/spectre.cli", + "isPrivate": false, + "isGitHubApp": false, + "skipBranchesWithoutAppveyorYml": false, + "enableSecureVariablesInPullRequests": false, + "enableSecureVariablesInPullRequestsFromSameRepo": false, + "enableDeploymentInPullRequests": false, + "saveBuildCacheInPullRequests": false, + "rollingBuilds": false, + "rollingBuildsDoNotCancelRunningBuilds": false, + "rollingBuildsOnlyForPullRequests": false, + "alwaysBuildClosedPullRequests": false, + "tags": "", + "securityDescriptor": { + "accessRightDefinitions": [ + { + "name": "View", + "description": "View" + }, + { + "name": "RunBuild", + "description": "Run build" + }, + { + "name": "Update", + "description": "Update settings" + }, + { + "name": "Delete", + "description": "Delete project" + } + ], + "roleAces": [ + { + "roleId": 13733, + "name": "Administrator", + "isAdmin": true, + "accessRights": [ + { + "name": "View", + "allowed": true + }, + { + "name": "RunBuild", + "allowed": true + }, + { + "name": "Update", + "allowed": true + }, + { + "name": "Delete", + "allowed": true + } + ] + }, + { + "roleId": 13734, + "name": "User", + "isAdmin": false, + "accessRights": [ + { + "name": "View" + }, + { + "name": "RunBuild" + }, + { + "name": "Update" + }, + { + "name": "Delete" + } + ] + } + ] + }, + "disablePushWebhooks": false, + "disablePullRequestWebhooks": false, + "created": "2018-01-24T12:56:52.639776+00:00", + "updated": "2019-10-16T16:02:01.7310893+00:00" + }, + "builds": [ + { + "buildId": 31395671, + "projectId": 0, + "jobs": [], + "buildNumber": 202, + "version": "0.29.0.build.202", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "isTag": false, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "success", + "started": "2020-03-11T12:32:40.8920767+00:00", + "finished": "2020-03-11T12:34:22.5186453+00:00", + "created": "2020-03-11T12:32:31.7873812+00:00", + "updated": "2020-03-11T12:34:22.5186453+00:00" + }, + { + "buildId": 31395124, + "projectId": 0, + "jobs": [], + "buildNumber": 201, + "version": "0.29.0.build.201", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "tag": "v0.29.0", + "isTag": true, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "failed", + "started": "2020-03-11T12:09:48.1638791+00:00", + "finished": "2020-03-11T12:11:33.9433798+00:00", + "created": "2020-03-11T12:09:37.1317375+00:00", + "updated": "2020-03-11T12:11:33.9433798+00:00" + }, + { + "buildId": 31394481, + "projectId": 0, + "jobs": [], + "buildNumber": 200, + "version": "1.0.200", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "tag": "v0.29.0", + "isTag": true, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "cancelled", + "started": "2020-03-11T11:46:17.4246895+00:00", + "finished": "2020-03-11T11:46:20.820751+00:00", + "created": "2020-03-11T11:46:10.6782877+00:00", + "updated": "2020-03-11T11:46:20.820751+00:00" + }, + { + "buildId": 31394449, + "projectId": 0, + "jobs": [], + "buildNumber": 199, + "version": "0.29.0.build.199", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "tag": "v0.29.0", + "isTag": true, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "failed", + "started": "2020-03-11T11:44:24.1943604+00:00", + "finished": "2020-03-11T11:45:51.4577106+00:00", + "created": "2020-03-11T11:44:17.4936394+00:00", + "updated": "2020-03-11T11:45:51.4577106+00:00" + }, + { + "buildId": 28543141, + "projectId": 0, + "jobs": [], + "buildNumber": 198, + "version": "0.29.0.build.198", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "tag": "v0.29.0", + "isTag": true, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "success", + "started": "2019-11-01T11:02:51.1677802+00:00", + "finished": "2019-11-01T11:03:59.1178171+00:00", + "created": "2019-11-01T11:02:43.9643725+00:00", + "updated": "2019-11-01T11:03:59.1178171+00:00" + }, + { + "buildId": 28527381, + "projectId": 0, + "jobs": [], + "buildNumber": 197, + "version": "0.28.1+4.build.197", + "message": "Merge pull request #87 from patriksvensson/feature/GH-63", + "messageExtended": "Add support for optional flag values", + "branch": "master", + "isTag": false, + "commitId": "1f63dc37e7688055bfa784bbdbcae8c0c8e97121", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-31T18:14:35+00:00", + "messages": [], + "status": "success", + "started": "2019-10-31T18:14:45.4950787+00:00", + "finished": "2019-10-31T18:16:07.4136462+00:00", + "created": "2019-10-31T18:14:38.4261908+00:00", + "updated": "2019-10-31T18:16:07.4136462+00:00" + }, + { + "buildId": 28517688, + "projectId": 0, + "jobs": [], + "buildNumber": 196, + "version": "0.28.1-PullRequest0087.4.build.196", + "message": "Add support for optional flag values", + "messageExtended": "Closes #63", + "branch": "master", + "isTag": false, + "commitId": "0d3c47dfb64531cc9e4d2e09d4063ccb7ee2b035", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "Patrik Svensson", + "committerUsername": "patriksvensson", + "committed": "2019-10-29T00:38:49+00:00", + "pullRequestId": "87", + "pullRequestName": "Add support for optional flag values", + "pullRequestHeadCommitId": "5965f595cf04c00179daf09676020ab8ee159288", + "pullRequestHeadRepository": "patriksvensson/spectre.cli", + "pullRequestHeadBranch": "feature/GH-63", + "messages": [], + "status": "success", + "started": "2019-10-31T12:42:23.6402998+00:00", + "finished": "2019-10-31T12:43:56.4375615+00:00", + "created": "2019-10-31T12:42:10.7022956+00:00", + "updated": "2019-10-31T12:43:56.4375615+00:00" + }, + { + "buildId": 28516409, + "projectId": 0, + "jobs": [], + "buildNumber": 195, + "version": "0.28.1-PullRequest0087.4.build.195", + "message": "Add support for optional flag values", + "messageExtended": "Closes #63", + "branch": "master", + "isTag": false, + "commitId": "94935e7a87a5c349688da28c37ccd1c276f3827a", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "Patrik Svensson", + "committerUsername": "patriksvensson", + "committed": "2019-10-29T00:38:49+00:00", + "pullRequestId": "87", + "pullRequestName": "Add support for optional flag values", + "pullRequestHeadCommitId": "40d87a6832a0b0f4d08e2f87aa048cd41ad0bee7", + "pullRequestHeadRepository": "patriksvensson/spectre.cli", + "pullRequestHeadBranch": "feature/GH-63", + "messages": [], + "status": "success", + "started": "2019-10-31T11:56:58.4897145+00:00", + "finished": "2019-10-31T11:58:07.0379397+00:00", + "created": "2019-10-31T11:56:51.126724+00:00", + "updated": "2019-10-31T11:58:07.0379397+00:00" + }, + { + "buildId": 28445623, + "projectId": 0, + "jobs": [], + "buildNumber": 194, + "version": "0.28.1-PullRequest0087.4.build.194", + "message": "Initial commit", + "branch": "master", + "isTag": false, + "commitId": "843fd025634661382424354d6b7272e9f3bb8422", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "Patrik Svensson", + "committerUsername": "patriksvensson", + "committed": "2019-10-29T00:38:49+00:00", + "pullRequestId": "87", + "pullRequestName": "Add support for optional flag values", + "pullRequestHeadCommitId": "bfcf38a10f27fae705fd51f71f5e0d67e3c7eb87", + "pullRequestHeadRepository": "patriksvensson/spectre.cli", + "pullRequestHeadBranch": "feature/GH-63", + "messages": [], + "status": "failed", + "started": "2019-10-29T00:40:33.4029314+00:00", + "finished": "2019-10-29T00:41:25.6388897+00:00", + "created": "2019-10-29T00:40:27.8695094+00:00", + "updated": "2019-10-29T00:41:25.6388897+00:00" + }, + { + "buildId": 28409216, + "projectId": 0, + "jobs": [], + "buildNumber": 193, + "version": "0.28.1+2.build.193", + "message": "Merge pull request #85 from patriksvensson/feature/GH-84", + "messageExtended": "Always validate examples strictly", + "branch": "master", + "isTag": false, + "commitId": "fa3bd3b360432a35ef726222449f90f5baf588d6", + "authorName": "Patrik Svensson", + "authorUsername": "patriksvensson", + "committerName": "GitHub", + "committerUsername": "web-flow", + "committed": "2019-10-27T10:27:48+00:00", + "messages": [], + "status": "success", + "started": "2019-10-27T10:27:57.718882+00:00", + "finished": "2019-10-27T10:29:02.2587457+00:00", + "created": "2019-10-27T10:27:51.1153983+00:00", + "updated": "2019-10-27T10:29:02.2587457+00:00" + } + ] +} \ No newline at end of file diff --git a/src/providers/collectors/appveyor/validation.rs b/src/providers/collectors/appveyor/validation.rs new file mode 100644 index 0000000..2b8745a --- /dev/null +++ b/src/providers/collectors/appveyor/validation.rs @@ -0,0 +1,129 @@ +use crate::config::{AppVeyorConfiguration, Validate}; +use crate::DuckResult; + +impl Validate for AppVeyorConfiguration { + fn validate(&self) -> DuckResult<()> { + if self.account.is_empty() { + return Err(format_err!("AppVeyor account is empty")); + } + if self.project.is_empty() { + return Err(format_err!("AppVeyor project is empty")); + } + match &self.credentials { + crate::config::AppVeyorCredentials::Bearer(token) => { + if token.is_empty() { + return Err(format_err!("AppVeyor bearer token is empty")); + } + } + }; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::config::*; + use crate::providers; + use crate::providers::collectors::Collector; + use crate::utils::text::TestVariableProvider; + + fn create_collectors_from_config(json: &str) -> Vec> { + providers::create_collectors( + &Configuration::from_json(&TestVariableProvider::new(), json).unwrap(), + ) + .unwrap() + } + + #[test] + #[should_panic(expected = "The id \\'\\' is invalid.")] + fn should_return_error_if_id_is_empty() { + create_collectors_from_config( + r#" + { + "collectors": [ + { + "appveyor": { + "id": "", + "credentials": { + "bearer": "SECRET" + }, + "account": "patriksvensson", + "project": "spectre-commandline", + "count": 5 + } + } + ] + }"#, + ); + } + + #[test] + #[should_panic(expected = "AppVeyor bearer token is empty")] + fn should_return_error_if_credentials_is_empty() { + create_collectors_from_config( + r#" + { + "collectors": [ + { + "appveyor": { + "id": "appveyor_spectrecli", + "credentials": { + "bearer": "" + }, + "account": "patriksvensson", + "project": "spectre-commandline", + "count": 5 + } + } + ] + }"#, + ); + } + + #[test] + #[should_panic(expected = "AppVeyor account is empty")] + fn should_return_error_if_account_is_empty() { + create_collectors_from_config( + r#" + { + "collectors": [ + { + "appveyor": { + "id": "appveyor_spectrecli", + "credentials": { + "bearer": "" + }, + "account": "", + "project": "spectre-commandline", + "count": 5 + } + } + ] + }"#, + ); + } + + #[test] + #[should_panic(expected = "AppVeyor project is empty")] + fn should_return_error_if_project_is_empty() { + create_collectors_from_config( + r#" + { + "collectors": [ + { + "appveyor": { + "id": "appveyor_spectrecli", + "credentials": { + "bearer": "" + }, + "account": "patriksvensson", + "project": "", + "count": 5 + } + } + ] + }"#, + ); + } +} diff --git a/src/providers/collectors/github/mod.rs b/src/providers/collectors/github/mod.rs index 31c6112..81ef67d 100644 --- a/src/providers/collectors/github/mod.rs +++ b/src/providers/collectors/github/mod.rs @@ -168,7 +168,6 @@ mod tests { // Then assert_eq!(4, result.len()); - assert_eq!("33801182", result[0].build_id); assert_eq!(BuildProvider::GitHub, result[0].provider); assert_eq!("github", result[0].collector); diff --git a/src/utils/date.rs b/src/utils/date.rs index 99e7bc5..916032b 100644 --- a/src/utils/date.rs +++ b/src/utils/date.rs @@ -6,6 +6,7 @@ pub static TEAMCITY_FORMAT: &str = "%Y%m%dT%H%M%S%z"; pub static AZURE_DEVOPS_FORMAT: &str = "%+"; pub static GITHUB_FORMAT: &str = "%+"; pub static OCTOPUS_DEPLOY_FORMAT: &str = "%+"; +pub static APPVEYOR_FORMAT: &str = "%+"; pub fn to_timestamp(input: &str, pattern: &str) -> DuckResult { match DateTime::parse_from_str(input, pattern) { @@ -41,4 +42,10 @@ mod tests { let result = to_timestamp("2020-02-01T20:43:16Z", GITHUB_FORMAT).unwrap(); assert_eq!(1580589796, result); } + + #[test] + fn should_parse_appveyor_format() { + let result = to_timestamp("2020-03-11T12:09:48.1638791+00:00", APPVEYOR_FORMAT).unwrap(); + assert_eq!(1583928588, result); + } } diff --git a/src/utils/http.rs b/src/utils/http.rs index 6b474d9..9686b32 100644 --- a/src/utils/http.rs +++ b/src/utils/http.rs @@ -63,6 +63,10 @@ impl HttpRequestBuilder { self.headers.insert(name.into(), value.into()); } + pub fn bearer(&mut self, token: T) { + self.add_header("Authorization", &format!("Bearer {}", token)) + } + // Borrowed from https://github.com/seanmonstar/reqwest/blob/master/src/blocking/request.rs#L234 pub fn basic_auth(&mut self, username: U, password: Option

) where diff --git a/web/package-lock.json b/web/package-lock.json index 5997fe3..2769235 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/web/package.json b/web/package.json index b43cbfd..62b2797 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.9.0", + "version": "0.10.0", "private": true, "scripts": { "serve": "vue-cli-service serve", diff --git a/web/src/assets/appveyor.svg b/web/src/assets/appveyor.svg new file mode 100644 index 0000000..71a23a1 --- /dev/null +++ b/web/src/assets/appveyor.svg @@ -0,0 +1,6 @@ + + + + + +