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
17 changes: 10 additions & 7 deletions gnd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ gnd build

# Deploy to a local Graph Node
gnd create --node http://localhost:8020 my-name/my-subgraph
gnd deploy --node http://localhost:8020 --ipfs http://localhost:5001 my-name/my-subgraph
gnd deploy --node http://localhost:8020 --ipfs http://localhost:5001 -l v0.0.1 my-name/my-subgraph

# Or deploy to Subgraph Studio
gnd auth YOUR_DEPLOY_KEY
gnd deploy my-name/my-subgraph
gnd deploy -l v0.0.1 my-name/my-subgraph
```

## Commands
Expand Down Expand Up @@ -165,25 +165,28 @@ gnd deploy <SUBGRAPH_NAME> [MANIFEST]
| `--node` | `-g` | Graph Node URL (defaults to Subgraph Studio) |
| `--ipfs` | `-i` | IPFS node URL |
| `--deploy-key` | | Deploy key for authentication |
| `--version-label` | `-l` | Version label for the deployment |
| `--version-label` | `-l` | Version label for the deployment (required in non-interactive mode) |
| `--ipfs-hash` | | IPFS hash of already-uploaded manifest |
| `--output-dir` | `-o` | Build output directory (default: `build/`) |
| `--skip-migrations` | | Skip manifest migrations |
| `--network` | | Network from networks.json |
| `--network-file` | | Path to networks config |
| `--debug-fork` | | Fork subgraph ID for debugging |

If `--version-label` is omitted in an interactive terminal, `gnd deploy` prompts for it.
In non-interactive environments (CI/scripts), you must pass `--version-label`.

**Examples:**

```bash
# Deploy to Subgraph Studio (uses saved auth key)
gnd deploy my-name/my-subgraph
gnd deploy -l v1.0.0 my-name/my-subgraph

# Deploy to local Graph Node
gnd deploy --node http://localhost:8020 --ipfs http://localhost:5001 my-name/my-subgraph
gnd deploy --node http://localhost:8020 --ipfs http://localhost:5001 -l v1.0.0 my-name/my-subgraph

# Deploy with version label
gnd deploy -l v1.0.0 my-name/my-subgraph
# Deploy without --version-label in an interactive terminal (prompts)
gnd deploy my-name/my-subgraph
```

### `gnd publish`
Expand Down
12 changes: 7 additions & 5 deletions gnd/docs/migrating-from-graph-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ graph deploy --studio my-subgraph
# Use:
gnd codegen
gnd build
gnd deploy my-subgraph # defaults to Studio
gnd deploy my-subgraph # defaults to Studio; prompts for version label in interactive terminals
```

## Command Mapping
Expand All @@ -25,7 +25,7 @@ gnd deploy my-subgraph # defaults to Studio
| `graph init` | `gnd init` | Identical flags |
| `graph codegen` | `gnd codegen` | Identical flags |
| `graph build` | `gnd build` | Identical flags |
| `graph deploy` | `gnd deploy` | Defaults to Studio if `--node` not provided |
| `graph deploy` | `gnd deploy` | Defaults to Studio if `--node` not provided; pass `--version-label` in non-interactive mode |
| `graph create` | `gnd create` | Identical flags |
| `graph remove` | `gnd remove` | Identical flags |
| `graph auth` | `gnd auth` | Identical flags |
Expand Down Expand Up @@ -208,12 +208,12 @@ Replace `graph` with `gnd` in your CI configuration:
# Before
- run: graph codegen
- run: graph build
- run: graph deploy --studio ${{ secrets.SUBGRAPH_NAME }}
- run: graph deploy --studio -l ${{ github.sha }} ${{ secrets.SUBGRAPH_NAME }}

# After
- run: gnd codegen
- run: gnd build
- run: gnd deploy ${{ secrets.SUBGRAPH_NAME }}
- run: gnd deploy -l ${{ github.sha }} ${{ secrets.SUBGRAPH_NAME }}
```

### Step 5: Update package.json Scripts (Optional)
Expand All @@ -225,11 +225,13 @@ If you have npm scripts calling graph-cli:
"scripts": {
"codegen": "gnd codegen",
"build": "gnd build",
"deploy": "gnd deploy"
"deploy": "gnd deploy -l $VERSION_LABEL"
}
}
```

Set `VERSION_LABEL` in your environment (for example, from a git tag or commit SHA).

## Troubleshooting

### "Command not found: gnd"
Expand Down
81 changes: 78 additions & 3 deletions gnd/src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
//! This command builds a subgraph (unless an IPFS hash is provided),
//! uploads it to IPFS, and deploys it to a Graph Node.

use std::path::PathBuf;
use std::{
io::{self, IsTerminal},
path::PathBuf,
};

use anyhow::{Context, Result, anyhow};
use clap::Parser;
Expand All @@ -12,6 +15,7 @@ use url::Url;
use crate::commands::auth::get_deploy_key;
use crate::commands::build::{BuildOpt, run_build};
use crate::output::{Step, step};
use crate::prompt::{normalize_version_label, prompt_version_label};
use crate::services::GraphNodeClient;

/// Default IPFS URL used by The Graph
Expand Down Expand Up @@ -91,6 +95,12 @@ pub async fn run_deploy(opt: DeployOpt) -> Result<()> {
None => get_deploy_key(node)?,
};

let version_label =
match resolve_version_label(opt.version_label.as_deref(), io::stdin().is_terminal())? {
Some(label) => label,
None => prompt_version_label()?,
};

// Get or build the IPFS hash
let ipfs_hash = match &opt.ipfs_hash {
Some(hash) => {
Expand All @@ -104,7 +114,14 @@ pub async fn run_deploy(opt: DeployOpt) -> Result<()> {
};

// Deploy to Graph Node
deploy_to_node(&opt, node, &ipfs_hash, deploy_key.as_deref()).await
deploy_to_node(
&opt,
node,
&ipfs_hash,
&version_label,
deploy_key.as_deref(),
)
.await
}

/// Validate that a URL is well-formed.
Expand All @@ -127,6 +144,32 @@ fn validate_url(url: &str, name: &str) -> Result<()> {
Ok(())
}

/// Parse and validate a normalized version label.
fn parse_version_label(label: &str) -> Result<String> {
let normalized = normalize_version_label(label);
if normalized.is_empty() {
return Err(anyhow!("Version label cannot be empty"));
}
Ok(normalized)
}

/// Resolve version label from flag or interactive prompt mode.
fn resolve_version_label(
version_label: Option<&str>,
stdin_is_terminal: bool,
) -> Result<Option<String>> {
match version_label {
Some(label) => parse_version_label(label)
.context("Invalid --version-label value")
.map(Some),
None if stdin_is_terminal => Ok(None),
None => Err(anyhow!(
"--version-label is required in non-interactive mode. \
Pass --version-label <LABEL>."
)),
}
}

/// Build the subgraph and upload to IPFS.
async fn build_and_upload(opt: &DeployOpt) -> Result<String> {
// Run the build command with IPFS upload enabled
Expand Down Expand Up @@ -155,6 +198,7 @@ async fn deploy_to_node(
opt: &DeployOpt,
node: &str,
ipfs_hash: &str,
version_label: &str,
deploy_key: Option<&str>,
) -> Result<()> {
step(Step::Deploy, &format!("Deploying to {}", node));
Expand All @@ -165,7 +209,7 @@ async fn deploy_to_node(
.deploy_subgraph(
&opt.subgraph_name,
ipfs_hash,
opt.version_label.as_deref(),
Some(version_label),
opt.debug_fork.as_deref(),
)
.await
Expand Down Expand Up @@ -240,4 +284,35 @@ mod tests {
// Verify the default IPFS URL is valid
assert!(validate_url(DEFAULT_IPFS_URL, "IPFS").is_ok());
}

#[test]
fn test_parse_version_label_rejects_empty_after_normalize() {
assert!(parse_version_label("").is_err());
assert!(parse_version_label(" ").is_err());
assert!(parse_version_label("\"\"").is_err());
assert!(parse_version_label(" \"\" ").is_err());
assert!(parse_version_label("\" \"").is_err());
}

#[test]
fn test_resolve_version_label_from_flag() {
assert_eq!(
resolve_version_label(Some(" \"v1.0.0\" "), false).unwrap(),
Some("v1.0.0".to_string())
);
}

#[test]
fn test_resolve_version_label_interactive_without_flag() {
assert_eq!(resolve_version_label(None, true).unwrap(), None);
}

#[test]
fn test_resolve_version_label_non_interactive_requires_flag() {
let err = resolve_version_label(None, false).unwrap_err();
assert!(
err.to_string()
.contains("--version-label is required in non-interactive mode")
);
}
}
39 changes: 38 additions & 1 deletion gnd/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use std::path::Path;

use anyhow::Result;
use console::{Term, style};
use inquire::parser::CustomTypeParser;
use inquire::validator::Validation;
use inquire::{Autocomplete, Confirm, CustomUserError, Select, Text};
use inquire::{Autocomplete, Confirm, CustomType, CustomUserError, Select, Text};

use crate::output::{Step, step};
use crate::services::{ContractInfo, ContractService, Network, NetworksRegistry};
Expand Down Expand Up @@ -387,6 +388,32 @@ pub fn get_subgraph_basename(name: &str) -> String {
name.split('/').next_back().unwrap_or(name).to_string()
}

/// Normalize a version label from either a flag or interactive prompt.
pub fn normalize_version_label(label: &str) -> String {
label.trim_matches(|c| c == '"' || c == ' ').to_string()
}

/// Prompt for a version label for deployment.
/// Uses CustomType instead of Text to normalize the input before validation
pub fn prompt_version_label() -> Result<String> {
let parser: CustomTypeParser<String> = &|val| {
let normalized = normalize_version_label(val);
Ok(normalized)
};
let label = CustomType::<String>::new("Which version label to use? (e.g. v0.0.1):")
.with_parser(parser)
.with_validator(|input: &String| {
if input.is_empty() {
Ok(Validation::Invalid("Version label cannot be empty".into()))
} else {
Ok(Validation::Valid)
}
})
.prompt()?;

Ok(label)
}

/// Interactive init form for when options are not fully provided.
pub struct InitForm {
pub network: String,
Expand Down Expand Up @@ -649,6 +676,16 @@ mod tests {
assert_eq!(get_subgraph_basename(""), "");
}

#[test]
fn test_normalize_version_label() {
assert_eq!(normalize_version_label("v1.0.0"), "v1.0.0");
assert_eq!(normalize_version_label(" v1.0.0 "), "v1.0.0");
assert_eq!(normalize_version_label("\"v1.0.0\""), "v1.0.0");
assert_eq!(normalize_version_label(" \"v1.0.0\" "), "v1.0.0");
assert_eq!(normalize_version_label("v1.0.0-beta"), "v1.0.0-beta");
assert_eq!(normalize_version_label(" my version "), "my version");
}

#[test]
fn test_format_network() {
let network = Network {
Expand Down
Loading