Skip to content

Commit 68bb8ac

Browse files
gnd(deploy): Add prompt for missing --version-label in interactive mode. Require the flag in non-interactive.
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
1 parent 545cd70 commit 68bb8ac

4 files changed

Lines changed: 133 additions & 16 deletions

File tree

gnd/README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ gnd build
2626

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

3131
# Or deploy to Subgraph Studio
3232
gnd auth YOUR_DEPLOY_KEY
33-
gnd deploy my-name/my-subgraph
33+
gnd deploy -l v0.0.1 my-name/my-subgraph
3434
```
3535

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

176+
If `--version-label` is omitted in an interactive terminal, `gnd deploy` prompts for it.
177+
In non-interactive environments (CI/scripts), you must pass `--version-label`.
178+
176179
**Examples:**
177180

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

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

185-
# Deploy with version label
186-
gnd deploy -l v1.0.0 my-name/my-subgraph
188+
# Deploy without --version-label in an interactive terminal (prompts)
189+
gnd deploy my-name/my-subgraph
187190
```
188191

189192
### `gnd publish`

gnd/docs/migrating-from-graph-cli.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ graph deploy --studio my-subgraph
1515
# Use:
1616
gnd codegen
1717
gnd build
18-
gnd deploy my-subgraph # defaults to Studio
18+
gnd deploy my-subgraph # defaults to Studio; prompts for version label in interactive terminals
1919
```
2020

2121
## Command Mapping
@@ -25,7 +25,7 @@ gnd deploy my-subgraph # defaults to Studio
2525
| `graph init` | `gnd init` | Identical flags |
2626
| `graph codegen` | `gnd codegen` | Identical flags |
2727
| `graph build` | `gnd build` | Identical flags |
28-
| `graph deploy` | `gnd deploy` | Defaults to Studio if `--node` not provided |
28+
| `graph deploy` | `gnd deploy` | Defaults to Studio if `--node` not provided; pass `--version-label` in non-interactive mode |
2929
| `graph create` | `gnd create` | Identical flags |
3030
| `graph remove` | `gnd remove` | Identical flags |
3131
| `graph auth` | `gnd auth` | Identical flags |
@@ -208,12 +208,12 @@ Replace `graph` with `gnd` in your CI configuration:
208208
# Before
209209
- run: graph codegen
210210
- run: graph build
211-
- run: graph deploy --studio ${{ secrets.SUBGRAPH_NAME }}
211+
- run: graph deploy --studio -l ${{ github.sha }} ${{ secrets.SUBGRAPH_NAME }}
212212

213213
# After
214214
- run: gnd codegen
215215
- run: gnd build
216-
- run: gnd deploy ${{ secrets.SUBGRAPH_NAME }}
216+
- run: gnd deploy -l ${{ github.sha }} ${{ secrets.SUBGRAPH_NAME }}
217217
```
218218
219219
### Step 5: Update package.json Scripts (Optional)
@@ -225,11 +225,13 @@ If you have npm scripts calling graph-cli:
225225
"scripts": {
226226
"codegen": "gnd codegen",
227227
"build": "gnd build",
228-
"deploy": "gnd deploy"
228+
"deploy": "gnd deploy -l $VERSION_LABEL"
229229
}
230230
}
231231
```
232232

233+
Set `VERSION_LABEL` in your environment (for example, from a git tag or commit SHA).
234+
233235
## Troubleshooting
234236

235237
### "Command not found: gnd"

gnd/src/commands/deploy.rs

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
//! This command builds a subgraph (unless an IPFS hash is provided),
44
//! uploads it to IPFS, and deploys it to a Graph Node.
55
6-
use std::path::PathBuf;
6+
use std::{
7+
io::{self, IsTerminal},
8+
path::PathBuf,
9+
};
710

811
use anyhow::{Context, Result, anyhow};
912
use clap::Parser;
@@ -12,6 +15,7 @@ use url::Url;
1215
use crate::commands::auth::get_deploy_key;
1316
use crate::commands::build::{BuildOpt, run_build};
1417
use crate::output::{Step, step};
18+
use crate::prompt::{normalize_version_label, prompt_version_label};
1519
use crate::services::GraphNodeClient;
1620

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

98+
let version_label =
99+
match resolve_version_label(opt.version_label.as_deref(), io::stdin().is_terminal())? {
100+
Some(label) => label,
101+
None => prompt_version_label()?,
102+
};
103+
94104
// Get or build the IPFS hash
95105
let ipfs_hash = match &opt.ipfs_hash {
96106
Some(hash) => {
@@ -104,7 +114,14 @@ pub async fn run_deploy(opt: DeployOpt) -> Result<()> {
104114
};
105115

106116
// Deploy to Graph Node
107-
deploy_to_node(&opt, node, &ipfs_hash, deploy_key.as_deref()).await
117+
deploy_to_node(
118+
&opt,
119+
node,
120+
&ipfs_hash,
121+
&version_label,
122+
deploy_key.as_deref(),
123+
)
124+
.await
108125
}
109126

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

147+
/// Parse and validate a normalized version label.
148+
fn parse_version_label(label: &str) -> Result<String> {
149+
let normalized = normalize_version_label(label);
150+
if normalized.is_empty() {
151+
return Err(anyhow!("Version label cannot be empty"));
152+
}
153+
Ok(normalized)
154+
}
155+
156+
/// Resolve version label from flag or interactive prompt mode.
157+
fn resolve_version_label(
158+
version_label: Option<&str>,
159+
stdin_is_terminal: bool,
160+
) -> Result<Option<String>> {
161+
match version_label {
162+
Some(label) => parse_version_label(label)
163+
.context("Invalid --version-label value")
164+
.map(Some),
165+
None if stdin_is_terminal => Ok(None),
166+
None => Err(anyhow!(
167+
"--version-label is required in non-interactive mode. \
168+
Pass --version-label <LABEL>."
169+
)),
170+
}
171+
}
172+
130173
/// Build the subgraph and upload to IPFS.
131174
async fn build_and_upload(opt: &DeployOpt) -> Result<String> {
132175
// Run the build command with IPFS upload enabled
@@ -155,6 +198,7 @@ async fn deploy_to_node(
155198
opt: &DeployOpt,
156199
node: &str,
157200
ipfs_hash: &str,
201+
version_label: &str,
158202
deploy_key: Option<&str>,
159203
) -> Result<()> {
160204
step(Step::Deploy, &format!("Deploying to {}", node));
@@ -165,7 +209,7 @@ async fn deploy_to_node(
165209
.deploy_subgraph(
166210
&opt.subgraph_name,
167211
ipfs_hash,
168-
opt.version_label.as_deref(),
212+
Some(version_label),
169213
opt.debug_fork.as_deref(),
170214
)
171215
.await
@@ -240,4 +284,35 @@ mod tests {
240284
// Verify the default IPFS URL is valid
241285
assert!(validate_url(DEFAULT_IPFS_URL, "IPFS").is_ok());
242286
}
287+
288+
#[test]
289+
fn test_parse_version_label_rejects_empty_after_normalize() {
290+
assert!(parse_version_label("").is_err());
291+
assert!(parse_version_label(" ").is_err());
292+
assert!(parse_version_label("\"\"").is_err());
293+
assert!(parse_version_label(" \"\" ").is_err());
294+
assert!(parse_version_label("\" \"").is_err());
295+
}
296+
297+
#[test]
298+
fn test_resolve_version_label_from_flag() {
299+
assert_eq!(
300+
resolve_version_label(Some(" \"v1.0.0\" "), false).unwrap(),
301+
Some("v1.0.0".to_string())
302+
);
303+
}
304+
305+
#[test]
306+
fn test_resolve_version_label_interactive_without_flag() {
307+
assert_eq!(resolve_version_label(None, true).unwrap(), None);
308+
}
309+
310+
#[test]
311+
fn test_resolve_version_label_non_interactive_requires_flag() {
312+
let err = resolve_version_label(None, false).unwrap_err();
313+
assert!(
314+
err.to_string()
315+
.contains("--version-label is required in non-interactive mode")
316+
);
317+
}
243318
}

gnd/src/prompt.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use std::path::Path;
77

88
use anyhow::Result;
99
use console::{Term, style};
10+
use inquire::parser::CustomTypeParser;
1011
use inquire::validator::Validation;
11-
use inquire::{Autocomplete, Confirm, CustomUserError, Select, Text};
12+
use inquire::{Autocomplete, Confirm, CustomType, CustomUserError, Select, Text};
1213

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

391+
/// Normalize a version label from either a flag or interactive prompt.
392+
pub fn normalize_version_label(label: &str) -> String {
393+
label.trim_matches(|c| c == '"' || c == ' ').to_string()
394+
}
395+
396+
/// Prompt for a version label for deployment.
397+
/// Uses CustomType instead of Text to normalize the input before validation
398+
pub fn prompt_version_label() -> Result<String> {
399+
let parser: CustomTypeParser<String> = &|val| {
400+
let normalized = normalize_version_label(val);
401+
Ok(normalized)
402+
};
403+
let label = CustomType::<String>::new("Which version label to use? (e.g. v0.0.1):")
404+
.with_parser(parser)
405+
.with_validator(|input: &String| {
406+
if input.is_empty() {
407+
Ok(Validation::Invalid("Version label cannot be empty".into()))
408+
} else {
409+
Ok(Validation::Valid)
410+
}
411+
})
412+
.prompt()?;
413+
414+
Ok(label)
415+
}
416+
390417
/// Interactive init form for when options are not fully provided.
391418
pub struct InitForm {
392419
pub network: String,
@@ -649,6 +676,16 @@ mod tests {
649676
assert_eq!(get_subgraph_basename(""), "");
650677
}
651678

679+
#[test]
680+
fn test_normalize_version_label() {
681+
assert_eq!(normalize_version_label("v1.0.0"), "v1.0.0");
682+
assert_eq!(normalize_version_label(" v1.0.0 "), "v1.0.0");
683+
assert_eq!(normalize_version_label("\"v1.0.0\""), "v1.0.0");
684+
assert_eq!(normalize_version_label(" \"v1.0.0\" "), "v1.0.0");
685+
assert_eq!(normalize_version_label("v1.0.0-beta"), "v1.0.0-beta");
686+
assert_eq!(normalize_version_label(" my version "), "my version");
687+
}
688+
652689
#[test]
653690
fn test_format_network() {
654691
let network = Network {

0 commit comments

Comments
 (0)