From 18785f69f706b9908d87ca40b0800d73b4702313 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 16:24:29 -0700 Subject: [PATCH 1/6] gltesting: Install bip39 --- libs/gl-testing/pyproject.toml | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/gl-testing/pyproject.toml b/libs/gl-testing/pyproject.toml index eb17ce0cf..46081c10c 100644 --- a/libs/gl-testing/pyproject.toml +++ b/libs/gl-testing/pyproject.toml @@ -5,7 +5,6 @@ description = "" readme = "README.md" requires-python = ">=3.8" dependencies = [ - # "cln-version-manager", "flaky>=3.8.1", "gl-client", "grpcio-tools>=1.66", @@ -20,6 +19,7 @@ dependencies = [ "sh>=1.14.3", "sonora>=0.2.3", "cln-version-manager", + "bip39>=0.0.2", ] diff --git a/pyproject.toml b/pyproject.toml index 0c00eee9f..87a5a428c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,8 @@ readme = "README.md" requires-python = ">=3.9" dependencies = [] -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ "pytest-timeout>=2.3.1", "python-kacl>=0.6.7", ] From 725d93b131b40e0c6060c417e8d43e256a310448 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 15:53:54 -0700 Subject: [PATCH 2/6] git: Add .venv to git ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 02133d211..b4f362eab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ dist +.venv/ +**/.venv/ __pycache__ device.crt device-key.pem From 6ac252a9793ca7f6f323898db9adf0034b919fa2 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 15:55:33 -0700 Subject: [PATCH 3/6] docs: Update instructions --- docs/README.md | 6 +++--- docs/src/getting-started/register.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 8642a5651..d1801a276 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,16 +9,16 @@ You must have a working installation of `python` and `uv` to contribute to the d To install dependencies make sure you are at the root of the repository ``` -uv sync --only-group docs +uv sync --package gl-docs ``` To build the docs ``` -cd docs; mkdocs build +cd docs; uv run mkdocs build ``` To serve the docs locally ``` -cd docs; mkdocs serve +cd docs; uv run mkdocs serve ``` diff --git a/docs/src/getting-started/register.md b/docs/src/getting-started/register.md index 50a387537..b05bfbba8 100644 --- a/docs/src/getting-started/register.md +++ b/docs/src/getting-started/register.md @@ -91,7 +91,7 @@ node to run on. You can chose between the following networks: - `testnet` - `bitcoin` -We'll pick `bitcoin`, because ... reckless 😉 +We'll set NETWORK as `bitcoin`, because ... reckless 😉 === "Rust" ```rust From 623a6004fe31f264bdf98d5185d3d56cb25aec80 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 15:58:10 -0700 Subject: [PATCH 4/6] examples: Test Rust example snippets and change gl-examples to executable --- examples/rust/Cargo.toml | 14 +- examples/rust/snippets/getting_started.rs | 217 ++++++++++++---------- 2 files changed, 127 insertions(+), 104 deletions(-) diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index 2134187ec..88a9128f4 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -3,17 +3,15 @@ name = "gl-examples" version = "0.1.0" edition = "2021" -[lib] -name = "getting_started_snippets" -path = "./snippets/getting_started.rs" -test = true - [dependencies] -gl-client = {path = "../../libs/gl-client"} +gl-client = { path = "../../libs/gl-client" } rand = "0.8" -bip39 = { version = "2", features=["rand_core"] } -tokio = {version="1.29.1", features=["rt", "macros"]} +bip39 = { version = "2", features = ["rand_core"] } +tokio = { version = "1.29.1", features = ["rt", "macros"] } hex = "0.4.3" runeauth = "0.1" anyhow.workspace = true +[[bin]] +name = "getting_started" +path = "snippets/getting_started.rs" diff --git a/examples/rust/snippets/getting_started.rs b/examples/rust/snippets/getting_started.rs index 0b37f51d9..f07bb4016 100644 --- a/examples/rust/snippets/getting_started.rs +++ b/examples/rust/snippets/getting_started.rs @@ -1,65 +1,70 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result}; use bip39::{Language, Mnemonic}; -use gl_client::credentials::{Device, Nobody}; -use gl_client::node::ClnClient; -use gl_client::pb::cln::{amount_or_any, Amount, AmountOrAny}; -use gl_client::pb::{self, cln}; -use gl_client::scheduler::Scheduler; -use gl_client::{bitcoin::Network, signer::Signer}; -use std::fs::{self}; +use gl_client::{ + bitcoin::Network, + credentials::{Device, Nobody}, + node::ClnClient, + pb::{cln, cln::{amount_or_any, Amount, AmountOrAny}}, + scheduler::Scheduler, + signer::Signer, +}; +use rand::RngCore; +use std::{env, fs, path::PathBuf}; use tokio; -#[allow(unused)] -// ---8<--- [start: upgrade_device_certs_to_creds] -async fn upgrade_device_certs_to_creds( - scheduler: &Scheduler, - signer: &Signer, - device_cert: Vec, - device_key: Vec, -) -> Result { - Device { - cert: device_cert, - key: device_key, - ..Default::default() +const NETWORK: Network = Network::Regtest; +const TEST_NODE_DATA_DIR: &str = "/tmp/gltests/node2"; + +fn save_to_file(file_name: &str, data: &[u8]) -> Result<()> { + let path = PathBuf::from(TEST_NODE_DATA_DIR).join(file_name); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; } - .upgrade(scheduler, signer) - .await - .map_err(|e| anyhow!("{}", e.to_string())) + fs::write(&path, data)?; + Ok(()) } -// ---8<--- [end: upgrade_device_certs_to_creds] -#[allow(unused)] -fn save_to_file(file_name: &str, data: Vec) { - fs::write(file_name, data).unwrap(); +fn read_file(file_name: &str) -> Result> { + let path = PathBuf::from(TEST_NODE_DATA_DIR).join(file_name); + Ok(fs::read(path)?) } -#[allow(unused)] -fn read_file(file_name: &str) -> Vec { - fs::read(file_name).unwrap() +async fn upgrade_device_certs_to_creds( + scheduler: &Scheduler, + signer: &Signer, + creds_path: &str, +) -> Result { + // ---8<--- [start: upgrade_device_certs_to_creds] + let device = Device::from_path(creds_path); + let upgraded = device.upgrade(scheduler, signer).await?; + save_to_file("credentials_upgraded.gfs", &upgraded.to_bytes())?; + // ---8<--- [end: upgrade_device_certs_to_creds] + Ok(upgraded) } -#[allow(unused)] -async fn create_seed() -> Vec { +fn create_seed() -> Result> { // ---8<--- [start: create_seed] let mut rng = rand::thread_rng(); - let m = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap(); + let mut entropy = [0u8; 32]; + rng.fill_bytes(&mut entropy); - //Show seed phrase to user - let _phrase = m.words().fold("".to_string(), |c, n| c + " " + n); + // Seed phrase for user + let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy)?; + let _phrase = mnemonic.words().collect::>().join(" "); const EMPTY_PASSPHRASE: &str = ""; - let seed = &m.to_seed(EMPTY_PASSPHRASE)[0..32]; // Only need the first 32 bytes + let seed = &mnemonic.to_seed(EMPTY_PASSPHRASE)[0..32]; // Only need the first 32 bytes // Store the seed on the filesystem, or secure configuration system - save_to_file("seed", seed.to_vec()); - + save_to_file("hsm_secret", seed)?; // ---8<--- [end: create_seed] - seed.to_vec() + Ok(seed.to_vec()) } -#[allow(unused)] -async fn register_node(seed: Vec, developer_cert_path: String, developer_key_path: String) { +fn load_developer_creds() -> Result { // ---8<--- [start: dev_creds] + let developer_cert_path = env::var("GL_NOBODY_CRT")?; + let developer_key_path = env::var("GL_NOBODY_KEY")?; let developer_cert = std::fs::read(developer_cert_path).unwrap_or_default(); let developer_key = std::fs::read(developer_key_path).unwrap_or_default(); let developer_creds = Nobody { @@ -68,56 +73,52 @@ async fn register_node(seed: Vec, developer_cert_path: String, developer_key ..Nobody::default() }; // ---8<--- [end: dev_creds] + Ok(developer_creds) +} +async fn register_node(seed: Vec, developer_creds: Nobody) -> Result<(Scheduler, Device, Signer)> { // ---8<--- [start: init_signer] - let network = Network::Bitcoin; - let signer = Signer::new(seed, network, developer_creds.clone()).unwrap(); + let signer = Signer::new(seed.clone(), NETWORK, developer_creds.clone())?; // ---8<--- [end: init_signer] // ---8<--- [start: register_node] - let scheduler = Scheduler::new(network, developer_creds).await.unwrap(); + let scheduler = Scheduler::new(NETWORK, developer_creds).await?; // Passing in the signer is required because the client needs to prove // ownership of the `node_id` - let registration_response = scheduler.register(&signer, None).await.unwrap(); - + let registration_response = scheduler.register(&signer, None).await?; // ---8<--- [start: device_creds] let device_creds = Device::from_bytes(registration_response.creds); - save_to_file("creds", device_creds.to_bytes()); + save_to_file("credentials.gfs", &device_creds.to_bytes())?; // ---8<--- [end: device_creds] - // ---8<--- [end: register_node] + Ok((scheduler, device_creds, signer)) +} +async fn get_node(scheduler: &Scheduler) -> Result { // ---8<--- [start: get_node] - let scheduler = scheduler.authenticate(device_creds).await.unwrap(); - let _node: ClnClient = scheduler.node().await.unwrap(); + let node = scheduler.node().await?; // ---8<--- [end: get_node] + Ok(node) } -#[allow(unused)] -async fn start_node(device_creds_path: String) { +async fn start_node(device_creds_file_path: &str) -> Result<(cln::GetinfoResponse, cln::ListpeersResponse, cln::InvoiceResponse)> { // ---8<--- [start: start_node] - let network = Network::Bitcoin; - let device_creds = Device::from_path(device_creds_path); - let scheduler = gl_client::scheduler::Scheduler::new(network, device_creds.clone()) - .await - .unwrap(); - - let mut node: gl_client::node::ClnClient = scheduler.node().await.unwrap(); + let creds = Device::from_path(device_creds_file_path); + let scheduler = Scheduler::new(NETWORK, creds.clone()).await?; + let mut node: ClnClient = scheduler.node().await?; // ---8<--- [end: start_node] // ---8<--- [start: list_peers] - let _info = node.getinfo(cln::GetinfoRequest::default()).await.unwrap(); - let _peers = node - .list_peers(gl_client::pb::cln::ListpeersRequest::default()) - .await - .unwrap(); + let info = node.getinfo(cln::GetinfoRequest::default()).await?; + let info = info.into_inner(); + let peers = node.list_peers(cln::ListpeersRequest::default()).await?; + let peers = peers.into_inner(); // ---8<--- [end: list_peers] // ---8<--- [start: start_signer] - let seed = read_file("seed"); - let signer = Signer::new(seed, network, device_creds.clone()).unwrap(); - + let seed = read_file("hsm_secret")?; + let signer = Signer::new(seed, NETWORK, creds.clone())?; let (_tx, rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { signer.run_forever(rx).await.unwrap(); @@ -126,41 +127,65 @@ async fn start_node(device_creds_path: String) { // ---8<--- [start: create_invoice] let amount = AmountOrAny { - value: Some(amount_or_any::Value::Amount(Amount { msat: 10000 })), + value: Some(amount_or_any::Value::Amount(Amount { msat: 10_000 })), }; - - node.invoice(cln::InvoiceRequest { - amount_msat: Some(amount), - description: "description".to_string(), - label: "label".to_string(), - ..Default::default() - }) - .await - .unwrap(); + let invoice = node + .invoice(cln::InvoiceRequest { + amount_msat: Some(amount), + description: format!("desc_{}", rand::random::()), + label: format!("label_{}", rand::random::()), + ..Default::default() + }) + .await?; + let invoice = invoice.into_inner(); // ---8<--- [end: create_invoice] + Ok((info, peers, invoice)) } -#[allow(unused)] -async fn recover_node( - nobody_cert: Vec, - nobody_key: Vec, -) -> Result { +async fn recover_node(dev_creds: Nobody) -> Result<(Scheduler, Device, Signer)> { // ---8<--- [start: recover_node] - let seed = read_file("seed"); - let network = gl_client::bitcoin::Network::Bitcoin; - let creds = Nobody { - cert: nobody_cert, - key: nobody_key, - ..Nobody::default() - }; + let seed = read_file("hsm_secret")?; + let signer = Signer::new(seed.clone(), NETWORK, dev_creds.clone())?; + let scheduler = Scheduler::new(NETWORK, dev_creds).await?; + let recover_response = scheduler.recover(&signer).await?; + // ---8<--- [end: recover_node] + let device_creds = Device::from_bytes(recover_response.creds); + save_to_file("credentials.gfs", &device_creds.to_bytes())?; + Ok((scheduler, device_creds, signer)) +} - let signer = gl_client::signer::Signer::new(seed, network, creds.clone()).unwrap(); +#[tokio::main] +async fn main() -> Result<()> { + println!("Creating seed..."); + let seed = create_seed()?; - let scheduler = - gl_client::scheduler::Scheduler::new(gl_client::bitcoin::Network::Bitcoin, creds) - .await - .unwrap(); + println!("Loading developer credentials..."); + let developer_creds = load_developer_creds()?; - scheduler.recover(&signer).await - // ---8<--- [end: recover_node] + println!("Registering node..."); + let (scheduler, device_creds, signer) = register_node(seed, developer_creds.clone()).await?; + println!("Node Registered!"); + + println!("Getting node information..."); + let device_scheduler = Scheduler::new(NETWORK, device_creds.clone()).await?; + let _gl_node = get_node(&device_scheduler).await?; + + let (info, peers, invoice) = start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + println!("Node pubkey: {}", hex::encode(info.id)); + println!("Peers list: {:?}", peers.peers); + println!("Invoice created: {}", invoice.bolt11); + + println!("Upgrading certs..."); + let _upgraded = upgrade_device_certs_to_creds(&scheduler, &signer, &format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + + println!("Recovering node..."); + let (_scheduler2, _device_creds2, _signer2) = recover_node(developer_creds.clone()).await?; + println!("Node Recovered!"); + + let (info, _peers, _invoice) = start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + println!("Node pubkey: {}", hex::encode(info.id)); + + println!("All steps completed successfully!"); + + Ok(()) } From a21fe358132fdbf6e8b1959caef298e14c7bc1be Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 15:58:41 -0700 Subject: [PATCH 5/6] examples: Test Python example snippets --- examples/python/snippets/getting_started.py | 149 ++++++++++++++------ 1 file changed, 104 insertions(+), 45 deletions(-) diff --git a/examples/python/snippets/getting_started.py b/examples/python/snippets/getting_started.py index 5c4fa1764..61033bdda 100644 --- a/examples/python/snippets/getting_started.py +++ b/examples/python/snippets/getting_started.py @@ -1,59 +1,75 @@ +import os +import sys +import string +from pathlib import Path import bip39 # type: ignore from glclient import Credentials, Signer, Scheduler # type: ignore from pathlib import Path from pyln import grpc as clnpb # type: ignore import secrets # Make sure to use cryptographically sound randomness - -# ---8<--- [start: upgrade_device_certs_to_creds] -def upgrade_device_certs_to_creds( - scheduler: Scheduler, signer: Signer, device_cert: bytes, device_key: bytes -): - device_creds = Credentials.from_parts(device_cert, device_key, "") - return device_creds.upgrade(scheduler.inner, signer.inner) -# ---8<--- [end: upgrade_device_certs_to_creds] +GL_NOBODY_CRT = os.environ.get('GL_NOBODY_CRT') +GL_NOBODY_KEY = os.environ.get('GL_NOBODY_KEY') +NETWORK="regtest" +TEST_NODE_DATA_DIR_1="/tmp/gltests/node1" def save_to_file(file_name: str, data: bytes) -> None: + file_name = Path(TEST_NODE_DATA_DIR_1) / file_name + os.makedirs(file_name.parent, exist_ok=True) with open(file_name, "wb") as file: file.write(data) def read_file(file_name: str) -> bytes: + file_name = Path(TEST_NODE_DATA_DIR_1) / file_name with open(file_name, "rb") as file: return file.read() +def upgrade_device_certs_to_creds(scheduler: Scheduler, signer: Signer, device_creds_file_path: str): + # ---8<--- [start: upgrade_device_certs_to_creds] + device_creds = Credentials.from_path(device_creds_file_path) + upgraded_device_creds = device_creds.upgrade(scheduler.inner, signer.inner) + # ---8<--- [end: upgrade_device_certs_to_creds] + return upgraded_device_creds + + def create_seed() -> bytes: # ---8<--- [start: create_seed] rand = secrets.randbits(256).to_bytes(32, "big") # 32 bytes of randomness - # Show seed phrase to user + # Seed phrase for user phrase = bip39.encode_bytes(rand) seed = bip39.phrase_to_seed(phrase)[:32] # Only need the first 32 bytes # Store the seed on the filesystem, or secure configuration system - save_to_file("seed", seed) + save_to_file("hsm_secret", seed) # ---8<--- [end: create_seed] return seed -def register_node(seed: bytes, developer_cert_path: str, developer_key_path: str) -> None: + +def load_developer_creds(): # ---8<--- [start: dev_creds] + developer_cert_path = os.environ.get('GL_NOBODY_CRT') + developer_key_path = os.environ.get('GL_NOBODY_KEY') developer_cert = Path(developer_cert_path).open(mode="rb").read() developer_key = Path(developer_key_path).open(mode="rb").read() developer_creds = Credentials.nobody_with(developer_cert, developer_key) # ---8<--- [end: dev_creds] + return developer_creds + +def register_node(seed: bytes, developer_creds: Credentials): # ---8<--- [start: init_signer] - network = "bitcoin" - signer = Signer(seed, network, developer_creds) + signer = Signer(seed, NETWORK, developer_creds) # ---8<--- [end: init_signer] # ---8<--- [start: register_node] - scheduler = Scheduler(network, developer_creds) + scheduler = Scheduler(NETWORK, developer_creds) # Passing in the signer is required because the client needs to prove # ownership of the `node_id` @@ -61,66 +77,109 @@ def register_node(seed: bytes, developer_cert_path: str, developer_key_path: str # ---8<--- [start: device_creds] device_creds = Credentials.from_bytes(registration_response.creds) - save_to_file("creds", device_creds.to_bytes()) + save_to_file("credentials.gfs", device_creds.to_bytes()) # ---8<--- [end: device_creds] - # ---8<--- [end: register_node] + return scheduler, device_creds, signer + +def get_node(scheduler: Scheduler, device_creds: Credentials) -> dict: # ---8<--- [start: get_node] scheduler = scheduler.authenticate(device_creds) node = scheduler.node() # ---8<--- [end: get_node] + return node -# TODO: Remove node_id from signature and add node_id to credentials -def start_node(device_creds_path: str, node_id: bytes) -> None: +def start_node(device_creds_file_path: str) -> None: # ---8<--- [start: start_node] - network = "bitcoin" - device_creds = Credentials.from_path(device_creds_path) - scheduler = Scheduler(network, device_creds) + device_creds = Credentials.from_path(device_creds_file_path) + scheduler = Scheduler(NETWORK, device_creds) node = scheduler.node() # ---8<--- [end: start_node] # ---8<--- [start: list_peers] - info = node.get_info() - peers = node.list_peers() + getinfo_response = node.get_info() + listpeers_response = node.list_peers() # ---8<--- [end: list_peers] # ---8<--- [start: start_signer] - seed = read_file("seed") - signer = Signer(seed, network, device_creds) + seed = read_file("hsm_secret") + signer = Signer(seed, NETWORK, device_creds) signer.run_in_thread() # ---8<--- [end: start_signer] # ---8<--- [start: create_invoice] - node.invoice( + invoice_response = node.invoice( amount_msat=clnpb.AmountOrAny(amount=clnpb.Amount(msat=10000)), - description="description", - label="label", + description="description".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(10)), + label="label".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(10)), ) # ---8<--- [end: create_invoice] + return getinfo_response, listpeers_response, invoice_response def recover_node(developer_cert: bytes, developer_key: bytes) -> None: # ---8<--- [start: recover_node] - seed = read_file("seed") - network = "bitcoin" + seed = read_file("hsm_secret") signer_creds = Credentials.nobody_with(developer_cert, developer_key) - signer = Signer(seed, network, signer_creds) - - scheduler = Scheduler( - network, - signer_creds, - ) - - scheduler_creds = signer_creds.upgrade(scheduler.inner, signer.inner) - - scheduler = Scheduler( - network, - scheduler_creds, - ) - - scheduler.recover(signer) + signer = Signer(seed, NETWORK, signer_creds) + scheduler = Scheduler(NETWORK, signer_creds) + recover_response = scheduler.recover(signer) # ---8<--- [end: recover_node] + device_creds = Credentials.from_bytes(recover_response.creds) + save_to_file("credentials.gfs", device_creds.to_bytes()) + return scheduler, device_creds, signer + + +def main(): + # Verify files exist + if not GL_NOBODY_CRT or not os.path.exists(GL_NOBODY_CRT): + print(f"Error: Developer certificate not found at {GL_NOBODY_CRT}") + sys.exit(1) + + if not GL_NOBODY_KEY or not os.path.exists(GL_NOBODY_KEY): + print(f"Error: Developer key not found at {GL_NOBODY_KEY}") + sys.exit(1) + + #Create seed + print("Creating seed...") + seed = create_seed() + + print("Loading developer credentials...") + developer_creds = load_developer_creds() + + # Register node + print("Registering node...") + scheduler, device_creds, signer = register_node(seed, developer_creds) + print("Node Registered!") + + # Get GL node + print("Getting node information...") + get_node(scheduler, device_creds) + + # Print node's information to check + getinfo_response, listpeers_response, invoice_response = start_node(TEST_NODE_DATA_DIR_1 + "/credentials.gfs") + print("Node pubkey:", getinfo_response.id.hex()) + print("Peers list:", listpeers_response.peers) + print("Invoice created:", invoice_response.bolt11) + + # Upgrade Certificates + print("Upgrading certs...") + upgrade_device_certs_to_creds(scheduler, signer, TEST_NODE_DATA_DIR_1 + "/credentials.gfs") + + # Recover the node + print("Recovering node...") + scheduler, device_creds, signer = recover_node(Path(GL_NOBODY_CRT).open(mode="rb").read(), Path(GL_NOBODY_KEY).open(mode="rb").read()) + print("Node Recovered!") + + # Print node's information to check + getinfo_response, listpeers_response, invoice_response = start_node(TEST_NODE_DATA_DIR_1 + "/credentials.gfs") + print("Node pubkey:", getinfo_response.id.hex()) + print("All steps completed successfully!") + + +if __name__ == "__main__": + main() From cfc495d10795cafcfe17d1597d11a98af9b5ce3f Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 30 Oct 2025 15:59:30 -0700 Subject: [PATCH 6/6] ci: Update Github Action for testing snippets and build docs --- .github/workflows/docs-action.yml | 147 ++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 40 deletions(-) diff --git a/.github/workflows/docs-action.yml b/.github/workflows/docs-action.yml index 62c9fbf69..da0804f86 100644 --- a/.github/workflows/docs-action.yml +++ b/.github/workflows/docs-action.yml @@ -7,67 +7,134 @@ on: paths: - "docs/**.md" - "docs/mkdocs.yml" + - "examples/python/**" + - "examples/rust/**" workflow_dispatch: jobs: - test-examples: - permissions: - contents: read + build-and-deploy-docs: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - run: | - mkdir /tmp/protoc && \ - cd /tmp/protoc && \ - wget --quiet -O protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.19.3/protoc-3.19.3-linux-x86_64.zip && \ - unzip protoc.zip && \ - mv /tmp/protoc/bin/protoc /usr/local/bin && \ - chmod a+x /usr/local/bin/protoc && \ - rm -rf /tmp/protoc - - run: cargo build --manifest-path ./examples/rust/Cargo.toml - - build-and-deploy: - needs: test-examples permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source contents: write - runs-on: ubuntu-latest + env: + RUSTFLAGS: "-A dead-code -A unused_variables -A mismatched_lifetime_syntaxes" + CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_DEBUG: "true" + DATA_DIR: "/tmp/gltests" + steps: - - name: Checkout Github repo + - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 with: - python-version: 3.9 + ref: testing-doc-snippets + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Install system dependencies + run: | + # Remove broken Git LFS source from old packagecloud, required for ACT testing + sudo rm -f /etc/apt/sources.list.d/github_git-lfs.list || true + sudo apt-get update + sudo apt-get install -y python3 protobuf-compiler openssl libpq5 golang-cfssl ca-certificates + + - name: Setup uv + uses: astral-sh/setup-uv@v1 + with: + version: "0.9.5" + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt + + - name: Ensure protoc is available + run: | + mkdir -p /tmp/protoc && cd /tmp/protoc + wget --quiet -O protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.19.3/protoc-3.19.3-linux-x86_64.zip + unzip protoc.zip + sudo mv bin/protoc /usr/local/bin + sudo chmod a+x /usr/local/bin/protoc + + - name: Install bitcoind manually + run: | + BITCOIN_VERSION=28.2 + curl -O https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + tar -xzf bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + sudo install -m 0755 -o root -g root -t /usr/local/bin bitcoin-${BITCOIN_VERSION}/bin/* + + - name: Build Rust packages + run: cargo build --workspace + + - name: Sync Python dependencies + run: | + uv lock + uv sync --locked -v --all-packages --dev + + - name: Install CLN versions + run: | + cd ./libs/cln-version-manager + uv run --extra cli clnvm get-all + echo "CLN Versions Installed" + + - name: Run gltestserver in background + shell: bash + run: | + mkdir -p $DATA_DIR + cd ./libs/gl-testserver + nohup uv run gltestserver run --directory $DATA_DIR > "$DATA_DIR"/gltestserver.log 2>&1 & + for i in {1..300}; do + if [ -f "$DATA_DIR/metadata.json" ]; then + echo "✅ metadata.json found" + GL_SCHEDULER_GRPC_URI=$(jq -r '.scheduler_grpc_uri' "$DATA_DIR"/metadata.json) + echo "GL_SCHEDULER_GRPC_URI=$GL_SCHEDULER_GRPC_URI" >> $GITHUB_ENV + echo "GL_CA_CRT=$DATA_DIR/gl-testserver/certs/ca.crt" >> $GITHUB_ENV + echo "GL_NOBODY_CRT=$DATA_DIR/gl-testserver/certs/users/nobody.crt" >> $GITHUB_ENV + echo "GL_NOBODY_KEY=$DATA_DIR/gl-testserver/certs/users/nobody-key.pem" >> $GITHUB_ENV + break + fi + echo "Waiting for metadata.json... ($i)" + sleep 3 + done - - name: Install dependencies + - name: Run Python getting_started example run: | - mkdir /tmp/protoc && \ - cd /tmp/protoc && \ - wget --quiet -O protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.19.3/protoc-3.19.3-linux-x86_64.zip && \ - unzip protoc.zip && \ - mv /tmp/protoc/bin/protoc /usr/local/bin && \ - chmod a+x /usr/local/bin/protoc && \ - rm -rf /tmp/protoc \ - - - name: Install dependencies - run: uv sync - + echo "Using scheduler URI for Python snippets: $GL_SCHEDULER_GRPC_URI" + uv run python examples/python/snippets/getting_started.py + + - name: Run Rust getting_started example + run: | + echo "Using scheduler URI for Rust snippets: $GL_SCHEDULER_GRPC_URI" + cargo run --manifest-path examples/rust/Cargo.toml --bin getting_started + - name: Build docs env: DOCSBRANCH: "gh-pages" DOCSREMOTE: "origin" GITHUB_TOKEN: "${{ secrets.GH_PAGES_PAT }}" - run: mkdir -p ${GITHUB_WORKSPACE}/site/ - - run: cd docs && uv run mkdocs build --verbose --strict --clean --site-dir=${GITHUB_WORKSPACE}/site/ + run: | + mkdir -p ${GITHUB_WORKSPACE}/site/ + cd docs + uv run mkdocs build --verbose --strict --clean --site-dir=${GITHUB_WORKSPACE}/site/ - - name: Deploy + - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./site + + - name: Upload gltestserver logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: gltestserver-logs + path: /tmp/gltests/gltestserver.log + + - name: Stop gltestserver + run: | + if [ -f "$DATA_DIR"/gltestserver.pid ]; then + kill $(cat "$DATA_DIR"/gltestserver.pid) || true + echo "✅ gltestserver stopped" + fi