Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_install_hook_types: [pre-commit, post-checkout]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
Expand All @@ -25,3 +26,12 @@ repos:
- id: clang-format
files: ^crates/memtrack/src/ebpf/c/.*\.(c|h|bpf\.c)$
args: [--style=file, -i]
- repo: local
hooks:
- id: init-worktree
name: Initialize worktree
entry: scripts/init-worktree.sh
language: script
stages: [post-checkout]
always_run: true
pass_filenames: false
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Contributing to CodSpeed Runner

## Initial Setup

After cloning, install the pre-commit hooks:

```bash
prek install
```

## Release Process

This repository is a Cargo workspace containing multiple crates. The release process differs depending on which crate you're releasing.
Expand Down
30 changes: 30 additions & 0 deletions scripts/init-worktree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
# Initialize a new worktree when it is first created.
#
# Runs from pre-commit's `post-checkout` stage. Git passes the all-zero SHA as
# the previous HEAD only during `git worktree add`, which is how we detect this
# case (pre-commit forwards it as PRE_COMMIT_FROM_REF).
set -eu

ZERO_SHA="0000000000000000000000000000000000000000"

if [ "${PRE_COMMIT_FROM_REF:-}" != "$ZERO_SHA" ]; then
exit 0
fi

MAIN_REPO=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')
CURRENT_DIR=$(pwd)

if [ "$MAIN_REPO" = "$CURRENT_DIR" ]; then
exit 0
fi

printf "\n🌿 Initializing worktree from %s\n\n" "$MAIN_REPO"

printf "📦 Initializing submodules...\n"
git submodule update --init --recursive

if command -v pre-commit >/dev/null 2>&1; then
printf "\n🪝 Installing pre-commit hooks...\n"
pre-commit install --hook-type pre-commit --hook-type post-checkout
fi
23 changes: 16 additions & 7 deletions src/cli/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ pub async fn run(
args: AuthArgs,
api_client: &CodSpeedAPIClient,
config_name: Option<&str>,
config: CodSpeedConfig,
) -> Result<()> {
match args.command {
AuthCommands::Login { with_token } => login(api_client, config_name, with_token).await?,
AuthCommands::Status => status(api_client).await?,
AuthCommands::Login { with_token } => {
login(api_client, config_name, config, with_token).await?
}
AuthCommands::Status => status(api_client, &config).await?,
}
Ok(())
}
Expand All @@ -52,6 +55,7 @@ const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 m
async fn login(
api_client: &CodSpeedAPIClient,
config_name: Option<&str>,
mut config: CodSpeedConfig,
with_token: bool,
) -> Result<()> {
debug!("Login to CodSpeed");
Expand Down Expand Up @@ -118,12 +122,14 @@ async fn login(
SessionError::Other(err) => err,
})?;

let mut config = CodSpeedConfig::load_with_override(config_name, None)?;
config.auth.token = Some(token);
config.persist(config_name)?;
debug!("Token saved to configuration file");

info!("Login successful, your are now authenticated on CodSpeed");
info!(
"Login successful, you are now authenticated on CodSpeed (profile: {})",
config.selected_profile_name()
);

Ok(())
}
Expand All @@ -147,8 +153,7 @@ struct AuthStatus {
detected_repository: Option<(ParsedRepository, Option<RepositoryOverviewPayload>)>,
}

pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
let config = CodSpeedConfig::load_with_override(None, None)?;
pub async fn status(api_client: &CodSpeedAPIClient, config: &CodSpeedConfig) -> Result<()> {
let has_token = config.auth.token.is_some();
let parsed = detect_repository();

Expand All @@ -161,7 +166,11 @@ pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
}
};

info!("{}", style("Authentication").bold());
info!(
"{} ({})",
style("Authentication").bold(),
config.selected_profile_name()
);
print_authentication_section(has_token, auth_status.session.as_ref());
info!("");

Expand Down
78 changes: 49 additions & 29 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod auth;
pub(crate) mod exec;
pub(crate) mod experimental;
mod profile;
pub(crate) mod run;
pub(crate) mod samply;
mod setup;
Expand Down Expand Up @@ -41,14 +42,8 @@ fn create_styles() -> Styles {
#[command(version, about = "The CodSpeed CLI tool", styles = create_styles())]
pub struct Cli {
/// The URL of the CodSpeed GraphQL API
#[arg(
long,
env = "CODSPEED_API_URL",
global = true,
hide = true,
default_value = "https://gql.codspeed.io/"
)]
pub api_url: String,
#[arg(long, env = "CODSPEED_API_URL", global = true, hide = true)]
pub api_url: Option<String>,

/// The OAuth token to use for all requests
#[arg(long, env = "CODSPEED_OAUTH_TOKEN", global = true, hide = true)]
Expand All @@ -60,6 +55,10 @@ pub struct Cli {
#[arg(long, env = "CODSPEED_CONFIG_NAME", global = true)]
pub config_name: Option<String>,

/// The CodSpeed profile to use
#[arg(long, env = "CODSPEED_PROFILE", global = true)]
pub profile: Option<String>,

/// Path to project configuration file (codspeed.yaml)
/// If provided, loads config from this path. Otherwise, searches for config files
/// in the current directory and upward to the git root.
Expand Down Expand Up @@ -88,6 +87,8 @@ enum Commands {
Exec(Box<exec::ExecArgs>),
/// Manage the CLI authentication state
Auth(auth::AuthArgs),
/// Manage CodSpeed profiles
Profile(profile::ProfileArgs),
/// Pre-install the codspeed executors
Setup(setup::SetupArgs),
/// Show the overall status of CodSpeed (authentication, tools, system)
Expand Down Expand Up @@ -130,7 +131,8 @@ impl InternalCommands {

pub async fn run() -> Result<()> {
let cli = Cli::parse();
let mut api_client = build_api_client(&cli)?;
let codspeed_config = load_config(&cli)?;
let mut api_client = build_api_client(&cli, &codspeed_config);

// Discover project configuration file
let discovered_config = DiscoveredProjectConfig::discover_and_load(
Expand All @@ -154,28 +156,47 @@ pub async fn run() -> Result<()> {

match cli.command {
Commands::Run(args) => {
let mut args = *args;
args.shared
.upload_url
.get_or_insert_with(|| codspeed_config.upload_url.clone());
args.shared.experimental.warn_if_active();
run::run(
*args,
args,
&mut api_client,
discovered_config.as_ref(),
setup_cache_dir,
)
.await?
}
Commands::Exec(args) => {
let mut args = *args;
args.shared
.upload_url
.get_or_insert_with(|| codspeed_config.upload_url.clone());
args.shared.experimental.warn_if_active();
exec::run(
*args,
args,
&mut api_client,
discovered_config.as_ref().map(|d| &d.config),
setup_cache_dir,
)
.await?
}
Commands::Auth(args) => auth::run(args, &api_client, cli.config_name.as_deref()).await?,
Commands::Auth(args) => {
auth::run(
args,
&api_client,
cli.config_name.as_deref(),
codspeed_config,
)
.await?
}
Commands::Profile(args) => {
profile::run(args, cli.config_name.as_deref(), cli.profile.as_deref())?
}
Commands::Setup(args) => setup::run(args, setup_cache_dir).await?,
Commands::Status => status::run(&api_client).await?,
Commands::Status => status::run(&api_client, &codspeed_config).await?,
Commands::Use(args) => use_mode::run(args)?,
Commands::Show => show::run()?,
Commands::Update => update::run().await?,
Expand All @@ -193,28 +214,27 @@ pub async fn run() -> Result<()> {
/// Priority (most specific first):
/// 1. `--token` / `CODSPEED_TOKEN` — run/exec-level override
/// 2. `--oauth-token` / `CODSPEED_OAUTH_TOKEN` and the persisted CLI
/// token — both live on disk and are loaded together by
/// [`CodSpeedConfig::load_with_override`].
///
/// The CLI config file is only read when no explicit token was passed,
/// so an invocation like `codspeed run --token <X>` never touches the
/// user's `~/.config/codspeed/`.
fn build_api_client(cli: &Cli) -> Result<CodSpeedAPIClient> {
/// token from the selected profile.
fn load_config(cli: &Cli) -> Result<CodSpeedConfig> {
CodSpeedConfig::load_with_profile(
cli.config_name.as_deref(),
cli.profile.as_deref(),
cli.oauth_token.as_deref(),
cli.api_url.as_deref(),
None,
matches!(&cli.command, Commands::Auth(_) | Commands::Profile(_)),
)
}

fn build_api_client(cli: &Cli, config: &CodSpeedConfig) -> CodSpeedAPIClient {
let explicit = match &cli.command {
Commands::Run(args) => args.shared.token.clone(),
Commands::Exec(args) => args.shared.token.clone(),
_ => None,
};
let token = match explicit {
Some(token) => Some(token),
None => {
CodSpeedConfig::load_with_override(
cli.config_name.as_deref(),
cli.oauth_token.as_deref(),
)?
.auth
.token
}
None => config.auth.token.clone(),
};
Ok(CodSpeedAPIClient::new(token, cli.api_url.clone()))
CodSpeedAPIClient::new(token, config.api_url.clone())
}
Loading