Skip to content
Merged
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
26 changes: 18 additions & 8 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,15 @@ async fn login(
SessionError::Other(err) => err,
})?;

let mut config = CodSpeedConfig::load_with_override(config_name, None)?;
config.auth.token = Some(token);
let selected = config.selected_profile_name().to_owned();
config.profile_mut(&selected).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 +154,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 +167,11 @@ pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
}
};

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

Expand Down
4 changes: 2 additions & 2 deletions src/cli/exec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::ExecAndRunSharedArgs;
use crate::api_client::CodSpeedAPIClient;
use crate::executor;
use crate::executor::config::{self, OrchestratorConfig, RepositoryOverride};
use crate::executor::config::{OrchestratorConfig, RepositoryOverride};
use crate::instruments::Instruments;
use crate::prelude::*;
use crate::project_config::ProjectConfig;
Expand Down Expand Up @@ -60,7 +60,7 @@ fn build_orchestrator_config(
let raw_upload_url = args
.shared
.upload_url
.unwrap_or_else(|| config::DEFAULT_UPLOAD_URL.into());
.unwrap_or_else(|| crate::config::DEFAULT_UPLOAD_URL.into());
let upload_url = Url::parse(&raw_upload_url)
.map_err(|e| anyhow!("Invalid upload URL: {raw_upload_url}, {e}"))?;

Expand Down
103 changes: 69 additions & 34 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 All @@ -16,7 +17,7 @@ use std::path::PathBuf;

use crate::{
api_client::CodSpeedAPIClient,
config::CodSpeedConfig,
config::{CodSpeedConfig, ConfigOverrides},
executor::helpers::command::CommandBuilder,
local_logger::{CODSPEED_U8_COLOR_CODE, init_local_logger},
prelude::*,
Expand All @@ -41,25 +42,23 @@ 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)]
pub oauth_token: Option<String>,

/// The configuration name to use
/// If provided, the configuration will be loaded from ~/.config/codspeed/{config-name}.yaml
/// Otherwise, loads from ~/.config/codspeed/config.yaml
#[arg(long, env = "CODSPEED_CONFIG_NAME", global = true)]
/// [deprecated] Load configuration from `~/.config/codspeed/{config-name}.yaml`
/// instead of the default `config.yaml`. Prefer `--profile` instead.
#[arg(long, env = "CODSPEED_CONFIG_NAME", global = true, hide = true)]
#[deprecated(note = "use `--profile` / `CODSPEED_PROFILE` instead")]
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,45 @@ 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) => {
#[allow(deprecated)]
let config_name = cli.config_name.as_deref();
auth::run(args, &api_client, config_name, codspeed_config).await?
}
Commands::Profile(args) => {
#[allow(deprecated)]
let config_name = cli.config_name.as_deref();
profile::run(args, config_name, 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 @@ -184,6 +203,34 @@ pub async fn run() -> Result<()> {
Ok(())
}

/// Load the CodSpeed config for this invocation, resolving the active
/// profile (CLI `--profile` / `CODSPEED_PROFILE` / shell-session / built-in
/// `default`) and applying CLI overrides for the OAuth token and api URL.
///
/// `auth` and `profile` subcommands are allowed to run against a config
/// where the selected profile does not yet exist (e.g. first-time setup).
fn load_config(cli: &Cli) -> Result<CodSpeedConfig> {
// The field carries a `#[deprecated]` marker but we still need to
// honour it during the deprecation window.
#[allow(deprecated)]
let config_name = cli.config_name.as_deref();
if config_name.is_some() {
warn!(
"`--config-name` / `CODSPEED_CONFIG_NAME` is deprecated; use `--profile` / `CODSPEED_PROFILE` instead."
);
}
CodSpeedConfig::load_with_profile(
config_name,
cli.profile.as_deref(),
ConfigOverrides {
oauth_token: cli.oauth_token.as_deref(),
api_url: cli.api_url.as_deref(),
upload_url: None,
},
matches!(&cli.command, Commands::Auth(_) | Commands::Profile(_)),
)
}

/// Build the api client for this invocation, resolving the auth token
/// from the most specific source available. This is the single source
/// of truth for token resolution; the result lives on the returned
Expand All @@ -193,28 +240,16 @@ 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 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