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
28 changes: 26 additions & 2 deletions .github/workflows/deploy-program.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ on:
description: Program directory containing Cargo.toml
required: true
type: string
environment:
description: Optional GitHub deployment environment; empty leaves the job without an environment
required: false
default: ""
type: string
expect-program-id:
description: Optional expected program ID (Base58 or 64-character hex); must match PROGRAM_KEY
required: false
default: ""
type: string
cargo-features:
description: Optional Cargo features to enable when building the program
required: false
Expand All @@ -33,10 +43,15 @@ on:
default: false
type: boolean
idl-size:
description: Initial IDL account size; zero uses the arch-kit default
description: Minimum IDL account size; zero uses the arch-kit default
required: false
default: 0
type: number
allow-idl-resize:
description: Opt into clearing, growing, and republishing a populated canonical IDL
required: false
default: false
type: boolean
secrets:
PROGRAM_KEY:
description: Program private key as hex or SDK-compatible JSON
Expand All @@ -53,14 +68,15 @@ permissions:
contents: read

env:
ARCH_KIT_VERSION: 0.1.3
ARCH_KIT_VERSION: 0.1.4
ANZA_VERSION: v3.1.9
CARGO_TERM_COLOR: always

jobs:
deploy:
name: Build and Deploy
runs-on: ubuntu-24.04
environment: ${{ inputs.environment }}
outputs:
program-id: ${{ steps.keys.outputs.program-id }}

Expand Down Expand Up @@ -218,6 +234,8 @@ jobs:
BITCOIN_NETWORK: ${{ inputs.bitcoin-network }}
FUND_AUTHORITY: ${{ inputs.fund-authority }}
IDL_SIZE: ${{ inputs.idl-size }}
ALLOW_IDL_RESIZE: ${{ inputs.allow-idl-resize }}
EXPECT_PROGRAM_ID: ${{ inputs.expect-program-id }}
ELF_PATH: ${{ steps.program.outputs.elf-path }}
IDL_PATH: ${{ steps.idl.outputs.idl-path }}
PROGRAM_KEY_PATH: ${{ steps.keys.outputs.program-key }}
Expand All @@ -240,6 +258,12 @@ jobs:
if (( IDL_SIZE > 0 )); then
args+=(--idl-size "${IDL_SIZE}")
fi
if [[ "${ALLOW_IDL_RESIZE}" == "true" ]]; then
args+=(--allow-idl-resize)
fi
if [[ -n "${EXPECT_PROGRAM_ID}" ]]; then
args+=(--expect-program-id "${EXPECT_PROGRAM_ID}")
fi
arch-kit "${args[@]}"

- name: Remove deployment keys
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "arch-kit"
version = "0.1.3"
version = "0.1.4"
edition = "2024"
rust-version = "1.85"
description = "Command-line toolkit for developing, deploying, and interacting with Arch Network programs"
Expand All @@ -27,9 +27,9 @@
bitcoin = "=0.32.7"
bs58 = "0.5"
clap = { version = "4.5", features = ["derive", "env"] }
flate2 = "1.1"

Check warning on line 30 in Cargo.toml

View workflow job for this annotation

GitHub Actions / Initialize and Deploy

unused build dependency `indexmap`
hex = "0.4"

Check warning on line 31 in Cargo.toml

View workflow job for this annotation

GitHub Actions / Initialize and Deploy

unused build dependency `proc-macro-crate`
include_dir = "0.7"

Check warning on line 32 in Cargo.toml

View workflow job for this annotation

GitHub Actions / Initialize and Deploy

unused build dependency `unicode-segmentation`
minijinja = "2"
rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
Expand Down
48 changes: 42 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ with the reusable workflow:
```yaml
jobs:
deploy:
uses: Arch-Network/arch-kit/.github/workflows/deploy-program.yml@v0.1.0
uses: Arch-Network/arch-kit/.github/workflows/deploy-program.yml@v0.1.4
with:
program-path: program
bitcoin-network: testnet
Expand All @@ -51,6 +51,19 @@ jobs:
AUTHORITY_KEY: ${{ secrets.AUTHORITY_KEY }}
```

To select a GitHub deployment environment, add `environment` under `with`:

```yaml
with:
program-path: program
environment: production
```

The input defaults to an empty string, so omitting it runs without an
environment. When set, the deployment job uses that environment's protection
rules and secrets. Environment secrets take precedence over caller-provided
secrets with the same name; see [GitHub's reusable workflow documentation](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#using-inputs-and-secrets-in-a-reusable-workflow).

When `idl-path` is omitted, the workflow builds the Satellite IDL from the
program. Programs with manually maintained IDLs can bypass that step:

Expand All @@ -66,6 +79,16 @@ the program and canonical IDL. Faucet funding is disabled unless explicitly
enabled. Reusing the same two key secrets updates the existing deployment;
changing the program key creates a new deployment.

Set `expect-program-id` to the intended program ID (Base58 or 64-character hex)
to check that `PROGRAM_KEY` derives that ID before faucet funding or deployment.
It defaults to an empty string, which skips this check.

Set `allow-idl-resize: true` to opt into growing an existing populated canonical
IDL when the new IDL no longer fits, or to reserve more space with `idl-size`.
The input defaults to `false`, preserving the existing fixed-capacity behavior.
Growth also supports Satellite 0.31.5/0.31.6 handlers: the CLI backs up the IDL,
then clears, grows, and restores it within each resize transaction.

## Program development

Programs using Satellite require Rust nightly during the IDL-building step.
Expand Down Expand Up @@ -350,17 +373,30 @@ byte array.

Useful deployment options:

- `--expect-program-id <PUBKEY>` checks that `--program-key` derives the expected
program ID before faucet funding or deployment. Accepts Base58 or 64-character
hex; omitted by default.
- `--generate-if-missing` securely creates missing program or authority keys.
- `--fund-authority` requests faucet funding before deployment; it is rejected
on mainnet.
- `--idl <PATH>` publishes or upgrades an IDL after deployment.
- `--idl-size <BYTES>` sets the initial IDL account size and requires `--idl`.
- `--idl-size <BYTES>` sets the minimum IDL account size and requires `--idl`.
Growing a populated account also requires `--allow-idl-resize`.
- `--allow-idl-resize` opts into growing a populated canonical IDL and requires
`--idl`. It is disabled by default.

IDL accounts default to at least 10,000 bytes, including the 44-byte header.
Reserve enough capacity for future upgrades because a populated IDL account
cannot be grown. The target program must include compatible canonical Satellite
IDL handlers. If IDL publication fails, the deployed program remains deployed
and its program ID is included in the error.
Populated accounts retain their capacity unless `--allow-idl-resize` is supplied.
With the flag, growth accommodates the new compressed IDL and any larger
`--idl-size`, including when the IDL contents are unchanged. Accounts are never
shrunk. The target program must include canonical Satellite IDL handlers.
For compatibility with older handlers, the CLI copies the current IDL into a
backup buffer, verifies the copy, then clears, grows, and restores the canonical
IDL within each resize transaction. A failed transaction leaves the published
IDL intact. The temporary resize buffers are closed after the final growth
step, returning their rent to the authority. The CLI verifies the restored IDL
before publishing an update. If IDL publication fails, the deployed program
remains deployed and its program ID is included in the error.

## Publishing arch-kit

Expand Down
74 changes: 73 additions & 1 deletion src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
error::{CliError, Result},
idl,
keys::{load_or_generate_key, pubkey_hex},
token::parse_pubkey,
};

#[derive(Debug, clap::Args)]
Expand All @@ -27,6 +28,10 @@ pub(crate) struct Args {
#[arg(long, value_name = "PATH")]
pub(crate) program_key: PathBuf,

/// Require --program-key to derive this program ID (Base58 or 64-character hex).
#[arg(long, value_name = "PUBKEY")]
pub(crate) expect_program_id: Option<String>,

/// Existing deployment and IDL authority keypair.
#[arg(long, value_name = "PATH")]
pub(crate) authority: PathBuf,
Expand All @@ -43,12 +48,23 @@ pub(crate) struct Args {
#[arg(long, value_name = "PATH")]
pub(crate) idl: Option<PathBuf>,

/// Minimum initial canonical IDL account size in bytes, including its 44-byte header.
/// Minimum canonical IDL account size in bytes, including its 44-byte header.
/// Growing a populated account also requires --allow-idl-resize.
#[arg(long, requires = "idl", value_name = "BYTES")]
pub(crate) idl_size: Option<usize>,

/// Allow clearing, growing, and republishing a populated canonical IDL.
#[arg(long, requires = "idl")]
pub(crate) allow_idl_resize: bool,
}

pub(crate) fn run(config: &Config, args: Args) -> Result<()> {
let expected_program = args
.expect_program_id
.as_deref()
.map(|value| parse_pubkey(value, "--expect-program-id"))
.transpose()?;

ensure_file(&args.elf, "program ELF")?;
// Read now so invalid permissions or I/O fail before optional faucet use.
std::fs::read(&args.elf).map_err(|source| CliError::ReadInput {
Expand All @@ -63,6 +79,13 @@ pub(crate) fn run(config: &Config, args: Args) -> Result<()> {
config.network,
args.generate_if_missing,
)?;
if let Some(expected) = expected_program
&& expected != program_pubkey
{
return Err(CliError::InvalidArgument(format!(
"program ID mismatch: expected {expected} from --expect-program-id, but --program-key derives {program_pubkey}"
)));
}
let (authority_keypair, authority_pubkey, generated_authority_key) = load_or_generate_key(
&args.authority,
"authority key",
Expand Down Expand Up @@ -140,6 +163,7 @@ pub(crate) fn run(config: &Config, args: Args) -> Result<()> {
authority_pubkey,
authority_keypair,
prepared,
args.allow_idl_resize,
)
{
return Err(CliError::IdlAfterDeployment {
Expand Down Expand Up @@ -273,6 +297,9 @@ mod tests {
"program.idl.json",
"--idl-size",
"20000",
"--allow-idl-resize",
"--expect-program-id",
"1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
])
.unwrap();

Expand All @@ -282,6 +309,11 @@ mod tests {
assert!(args.fund_authority);
assert!(!args.generate_if_missing);
assert_eq!(args.idl_size, Some(20_000));
assert!(args.allow_idl_resize);
assert_eq!(
args.expect_program_id.as_deref(),
Some("1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")
);
assert_eq!(args.idl, Some(PathBuf::from("program.idl.json")));
}

Expand All @@ -303,6 +335,46 @@ mod tests {
assert!(parsed.is_err());
}

#[test]
fn idl_resize_defaults_to_false() {
let cli = Cli::try_parse_from([
"arch-kit",
"deploy",
"--elf",
"program.so",
"--program-key",
"program.json",
"--authority",
"authority.json",
"--idl",
"program.idl.json",
])
.unwrap();

let Command::Deploy(args) = cli.command else {
panic!("expected deploy command");
};
assert!(!args.allow_idl_resize);
assert!(args.expect_program_id.is_none());
}

#[test]
fn rejects_idl_resize_without_idl() {
let parsed = Cli::try_parse_from([
"arch-kit",
"deploy",
"--elf",
"program.so",
"--program-key",
"program.json",
"--authority",
"authority.json",
"--allow-idl-resize",
]);

assert!(parsed.is_err());
}

#[test]
fn accepts_generate_if_missing() {
let cli = Cli::try_parse_from([
Expand Down
Loading