Skip to content

Commit 9a910f9

Browse files
committed
feat: add profile-based auth configuration
1 parent db14de8 commit 9a910f9

7 files changed

Lines changed: 443 additions & 64 deletions

File tree

src/cli/auth.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@ pub async fn run(
3939
args: AuthArgs,
4040
api_client: &CodSpeedAPIClient,
4141
config_name: Option<&str>,
42+
profile_name: &str,
4243
) -> Result<()> {
4344
match args.command {
44-
AuthCommands::Login { with_token } => login(api_client, config_name, with_token).await?,
45-
AuthCommands::Status => status(api_client).await?,
45+
AuthCommands::Login { with_token } => {
46+
login(api_client, config_name, profile_name, with_token).await?
47+
}
48+
AuthCommands::Status => status(api_client, config_name, profile_name).await?,
4649
}
4750
Ok(())
4851
}
@@ -52,6 +55,7 @@ const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 m
5255
async fn login(
5356
api_client: &CodSpeedAPIClient,
5457
config_name: Option<&str>,
58+
profile_name: &str,
5559
with_token: bool,
5660
) -> Result<()> {
5761
debug!("Login to CodSpeed");
@@ -119,11 +123,11 @@ async fn login(
119123
})?;
120124

121125
let mut config = CodSpeedConfig::load_with_override(config_name, None)?;
122-
config.auth.token = Some(token);
126+
config.profile_mut(profile_name).auth.token = Some(token);
123127
config.persist(config_name)?;
124128
debug!("Token saved to configuration file");
125129

126-
info!("Login successful, your are now authenticated on CodSpeed");
130+
info!("Login successful, your are now authenticated on CodSpeed profile `{profile_name}`");
127131

128132
Ok(())
129133
}
@@ -147,9 +151,16 @@ struct AuthStatus {
147151
detected_repository: Option<(ParsedRepository, Option<RepositoryOverviewPayload>)>,
148152
}
149153

150-
pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
151-
let config = CodSpeedConfig::load_with_override(None, None)?;
152-
let has_token = config.auth.token.is_some();
154+
pub async fn status(
155+
api_client: &CodSpeedAPIClient,
156+
config_name: Option<&str>,
157+
profile_name: &str,
158+
) -> Result<()> {
159+
let config = CodSpeedConfig::load_with_override(config_name, None)?;
160+
let has_token = config
161+
.profile(profile_name)
162+
.and_then(|profile| profile.auth.token.as_ref())
163+
.is_some();
153164
let parsed = detect_repository();
154165

155166
let auth_status = if has_token {
@@ -161,7 +172,7 @@ pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
161172
}
162173
};
163174

164-
info!("{}", style("Authentication").bold());
175+
info!("{} ({profile_name})", style("Authentication").bold());
165176
print_authentication_section(has_token, auth_status.session.as_ref());
166177
info!("");
167178

src/cli/exec/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use super::ExecAndRunSharedArgs;
22
use crate::api_client::CodSpeedAPIClient;
3+
use crate::config::ResolvedProfileConfig;
34
use crate::executor;
4-
use crate::executor::config::{self, OrchestratorConfig, RepositoryOverride};
5+
use crate::executor::config::{OrchestratorConfig, RepositoryOverride};
56
use crate::instruments::Instruments;
67
use crate::prelude::*;
78
use crate::project_config::ProjectConfig;
@@ -55,12 +56,13 @@ fn build_orchestrator_config(
5556
args: ExecArgs,
5657
target: executor::BenchmarkTarget,
5758
poll_results_options: PollResultsOptions,
59+
resolved_profile: &ResolvedProfileConfig,
5860
) -> Result<OrchestratorConfig> {
5961
let modes = args.shared.resolve_modes()?;
6062
let raw_upload_url = args
6163
.shared
6264
.upload_url
63-
.unwrap_or_else(|| config::DEFAULT_UPLOAD_URL.into());
65+
.unwrap_or_else(|| resolved_profile.upload_url.clone());
6466
let upload_url = Url::parse(&raw_upload_url)
6567
.map_err(|e| anyhow!("Invalid upload URL: {raw_upload_url}, {e}"))?;
6668

@@ -94,6 +96,7 @@ fn build_orchestrator_config(
9496
pub async fn run(
9597
args: ExecArgs,
9698
api_client: &mut CodSpeedAPIClient,
99+
resolved_profile: &ResolvedProfileConfig,
97100
project_config: Option<&ProjectConfig>,
98101
setup_cache_dir: Option<&Path>,
99102
) -> Result<()> {
@@ -108,6 +111,7 @@ pub async fn run(
108111
merged_args,
109112
target,
110113
PollResultsOptions::new(false, base_run_id),
114+
resolved_profile,
111115
)?;
112116

113117
execute_config(config, api_client, setup_cache_dir).await

src/cli/mod.rs

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod auth;
22
pub(crate) mod exec;
33
pub(crate) mod experimental;
4+
mod profile;
45
pub(crate) mod run;
56
pub(crate) mod samply;
67
mod setup;
@@ -16,7 +17,7 @@ use std::path::PathBuf;
1617

1718
use crate::{
1819
api_client::CodSpeedAPIClient,
19-
config::CodSpeedConfig,
20+
config::{CodSpeedConfig, ResolvedProfileConfig},
2021
executor::helpers::command::CommandBuilder,
2122
local_logger::{CODSPEED_U8_COLOR_CODE, init_local_logger},
2223
prelude::*,
@@ -41,14 +42,8 @@ fn create_styles() -> Styles {
4142
#[command(version, about = "The CodSpeed CLI tool", styles = create_styles())]
4243
pub struct Cli {
4344
/// The URL of the CodSpeed GraphQL API
44-
#[arg(
45-
long,
46-
env = "CODSPEED_API_URL",
47-
global = true,
48-
hide = true,
49-
default_value = "https://gql.codspeed.io/"
50-
)]
51-
pub api_url: String,
45+
#[arg(long, env = "CODSPEED_API_URL", global = true, hide = true)]
46+
pub api_url: Option<String>,
5247

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

58+
/// The CodSpeed profile to use
59+
#[arg(long, env = "CODSPEED_PROFILE", global = true)]
60+
pub profile: Option<String>,
61+
6362
/// Path to project configuration file (codspeed.yaml)
6463
/// If provided, loads config from this path. Otherwise, searches for config files
6564
/// in the current directory and upward to the git root.
@@ -88,6 +87,8 @@ enum Commands {
8887
Exec(Box<exec::ExecArgs>),
8988
/// Manage the CLI authentication state
9089
Auth(auth::AuthArgs),
90+
/// Manage CodSpeed profiles
91+
Profile(profile::ProfileArgs),
9192
/// Pre-install the codspeed executors
9293
Setup(setup::SetupArgs),
9394
/// Show the overall status of CodSpeed (authentication, tools, system)
@@ -130,7 +131,8 @@ impl InternalCommands {
130131

131132
pub async fn run() -> Result<()> {
132133
let cli = Cli::parse();
133-
let mut api_client = build_api_client(&cli)?;
134+
let resolved_profile = resolve_profile_config(&cli)?;
135+
let mut api_client = build_api_client(&cli, &resolved_profile);
134136

135137
// Discover project configuration file
136138
let discovered_config = DiscoveredProjectConfig::discover_and_load(
@@ -158,6 +160,7 @@ pub async fn run() -> Result<()> {
158160
run::run(
159161
*args,
160162
&mut api_client,
163+
&resolved_profile,
161164
discovered_config.as_ref(),
162165
setup_cache_dir,
163166
)
@@ -168,14 +171,33 @@ pub async fn run() -> Result<()> {
168171
exec::run(
169172
*args,
170173
&mut api_client,
174+
&resolved_profile,
171175
discovered_config.as_ref().map(|d| &d.config),
172176
setup_cache_dir,
173177
)
174178
.await?
175179
}
176-
Commands::Auth(args) => auth::run(args, &api_client, cli.config_name.as_deref()).await?,
180+
Commands::Auth(args) => {
181+
auth::run(
182+
args,
183+
&api_client,
184+
cli.config_name.as_deref(),
185+
&resolved_profile.name,
186+
)
187+
.await?
188+
}
189+
Commands::Profile(args) => {
190+
profile::run(args, cli.config_name.as_deref(), cli.profile.as_deref())?
191+
}
177192
Commands::Setup(args) => setup::run(args, setup_cache_dir).await?,
178-
Commands::Status => status::run(&api_client).await?,
193+
Commands::Status => {
194+
status::run(
195+
&api_client,
196+
cli.config_name.as_deref(),
197+
&resolved_profile.name,
198+
)
199+
.await?
200+
}
179201
Commands::Use(args) => use_mode::run(args)?,
180202
Commands::Show => show::run()?,
181203
Commands::Update => update::run().await?,
@@ -193,28 +215,35 @@ pub async fn run() -> Result<()> {
193215
/// Priority (most specific first):
194216
/// 1. `--token` / `CODSPEED_TOKEN` — run/exec-level override
195217
/// 2. `--oauth-token` / `CODSPEED_OAUTH_TOKEN` and the persisted CLI
196-
/// token — both live on disk and are loaded together by
197-
/// [`CodSpeedConfig::load_with_override`].
198-
///
199-
/// The CLI config file is only read when no explicit token was passed,
200-
/// so an invocation like `codspeed run --token <X>` never touches the
201-
/// user's `~/.config/codspeed/`.
202-
fn build_api_client(cli: &Cli) -> Result<CodSpeedAPIClient> {
218+
/// token from the selected profile.
219+
fn resolve_profile_config(cli: &Cli) -> Result<ResolvedProfileConfig> {
220+
let config = CodSpeedConfig::load_with_override(cli.config_name.as_deref(), None)?;
221+
if matches!(&cli.command, Commands::Auth(_) | Commands::Profile(_)) {
222+
Ok(config.resolve_profile_or_default(
223+
cli.profile.as_deref(),
224+
cli.oauth_token.as_deref(),
225+
cli.api_url.as_deref(),
226+
None,
227+
))
228+
} else {
229+
config.resolve_profile(
230+
cli.profile.as_deref(),
231+
cli.oauth_token.as_deref(),
232+
cli.api_url.as_deref(),
233+
None,
234+
)
235+
}
236+
}
237+
238+
fn build_api_client(cli: &Cli, resolved_profile: &ResolvedProfileConfig) -> CodSpeedAPIClient {
203239
let explicit = match &cli.command {
204240
Commands::Run(args) => args.shared.token.clone(),
205241
Commands::Exec(args) => args.shared.token.clone(),
206242
_ => None,
207243
};
208244
let token = match explicit {
209245
Some(token) => Some(token),
210-
None => {
211-
CodSpeedConfig::load_with_override(
212-
cli.config_name.as_deref(),
213-
cli.oauth_token.as_deref(),
214-
)?
215-
.auth
216-
.token
217-
}
246+
None => resolved_profile.auth.token.clone(),
218247
};
219-
Ok(CodSpeedAPIClient::new(token, cli.api_url.clone()))
248+
CodSpeedAPIClient::new(token, resolved_profile.api_url.clone())
220249
}

src/cli/profile.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use crate::config::CodSpeedConfig;
2+
use crate::prelude::*;
3+
use clap::{Args, Subcommand};
4+
use console::style;
5+
6+
#[derive(Debug, Args)]
7+
pub struct ProfileArgs {
8+
#[command(subcommand)]
9+
command: ProfileCommands,
10+
}
11+
12+
#[derive(Debug, Subcommand)]
13+
enum ProfileCommands {
14+
/// List configured profiles
15+
List,
16+
/// Show a profile
17+
Show {
18+
/// Profile name. Defaults to the configured default profile.
19+
name: Option<String>,
20+
},
21+
/// Set profile URLs, creating the profile if it does not exist
22+
Set {
23+
/// Profile name
24+
name: String,
25+
/// The URL of the CodSpeed GraphQL API
26+
#[arg(long)]
27+
api_url: Option<String>,
28+
/// The URL to use for uploading results
29+
#[arg(long)]
30+
upload_url: Option<String>,
31+
},
32+
/// Use this profile by default for future commands
33+
Use {
34+
/// Profile name
35+
name: String,
36+
},
37+
}
38+
39+
pub fn run(
40+
args: ProfileArgs,
41+
config_name: Option<&str>,
42+
selected_profile: Option<&str>,
43+
) -> Result<()> {
44+
match args.command {
45+
ProfileCommands::List => list(config_name),
46+
ProfileCommands::Show { name } => show(config_name, name.as_deref().or(selected_profile)),
47+
ProfileCommands::Set {
48+
name,
49+
api_url,
50+
upload_url,
51+
} => set(config_name, &name, api_url, upload_url),
52+
ProfileCommands::Use { name } => use_profile(config_name, &name),
53+
}
54+
}
55+
56+
fn list(config_name: Option<&str>) -> Result<()> {
57+
let config = CodSpeedConfig::load_with_override(config_name, None)?;
58+
59+
info!("{}", style("Profiles").bold());
60+
for name in config.profiles.keys() {
61+
let marker = if name == &config.default_profile {
62+
"*"
63+
} else {
64+
" "
65+
};
66+
info!(" {marker} {name}");
67+
}
68+
69+
Ok(())
70+
}
71+
72+
fn show(config_name: Option<&str>, profile_name: Option<&str>) -> Result<()> {
73+
let config = CodSpeedConfig::load_with_override(config_name, None)?;
74+
let name = profile_name.unwrap_or(&config.default_profile);
75+
let resolved = config.resolve_profile(Some(name), None, None, None)?;
76+
let profile = config.profile(name).ok_or_else(|| {
77+
anyhow!("CodSpeed profile `{name}` does not exist. Run `codspeed profile set {name}` to create it.")
78+
})?;
79+
80+
info!("{} ({})", style("Profile").bold(), resolved.name);
81+
info!(" api url: {}", resolved.api_url);
82+
info!(" upload url: {}", resolved.upload_url);
83+
info!(
84+
" authenticated: {}",
85+
if profile.auth.token.is_some() {
86+
"yes"
87+
} else {
88+
"no"
89+
}
90+
);
91+
92+
Ok(())
93+
}
94+
95+
fn set(
96+
config_name: Option<&str>,
97+
profile_name: &str,
98+
api_url: Option<String>,
99+
upload_url: Option<String>,
100+
) -> Result<()> {
101+
let mut config = CodSpeedConfig::load_with_override(config_name, None)?;
102+
let profile = config.profile_mut(profile_name);
103+
104+
if let Some(api_url) = api_url {
105+
profile.api_url = Some(api_url);
106+
}
107+
if let Some(upload_url) = upload_url {
108+
profile.upload_url = Some(upload_url);
109+
}
110+
111+
config.persist(config_name)?;
112+
info!("Profile `{profile_name}` saved");
113+
114+
Ok(())
115+
}
116+
117+
fn use_profile(config_name: Option<&str>, profile_name: &str) -> Result<()> {
118+
let mut config = CodSpeedConfig::load_with_override(config_name, None)?;
119+
ensure!(
120+
config.profile(profile_name).is_some(),
121+
"CodSpeed profile `{profile_name}` does not exist. Run `codspeed profile set {profile_name}` to create it."
122+
);
123+
124+
config.default_profile = profile_name.to_owned();
125+
config.persist(config_name)?;
126+
info!("Default CodSpeed profile set to `{profile_name}`");
127+
128+
Ok(())
129+
}

0 commit comments

Comments
 (0)