Skip to content

Commit 49c7e98

Browse files
lutterclaude
andcommitted
gnd: Implement publish command matching graph-cli behavior
The publish command now: - Builds the subgraph and uploads to IPFS (reusing build command) - Opens browser to cli.thegraph.com/publish with query params - Supports --ipfs-hash to skip build and use existing hash - Supports --subgraph-id and --api-key for updating existing subgraphs - Supports --protocol-network (arbitrum-one, arbitrum-sepolia) - Prompts user before opening browser This matches graph-cli's approach of delegating wallet/GRT handling to the web UI rather than implementing it in the CLI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 34b6e84 commit 49c7e98

File tree

4 files changed

+244
-62
lines changed

4 files changed

+244
-62
lines changed

Cargo.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gnd/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ which = "7"
6161
# Interactive prompts
6262
inquire = "0.7"
6363

64+
# Browser opening for publish command
65+
open = "5"
66+
6467
[target.'cfg(unix)'.dependencies]
6568
pgtemp = { git = "https://github.com/graphprotocol/pgtemp", branch = "initdb-args" }
6669

gnd/src/commands/publish.rs

Lines changed: 203 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,244 @@
11
//! Publish command for publishing subgraphs to The Graph's decentralized network.
22
//!
3-
//! This command publishes a subgraph to The Graph's decentralized network,
4-
//! which requires GRT tokens for indexer rewards and query fees.
3+
//! This command builds a subgraph, uploads it to IPFS, and opens a browser
4+
//! to complete the publishing process on The Graph Network.
55
66
use std::path::PathBuf;
77

88
use anyhow::{anyhow, Result};
99
use clap::Parser;
10+
use inquire::Confirm;
1011

12+
use crate::commands::build::{run_build, BuildOpt};
1113
use crate::output::{step, Step};
1214

15+
/// Default IPFS URL for The Graph's hosted IPFS node.
16+
const DEFAULT_IPFS_URL: &str = "https://api.thegraph.com/ipfs/api/v0";
17+
18+
/// Default webapp URL for publishing.
19+
const DEFAULT_WEBAPP_URL: &str = "https://cli.thegraph.com/publish";
20+
1321
#[derive(Clone, Debug, Parser)]
1422
#[clap(about = "Publish a subgraph to The Graph's decentralized network")]
1523
pub struct PublishOpt {
16-
/// Subgraph deployment ID (IPFS hash)
17-
#[clap()]
18-
pub deployment_id: Option<String>,
19-
2024
/// Path to the subgraph manifest
21-
#[clap(short = 'm', long, default_value = "subgraph.yaml")]
25+
#[clap(default_value = "subgraph.yaml")]
2226
pub manifest: PathBuf,
2327

24-
/// IPFS node URL
25-
#[clap(short = 'i', long)]
26-
pub ipfs: Option<String>,
28+
/// IPFS node URL to upload build results to
29+
#[clap(short = 'i', long, default_value = DEFAULT_IPFS_URL)]
30+
pub ipfs: String,
2731

28-
/// Subgraph Studio deploy key
32+
/// IPFS hash of the subgraph manifest to deploy (skips build)
2933
#[clap(long)]
30-
pub deploy_key: Option<String>,
34+
pub ipfs_hash: Option<String>,
3135

32-
/// Network to publish to (mainnet, arbitrum-one, etc.)
36+
/// Subgraph ID to publish to (for updating existing subgraphs)
3337
#[clap(long)]
34-
pub network: Option<String>,
38+
pub subgraph_id: Option<String>,
39+
40+
/// The network to use for the subgraph deployment
41+
#[clap(long, default_value = "arbitrum-one")]
42+
pub protocol_network: String,
3543

36-
/// Version label for the deployment
37-
#[clap(short = 'l', long)]
38-
pub version_label: Option<String>,
44+
/// The API key for Subgraph queries (required when updating existing subgraph)
45+
#[clap(long)]
46+
pub api_key: Option<String>,
47+
48+
/// URL of the web UI to use for publishing
49+
#[clap(long, default_value = DEFAULT_WEBAPP_URL)]
50+
pub webapp_url: String,
51+
52+
/// Output directory for build results
53+
#[clap(short = 'o', long, default_value = "build/")]
54+
pub output_dir: PathBuf,
55+
56+
/// Skip subgraph migrations
57+
#[clap(long)]
58+
pub skip_migrations: bool,
3959

40-
/// Skip confirmation prompt
60+
/// Network configuration to use from the networks config file
4161
#[clap(long)]
42-
pub skip_confirmation: bool,
62+
pub network: Option<String>,
63+
64+
/// Networks config file path
65+
#[clap(long, default_value = "networks.json")]
66+
pub network_file: PathBuf,
4367
}
4468

4569
/// Run the publish command.
46-
pub fn run_publish(opt: PublishOpt) -> Result<()> {
47-
step(
48-
Step::Generate,
49-
"Publishing to The Graph's decentralized network",
50-
);
70+
pub async fn run_publish(opt: PublishOpt) -> Result<()> {
71+
// Validate: if subgraph_id is provided, api_key is required
72+
if opt.subgraph_id.is_some() && opt.api_key.is_none() {
73+
return Err(anyhow!(
74+
"API key is required to publish to an existing subgraph (--api-key).\n\
75+
See https://thegraph.com/docs/en/deploying/subgraph-studio-faqs/#2-how-do-i-create-an-api-key"
76+
));
77+
}
5178

52-
// Validate we have a deployment ID or manifest
53-
if opt.deployment_id.is_none() && !opt.manifest.exists() {
79+
// Validate protocol network
80+
if opt.protocol_network != "arbitrum-one" && opt.protocol_network != "arbitrum-sepolia" {
5481
return Err(anyhow!(
55-
"No deployment ID provided and manifest '{}' not found.\n\
56-
Please provide either:\n\
57-
- A deployment ID (IPFS hash): gnd publish <deployment-id>\n\
58-
- A valid manifest path: gnd publish -m <path>",
59-
opt.manifest.display()
82+
"Invalid protocol network '{}'. Must be 'arbitrum-one' or 'arbitrum-sepolia'",
83+
opt.protocol_network
6084
));
6185
}
6286

63-
Err(anyhow!(
64-
"Publish command is not yet implemented.\n\
65-
This feature requires:\n\
66-
- Integration with The Graph Network subgraph\n\
67-
- Wallet connection for signing transactions\n\
68-
- GRT token handling for indexer rewards\n\n\
69-
To publish your subgraph:\n\
70-
1. Visit https://thegraph.com/studio\n\
71-
2. Create your subgraph in the Studio\n\
72-
3. Deploy using: gnd deploy --studio <subgraph-name>\n\
73-
4. Use the Studio UI to publish to the network\n\n\
74-
Or use the TypeScript graph-cli:\n\
75-
graph publish"
76-
))
87+
// Extract URL-related fields before potentially moving opt fields into BuildOpt
88+
let webapp_url = opt.webapp_url.clone();
89+
let subgraph_id = opt.subgraph_id.clone();
90+
let protocol_network = opt.protocol_network.clone();
91+
let api_key = opt.api_key.clone();
92+
93+
// Get the IPFS hash - either from flag or by building
94+
let ipfs_hash = if let Some(hash) = opt.ipfs_hash {
95+
step(Step::Skip, "Using provided IPFS hash");
96+
hash
97+
} else {
98+
// Build the subgraph and upload to IPFS
99+
step(Step::Load, "Building subgraph for publishing");
100+
101+
// Validate manifest exists
102+
if !opt.manifest.exists() {
103+
return Err(anyhow!(
104+
"Manifest '{}' not found.\n\
105+
Please provide either:\n\
106+
- A valid manifest path: gnd publish <manifest>\n\
107+
- An IPFS hash: gnd publish --ipfs-hash <hash>",
108+
opt.manifest.display()
109+
));
110+
}
111+
112+
let build_opt = BuildOpt {
113+
manifest: opt.manifest,
114+
output_dir: opt.output_dir,
115+
output_format: "wasm".to_string(),
116+
skip_migrations: opt.skip_migrations,
117+
watch: false,
118+
ipfs: Some(opt.ipfs),
119+
network: opt.network,
120+
network_file: opt.network_file,
121+
};
122+
123+
match run_build(build_opt).await? {
124+
Some(hash) => hash,
125+
None => {
126+
return Err(anyhow!(
127+
"Build completed but IPFS upload failed. Cannot proceed with publish."
128+
));
129+
}
130+
}
131+
};
132+
133+
// Prompt user to open browser
134+
step(Step::Generate, "Ready to publish to The Graph Network");
135+
136+
let open_browser = Confirm::new("Open browser to continue publishing?")
137+
.with_default(true)
138+
.prompt()?;
139+
140+
if !open_browser {
141+
println!("\nTo publish manually, visit:");
142+
println!(
143+
" {}",
144+
build_publish_url(
145+
&webapp_url,
146+
&ipfs_hash,
147+
&subgraph_id,
148+
&protocol_network,
149+
&api_key
150+
)
151+
);
152+
return Ok(());
153+
}
154+
155+
// Build the URL with query parameters
156+
let url = build_publish_url(
157+
&webapp_url,
158+
&ipfs_hash,
159+
&subgraph_id,
160+
&protocol_network,
161+
&api_key,
162+
);
163+
164+
step(Step::Done, "Opening browser to complete publishing");
165+
println!("\n {}\n", url);
166+
167+
// Open the browser
168+
open::that(&url).map_err(|e| anyhow!("Failed to open browser: {}", e))?;
169+
170+
Ok(())
171+
}
172+
173+
/// Build the publish URL with query parameters.
174+
fn build_publish_url(
175+
base_url: &str,
176+
ipfs_hash: &str,
177+
subgraph_id: &Option<String>,
178+
protocol_network: &str,
179+
api_key: &Option<String>,
180+
) -> String {
181+
let mut params = vec![format!("id={}", ipfs_hash)];
182+
183+
if let Some(id) = subgraph_id {
184+
params.push(format!("subgraphId={}", id));
185+
}
186+
187+
params.push(format!("network={}", protocol_network));
188+
189+
if let Some(key) = api_key {
190+
params.push(format!("apiKey={}", key));
191+
}
192+
193+
format!("{}?{}", base_url, params.join("&"))
77194
}
78195

79196
#[cfg(test)]
80197
mod tests {
81198
use super::*;
82199

83200
#[test]
84-
fn test_publish_not_implemented() {
85-
let opt = PublishOpt {
86-
deployment_id: Some("QmTest".to_string()),
87-
manifest: PathBuf::from("subgraph.yaml"),
88-
ipfs: None,
89-
deploy_key: None,
90-
network: None,
91-
version_label: None,
92-
skip_confirmation: false,
93-
};
201+
fn test_build_publish_url_basic() {
202+
let url = build_publish_url(
203+
DEFAULT_WEBAPP_URL,
204+
"QmTest123",
205+
&None,
206+
"arbitrum-one",
207+
&None,
208+
);
209+
assert_eq!(
210+
url,
211+
"https://cli.thegraph.com/publish?id=QmTest123&network=arbitrum-one"
212+
);
213+
}
94214

95-
let result = run_publish(opt);
96-
assert!(result.is_err());
97-
assert!(result
98-
.unwrap_err()
99-
.to_string()
100-
.contains("not yet implemented"));
215+
#[test]
216+
fn test_build_publish_url_with_subgraph_id() {
217+
let url = build_publish_url(
218+
DEFAULT_WEBAPP_URL,
219+
"QmTest456",
220+
&Some("my-subgraph".to_string()),
221+
"arbitrum-sepolia",
222+
&Some("abc123".to_string()),
223+
);
224+
assert_eq!(
225+
url,
226+
"https://cli.thegraph.com/publish?id=QmTest456&subgraphId=my-subgraph&network=arbitrum-sepolia&apiKey=abc123"
227+
);
228+
}
229+
230+
#[test]
231+
fn test_build_publish_url_custom_webapp() {
232+
let url = build_publish_url(
233+
"https://custom.example.com/publish",
234+
"QmCustom",
235+
&None,
236+
"arbitrum-one",
237+
&None,
238+
);
239+
assert_eq!(
240+
url,
241+
"https://custom.example.com/publish?id=QmCustom&network=arbitrum-one"
242+
);
101243
}
102244
}

gnd/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ async fn main() -> Result<()> {
204204
}
205205
}
206206
Commands::Publish(publish_opt) => {
207-
if let Err(e) = run_publish(publish_opt) {
207+
if let Err(e) = run_publish(publish_opt).await {
208208
eprintln!("Error: {}", e);
209209
std::process::exit(1);
210210
}

0 commit comments

Comments
 (0)