[O] UX
This commit is contained in:
@@ -52,6 +52,8 @@ refray config
|
|||||||
<details><summary>Example Config</summary>
|
<details><summary>Example Config</summary>
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
jobs = 8
|
||||||
|
|
||||||
[[sites]]
|
[[sites]]
|
||||||
name = "github"
|
name = "github"
|
||||||
provider = "github"
|
provider = "github"
|
||||||
@@ -123,10 +125,10 @@ Retry only repositories that failed during the previous non-dry-run sync:
|
|||||||
refray sync --retry-failed
|
refray sync --retry-failed
|
||||||
```
|
```
|
||||||
|
|
||||||
Control repo-level parallelism:
|
Control parallelism for sync, serve, and webhook commands in config:
|
||||||
|
|
||||||
```sh
|
```toml
|
||||||
refray sync --jobs 8
|
jobs = 8
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -139,18 +141,17 @@ You can run `refray` as a service that listens for webhook events and runs full
|
|||||||
> If you want to use webhooks, you need to expose port 8787 to a public URL that can be accessed by the git provider.<br>
|
> If you want to use webhooks, you need to expose port 8787 to a public URL that can be accessed by the git provider.<br>
|
||||||
> This can be done using e.g. port forwarding, reverse proxy, cloudflare tunnel, or tailscale funnel.
|
> This can be done using e.g. port forwarding, reverse proxy, cloudflare tunnel, or tailscale funnel.
|
||||||
|
|
||||||
Start the service:
|
Start the service (to sync on push and also do full sync periodically):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
refray serve
|
refray serve
|
||||||
```
|
```
|
||||||
|
|
||||||
Install webhooks on all repos:
|
Install webhooks on all repos (with the URL in config):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
refray webhook install
|
refray webhook install
|
||||||
```
|
```
|
||||||
Webhook install uses `[webhook].url` and `[webhook].secret` from your config.
|
|
||||||
|
|
||||||
To uninstall webhooks previously installed by `refray`:
|
To uninstall webhooks previously installed by `refray`:
|
||||||
|
|
||||||
@@ -161,20 +162,18 @@ To uninstall webhooks previously installed by `refray`:
|
|||||||
refray webhook uninstall
|
refray webhook uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default, uninstall uses `[webhook].url` from your config. To remove hooks for a previous URL, pass it explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
refray webhook uninstall https://old.example.com/webhook
|
||||||
|
```
|
||||||
|
|
||||||
To move installed hooks to a new public URL, use `webhook update`. It removes hooks matching the current configured `[webhook].url`, installs the new URL, updates `[webhook].url` in the config, and refreshes local webhook state:
|
To move installed hooks to a new public URL, use `webhook update`. It removes hooks matching the current configured `[webhook].url`, installs the new URL, updates `[webhook].url` in the config, and refreshes local webhook state:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
refray webhook update https://new.example.com/webhook
|
refray webhook update https://new.example.com/webhook
|
||||||
```
|
```
|
||||||
|
|
||||||
Serve can also run periodic full syncs. The interval can be configured in `[webhook].full_sync_interval_minutes` or overridden at startup:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
refray serve --full-sync-interval-minutes 30
|
|
||||||
```
|
|
||||||
|
|
||||||
If `[webhook].reachability_check_interval_minutes` is configured, `serve` periodically checks that the public webhook URL is still reachable and logs a warning when it is not.
|
|
||||||
|
|
||||||
## Sync Semantics
|
## Sync Semantics
|
||||||
|
|
||||||
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
|
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
|
||||||
|
|||||||
+26
-1
@@ -9,9 +9,12 @@ use regex::Regex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const APP_NAME: &str = "refray";
|
const APP_NAME: &str = "refray";
|
||||||
|
pub const DEFAULT_JOBS: usize = 4;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default = "default_jobs", skip_serializing_if = "is_default_jobs")]
|
||||||
|
pub jobs: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sites: Vec<SiteConfig>,
|
pub sites: Vec<SiteConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -90,6 +93,17 @@ pub struct WebhookConfig {
|
|||||||
pub reachability_check_interval_minutes: Option<u64>,
|
pub reachability_check_interval_minutes: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
jobs: DEFAULT_JOBS,
|
||||||
|
sites: Vec::new(),
|
||||||
|
mirrors: Vec::new(),
|
||||||
|
webhook: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
pub struct EndpointConfig {
|
pub struct EndpointConfig {
|
||||||
pub site: String,
|
pub site: String,
|
||||||
@@ -176,6 +190,14 @@ fn default_true() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_jobs() -> usize {
|
||||||
|
DEFAULT_JOBS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default_jobs(jobs: &usize) -> bool {
|
||||||
|
*jobs == DEFAULT_JOBS
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(path: &Path) -> Result<Self> {
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
let contents = fs::read_to_string(path)
|
let contents = fs::read_to_string(path)
|
||||||
@@ -321,6 +343,9 @@ fn protect_file(_path: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_config(config: &Config) -> Result<()> {
|
pub fn validate_config(config: &Config) -> Result<()> {
|
||||||
|
if config.jobs == 0 {
|
||||||
|
bail!("jobs must be at least 1");
|
||||||
|
}
|
||||||
if config.sites.is_empty() {
|
if config.sites.is_empty() {
|
||||||
bail!("no sites configured");
|
bail!("no sites configured");
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-52
@@ -7,14 +7,13 @@ mod state;
|
|||||||
mod sync;
|
mod sync;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
use crate::config::{Config, default_config_path};
|
use crate::config::{Config, default_config_path};
|
||||||
use crate::sync::{DEFAULT_JOBS, SyncOptions, sync_all};
|
use crate::sync::{SyncOptions, sync_all};
|
||||||
use crate::webhook::{
|
use crate::webhook::{
|
||||||
ServeOptions, WebhookInstallOptions, WebhookUninstallOptions, WebhookUpdateOptions,
|
ServeOptions, WebhookInstallOptions, WebhookUninstallOptions, WebhookUpdateOptions,
|
||||||
install_webhooks, serve, uninstall_webhooks, update_webhooks,
|
install_webhooks, serve, uninstall_webhooks, update_webhooks,
|
||||||
@@ -60,24 +59,14 @@ struct SyncCommand {
|
|||||||
retry_failed: bool,
|
retry_failed: bool,
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
work_dir: Option<PathBuf>,
|
work_dir: Option<PathBuf>,
|
||||||
#[arg(long, default_value_t = DEFAULT_JOBS, value_name = "N")]
|
|
||||||
jobs: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct ServeCommand {
|
struct ServeCommand {
|
||||||
#[arg(long, default_value = "127.0.0.1:8787", value_name = "HOST:PORT")]
|
#[arg(long, default_value = "127.0.0.1:8787", value_name = "HOST:PORT")]
|
||||||
listen: String,
|
listen: String,
|
||||||
#[arg(long, conflicts_with = "secret_env")]
|
|
||||||
secret: Option<String>,
|
|
||||||
#[arg(long, value_name = "ENV", conflicts_with = "secret")]
|
|
||||||
secret_env: Option<String>,
|
|
||||||
#[arg(long, default_value_t = DEFAULT_JOBS, value_name = "N")]
|
|
||||||
jobs: usize,
|
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
work_dir: Option<PathBuf>,
|
work_dir: Option<PathBuf>,
|
||||||
#[arg(long, value_name = "MINUTES")]
|
|
||||||
full_sync_interval_minutes: Option<u64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -91,16 +80,14 @@ enum WebhookCommand {
|
|||||||
struct WebhookInstallCommand {
|
struct WebhookInstallCommand {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
#[arg(long, default_value_t = DEFAULT_JOBS, value_name = "N")]
|
|
||||||
jobs: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct WebhookUninstallCommand {
|
struct WebhookUninstallCommand {
|
||||||
|
#[arg(value_name = "URL", value_parser = parse_webhook_url)]
|
||||||
|
url: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
#[arg(long, default_value_t = DEFAULT_JOBS, value_name = "N")]
|
|
||||||
jobs: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
@@ -111,8 +98,6 @@ struct WebhookUpdateCommand {
|
|||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
work_dir: Option<PathBuf>,
|
work_dir: Option<PathBuf>,
|
||||||
#[arg(long, default_value_t = DEFAULT_JOBS, value_name = "N")]
|
|
||||||
jobs: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@@ -123,7 +108,13 @@ fn main() -> Result<()> {
|
|||||||
Command::Config => {
|
Command::Config => {
|
||||||
let outcome = interactive::run_config_wizard(&config_path)?;
|
let outcome = interactive::run_config_wizard(&config_path)?;
|
||||||
if outcome.run_full_sync_now {
|
if outcome.run_full_sync_now {
|
||||||
sync_all(&outcome.config, SyncOptions::default())
|
sync_all(
|
||||||
|
&outcome.config,
|
||||||
|
SyncOptions {
|
||||||
|
jobs: outcome.config.jobs,
|
||||||
|
..SyncOptions::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -140,30 +131,29 @@ fn main() -> Result<()> {
|
|||||||
repo_pattern: command.repo_pattern,
|
repo_pattern: command.repo_pattern,
|
||||||
retry_failed: command.retry_failed,
|
retry_failed: command.retry_failed,
|
||||||
work_dir: command.work_dir,
|
work_dir: command.work_dir,
|
||||||
jobs: command.jobs,
|
jobs: config.jobs,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Command::Serve(command) => {
|
Command::Serve(command) => {
|
||||||
let config = load_config(&config_path)?;
|
let config = load_config(&config_path)?;
|
||||||
let full_sync_interval_minutes = command.full_sync_interval_minutes.or_else(|| {
|
let full_sync_interval_minutes = config
|
||||||
config
|
.webhook
|
||||||
.webhook
|
.as_ref()
|
||||||
.as_ref()
|
.and_then(|webhook| webhook.full_sync_interval_minutes);
|
||||||
.and_then(|webhook| webhook.full_sync_interval_minutes)
|
|
||||||
});
|
|
||||||
let reachability_url = config.webhook.as_ref().map(|webhook| webhook.url.clone());
|
let reachability_url = config.webhook.as_ref().map(|webhook| webhook.url.clone());
|
||||||
let reachability_check_interval_minutes = config
|
let reachability_check_interval_minutes = config
|
||||||
.webhook
|
.webhook
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|webhook| webhook.reachability_check_interval_minutes);
|
.and_then(|webhook| webhook.reachability_check_interval_minutes);
|
||||||
let secret = resolve_webhook_secret(&config, command.secret, command.secret_env)?;
|
let secret = resolve_config_webhook_secret(&config)?;
|
||||||
|
let workers = config.jobs;
|
||||||
serve(
|
serve(
|
||||||
config,
|
config,
|
||||||
ServeOptions {
|
ServeOptions {
|
||||||
listen: command.listen,
|
listen: command.listen,
|
||||||
secret,
|
secret,
|
||||||
workers: command.jobs,
|
workers,
|
||||||
work_dir: command.work_dir,
|
work_dir: command.work_dir,
|
||||||
full_sync_interval_minutes,
|
full_sync_interval_minutes,
|
||||||
reachability_url,
|
reachability_url,
|
||||||
@@ -182,20 +172,20 @@ fn main() -> Result<()> {
|
|||||||
secret,
|
secret,
|
||||||
dry_run: command.dry_run,
|
dry_run: command.dry_run,
|
||||||
work_dir: None,
|
work_dir: None,
|
||||||
jobs: command.jobs,
|
jobs: config.jobs,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Command::Webhook(WebhookCommand::Uninstall(command)) => {
|
Command::Webhook(WebhookCommand::Uninstall(command)) => {
|
||||||
let config = load_config(&config_path)?;
|
let config = load_config(&config_path)?;
|
||||||
let url = resolve_config_webhook_url(&config)?;
|
let url = resolve_uninstall_webhook_url(&config, command.url)?;
|
||||||
uninstall_webhooks(
|
uninstall_webhooks(
|
||||||
&config,
|
&config,
|
||||||
WebhookUninstallOptions {
|
WebhookUninstallOptions {
|
||||||
url,
|
url,
|
||||||
dry_run: command.dry_run,
|
dry_run: command.dry_run,
|
||||||
work_dir: None,
|
work_dir: None,
|
||||||
jobs: command.jobs,
|
jobs: config.jobs,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -217,7 +207,7 @@ fn main() -> Result<()> {
|
|||||||
secret,
|
secret,
|
||||||
dry_run: command.dry_run,
|
dry_run: command.dry_run,
|
||||||
work_dir: command.work_dir,
|
work_dir: command.work_dir,
|
||||||
jobs: command.jobs,
|
jobs: config.jobs,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
if !command.dry_run {
|
if !command.dry_run {
|
||||||
@@ -246,29 +236,10 @@ fn resolve_config_webhook_secret(config: &Config) -> Result<String> {
|
|||||||
.map(|webhook| webhook.secret())
|
.map(|webhook| webhook.secret())
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow::anyhow!("configure [webhook].secret before running webhook commands")
|
anyhow::anyhow!("configure [webhook].secret before running serve or webhook commands")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_webhook_secret(
|
|
||||||
config: &Config,
|
|
||||||
value: Option<String>,
|
|
||||||
env_name: Option<String>,
|
|
||||||
) -> Result<String> {
|
|
||||||
match (value, env_name) {
|
|
||||||
(Some(value), None) => Ok(value),
|
|
||||||
(None, Some(env_name)) => env::var(&env_name)
|
|
||||||
.with_context(|| format!("environment variable {env_name} is not set")),
|
|
||||||
(None, None) => config
|
|
||||||
.webhook
|
|
||||||
.as_ref()
|
|
||||||
.map(|webhook| webhook.secret())
|
|
||||||
.transpose()?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("pass either --secret or --secret-env")),
|
|
||||||
(Some(_), Some(_)) => unreachable!("clap enforces secret conflicts"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_config_webhook_url(config: &Config) -> Result<String> {
|
fn resolve_config_webhook_url(config: &Config) -> Result<String> {
|
||||||
config
|
config
|
||||||
.webhook
|
.webhook
|
||||||
@@ -277,6 +248,24 @@ fn resolve_config_webhook_url(config: &Config) -> Result<String> {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("configure [webhook].url before running webhook commands"))
|
.ok_or_else(|| anyhow::anyhow!("configure [webhook].url before running webhook commands"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_uninstall_webhook_url(config: &Config, url: Option<String>) -> Result<String> {
|
||||||
|
match url {
|
||||||
|
Some(url) => Ok(url),
|
||||||
|
None => resolve_config_webhook_url(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_webhook_url(value: &str) -> std::result::Result<String, String> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Err("A value is required".to_string());
|
||||||
|
}
|
||||||
|
let url = url::Url::parse(value).map_err(|error| format!("Invalid URL: {error}"))?;
|
||||||
|
match url.scheme() {
|
||||||
|
"http" | "https" => Ok(value.to_string()),
|
||||||
|
_ => Err("URL must start with http:// or https://".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn set_config_webhook_url(config: &mut Config, url: String) {
|
fn set_config_webhook_url(config: &mut Config, url: String) {
|
||||||
let Some(webhook) = &mut config.webhook else {
|
let Some(webhook) = &mut config.webhook else {
|
||||||
unreachable!("caller verifies webhook config exists before saving update")
|
unreachable!("caller verifies webhook config exists before saving update")
|
||||||
|
|||||||
+2
-3
@@ -9,7 +9,7 @@ use console::style;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
Config, ConflictResolutionStrategy, EndpointConfig, MirrorConfig, RepoNameFilter,
|
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, RepoNameFilter,
|
||||||
SyncVisibility, default_work_dir, validate_config,
|
SyncVisibility, default_work_dir, validate_config,
|
||||||
};
|
};
|
||||||
use crate::git::{
|
use crate::git::{
|
||||||
@@ -34,7 +34,6 @@ use self::state::{
|
|||||||
save_failure_state, save_ref_state,
|
save_failure_state, save_ref_state,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DEFAULT_JOBS: usize = 4;
|
|
||||||
const CONFLICT_BRANCH_ROOT: &str = "refray/conflicts/";
|
const CONFLICT_BRANCH_ROOT: &str = "refray/conflicts/";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -67,7 +66,7 @@ impl Default for SyncOptions {
|
|||||||
pub fn sync_all(config: &Config, options: SyncOptions) -> Result<()> {
|
pub fn sync_all(config: &Config, options: SyncOptions) -> Result<()> {
|
||||||
validate_config(config)?;
|
validate_config(config)?;
|
||||||
if options.jobs == 0 {
|
if options.jobs == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
||||||
fs::create_dir_all(&work_dir)
|
fs::create_dir_all(&work_dir)
|
||||||
|
|||||||
+25
-9
@@ -77,7 +77,7 @@ struct JobQueue {
|
|||||||
pub fn serve(config: Config, options: ServeOptions) -> Result<()> {
|
pub fn serve(config: Config, options: ServeOptions) -> Result<()> {
|
||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
if options.workers == 0 {
|
if options.workers == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
let server = Server::http(&options.listen)
|
let server = Server::http(&options.listen)
|
||||||
.map_err(|error| anyhow::anyhow!("failed to listen on {}: {error}", options.listen))?;
|
.map_err(|error| anyhow::anyhow!("failed to listen on {}: {error}", options.listen))?;
|
||||||
@@ -174,7 +174,7 @@ fn reachability_timer_loop(url: String, minutes: u64) {
|
|||||||
pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Result<()> {
|
pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Result<()> {
|
||||||
validate_config(config)?;
|
validate_config(config)?;
|
||||||
if options.jobs == 0 {
|
if options.jobs == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
||||||
let state = Arc::new(Mutex::new(load_webhook_state(&work_dir)?));
|
let state = Arc::new(Mutex::new(load_webhook_state(&work_dir)?));
|
||||||
@@ -229,7 +229,7 @@ pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Resu
|
|||||||
pub fn uninstall_webhooks(config: &Config, options: WebhookUninstallOptions) -> Result<()> {
|
pub fn uninstall_webhooks(config: &Config, options: WebhookUninstallOptions) -> Result<()> {
|
||||||
validate_config(config)?;
|
validate_config(config)?;
|
||||||
if options.jobs == 0 {
|
if options.jobs == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
let work_dir = options.work_dir.clone().unwrap_or_else(default_work_dir);
|
||||||
let mut state = load_webhook_state(&work_dir)?;
|
let mut state = load_webhook_state(&work_dir)?;
|
||||||
@@ -267,10 +267,7 @@ pub fn uninstall_webhooks(config: &Config, options: WebhookUninstallOptions) ->
|
|||||||
let removed_keys = run_uninstall_tasks(tasks, options.jobs)?;
|
let removed_keys = run_uninstall_tasks(tasks, options.jobs)?;
|
||||||
|
|
||||||
if !options.dry_run {
|
if !options.dry_run {
|
||||||
for key in removed_keys {
|
remove_webhook_state_keys(&mut state, removed_keys, &options.url);
|
||||||
state.installations.remove(&key);
|
|
||||||
state.skipped.remove(&key);
|
|
||||||
}
|
|
||||||
save_webhook_state(&work_dir, &state)?;
|
save_webhook_state(&work_dir, &state)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -279,7 +276,7 @@ pub fn uninstall_webhooks(config: &Config, options: WebhookUninstallOptions) ->
|
|||||||
pub fn update_webhooks(config: &Config, options: WebhookUpdateOptions) -> Result<()> {
|
pub fn update_webhooks(config: &Config, options: WebhookUpdateOptions) -> Result<()> {
|
||||||
validate_config(config)?;
|
validate_config(config)?;
|
||||||
if options.jobs == 0 {
|
if options.jobs == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
if options.old_url != options.new_url {
|
if options.old_url != options.new_url {
|
||||||
crate::logln!(
|
crate::logln!(
|
||||||
@@ -325,7 +322,7 @@ pub fn ensure_configured_webhooks(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if jobs == 0 {
|
if jobs == 0 {
|
||||||
bail!("--jobs must be at least 1");
|
bail!("jobs must be at least 1");
|
||||||
}
|
}
|
||||||
let secret = webhook.secret()?;
|
let secret = webhook.secret()?;
|
||||||
let state = Arc::new(Mutex::new(load_webhook_state(work_dir)?));
|
let state = Arc::new(Mutex::new(load_webhook_state(work_dir)?));
|
||||||
@@ -696,6 +693,25 @@ fn record_webhook_installation(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_webhook_state_keys(state: &mut WebhookState, keys: Vec<String>, url: &str) {
|
||||||
|
for key in keys {
|
||||||
|
if state
|
||||||
|
.installations
|
||||||
|
.get(&key)
|
||||||
|
.is_some_and(|installation| installation.url == url)
|
||||||
|
{
|
||||||
|
state.installations.remove(&key);
|
||||||
|
}
|
||||||
|
if state
|
||||||
|
.skipped
|
||||||
|
.get(&key)
|
||||||
|
.is_some_and(|skipped| skipped.url == url)
|
||||||
|
{
|
||||||
|
state.skipped.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn uninstall_webhook_task(task: WebhookUninstallTask) -> Result<Option<String>> {
|
fn uninstall_webhook_task(task: WebhookUninstallTask) -> Result<Option<String>> {
|
||||||
let key = webhook_installation_key(&task.group, &task.endpoint, &task.repo.name);
|
let key = webhook_installation_key(&task.group, &task.endpoint, &task.repo.name);
|
||||||
crate::logln!(
|
crate::logln!(
|
||||||
|
|||||||
+4
-24
@@ -313,7 +313,7 @@ impl E2eRun {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let default_whitelist = format!("^{REPO_PREFIX}{}-", self.run_id);
|
let default_whitelist = format!("^{REPO_PREFIX}{}-", self.run_id);
|
||||||
let whitelist = repo_pattern.unwrap_or(&default_whitelist);
|
let whitelist = repo_pattern.unwrap_or(&default_whitelist);
|
||||||
let mut contents = String::new();
|
let mut contents = "jobs = 1\n\n".to_string();
|
||||||
for provider in &self.settings.providers {
|
for provider in &self.settings.providers {
|
||||||
contents.push_str(&format!(
|
contents.push_str(&format!(
|
||||||
r#"[[sites]]
|
r#"[[sites]]
|
||||||
@@ -658,15 +658,7 @@ namespace = "{}"
|
|||||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||||
let addr = listener.local_addr()?;
|
let addr = listener.local_addr()?;
|
||||||
drop(listener);
|
drop(listener);
|
||||||
let mut server = self.spawn_refray([
|
let mut server = self.spawn_refray(["serve", "--listen", &addr.to_string()])?;
|
||||||
"serve",
|
|
||||||
"--listen",
|
|
||||||
&addr.to_string(),
|
|
||||||
"--secret",
|
|
||||||
WEBHOOK_SECRET,
|
|
||||||
"--jobs",
|
|
||||||
"1",
|
|
||||||
])?;
|
|
||||||
thread::sleep(Duration::from_millis(750));
|
thread::sleep(Duration::from_millis(750));
|
||||||
|
|
||||||
let (body, headers) = source.webhook_payload(&repo, WEBHOOK_SECRET);
|
let (body, headers) = source.webhook_payload(&repo, WEBHOOK_SECRET);
|
||||||
@@ -794,25 +786,13 @@ namespace = "{}"
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sync<const N: usize>(&self, args: [&str; N]) -> Result<()> {
|
fn sync<const N: usize>(&self, args: [&str; N]) -> Result<()> {
|
||||||
let mut command = vec![
|
let mut command = vec!["sync", "--work-dir", self.work_dir.to_str().unwrap()];
|
||||||
"sync",
|
|
||||||
"--work-dir",
|
|
||||||
self.work_dir.to_str().unwrap(),
|
|
||||||
"--jobs",
|
|
||||||
"1",
|
|
||||||
];
|
|
||||||
command.extend(args);
|
command.extend(args);
|
||||||
self.refray(command)
|
self.refray(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_expect_failure<const N: usize>(&self, args: [&str; N]) -> Result<()> {
|
fn sync_expect_failure<const N: usize>(&self, args: [&str; N]) -> Result<()> {
|
||||||
let mut command = vec![
|
let mut command = vec!["sync", "--work-dir", self.work_dir.to_str().unwrap()];
|
||||||
"sync",
|
|
||||||
"--work-dir",
|
|
||||||
self.work_dir.to_str().unwrap(),
|
|
||||||
"--jobs",
|
|
||||||
"1",
|
|
||||||
];
|
|
||||||
command.extend(args);
|
command.extend(args);
|
||||||
let output = self.refray_output(command)?;
|
let output = self.refray_output(command)?;
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
|
|||||||
+89
-35
@@ -48,49 +48,56 @@ fn cli_accepts_sync_retry_failed() {
|
|||||||
assert!(args.retry_failed);
|
assert!(args.retry_failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_accepts_sync_jobs() {
|
|
||||||
let cli = Cli::try_parse_from(["refray", "sync", "--jobs", "8"]).unwrap();
|
|
||||||
|
|
||||||
let Command::Sync(args) = cli.command else {
|
|
||||||
panic!("parsed unexpected command");
|
|
||||||
};
|
|
||||||
assert_eq!(args.jobs, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_accepts_webhook_serve() {
|
fn cli_accepts_webhook_serve() {
|
||||||
let cli = Cli::try_parse_from([
|
let cli = Cli::try_parse_from(["refray", "serve", "--listen", "127.0.0.1:9000"]).unwrap();
|
||||||
"refray",
|
|
||||||
"serve",
|
|
||||||
"--listen",
|
|
||||||
"127.0.0.1:9000",
|
|
||||||
"--secret-env",
|
|
||||||
"WEBHOOK_SECRET",
|
|
||||||
"--jobs",
|
|
||||||
"2",
|
|
||||||
"--full-sync-interval-minutes",
|
|
||||||
"30",
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let Command::Serve(args) = cli.command else {
|
let Command::Serve(args) = cli.command else {
|
||||||
panic!("parsed unexpected command");
|
panic!("parsed unexpected command");
|
||||||
};
|
};
|
||||||
assert_eq!(args.listen, "127.0.0.1:9000");
|
assert_eq!(args.listen, "127.0.0.1:9000");
|
||||||
assert_eq!(args.secret_env, Some("WEBHOOK_SECRET".to_string()));
|
}
|
||||||
assert_eq!(args.jobs, 2);
|
|
||||||
assert_eq!(args.full_sync_interval_minutes, Some(30));
|
#[test]
|
||||||
|
fn cli_rejects_removed_jobs_args() {
|
||||||
|
for args in [
|
||||||
|
["refray", "sync", "--jobs", "8"].as_slice(),
|
||||||
|
["refray", "serve", "--jobs", "2"].as_slice(),
|
||||||
|
["refray", "webhook", "install", "--jobs", "6"].as_slice(),
|
||||||
|
["refray", "webhook", "uninstall", "--jobs", "3"].as_slice(),
|
||||||
|
[
|
||||||
|
"refray",
|
||||||
|
"webhook",
|
||||||
|
"update",
|
||||||
|
"https://new.example.test/webhook",
|
||||||
|
"--jobs",
|
||||||
|
"5",
|
||||||
|
]
|
||||||
|
.as_slice(),
|
||||||
|
] {
|
||||||
|
assert!(Cli::try_parse_from(args).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_rejects_removed_serve_args() {
|
||||||
|
for args in [
|
||||||
|
["refray", "serve", "--secret", "secret"].as_slice(),
|
||||||
|
["refray", "serve", "--secret-env", "WEBHOOK_SECRET"].as_slice(),
|
||||||
|
["refray", "serve", "--full-sync-interval-minutes", "30"].as_slice(),
|
||||||
|
] {
|
||||||
|
assert!(Cli::try_parse_from(args).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_accepts_webhook_install() {
|
fn cli_accepts_webhook_install() {
|
||||||
let cli = Cli::try_parse_from(["refray", "webhook", "install", "--jobs", "6"]).unwrap();
|
let cli = Cli::try_parse_from(["refray", "webhook", "install"]).unwrap();
|
||||||
|
|
||||||
let Command::Webhook(WebhookCommand::Install(args)) = cli.command else {
|
let Command::Webhook(WebhookCommand::Install(args)) = cli.command else {
|
||||||
panic!("parsed unexpected command");
|
panic!("parsed unexpected command");
|
||||||
};
|
};
|
||||||
assert_eq!(args.jobs, 6);
|
assert!(!args.dry_run);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -124,20 +131,37 @@ fn cli_rejects_removed_webhook_install_args() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_accepts_webhook_uninstall() {
|
fn cli_accepts_webhook_uninstall() {
|
||||||
let cli = Cli::try_parse_from(["refray", "webhook", "uninstall", "--dry-run", "--jobs", "3"])
|
let cli = Cli::try_parse_from(["refray", "webhook", "uninstall", "--dry-run"]).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let Command::Webhook(WebhookCommand::Uninstall(args)) = cli.command else {
|
let Command::Webhook(WebhookCommand::Uninstall(args)) = cli.command else {
|
||||||
panic!("parsed unexpected command");
|
panic!("parsed unexpected command");
|
||||||
};
|
};
|
||||||
|
assert_eq!(args.url, None);
|
||||||
assert!(args.dry_run);
|
assert!(args.dry_run);
|
||||||
assert_eq!(args.jobs, 3);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_accepts_webhook_uninstall_url() {
|
||||||
|
let cli = Cli::try_parse_from([
|
||||||
|
"refray",
|
||||||
|
"webhook",
|
||||||
|
"uninstall",
|
||||||
|
"https://old.example.test/webhook",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let Command::Webhook(WebhookCommand::Uninstall(args)) = cli.command else {
|
||||||
|
panic!("parsed unexpected command");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
args.url,
|
||||||
|
Some("https://old.example.test/webhook".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_rejects_removed_webhook_uninstall_args() {
|
fn cli_rejects_removed_webhook_uninstall_args() {
|
||||||
for args in [
|
for args in [
|
||||||
["refray", "webhook", "uninstall", "repo-one"].as_slice(),
|
|
||||||
[
|
[
|
||||||
"refray",
|
"refray",
|
||||||
"webhook",
|
"webhook",
|
||||||
@@ -160,6 +184,39 @@ fn cli_rejects_removed_webhook_uninstall_args() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_rejects_invalid_webhook_uninstall_url() {
|
||||||
|
for args in [
|
||||||
|
["refray", "webhook", "uninstall", "repo-one"].as_slice(),
|
||||||
|
[
|
||||||
|
"refray",
|
||||||
|
"webhook",
|
||||||
|
"uninstall",
|
||||||
|
"ftp://example.test/webhook",
|
||||||
|
]
|
||||||
|
.as_slice(),
|
||||||
|
] {
|
||||||
|
assert!(Cli::try_parse_from(args).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn webhook_uninstall_url_uses_arg_before_config() {
|
||||||
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
|
webhook: None,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = resolve_uninstall_webhook_url(
|
||||||
|
&config,
|
||||||
|
Some("https://old.example.test/webhook".to_string()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(url, "https://old.example.test/webhook");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_accepts_webhook_update() {
|
fn cli_accepts_webhook_update() {
|
||||||
let cli = Cli::try_parse_from([
|
let cli = Cli::try_parse_from([
|
||||||
@@ -167,8 +224,6 @@ fn cli_accepts_webhook_update() {
|
|||||||
"webhook",
|
"webhook",
|
||||||
"update",
|
"update",
|
||||||
"https://new.example.test/webhook",
|
"https://new.example.test/webhook",
|
||||||
"--jobs",
|
|
||||||
"5",
|
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -176,7 +231,6 @@ fn cli_accepts_webhook_update() {
|
|||||||
panic!("parsed unexpected command");
|
panic!("parsed unexpected command");
|
||||||
};
|
};
|
||||||
assert_eq!(args.url, "https://new.example.test/webhook");
|
assert_eq!(args.url, "https://new.example.test/webhook");
|
||||||
assert_eq!(args.jobs, 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use super::*;
|
|||||||
fn parses_token_forms() {
|
fn parses_token_forms() {
|
||||||
let config: Config = toml::from_str(
|
let config: Config = toml::from_str(
|
||||||
r#"
|
r#"
|
||||||
|
jobs = 8
|
||||||
|
|
||||||
[webhook]
|
[webhook]
|
||||||
install = true
|
install = true
|
||||||
url = "https://mirror.example.test/webhook"
|
url = "https://mirror.example.test/webhook"
|
||||||
@@ -40,6 +42,7 @@ fn parses_token_forms() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.jobs, 8);
|
||||||
assert_eq!(config.sites.len(), 1);
|
assert_eq!(config.sites.len(), 1);
|
||||||
assert_eq!(config.mirrors[0].endpoints.len(), 2);
|
assert_eq!(config.mirrors[0].endpoints.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -65,9 +68,17 @@ fn parses_token_forms() {
|
|||||||
assert_eq!(webhook.full_sync_interval_minutes, Some(60));
|
assert_eq!(webhook.full_sync_interval_minutes, Some(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_defaults_jobs() {
|
||||||
|
let config: Config = toml::from_str("").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.jobs, DEFAULT_JOBS);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![site("github", ProviderKind::Github)],
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
mirrors: vec![MirrorConfig {
|
mirrors: vec![MirrorConfig {
|
||||||
name: "broken".to_string(),
|
name: "broken".to_string(),
|
||||||
@@ -90,6 +101,7 @@ fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
|||||||
assert!(err.contains("at least two endpoints"));
|
assert!(err.contains("at least two endpoints"));
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![site("github", ProviderKind::Github)],
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
mirrors: vec![MirrorConfig {
|
mirrors: vec![MirrorConfig {
|
||||||
name: "broken".to_string(),
|
name: "broken".to_string(),
|
||||||
@@ -185,6 +197,7 @@ fn repo_name_filter_applies_whitelist_then_blacklist() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validation_rejects_invalid_repo_filter_regex() {
|
fn validation_rejects_invalid_repo_filter_regex() {
|
||||||
let mut config = Config {
|
let mut config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![site("github", ProviderKind::Github)],
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
mirrors: vec![mirror_config()],
|
mirrors: vec![mirror_config()],
|
||||||
webhook: None,
|
webhook: None,
|
||||||
@@ -196,6 +209,22 @@ fn validation_rejects_invalid_repo_filter_regex() {
|
|||||||
assert!(err.contains("invalid repo_whitelist regex"));
|
assert!(err.contains("invalid repo_whitelist regex"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_zero_jobs() {
|
||||||
|
let mut config = Config {
|
||||||
|
jobs: 0,
|
||||||
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
|
mirrors: vec![mirror_config()],
|
||||||
|
webhook: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = validate_config(&config).unwrap_err().to_string();
|
||||||
|
|
||||||
|
assert!(err.contains("jobs must be at least 1"));
|
||||||
|
config.jobs = DEFAULT_JOBS;
|
||||||
|
validate_config(&config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn mirror_config() -> MirrorConfig {
|
fn mirror_config() -> MirrorConfig {
|
||||||
MirrorConfig {
|
MirrorConfig {
|
||||||
name: "personal".to_string(),
|
name: "personal".to_string(),
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ fn wizard_can_enable_webhooks() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_reuses_existing_credentials_for_same_instance() {
|
fn wizard_reuses_existing_credentials_for_same_instance() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![SiteConfig {
|
sites: vec![SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
provider: ProviderKind::Github,
|
provider: ProviderKind::Github,
|
||||||
@@ -177,6 +178,7 @@ fn wizard_reuses_existing_credentials_for_same_instance() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_starts_existing_config_at_sync_group_menu() {
|
fn wizard_starts_existing_config_at_sync_group_menu() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
@@ -234,6 +236,7 @@ fn wizard_starts_existing_config_at_sync_group_menu() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_can_ask_to_run_full_sync_after_config() {
|
fn wizard_can_ask_to_run_full_sync_after_config() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: Vec::new(),
|
sites: Vec::new(),
|
||||||
mirrors: vec![MirrorConfig {
|
mirrors: vec![MirrorConfig {
|
||||||
name: "sync-1".to_string(),
|
name: "sync-1".to_string(),
|
||||||
@@ -274,6 +277,7 @@ fn wizard_skips_full_sync_prompt_without_sync_groups() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_edits_existing_sync_group_from_menu() {
|
fn wizard_edits_existing_sync_group_from_menu() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
@@ -371,6 +375,7 @@ fn wizard_edits_existing_sync_group_from_menu() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_prefills_existing_sync_group_when_editing() {
|
fn wizard_prefills_existing_sync_group_when_editing() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
@@ -435,6 +440,7 @@ fn wizard_prefills_existing_sync_group_when_editing() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_deletes_existing_sync_group_from_menu() {
|
fn wizard_deletes_existing_sync_group_from_menu() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
@@ -494,6 +500,7 @@ fn wizard_deletes_existing_sync_group_from_menu() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wizard_can_go_back_from_delete_menu() {
|
fn wizard_can_go_back_from_delete_menu() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: "github".to_string(),
|
name: "github".to_string(),
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fn parses_gitlab_push_payload() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn matches_jobs_by_provider_and_namespace() {
|
fn matches_jobs_by_provider_and_namespace() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![
|
sites: vec![
|
||||||
site("github", ProviderKind::Github),
|
site("github", ProviderKind::Github),
|
||||||
site("gitea", ProviderKind::Gitea),
|
site("gitea", ProviderKind::Gitea),
|
||||||
@@ -147,6 +148,7 @@ fn matching_jobs_respects_repo_name_filters() {
|
|||||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||||
};
|
};
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![site("github", ProviderKind::Github)],
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
mirrors: vec![mirror.clone()],
|
mirrors: vec![mirror.clone()],
|
||||||
webhook: None,
|
webhook: None,
|
||||||
@@ -161,6 +163,7 @@ fn matching_jobs_respects_repo_name_filters() {
|
|||||||
|
|
||||||
mirror.repo_whitelist.clear();
|
mirror.repo_whitelist.clear();
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
sites: vec![site("github", ProviderKind::Github)],
|
sites: vec![site("github", ProviderKind::Github)],
|
||||||
mirrors: vec![mirror],
|
mirrors: vec![mirror],
|
||||||
webhook: None,
|
webhook: None,
|
||||||
@@ -358,6 +361,50 @@ fn uninstall_task_removes_state_even_when_hook_is_missing() {
|
|||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uninstall_state_cleanup_only_removes_matching_url() {
|
||||||
|
let endpoint = endpoint("github", NamespaceKind::User, "alice");
|
||||||
|
let key = webhook_installation_key("sync-1", &endpoint, "repo");
|
||||||
|
let mut state = WebhookState::default();
|
||||||
|
state.installations.insert(
|
||||||
|
key.clone(),
|
||||||
|
WebhookInstallation {
|
||||||
|
group: "sync-1".to_string(),
|
||||||
|
endpoint: endpoint.clone(),
|
||||||
|
repo: "repo".to_string(),
|
||||||
|
url: "https://current.example.test/webhook".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
state.skipped.insert(
|
||||||
|
key.clone(),
|
||||||
|
SkippedWebhookInstallation {
|
||||||
|
group: "sync-1".to_string(),
|
||||||
|
endpoint,
|
||||||
|
repo: "repo".to_string(),
|
||||||
|
url: "https://current.example.test/webhook".to_string(),
|
||||||
|
reason: "provider blocked access".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_webhook_state_keys(
|
||||||
|
&mut state,
|
||||||
|
vec![key.clone()],
|
||||||
|
"https://old.example.test/webhook",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(state.installations.contains_key(&key));
|
||||||
|
assert!(state.skipped.contains_key(&key));
|
||||||
|
|
||||||
|
remove_webhook_state_keys(
|
||||||
|
&mut state,
|
||||||
|
vec![key.clone()],
|
||||||
|
"https://current.example.test/webhook",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!state.installations.contains_key(&key));
|
||||||
|
assert!(!state.skipped.contains_key(&key));
|
||||||
|
}
|
||||||
|
|
||||||
fn site(name: &str, provider: ProviderKind) -> SiteConfig {
|
fn site(name: &str, provider: ProviderKind) -> SiteConfig {
|
||||||
SiteConfig {
|
SiteConfig {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user