Skip to content

Commit 1fdd24f

Browse files
committed
Add crates.io release workflow and make deps publishable
- Add workspace version (0.2.0), crate Cargo.tomls inherit via version.workspace - Switch litesvm workspace dep to crates.io 0.8, add [patch.crates-io] for local git fork - Add version field to internal workspace dep entries for publish compatibility - Add .github/workflows/release.yml triggered on v* tags - Add scripts/publish.sh with --validate and --publish modes - Add instruction_decoder_test.rs with 6 unit tests for CounterInstructionDecoder
1 parent e6cf8b9 commit 1fdd24f

6 files changed

Lines changed: 254 additions & 6 deletions

File tree

.github/workflows/release.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
env:
9+
RUST_TOOLCHAIN: "1.93.0"
10+
11+
jobs:
12+
release:
13+
name: Publish to crates.io
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions-rust-lang/setup-rust-toolchain@v1
19+
with:
20+
toolchain: ${{ env.RUST_TOOLCHAIN }}
21+
22+
- name: Validate tag matches crate versions
23+
run: ./scripts/publish.sh --validate
24+
25+
- name: Publish to crates.io
26+
env:
27+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
28+
run: ./scripts/publish.sh --publish
29+
30+
- name: Create GitHub release
31+
env:
32+
GH_TOKEN: ${{ github.token }}
33+
run: |
34+
TAG="${GITHUB_REF#refs/tags/}"
35+
gh release create "$TAG" \
36+
--title "$TAG" \
37+
--generate-notes

Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace.package]
2+
version = "0.2.0"
3+
14
[workspace]
25
resolver = "2"
36
members = [
@@ -35,8 +38,8 @@ syn = { version = "2.0", features = ["visit", "visit-mut", "full"] }
3538
darling = "0.21"
3639
heck = "0.5"
3740
sha2 = "0.10"
38-
# LiteSVM fork + pinned transitive deps to match litesvm 3.0.x
39-
litesvm = { git = "https://github.com/Lightprotocol/litesvm.git", branch = "jorrit/feat-get-program-accounts-v3" }
41+
# LiteSVM from crates.io (patched locally to git fork via [patch.crates-io])
42+
litesvm = "0.8"
4043
# Pin litesvm transitive deps to =3.0.5 (litesvm not yet compatible with 3.1.x)
4144
agave-feature-set = "=3.0.5"
4245
agave-reserved-account-keys = "=3.0.5"
@@ -63,5 +66,8 @@ solana-vote-program = "=3.0.5"
6366
# Testing
6467
insta = { version = "1", features = ["json"] }
6568
# Internal
66-
light-instruction-decoder = { path = "light-instruction-decoder" }
67-
light-instruction-decoder-derive = { path = "light-instruction-decoder-derive" }
69+
light-instruction-decoder = { path = "light-instruction-decoder", version = "0.2.0" }
70+
light-instruction-decoder-derive = { path = "light-instruction-decoder-derive", version = "0.2.0" }
71+
72+
[patch.crates-io]
73+
litesvm = { git = "https://github.com/Lightprotocol/litesvm.git", branch = "jorrit/feat-get-program-accounts-v3" }

light-instruction-decoder-derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "light-instruction-decoder-derive"
3-
version = "0.2.0"
3+
version.workspace = true
44
description = "Derive macros for InstructionDecoder implementations"
55
license = "Apache-2.0"
66
edition = "2021"

light-instruction-decoder/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "light-instruction-decoder"
3-
version = "0.2.0"
3+
version.workspace = true
44
description = "Instruction decoder library for litesvm tests."
55
license = "Apache-2.0"
66
edition = "2021"

scripts/publish.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
CRATES=(
5+
"light-instruction-decoder-derive"
6+
"light-instruction-decoder"
7+
)
8+
9+
get_workspace_version() {
10+
# Extract version from [workspace.package] section in root Cargo.toml
11+
sed -n '/\[workspace\.package\]/,/\[/p' Cargo.toml | grep '^version' | head -1 | sed 's/.*"\(.*\)".*/\1/'
12+
}
13+
14+
get_tag_version() {
15+
local ref="${GITHUB_REF:-}"
16+
if [ -z "$ref" ]; then
17+
echo "ERROR: GITHUB_REF is not set" >&2
18+
exit 1
19+
fi
20+
echo "${ref#refs/tags/v}"
21+
}
22+
23+
validate() {
24+
local tag_version
25+
tag_version="$(get_tag_version)"
26+
echo "Tag version: $tag_version"
27+
28+
local workspace_version
29+
workspace_version="$(get_workspace_version)"
30+
echo "Workspace version: $workspace_version"
31+
32+
if [ "$tag_version" != "$workspace_version" ]; then
33+
echo "ERROR: Tag version ($tag_version) does not match workspace version ($workspace_version)" >&2
34+
exit 1
35+
fi
36+
37+
echo "Version match: $tag_version"
38+
echo ""
39+
40+
echo "Dry-run publishing derive crate..."
41+
cargo publish -p light-instruction-decoder-derive --dry-run
42+
43+
echo "Dry-run publishing main library..."
44+
cargo publish -p light-instruction-decoder --dry-run
45+
46+
echo "Validation passed."
47+
}
48+
49+
publish() {
50+
local tag_version
51+
tag_version="$(get_tag_version)"
52+
53+
echo "Publishing light-instruction-decoder-derive v${tag_version}..."
54+
cargo publish -p light-instruction-decoder-derive
55+
56+
echo "Waiting for crates.io index to propagate..."
57+
for i in $(seq 1 30); do
58+
if cargo search light-instruction-decoder-derive 2>/dev/null | grep -q "$tag_version"; then
59+
echo "Derive crate is available on crates.io."
60+
break
61+
fi
62+
if [ "$i" -eq 30 ]; then
63+
echo "WARNING: Timed out waiting for derive crate to appear on crates.io. Attempting publish anyway."
64+
fi
65+
sleep 10
66+
done
67+
68+
echo "Publishing light-instruction-decoder v${tag_version}..."
69+
cargo publish -p light-instruction-decoder
70+
71+
echo "Published successfully."
72+
}
73+
74+
case "${1:-}" in
75+
--validate)
76+
validate
77+
;;
78+
--publish)
79+
publish
80+
;;
81+
*)
82+
echo "Usage: $0 [--validate|--publish]" >&2
83+
exit 1
84+
;;
85+
esac
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use light_instruction_decoder::{DecoderRegistry, InstructionDecoder};
2+
use sha2::{Digest, Sha256};
3+
use solana_instruction::AccountMeta;
4+
use solana_pubkey::Pubkey;
5+
6+
const COUNTER_PROGRAM_ID: Pubkey =
7+
solana_pubkey::pubkey!("Counter111111111111111111111111111111111111");
8+
9+
fn anchor_discriminator(name: &str) -> [u8; 8] {
10+
let mut hasher = Sha256::new();
11+
hasher.update(format!("global:{name}").as_bytes());
12+
let hash = hasher.finalize();
13+
let mut disc = [0u8; 8];
14+
disc.copy_from_slice(&hash[..8]);
15+
disc
16+
}
17+
18+
fn make_accounts(names: &[&str]) -> Vec<AccountMeta> {
19+
names
20+
.iter()
21+
.map(|_| AccountMeta::new(Pubkey::new_unique(), false))
22+
.collect()
23+
}
24+
25+
#[test]
26+
fn test_decoder_program_id_and_name() {
27+
let decoder = counter::CounterInstructionDecoder;
28+
assert_eq!(decoder.program_id(), COUNTER_PROGRAM_ID);
29+
assert_eq!(decoder.program_name(), "Counter");
30+
}
31+
32+
#[test]
33+
fn test_decoder_can_be_registered() {
34+
let mut registry = DecoderRegistry::new();
35+
registry.register(Box::new(counter::CounterInstructionDecoder));
36+
assert!(registry.has_decoder(&COUNTER_PROGRAM_ID));
37+
}
38+
39+
#[test]
40+
fn test_decoder_decodes_initialize() {
41+
let decoder = counter::CounterInstructionDecoder;
42+
let disc = anchor_discriminator("initialize");
43+
let accounts = make_accounts(&["counter", "authority", "system_program"]);
44+
45+
let result = decoder.decode(&disc, &accounts);
46+
assert!(result.is_some());
47+
48+
let decoded = result.unwrap();
49+
assert_eq!(decoded.name, "Initialize");
50+
assert_eq!(
51+
decoded.account_names,
52+
vec!["counter", "authority", "system_program"]
53+
);
54+
}
55+
56+
#[test]
57+
fn test_decoder_decodes_set_with_params() {
58+
let decoder = counter::CounterInstructionDecoder;
59+
let disc = anchor_discriminator("set");
60+
61+
let value: u64 = 42;
62+
let mut data = disc.to_vec();
63+
data.extend_from_slice(&value.to_le_bytes());
64+
65+
let accounts = make_accounts(&["counter", "authority"]);
66+
67+
let result = decoder.decode(&data, &accounts);
68+
assert!(result.is_some());
69+
70+
let decoded = result.unwrap();
71+
assert_eq!(decoded.name, "Set");
72+
assert!(decoded.fields.iter().any(|f| f.name == "value" && f.value == "42"));
73+
}
74+
75+
#[test]
76+
fn test_decoder_returns_none_for_unknown() {
77+
let decoder = counter::CounterInstructionDecoder;
78+
let bogus = [0xFF; 8];
79+
let accounts = make_accounts(&["a"]);
80+
81+
let result = decoder.decode(&bogus, &accounts);
82+
assert!(result.is_none());
83+
}
84+
85+
#[test]
86+
fn test_discriminators_match_anchor() {
87+
let decoder = counter::CounterInstructionDecoder;
88+
89+
let instructions = [
90+
"initialize",
91+
"increment",
92+
"decrement",
93+
"set",
94+
"configure",
95+
];
96+
97+
let expected_names = [
98+
"Initialize",
99+
"Increment",
100+
"Decrement",
101+
"Set",
102+
"Configure",
103+
];
104+
105+
for (ix_name, expected_name) in instructions.iter().zip(expected_names.iter()) {
106+
let disc = anchor_discriminator(ix_name);
107+
let accounts = make_accounts(&["a", "b", "c"]);
108+
109+
let result = decoder.decode(&disc, &accounts);
110+
assert!(
111+
result.is_some(),
112+
"Decoder should recognize discriminator for '{ix_name}'"
113+
);
114+
assert_eq!(
115+
result.unwrap().name,
116+
*expected_name,
117+
"Instruction name mismatch for '{ix_name}'"
118+
);
119+
}
120+
}

0 commit comments

Comments
 (0)