Skip to content

Commit 2522580

Browse files
committed
cli: Add {pre,post}-{build,test,deploy} hooks
1 parent 292b095 commit 2522580

File tree

3 files changed

+88
-4
lines changed

3 files changed

+88
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The minor version will be incremented upon a breaking change and the patch versi
4747
- lang: Add support for tuple types in space calculation ([#3744](https://github.com/solana-foundation/anchor/pull/3744)).
4848
- lang: Add missing pubkey const generation ([#3677](https://github.com/solana-foundation/anchor/pull/3677)).
4949
- cli: Add the Minimum Supported Rust Version (MSRV) to the Rust template, since an arbitrary compiler version isn't supported ([#3873](https://github.com/solana-foundation/anchor/pull/3873)).
50+
- cli: Add `hooks` section to `Anchor.toml` ([#3862](https://github.com/solana-foundation/anchor/pull/3862)).
5051

5152
### Fixes
5253

cli/src/config.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{get_keypair, is_hidden, keys_sync, DEFAULT_RPC_PORT};
22
use anchor_client::Cluster;
33
use anchor_lang_idl::types::Idl;
4-
use anyhow::{anyhow, Context, Error, Result};
4+
use anyhow::{anyhow, bail, Context, Error, Result};
55
use clap::{Parser, ValueEnum};
66
use dirs::home_dir;
77
use heck::ToSnakeCase;
@@ -21,6 +21,7 @@ use std::marker::PhantomData;
2121
use std::ops::Deref;
2222
use std::path::Path;
2323
use std::path::PathBuf;
24+
use std::process::Command;
2425
use std::str::FromStr;
2526
use std::{fmt, io};
2627
use walkdir::WalkDir;
@@ -291,6 +292,7 @@ pub struct Config {
291292
pub provider: ProviderConfig,
292293
pub programs: ProgramsConfig,
293294
pub scripts: ScriptsConfig,
295+
pub hooks: HooksConfig,
294296
pub workspace: WorkspaceConfig,
295297
// Separate entry next to test_config because
296298
// "anchor localnet" only has access to the Anchor.toml,
@@ -384,6 +386,49 @@ pub type ScriptsConfig = BTreeMap<String, String>;
384386

385387
pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
386388

389+
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
390+
#[serde(deny_unknown_fields)]
391+
pub struct HooksConfig {
392+
#[serde(alias = "pre-build")]
393+
pre_build: Option<Hook>,
394+
#[serde(alias = "post-build")]
395+
post_build: Option<Hook>,
396+
#[serde(alias = "pre-test")]
397+
pre_test: Option<Hook>,
398+
#[serde(alias = "post-test")]
399+
post_test: Option<Hook>,
400+
#[serde(alias = "pre-deploy")]
401+
pre_deploy: Option<Hook>,
402+
#[serde(alias = "post-deploy")]
403+
post_deploy: Option<Hook>,
404+
}
405+
406+
#[derive(Clone, Debug, Serialize, Deserialize)]
407+
#[serde(untagged)]
408+
enum Hook {
409+
Single(String),
410+
List(Vec<String>),
411+
}
412+
413+
impl Hook {
414+
pub fn hooks(&self) -> &[String] {
415+
match self {
416+
Self::Single(h) => std::slice::from_ref(h),
417+
Self::List(l) => l.as_slice(),
418+
}
419+
}
420+
}
421+
422+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423+
pub enum HookType {
424+
PreBuild,
425+
PostBuild,
426+
PreTest,
427+
PostTest,
428+
PreDeploy,
429+
PostDeploy,
430+
}
431+
387432
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
388433
pub struct WorkspaceConfig {
389434
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -501,6 +546,32 @@ impl Config {
501546
pub fn wallet_kp(&self) -> Result<Keypair> {
502547
get_keypair(&self.provider.wallet.to_string())
503548
}
549+
550+
pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
551+
let hooks = match hook_type {
552+
HookType::PreBuild => &self.hooks.pre_build,
553+
HookType::PostBuild => &self.hooks.post_build,
554+
HookType::PreTest => &self.hooks.pre_test,
555+
HookType::PostTest => &self.hooks.post_test,
556+
HookType::PreDeploy => &self.hooks.pre_deploy,
557+
HookType::PostDeploy => &self.hooks.post_deploy,
558+
};
559+
let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
560+
for cmd in cmds {
561+
let status = Command::new("bash")
562+
.arg("-c")
563+
.arg(cmd)
564+
.status()
565+
.with_context(|| format!("failed to execute `{cmd}`"))?;
566+
if !status.success() {
567+
match status.code() {
568+
Some(code) => bail!("`{cmd}` failed with exit code {code}"),
569+
None => bail!("`{cmd}` killed by signal"),
570+
}
571+
}
572+
}
573+
Ok(())
574+
}
504575
}
505576

506577
#[derive(Debug, Serialize, Deserialize)]
@@ -512,6 +583,7 @@ struct _Config {
512583
provider: Provider,
513584
workspace: Option<WorkspaceConfig>,
514585
scripts: Option<ScriptsConfig>,
586+
hooks: Option<HooksConfig>,
515587
test: Option<_TestValidator>,
516588
}
517589

@@ -610,6 +682,7 @@ impl fmt::Display for Config {
610682
true => None,
611683
false => Some(self.scripts.clone()),
612684
},
685+
hooks: Some(self.hooks.clone()),
613686
programs,
614687
workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
615688
.then(|| self.workspace.clone()),
@@ -635,6 +708,7 @@ impl FromStr for Config {
635708
wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
636709
},
637710
scripts: cfg.scripts.unwrap_or_default(),
711+
hooks: cfg.hooks.unwrap_or_default(),
638712
test_validator: cfg.test.map(Into::into),
639713
test_config: None,
640714
programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,

cli/src/lib.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::config::{
2-
get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest,
3-
PackageManager, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig, TestValidator,
4-
WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
2+
get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, HookType,
3+
Manifest, PackageManager, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig,
4+
TestValidator, WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
55
};
66
use anchor_client::Cluster;
77
use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY};
@@ -1333,6 +1333,8 @@ pub fn build(
13331333
fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
13341334
};
13351335

1336+
cfg.run_hooks(HookType::PreBuild)?;
1337+
13361338
let cargo = Manifest::discover()?;
13371339
let build_config = BuildConfig {
13381340
verifiable,
@@ -1390,6 +1392,7 @@ pub fn build(
13901392
&arch,
13911393
)?,
13921394
}
1395+
cfg.run_hooks(HookType::PostBuild)?;
13931396

13941397
set_workspace_dir_or_exit();
13951398

@@ -2989,6 +2992,9 @@ fn test(
29892992
if (!is_localnet || skip_local_validator) && !skip_deploy {
29902993
deploy(cfg_override, None, None, false, true, vec![])?;
29912994
}
2995+
2996+
cfg.run_hooks(HookType::PreTest)?;
2997+
29922998
let mut is_first_suite = true;
29932999
if let Some(test_script) = cfg.scripts.get_mut("test") {
29943000
is_first_suite = false;
@@ -3057,6 +3063,7 @@ fn test(
30573063
)?;
30583064
}
30593065
}
3066+
cfg.run_hooks(HookType::PostTest)?;
30603067
Ok(())
30613068
})
30623069
}
@@ -3566,6 +3573,7 @@ fn deploy(
35663573
let client = create_client(&url);
35673574
let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?;
35683575

3576+
cfg.run_hooks(HookType::PreDeploy)?;
35693577
// Deploy the programs.
35703578
println!("Deploying cluster: {url}");
35713579
println!("Upgrade authority: {keypair}");
@@ -3676,6 +3684,7 @@ fn deploy(
36763684
}
36773685

36783686
println!("Deploy success");
3687+
cfg.run_hooks(HookType::PostDeploy)?;
36793688

36803689
Ok(())
36813690
})

0 commit comments

Comments
 (0)