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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- lang: Add support for tuple types in space calculation ([#3744](https://github.com/solana-foundation/anchor/pull/3744)).
- lang: Add missing pubkey const generation ([#3677](https://github.com/solana-foundation/anchor/pull/3677)).
- 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)).
- cli: Add `hooks` section to `Anchor.toml` ([#3862](https://github.com/solana-foundation/anchor/pull/3862)).

### Fixes

Expand Down
76 changes: 75 additions & 1 deletion cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{get_keypair, is_hidden, keys_sync, DEFAULT_RPC_PORT};
use anchor_client::Cluster;
use anchor_lang_idl::types::Idl;
use anyhow::{anyhow, Context, Error, Result};
use anyhow::{anyhow, bail, Context, Error, Result};
use clap::{Parser, ValueEnum};
use dirs::home_dir;
use heck::ToSnakeCase;
Expand All @@ -21,6 +21,7 @@ use std::marker::PhantomData;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use std::{fmt, io};
use walkdir::WalkDir;
Expand Down Expand Up @@ -291,6 +292,7 @@ pub struct Config {
pub provider: ProviderConfig,
pub programs: ProgramsConfig,
pub scripts: ScriptsConfig,
pub hooks: HooksConfig,
pub workspace: WorkspaceConfig,
// Separate entry next to test_config because
// "anchor localnet" only has access to the Anchor.toml,
Expand Down Expand Up @@ -384,6 +386,49 @@ pub type ScriptsConfig = BTreeMap<String, String>;

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

#[derive(Default, Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HooksConfig {
#[serde(alias = "pre-build")]
pre_build: Option<Hook>,
#[serde(alias = "post-build")]
post_build: Option<Hook>,
#[serde(alias = "pre-test")]
pre_test: Option<Hook>,
#[serde(alias = "post-test")]
post_test: Option<Hook>,
#[serde(alias = "pre-deploy")]
pre_deploy: Option<Hook>,
#[serde(alias = "post-deploy")]
post_deploy: Option<Hook>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Hook {
Single(String),
List(Vec<String>),
}

impl Hook {
pub fn hooks(&self) -> &[String] {
match self {
Self::Single(h) => std::slice::from_ref(h),
Self::List(l) => l.as_slice(),
}
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HookType {
PreBuild,
PostBuild,
PreTest,
PostTest,
PreDeploy,
PostDeploy,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
Expand Down Expand Up @@ -501,6 +546,32 @@ impl Config {
pub fn wallet_kp(&self) -> Result<Keypair> {
get_keypair(&self.provider.wallet.to_string())
}

pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
let hooks = match hook_type {
HookType::PreBuild => &self.hooks.pre_build,
HookType::PostBuild => &self.hooks.post_build,
HookType::PreTest => &self.hooks.pre_test,
HookType::PostTest => &self.hooks.post_test,
HookType::PreDeploy => &self.hooks.pre_deploy,
HookType::PostDeploy => &self.hooks.post_deploy,
};
let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
for cmd in cmds {
let status = Command::new("bash")
.arg("-c")
.arg(cmd)
.status()
.with_context(|| format!("failed to execute `{cmd}`"))?;
if !status.success() {
match status.code() {
Some(code) => bail!("`{cmd}` failed with exit code {code}"),
None => bail!("`{cmd}` killed by signal"),
}
}
}
Ok(())
}
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -512,6 +583,7 @@ struct _Config {
provider: Provider,
workspace: Option<WorkspaceConfig>,
scripts: Option<ScriptsConfig>,
hooks: Option<HooksConfig>,
test: Option<_TestValidator>,
}

Expand Down Expand Up @@ -610,6 +682,7 @@ impl fmt::Display for Config {
true => None,
false => Some(self.scripts.clone()),
},
hooks: Some(self.hooks.clone()),
programs,
workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
.then(|| self.workspace.clone()),
Expand All @@ -635,6 +708,7 @@ impl FromStr for Config {
wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
},
scripts: cfg.scripts.unwrap_or_default(),
hooks: cfg.hooks.unwrap_or_default(),
test_validator: cfg.test.map(Into::into),
test_config: None,
programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
Expand Down
15 changes: 12 additions & 3 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::config::{
get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest,
PackageManager, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig, TestValidator,
WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, HookType,
Manifest, PackageManager, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig,
TestValidator, WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
};
use anchor_client::Cluster;
use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY};
Expand Down Expand Up @@ -1333,6 +1333,8 @@ pub fn build(
fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
};

cfg.run_hooks(HookType::PreBuild)?;

let cargo = Manifest::discover()?;
let build_config = BuildConfig {
verifiable,
Expand Down Expand Up @@ -1390,6 +1392,7 @@ pub fn build(
&arch,
)?,
}
cfg.run_hooks(HookType::PostBuild)?;

set_workspace_dir_or_exit();

Expand Down Expand Up @@ -2989,6 +2992,9 @@ fn test(
if (!is_localnet || skip_local_validator) && !skip_deploy {
deploy(cfg_override, None, None, false, true, vec![])?;
}

cfg.run_hooks(HookType::PreTest)?;

let mut is_first_suite = true;
if let Some(test_script) = cfg.scripts.get_mut("test") {
is_first_suite = false;
Expand Down Expand Up @@ -3057,6 +3063,7 @@ fn test(
)?;
}
}
cfg.run_hooks(HookType::PostTest)?;
Ok(())
})
}
Expand Down Expand Up @@ -3566,6 +3573,7 @@ fn deploy(
let client = create_client(&url);
let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?;

cfg.run_hooks(HookType::PreDeploy)?;
// Deploy the programs.
println!("Deploying cluster: {url}");
println!("Upgrade authority: {keypair}");
Expand Down Expand Up @@ -3676,6 +3684,7 @@ fn deploy(
}

println!("Deploy success");
cfg.run_hooks(HookType::PostDeploy)?;

Ok(())
})
Expand Down
20 changes: 20 additions & 0 deletions docs/content/docs/references/anchor-toml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,23 @@ Example:
[toolchain]
package_manager = "pnpm"
```

### hooks

The `hooks` table allows you to configure commands that may be run at specific stages of the build/test/deploy pipeline.

Example:
```toml
[hooks]
# Accepts kebab-case names...
pre-build = "echo foo"
# ...and snake-case names
post_build = "echo bar"
# Accepts a list of commands, run in series
pre-test = ["echo 1", "echo 2"]
# Non-zero exit codes will abort the CLI
post-test = "exit 1"
# Unused hooks may be omitted
# pre-deploy = []
# post-deploy = []
```