diff --git a/.github/workflows/cli-lint.yml b/.github/workflows/cli-lint.yml new file mode 100644 index 000000000..6e307c337 --- /dev/null +++ b/.github/workflows/cli-lint.yml @@ -0,0 +1,77 @@ +name: CLI Lint + +on: + push: + branches: + - main + paths: + - 'svix-cli/**' + - '.github/workflows/cli-lint.yml' + pull_request: + paths: + - 'svix-cli/**' + - '.github/workflows/cli-lint.yml' + - 'openapi.json' + +# When pushing to a PR, cancel any jobs still running for the previous head commit of the PR +concurrency: + # head_ref is only defined for pull requests, run_id is always unique and defined so if this + # workflow was not triggered by a pull request, nothing gets cancelled. + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + check-fmt: + name: Check formatting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + + - name: rustfmt + run: cargo fmt -- --check + working-directory: svix-cli + + test-versions: + name: CLI Lint + runs-on: ubuntu-24.04 + strategy: + matrix: + rust: [stable, beta] + steps: + - uses: actions/checkout@v4 + + - name: Regen openapi libs + run: | + yarn + ./regen_openapi.sh + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: clippy + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "svix-cli -> target" + # only save the cache on the main branch + # cf https://github.com/Swatinem/rust-cache/issues/95 + save-if: ${{ github.ref == 'refs/heads/main' }} + # include relevant information in the cache name + prefix-key: "cli-${{ matrix.rust }}" + + - uses: taiki-e/install-action@nextest + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + working-directory: svix-cli + + - name: Run tests + run: cargo nextest run + working-directory: svix-cli diff --git a/.github/workflows/cli-security.yml b/.github/workflows/cli-security.yml new file mode 100644 index 000000000..cce85dcec --- /dev/null +++ b/.github/workflows/cli-security.yml @@ -0,0 +1,24 @@ +name: CLI Security + +on: + push: + branches: + - main + paths: + - 'svix-cli/**/Cargo.toml' + - 'svix-cli/**/Cargo.lock' + - '.github/workflows/cli-security.yml' + pull_request: + paths: + - 'rust/**/Cargo.toml' + - 'rust/**/Cargo.lock' + - '.github/workflows/cli-security.yml' + +jobs: + security_audit: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + manifest-path: svix-cli/Cargo.toml diff --git a/svix-cli/.rustfmt.toml b/svix-cli/.rustfmt.toml new file mode 100644 index 000000000..455c8209c --- /dev/null +++ b/svix-cli/.rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity = "Crate" +group_imports = "StdExternalCrate" diff --git a/svix-cli/Cargo.lock b/svix-cli/Cargo.lock index 88291cb40..1d197d50e 100644 --- a/svix-cli/Cargo.lock +++ b/svix-cli/Cargo.lock @@ -1643,7 +1643,7 @@ dependencies = [ [[package]] name = "svix-cli" -version = "0.1.0" +version = "1.44.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/svix-cli/src/cli_types/application.rs b/svix-cli/src/cli_types/application.rs index 20232319a..e546603f8 100644 --- a/svix-cli/src/cli_types/application.rs +++ b/svix-cli/src/cli_types/application.rs @@ -1,6 +1,5 @@ use clap::Args; -use svix::api; -use svix::api::Ordering; +use svix::{api, api::Ordering}; #[derive(Args, Clone)] pub struct ApplicationListOptions { diff --git a/svix-cli/src/cli_types/endpoint.rs b/svix-cli/src/cli_types/endpoint.rs index 20a6ceec5..6a6e98bec 100644 --- a/svix-cli/src/cli_types/endpoint.rs +++ b/svix-cli/src/cli_types/endpoint.rs @@ -1,7 +1,6 @@ use chrono::{DateTime, Utc}; use clap::Args; -use svix::api; -use svix::api::Ordering; +use svix::{api, api::Ordering}; #[derive(Args, Clone)] pub struct EndpointListOptions { diff --git a/svix-cli/src/cli_types/event_type.rs b/svix-cli/src/cli_types/event_type.rs index 6d5d11a71..b8e94133c 100644 --- a/svix-cli/src/cli_types/event_type.rs +++ b/svix-cli/src/cli_types/event_type.rs @@ -1,6 +1,5 @@ use clap::Args; -use svix::api; -use svix::api::Ordering; +use svix::{api, api::Ordering}; #[derive(Args, Clone)] pub struct EventTypeListOptions { diff --git a/svix-cli/src/cli_types/integration.rs b/svix-cli/src/cli_types/integration.rs index 47cd5004d..de14f83b4 100644 --- a/svix-cli/src/cli_types/integration.rs +++ b/svix-cli/src/cli_types/integration.rs @@ -1,6 +1,5 @@ use clap::Args; -use svix::api; -use svix::api::Ordering; +use svix::{api, api::Ordering}; #[derive(Args, Clone)] pub struct IntegrationListOptions { diff --git a/svix-cli/src/cli_types/message_attempt.rs b/svix-cli/src/cli_types/message_attempt.rs index df9375302..a2592ab26 100644 --- a/svix-cli/src/cli_types/message_attempt.rs +++ b/svix-cli/src/cli_types/message_attempt.rs @@ -1,7 +1,9 @@ use chrono::{DateTime, Utc}; use clap::Args; -use svix::api; -use svix::api::{MessageStatus, StatusCodeClass}; +use svix::{ + api, + api::{MessageStatus, StatusCodeClass}, +}; #[derive(Args, Clone)] pub struct MessageAttemptListByEndpointOptions { diff --git a/svix-cli/src/cmds/api/application.rs b/svix-cli/src/cmds/api/application.rs index a7347282d..a7f6e6dfd 100644 --- a/svix-cli/src/cmds/api/application.rs +++ b/svix-cli/src/cmds/api/application.rs @@ -1,10 +1,12 @@ -use crate::cli_types::application::ApplicationListOptions; -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::{ApplicationIn, ApplicationPatch}; +use crate::{ + cli_types::{application::ApplicationListOptions, PostOptions}, + json::JsonOf, +}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/authentication.rs b/svix-cli/src/cmds/api/authentication.rs index f746bdce6..b84f29c3e 100644 --- a/svix-cli/src/cmds/api/authentication.rs +++ b/svix-cli/src/cmds/api/authentication.rs @@ -1,9 +1,9 @@ -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::{AppPortalAccessIn, ApplicationTokenExpireIn}; +use crate::{cli_types::PostOptions, json::JsonOf}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/endpoint.rs b/svix-cli/src/cmds/api/endpoint.rs index 07eeb3433..5a6c6f161 100644 --- a/svix-cli/src/cmds/api/endpoint.rs +++ b/svix-cli/src/cmds/api/endpoint.rs @@ -1,6 +1,3 @@ -use crate::cli_types::endpoint::{EndpointListOptions, EndpointStatsOptions}; -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::{ @@ -8,6 +5,14 @@ use svix::api::{ EndpointTransformationIn, EndpointUpdate, EventExampleIn, RecoverIn, ReplayIn, }; +use crate::{ + cli_types::{ + endpoint::{EndpointListOptions, EndpointStatsOptions}, + PostOptions, + }, + json::JsonOf, +}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/event_type.rs b/svix-cli/src/cmds/api/event_type.rs index cebb546cb..886981ef2 100644 --- a/svix-cli/src/cmds/api/event_type.rs +++ b/svix-cli/src/cmds/api/event_type.rs @@ -1,10 +1,12 @@ -use crate::cli_types::event_type::EventTypeListOptions; -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::{EventTypeImportOpenApiIn, EventTypeIn, EventTypePatch, EventTypeUpdate}; +use crate::{ + cli_types::{event_type::EventTypeListOptions, PostOptions}, + json::JsonOf, +}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/integration.rs b/svix-cli/src/cmds/api/integration.rs index 3b8f902db..f9b0b89cc 100644 --- a/svix-cli/src/cmds/api/integration.rs +++ b/svix-cli/src/cmds/api/integration.rs @@ -1,10 +1,12 @@ -use crate::cli_types::integration::IntegrationListOptions; -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::{IntegrationIn, IntegrationUpdate}; +use crate::{ + cli_types::{integration::IntegrationListOptions, PostOptions}, + json::JsonOf, +}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/message.rs b/svix-cli/src/cmds/api/message.rs index b58431e20..521534d06 100644 --- a/svix-cli/src/cmds/api/message.rs +++ b/svix-cli/src/cmds/api/message.rs @@ -1,10 +1,12 @@ -use crate::cli_types::message::MessageListOptions; -use crate::cli_types::PostOptions; -use crate::json::JsonOf; use clap::{Args, Subcommand}; use colored_json::ColorMode; use svix::api::MessageIn; +use crate::{ + cli_types::{message::MessageListOptions, PostOptions}, + json::JsonOf, +}; + #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] diff --git a/svix-cli/src/cmds/api/message_attempt.rs b/svix-cli/src/cmds/api/message_attempt.rs index e7f7fc57b..decd9d48a 100644 --- a/svix-cli/src/cmds/api/message_attempt.rs +++ b/svix-cli/src/cmds/api/message_attempt.rs @@ -1,9 +1,10 @@ +use clap::{Args, Subcommand}; +use colored_json::ColorMode; + use crate::cli_types::message_attempt::{ MessageAttemptListAttemptedDestinationsOptions, MessageAttemptListAttemptedMessagesOptions, MessageAttemptListByEndpointOptions, MessageAttemptListByMsgOptions, }; -use clap::{Args, Subcommand}; -use colored_json::ColorMode; #[derive(Args)] #[command(args_conflicts_with_subcommands = true)] diff --git a/svix-cli/src/cmds/completion.rs b/svix-cli/src/cmds/completion.rs index 51393ff84..04519ce53 100644 --- a/svix-cli/src/cmds/completion.rs +++ b/svix-cli/src/cmds/completion.rs @@ -1,8 +1,8 @@ -use crate::BIN_NAME; use anyhow::Result; use clap::CommandFactory; -use clap_complete::shells; -use clap_complete::{generate as generate_, Shell}; +use clap_complete::{generate as generate_, shells, Shell}; + +use crate::BIN_NAME; pub fn generate(shell: &Shell) -> Result<()> { let mut writer = std::io::stdout().lock(); diff --git a/svix-cli/src/cmds/listen.rs b/svix-cli/src/cmds/listen.rs index 8d1f5ea32..b0f821b88 100644 --- a/svix-cli/src/cmds/listen.rs +++ b/svix-cli/src/cmds/listen.rs @@ -1,7 +1,8 @@ -use crate::config::{get_config_file_path, Config}; use anyhow::{Context, Result}; use clap::Args; +use crate::config::{get_config_file_path, Config}; + #[derive(Args)] pub struct ListenArgs { /// The local URL to forward webhooks to diff --git a/svix-cli/src/cmds/login.rs b/svix-cli/src/cmds/login.rs index 2bb0ad5b9..8aef74a30 100644 --- a/svix-cli/src/cmds/login.rs +++ b/svix-cli/src/cmds/login.rs @@ -1,8 +1,8 @@ -use crate::config; -use crate::config::Config; use anyhow::Result; use dialoguer::Input; +use crate::{config, config::Config}; + pub fn prompt() -> Result<()> { print!("Welcome to the Svix CLI!\n\n"); diff --git a/svix-cli/src/config.rs b/svix-cli/src/config.rs index 7d46c3014..36ca80126 100644 --- a/svix-cli/src/config.rs +++ b/svix-cli/src/config.rs @@ -1,12 +1,15 @@ +use std::{ + io::Write, + os::unix::fs::OpenOptionsExt, + path::{Path, PathBuf}, +}; + use anyhow::Result; use figment::{ providers::{Env, Format, Toml}, Figment, }; use serde::{Deserialize, Serialize}; -use std::io::Write; -use std::os::unix::fs::OpenOptionsExt; -use std::path::{Path, PathBuf}; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Config { diff --git a/svix-cli/src/json.rs b/svix-cli/src/json.rs index 32f0491ec..0086a2dc3 100644 --- a/svix-cli/src/json.rs +++ b/svix-cli/src/json.rs @@ -1,8 +1,8 @@ +use std::str::FromStr; + use anyhow::{Error, Result}; use colored_json::{Color, ColorMode, ToColoredJson}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::str::FromStr; +use serde::{de::DeserializeOwned, Serialize}; #[derive(Clone, Debug, PartialEq)] pub struct JsonOf(T); diff --git a/svix-cli/src/main.rs b/svix-cli/src/main.rs index 000a39ebe..d491db575 100644 --- a/svix-cli/src/main.rs +++ b/svix-cli/src/main.rs @@ -1,13 +1,3 @@ -use crate::cmds::api::authentication::AuthenticationArgs; -use crate::cmds::api::endpoint::EndpointArgs; -use crate::cmds::api::event_type::EventTypeArgs; -use crate::cmds::api::integration::IntegrationArgs; -use crate::cmds::api::message::MessageArgs; -use crate::cmds::api::message_attempt::MessageAttemptArgs; -use crate::cmds::listen::ListenArgs; -use crate::cmds::open::OpenArgs; -use crate::cmds::signature::SignatureArgs; -use crate::config::Config; use anyhow::Result; use clap::{Parser, Subcommand}; use clap_complete::Shell; @@ -15,6 +5,20 @@ use cmds::api::application::ApplicationArgs; use colored_json::{ColorMode, Output}; use concolor_clap::{Color, ColorChoice}; +use crate::{ + cmds::{ + api::{ + authentication::AuthenticationArgs, endpoint::EndpointArgs, event_type::EventTypeArgs, + integration::IntegrationArgs, message::MessageArgs, + message_attempt::MessageAttemptArgs, + }, + listen::ListenArgs, + open::OpenArgs, + signature::SignatureArgs, + }, + config::Config, +}; + mod cli_types; mod cmds; mod config; diff --git a/svix-cli/src/relay/message.rs b/svix-cli/src/relay/message.rs index 09bb43e85..ac5f6b05e 100644 --- a/svix-cli/src/relay/message.rs +++ b/svix-cli/src/relay/message.rs @@ -8,9 +8,10 @@ //! The main difference between the two is the `-In` has an HTTP method on it (needed so we can //! recreate the request properly to the local server, whereas the `-Out` has a status code. -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + pub const VERSION: u16 = 1; #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/svix-cli/src/relay/mod.rs b/svix-cli/src/relay/mod.rs index 60fe6c92e..6e8641228 100644 --- a/svix-cli/src/relay/mod.rs +++ b/svix-cli/src/relay/mod.rs @@ -1,24 +1,36 @@ -use crate::relay::message::{MessageOut, MessageOutEvent, MessageOutStart}; -use crate::relay::token::generate_token; +use std::{ + collections::HashMap, + fmt::{Debug, Display, Formatter}, + time::Duration, +}; + use anyhow::{Context, Result}; -use futures_util::stream::{SplitSink, SplitStream}; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{ + stream::{SplitSink, SplitStream}, + SinkExt, StreamExt, +}; use http::{HeaderMap, HeaderName, HeaderValue}; use indoc::printdoc; use message::{MessageIn, MessageInEvent}; -use std::collections::HashMap; -use std::fmt::{Debug, Display, Formatter}; -use std::time::Duration; -use tokio::net::TcpStream; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::task::JoinSet; -use tokio::time::Instant; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Policy; -use tokio_tungstenite::tungstenite::protocol::CloseFrame; -use tokio_tungstenite::tungstenite::{Bytes, Utf8Bytes}; +use tokio::{ + net::TcpStream, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinSet, + time::Instant, +}; use tokio_tungstenite::{ - connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream, + connect_async, + tungstenite::{ + client::IntoClientRequest, + protocol::{frame::coding::CloseCode::Policy, CloseFrame, Message}, + Bytes, Utf8Bytes, + }, + MaybeTlsStream, WebSocketStream, +}; + +use crate::relay::{ + message::{MessageOut, MessageOutEvent, MessageOutStart}, + token::generate_token, }; mod message;