Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/add codspeed cli #36

Merged
merged 26 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0ad8ce2
chore(runner): remove useless snapshots
adriencaccia May 24, 2024
832acd1
refactor(runner): move runner to run subcommand
adriencaccia May 24, 2024
6f3aeb7
feat(auth): first implementation of auth login command
adriencaccia May 28, 2024
59da1c1
chore(runner): remove useless code in GitHubActionsProvide
adriencaccia May 28, 2024
ec91e65
feat(runner): add local provider
adriencaccia May 29, 2024
692b633
feat(runner): handle local run
adriencaccia May 31, 2024
e3257e3
feat(runner/setup): do not install valgrind if correct version is ins…
adriencaccia May 31, 2024
9e81bc1
feat(runner): support arm64 arch
adriencaccia Jun 6, 2024
e4fc871
feat: add system info to upload metadata runner property
adriencaccia Jun 6, 2024
2545517
refactor: rename bin to codspeed
adriencaccia Jun 10, 2024
eb1479d
feat: update CLI style
adriencaccia Jun 10, 2024
4822152
refactor(logger): move logger group logic to root logger
adriencaccia Jun 19, 2024
8eb6cd1
feat(cli): create custom local logger with spinner
adriencaccia Jun 19, 2024
3410f2b
feat(auth): add log groups
adriencaccia Jul 1, 2024
b601ca8
feat(auth): style auth link log
adriencaccia Jul 2, 2024
68b3948
feat(runner/poll_results): add regressions threshold, colors and bett…
adriencaccia Jul 2, 2024
20a36f3
feat(run): change verbs tense to continuous
adriencaccia Jul 17, 2024
f6b089f
feat(cli): update style of terminal output
adriencaccia Jul 17, 2024
dc177a1
feat(valgrind): prevent trace valgrind logs to duplicate spinner lines
adriencaccia Jul 17, 2024
aaf0e79
feat(run): disallow empty bench command
adriencaccia Jul 17, 2024
e35253e
feat(run): do not display codspeed banner during local run
adriencaccia Jul 17, 2024
64ea4f8
feat(run): update some logging
adriencaccia Jul 17, 2024
0e0258c
docs: update readme with CLI usage
adriencaccia Jul 23, 2024
3b6cbdc
feat(cli): handle invalid token
adriencaccia Jul 23, 2024
7af9b55
chore(runner): remove useless code in BuildkiteProvider
adriencaccia Jul 24, 2024
04f0e4d
refactor(cli): move local logger to its own file
adriencaccia Jul 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 266 additions & 103 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ edition = "2021"
repository = "https://github.com/CodSpeedHQ/runner"
publish = false

[[bin]]
name = "codspeed"
path = "src/main.rs"


[dependencies]
anyhow = "1.0.75"
clap = { version = "4.4.8", features = ["derive", "env"] }
clap = { version = "4.4.8", features = ["derive", "env", "color"] }
itertools = "0.11.0"
lazy_static = "1.4.0"
log = "0.4.20"
Expand All @@ -30,9 +34,17 @@ tokio-tar = "0.3.1"
md5 = "0.7.0"
base64 = "0.21.0"
async-compression = { version = "0.4.5", features = ["tokio", "gzip"] }
simplelog = { version = "0.12.1", default-features = false }
simplelog = { version = "0.12.1", default-features = false, features = [
"termcolor",
] }
tempfile = "3.10.0"
git2 = "0.18.3"
nestify = "0.3.3"
gql_client = { git = "https://github.com/CodSpeedHQ/gql-client-rs" }
serde_yaml = "0.9.34"
sysinfo = { version = "0.30.12", features = ["serde"] }
indicatif = "0.17.8"
console = "0.15.8"

[dev-dependencies]
temp-env = { version = "0.3.6", features = ["async_closure"] }
Expand Down
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<div align="center">
<h1><code>codspeed-runner</code></h1>
<h1>CodSpeed CLI</h1>

CLI to gather performance data from CI environments and upload performance reports to [CodSpeed](https://codspeed.io)
CLI to gather performance data and upload performance reports to [CodSpeed](https://codspeed.io)

[![CI](https://github.com/CodSpeedHQ/runner/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/runner/actions/workflows/ci.yml)
[![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
[![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/)

</div>

The `codspeed-runner` CLI is designed to be used in CI environments.
The `codspeed` CLI is designed to be used both in **local** in **CI environments**.

The following providers are supported:
The following CI providers are supported:

- [GitHub Actions](https://docs.codspeed.io/ci/github-actions): Usage with [`@CodSpeedHQ/action`](https://github.com/CodSpeedHQ/action) is recommended.
- [Buildkite](https://docs.codspeed.io/ci/buildkite)
Expand All @@ -20,7 +20,7 @@ The following providers are supported:

If you want to use the CLI with another provider, you can open an issue or chat with us on [Discord](https://discord.com/invite/MxpaCfKSqF) 🚀

You can check out the implementation of the [supported providers](https://github.com/CodSpeedHQ/runner/tree/main/src/ci_provider) for reference.
You can check out the implementation of the [supported providers](https://github.com/CodSpeedHQ/runner/tree/main/src/run/ci_provider) for reference.

## Installation

Expand All @@ -35,16 +35,31 @@ Refer to the [releases page](https://github.com/CodSpeedHQ/runner/releases) to s
## Usage

> [!NOTE]
> For now, the CLI only supports Ubuntu 20.04 and 22.04.
> For now, the CLI only supports Ubuntu 20.04, 22.04, and Debian 11, 12.

Example of a command to run benchmarks with [Vitest](https://docs.codspeed.io/benchmarks/nodejs/vitest):
First, authenticate with your CodSpeed account:

```bash
codspeed-runner --token=$CODSPEED_TOKEN -- pnpm vitest bench
codspeed auth login
```

Then, run benchmarks with the following command:

```bash
codspeed run <my-benchmark-command>

# Example, using https://github.com/CodSpeedHQ/codspeed-rust
codspeed run cargo codspeed run

# Example, using https://github.com/CodSpeedHQ/pytest-codspeed
codspeed run pytest ./tests --codspeed

# Example, using https://github.com/CodSpeedHQ/codspeed-node/tree/main/packages/vitest-plugin
codspeed run pnpm vitest bench
```

```
Usage: codspeed-runner [OPTIONS] [COMMAND]...
Usage: codspeed run [OPTIONS] [COMMAND]...

Arguments:
[COMMAND]... The bench command to run
Expand All @@ -65,5 +80,5 @@ Options:
Use the `CODSPEED_LOG` environment variable to set the logging level:

```bash
CODSPEED_LOG=debug codspeed-runner ...
CODSPEED_LOG=debug codspeed run ...
```
216 changes: 216 additions & 0 deletions src/api_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use std::fmt::Display;

use crate::prelude::*;
use crate::{app::Cli, config::CodSpeedConfig};
use console::style;
use gql_client::{Client as GQLClient, ClientConfig};
use nestify::nest;
use serde::{Deserialize, Serialize};

pub struct CodSpeedAPIClient {
gql_client: GQLClient,
unauthenticated_gql_client: GQLClient,
}

impl TryFrom<&Cli> for CodSpeedAPIClient {
type Error = Error;
fn try_from(args: &Cli) -> Result<Self> {
let codspeed_config = CodSpeedConfig::load()?;

Ok(Self {
gql_client: build_gql_api_client(&codspeed_config, args.api_url.clone(), true),
unauthenticated_gql_client: build_gql_api_client(
&codspeed_config,
args.api_url.clone(),
false,
),
})
}
}

fn build_gql_api_client(
codspeed_config: &CodSpeedConfig,
api_url: String,
with_auth: bool,
) -> GQLClient {
let headers = if with_auth && codspeed_config.auth.token.is_some() {
let mut headers = std::collections::HashMap::new();
headers.insert(
"Authorization".to_string(),
codspeed_config.auth.token.clone().unwrap(),
);
headers
} else {
Default::default()
};

GQLClient::new_with_config(ClientConfig {
endpoint: api_url,
timeout: Some(10),
headers: Some(headers),
proxy: None,
})
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct CreateLoginSessionData {
create_login_session: pub struct CreateLoginSessionPayload {
pub callback_url: String,
pub session_id: String,
}
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConsumeLoginSessionVars {
session_id: String,
}
nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct ConsumeLoginSessionData {
consume_login_session: pub struct ConsumeLoginSessionPayload {
pub token: Option<String>
}
}
}

#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FetchLocalRunReportVars {
pub owner: String,
pub name: String,
pub run_id: String,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub enum ReportConclusion {
AcknowledgedFailure,
Failure,
MissingBaseRun,
Success,
}

impl Display for ReportConclusion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReportConclusion::AcknowledgedFailure => {
write!(f, "{}", style("Acknowledged Failure").yellow().bold())
}
ReportConclusion::Failure => write!(f, "{}", style("Failure").red().bold()),
ReportConclusion::MissingBaseRun => {
write!(f, "{}", style("Missing Base Run").yellow().bold())
}
ReportConclusion::Success => write!(f, "{}", style("Success").green().bold()),
}
}
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchLocalRunReportHeadReport {
pub id: String,
pub impact: Option<f64>,
pub conclusion: ReportConclusion,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RunStatus {
Pending,
Processing,
Completed,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchLocalRunReportRun {
pub id: String,
pub status: RunStatus,
pub url: String,
pub head_reports: Vec<FetchLocalRunReportHeadReport>,
}
nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct FetchLocalRunReportData {
repository: pub struct FetchLocalRunReportRepository {
settings: struct FetchLocalRunReportSettings {
allowed_regression: f64,
},
runs: Vec<FetchLocalRunReportRun>,
}
}
}

pub struct FetchLocalRunReportResponse {
pub allowed_regression: f64,
pub run: FetchLocalRunReportRun,
}

impl CodSpeedAPIClient {
pub async fn create_login_session(&self) -> Result<CreateLoginSessionPayload> {
let response = self
.unauthenticated_gql_client
.query_unwrap::<CreateLoginSessionData>(include_str!("queries/CreateLoginSession.gql"))
.await;
match response {
Ok(response) => Ok(response.create_login_session),
Err(err) => bail!("Failed to create login session: {}", err),
}
}

pub async fn consume_login_session(
&self,
session_id: &str,
) -> Result<ConsumeLoginSessionPayload> {
let response = self
.unauthenticated_gql_client
.query_with_vars_unwrap::<ConsumeLoginSessionData, ConsumeLoginSessionVars>(
include_str!("queries/ConsumeLoginSession.gql"),
ConsumeLoginSessionVars {
session_id: session_id.to_string(),
},
)
.await;
match response {
Ok(response) => Ok(response.consume_login_session),
Err(err) => bail!("Failed to use login session: {}", err),
}
}

pub async fn fetch_local_run_report(
&self,
vars: FetchLocalRunReportVars,
) -> Result<FetchLocalRunReportResponse> {
let response = self
.gql_client
.query_with_vars_unwrap::<FetchLocalRunReportData, FetchLocalRunReportVars>(
include_str!("queries/FetchLocalRunReport.gql"),
vars.clone(),
)
.await;
match response {
Ok(response) => {
let allowed_regression = response.repository.settings.allowed_regression;

match response.repository.runs.into_iter().next() {
Some(run) => Ok(FetchLocalRunReportResponse {
allowed_regression,
run,
}),
None => bail!(
"No runs found for owner: {}, name: {}, run_id: {}",
vars.owner,
vars.name,
vars.run_id
),
}
}
Err(err) if err.contains_error_code("UNAUTHENTICATED") => {
bail!("Your session has expired, please login again using `codspeed auth login`")
}
Err(err) => bail!("Failed to fetch local run report: {}", err),
}
}
}
Loading
Loading