Compare commits
1 Commits
master
...
feat/api-b
| Author | SHA1 | Date |
|---|---|---|
|
|
2d0972fea0 |
|
|
@ -695,6 +695,10 @@ Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-s
|
|||
|
||||
# With query parameters
|
||||
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
|
||||
|
||||
# Multi-step batch plan, including saved variables from earlier responses
|
||||
gitlink-cli api --batch-file plan.json --dry-run
|
||||
gitlink-cli api --batch-file plan.json --var owner=Gitlink --var repo=forgeplus
|
||||
```
|
||||
|
||||
## Global Parameters
|
||||
|
|
|
|||
|
|
@ -569,6 +569,10 @@ Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-s
|
|||
|
||||
# 带查询参数
|
||||
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
|
||||
|
||||
# 多步骤批处理,可保存前序响应字段作为后续变量
|
||||
gitlink-cli api --batch-file plan.json --dry-run
|
||||
gitlink-cli api --batch-file plan.json --var owner=Gitlink --var repo=forgeplus
|
||||
```
|
||||
|
||||
## 全局参数
|
||||
|
|
|
|||
|
|
@ -402,3 +402,87 @@ func writeBatchPlan(t *testing.T, payload interface{}) string {
|
|||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestRunAPIBatchSavesVarsForLaterRequests(t *testing.T) {
|
||||
var gotBody map[string]interface{}
|
||||
var seen []string
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen = append(seen, r.Method+" "+r.URL.Path)
|
||||
switch r.URL.Path {
|
||||
case "/v1/owner/repo/issues.json":
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issues": []interface{}{
|
||||
map[string]interface{}{"id": float64(42), "subject": "Saved subject"},
|
||||
},
|
||||
})
|
||||
case "/v1/owner/repo/issues/42/journals.json":
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": 99})
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"vars": map[string]string{"owner": "owner", "repo": "repo"},
|
||||
"requests": []map[string]interface{}{
|
||||
{
|
||||
"name": "find-issue",
|
||||
"method": "GET",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues",
|
||||
"save": map[string]string{
|
||||
"issue": "issues.0.id",
|
||||
"subject": "data.issues.0.subject",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "comment-{{issue}}",
|
||||
"method": "POST",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues/{{issue}}/journals",
|
||||
"body": map[string]interface{}{"notes": "Follow up: {{subject}}"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("batch execute error: %v", err)
|
||||
}
|
||||
if len(seen) != 2 {
|
||||
t.Fatalf("seen = %v, want 2 requests", seen)
|
||||
}
|
||||
if gotBody["notes"] != "Follow up: Saved subject" {
|
||||
t.Fatalf("notes = %#v", gotBody["notes"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAPIBatchDryRunAllowsSavedVars(t *testing.T) {
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("dry-run should not reach server")
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"requests": []map[string]interface{}{
|
||||
{"method": "GET", "path": "/issues", "save": map[string]string{"issue": "issues.0.id"}},
|
||||
{"method": "GET", "path": "/issues/{{issue}}"},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan, "--dry-run"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("dry-run with future save var failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBatchSavedVarsErrorsOnMissingPath(t *testing.T) {
|
||||
_, err := extractBatchSavedVars(map[string]interface{}{"issues": []interface{}{}}, map[string]string{"issue": "issues.0.id"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing path error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
272
cmd/api/batch.go
272
cmd/api/batch.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -28,25 +29,28 @@ type batchRequest struct {
|
|||
Path string `json:"path"`
|
||||
Query map[string]interface{} `json:"query"`
|
||||
Body interface{} `json:"body"`
|
||||
Save map[string]string `json:"save"`
|
||||
}
|
||||
|
||||
type renderedBatchRequest struct {
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Query url.Values `json:"query,omitempty" yaml:"query,omitempty"`
|
||||
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Query url.Values `json:"query,omitempty" yaml:"query,omitempty"`
|
||||
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Save map[string]string `json:"save,omitempty" yaml:"save,omitempty"`
|
||||
}
|
||||
|
||||
type batchResult struct {
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
OK bool `json:"ok" yaml:"ok"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
OK bool `json:"ok" yaml:"ok"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Saved map[string]string `json:"saved,omitempty" yaml:"saved,omitempty"`
|
||||
}
|
||||
|
||||
type batchSummary struct {
|
||||
|
|
@ -79,7 +83,7 @@ func runAPIBatch(c *cobra.Command, batchFile string) error {
|
|||
}
|
||||
|
||||
vars := mergeBatchVars(plan.Vars, overrides)
|
||||
requests, err := renderBatchRequests(plan.Requests, vars)
|
||||
requests, err := renderBatchRequestsForDryRun(plan.Requests, vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -102,17 +106,16 @@ func runAPIBatch(c *cobra.Command, batchFile string) error {
|
|||
summary := batchSummary{
|
||||
DryRun: false,
|
||||
ContinueOnError: continueOnError,
|
||||
Total: len(requests),
|
||||
Total: len(plan.Requests),
|
||||
Variables: sortedVars(vars),
|
||||
Results: make([]batchResult, 0, len(requests)),
|
||||
Results: make([]batchResult, 0, len(plan.Requests)),
|
||||
}
|
||||
for _, req := range requests {
|
||||
result := batchResult{
|
||||
Index: req.Index,
|
||||
Name: req.Name,
|
||||
Method: req.Method,
|
||||
Path: req.Path,
|
||||
for i, rawReq := range plan.Requests {
|
||||
req, err := renderBatchRequest(i, rawReq, vars, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := batchResult{Index: req.Index, Name: req.Name, Method: req.Method, Path: req.Path}
|
||||
env, callErr := cli.Do(req.Method, req.Path, req.Body, req.Query)
|
||||
if callErr != nil {
|
||||
summary.Failed++
|
||||
|
|
@ -129,6 +132,25 @@ func runAPIBatch(c *cobra.Command, batchFile string) error {
|
|||
result.OK = true
|
||||
if env != nil {
|
||||
result.Data = env.Data
|
||||
if len(req.Save) > 0 {
|
||||
saved, err := extractBatchSavedVars(env.Data, req.Save)
|
||||
if err != nil {
|
||||
summary.Succeeded--
|
||||
summary.Failed++
|
||||
result.OK = false
|
||||
result.Error = err.Error()
|
||||
summary.Results = append(summary.Results, result)
|
||||
if !continueOnError {
|
||||
_ = output.Print(output.SuccessEnvelope(summary, nil), resolveFormat())
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for key, value := range saved {
|
||||
vars[key] = value
|
||||
}
|
||||
result.Saved = saved
|
||||
}
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
|
@ -188,46 +210,67 @@ func mergeBatchVars(planVars, overrides map[string]string) map[string]string {
|
|||
func renderBatchRequests(requests []batchRequest, vars map[string]string) ([]renderedBatchRequest, error) {
|
||||
rendered := make([]renderedBatchRequest, 0, len(requests))
|
||||
for i, req := range requests {
|
||||
method := strings.ToUpper(strings.TrimSpace(req.Method))
|
||||
if method == "" {
|
||||
return nil, fmt.Errorf("request %d method is required", i+1)
|
||||
}
|
||||
path, err := renderTemplate(req.Path, vars)
|
||||
renderedReq, err := renderBatchRequest(i, req, vars, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d path: %w", i+1, err)
|
||||
return nil, err
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("request %d path is required", i+1)
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
query, err := renderBatchQuery(req.Query, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d query: %w", i+1, err)
|
||||
}
|
||||
body, err := renderBatchValue(req.Body, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d body: %w", i+1, err)
|
||||
}
|
||||
name, err := renderTemplate(req.Name, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d name: %w", i+1, err)
|
||||
}
|
||||
rendered = append(rendered, renderedBatchRequest{
|
||||
Index: i + 1,
|
||||
Name: name,
|
||||
Method: method,
|
||||
Path: path,
|
||||
Query: query,
|
||||
Body: body,
|
||||
})
|
||||
rendered = append(rendered, renderedReq)
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func renderBatchRequestsForDryRun(requests []batchRequest, vars map[string]string) ([]renderedBatchRequest, error) {
|
||||
rendered := make([]renderedBatchRequest, 0, len(requests))
|
||||
for i, req := range requests {
|
||||
renderedReq, err := renderBatchRequest(i, req, vars, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rendered = append(rendered, renderedReq)
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func renderBatchRequest(i int, req batchRequest, vars map[string]string, allowMissing bool) (renderedBatchRequest, error) {
|
||||
method := strings.ToUpper(strings.TrimSpace(req.Method))
|
||||
if method == "" {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d method is required", i+1)
|
||||
}
|
||||
path, err := renderTemplateWithOptions(req.Path, vars, allowMissing)
|
||||
if err != nil {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d path: %w", i+1, err)
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d path is required", i+1)
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
query, err := renderBatchQueryWithOptions(req.Query, vars, allowMissing)
|
||||
if err != nil {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d query: %w", i+1, err)
|
||||
}
|
||||
body, err := renderBatchValueWithOptions(req.Body, vars, allowMissing)
|
||||
if err != nil {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d body: %w", i+1, err)
|
||||
}
|
||||
name, err := renderTemplateWithOptions(req.Name, vars, allowMissing)
|
||||
if err != nil {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d name: %w", i+1, err)
|
||||
}
|
||||
save, err := renderBatchSave(req.Save, vars, allowMissing)
|
||||
if err != nil {
|
||||
return renderedBatchRequest{}, fmt.Errorf("request %d save: %w", i+1, err)
|
||||
}
|
||||
return renderedBatchRequest{Index: i + 1, Name: name, Method: method, Path: path, Query: query, Body: body, Save: save}, nil
|
||||
}
|
||||
|
||||
func renderBatchQuery(raw map[string]interface{}, vars map[string]string) (url.Values, error) {
|
||||
return renderBatchQueryWithOptions(raw, vars, false)
|
||||
}
|
||||
|
||||
func renderBatchQueryWithOptions(raw map[string]interface{}, vars map[string]string, allowMissing bool) (url.Values, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -238,11 +281,11 @@ func renderBatchQuery(raw map[string]interface{}, vars map[string]string) (url.V
|
|||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
renderedKey, err := renderTemplate(key, vars)
|
||||
renderedKey, err := renderTemplateWithOptions(key, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := renderQueryValues(raw[key], vars)
|
||||
values, err := renderQueryValuesWithOptions(raw[key], vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
|
|
@ -254,16 +297,20 @@ func renderBatchQuery(raw map[string]interface{}, vars map[string]string) (url.V
|
|||
}
|
||||
|
||||
func renderQueryValues(raw interface{}, vars map[string]string) ([]string, error) {
|
||||
return renderQueryValuesWithOptions(raw, vars, false)
|
||||
}
|
||||
|
||||
func renderQueryValuesWithOptions(raw interface{}, vars map[string]string, allowMissing bool) ([]string, error) {
|
||||
switch value := raw.(type) {
|
||||
case nil:
|
||||
return []string{""}, nil
|
||||
case string:
|
||||
rendered, err := renderTemplate(value, vars)
|
||||
rendered, err := renderTemplateWithOptions(value, vars, allowMissing)
|
||||
return []string{rendered}, err
|
||||
case []interface{}:
|
||||
values := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
itemValues, err := renderQueryValues(item, vars)
|
||||
itemValues, err := renderQueryValuesWithOptions(item, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -276,15 +323,19 @@ func renderQueryValues(raw interface{}, vars map[string]string) ([]string, error
|
|||
}
|
||||
|
||||
func renderBatchValue(raw interface{}, vars map[string]string) (interface{}, error) {
|
||||
return renderBatchValueWithOptions(raw, vars, false)
|
||||
}
|
||||
|
||||
func renderBatchValueWithOptions(raw interface{}, vars map[string]string, allowMissing bool) (interface{}, error) {
|
||||
switch value := raw.(type) {
|
||||
case nil:
|
||||
return nil, nil
|
||||
case string:
|
||||
return renderTemplate(value, vars)
|
||||
return renderTemplateWithOptions(value, vars, allowMissing)
|
||||
case []interface{}:
|
||||
items := make([]interface{}, 0, len(value))
|
||||
for _, item := range value {
|
||||
rendered, err := renderBatchValue(item, vars)
|
||||
rendered, err := renderBatchValueWithOptions(item, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -294,11 +345,11 @@ func renderBatchValue(raw interface{}, vars map[string]string) (interface{}, err
|
|||
case map[string]interface{}:
|
||||
obj := make(map[string]interface{}, len(value))
|
||||
for key, item := range value {
|
||||
renderedKey, err := renderTemplate(key, vars)
|
||||
renderedKey, err := renderTemplateWithOptions(key, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rendered, err := renderBatchValue(item, vars)
|
||||
rendered, err := renderBatchValueWithOptions(item, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -310,7 +361,43 @@ func renderBatchValue(raw interface{}, vars map[string]string) (interface{}, err
|
|||
}
|
||||
}
|
||||
|
||||
func renderBatchSave(raw map[string]string, vars map[string]string, allowMissing bool) (map[string]string, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
save := make(map[string]string, len(raw))
|
||||
keys := make([]string, 0, len(raw))
|
||||
for key := range raw {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
renderedKey, err := renderTemplateWithOptions(key, vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renderedKey = strings.TrimSpace(renderedKey)
|
||||
if renderedKey == "" {
|
||||
return nil, fmt.Errorf("save variable name cannot be empty")
|
||||
}
|
||||
renderedPath, err := renderTemplateWithOptions(raw[key], vars, allowMissing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renderedPath = strings.TrimSpace(renderedPath)
|
||||
if renderedPath == "" {
|
||||
return nil, fmt.Errorf("save path for %q cannot be empty", renderedKey)
|
||||
}
|
||||
save[renderedKey] = renderedPath
|
||||
}
|
||||
return save, nil
|
||||
}
|
||||
|
||||
func renderTemplate(value string, vars map[string]string) (string, error) {
|
||||
return renderTemplateWithOptions(value, vars, false)
|
||||
}
|
||||
|
||||
func renderTemplateWithOptions(value string, vars map[string]string, allowMissing bool) (string, error) {
|
||||
var missing []string
|
||||
rendered := templatePattern.ReplaceAllStringFunc(value, func(match string) string {
|
||||
parts := templatePattern.FindStringSubmatch(match)
|
||||
|
|
@ -319,6 +406,9 @@ func renderTemplate(value string, vars map[string]string) (string, error) {
|
|||
}
|
||||
replacement, ok := vars[parts[1]]
|
||||
if !ok {
|
||||
if allowMissing {
|
||||
return match
|
||||
}
|
||||
missing = append(missing, parts[1])
|
||||
return match
|
||||
}
|
||||
|
|
@ -331,6 +421,62 @@ func renderTemplate(value string, vars map[string]string) (string, error) {
|
|||
return rendered, nil
|
||||
}
|
||||
|
||||
func extractBatchSavedVars(data interface{}, save map[string]string) (map[string]string, error) {
|
||||
if len(save) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
saved := make(map[string]string, len(save))
|
||||
keys := make([]string, 0, len(save))
|
||||
for key := range save {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
value, err := extractBatchPath(data, save[key])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("save %s from %s: %w", key, save[key], err)
|
||||
}
|
||||
saved[key] = fmt.Sprint(value)
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func extractBatchPath(data interface{}, path string) (interface{}, error) {
|
||||
current := data
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) > 0 && parts[0] == "data" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
return nil, fmt.Errorf("empty path segment")
|
||||
}
|
||||
switch value := current.(type) {
|
||||
case map[string]interface{}:
|
||||
next, ok := value[part]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field %q not found", part)
|
||||
}
|
||||
current = next
|
||||
case []interface{}:
|
||||
index, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("segment %q is not an array index", part)
|
||||
}
|
||||
if index < 0 || index >= len(value) {
|
||||
return nil, fmt.Errorf("array index %d out of range", index)
|
||||
}
|
||||
current = value[index]
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot descend into %T", current)
|
||||
}
|
||||
}
|
||||
if current == nil {
|
||||
return nil, fmt.Errorf("value is null")
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func sortedVars(vars map[string]string) map[string]string {
|
||||
if len(vars) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# API Batch Saved Variables
|
||||
|
||||
## Summary
|
||||
|
||||
Enhances `gitlink-cli api --batch-file` so a request can save fields from its response and reuse them in later requests.
|
||||
|
||||
A batch request can now include:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "find-issue",
|
||||
"method": "GET",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues",
|
||||
"save": {
|
||||
"issue": "issues.0.id",
|
||||
"subject": "data.issues.0.subject"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Later requests can reference `{{issue}}` and `{{subject}}` in `name`, `path`, `query`, `body`, and `save` fields.
|
||||
|
||||
## JSON path syntax
|
||||
|
||||
- Dot-separated object fields: `issues.0.id`
|
||||
- Array indexes: `issues.0.id`
|
||||
- Optional leading `data.` is accepted for readability: `data.issues.0.id`
|
||||
|
||||
## Safety and behavior
|
||||
|
||||
- `--dry-run` now tolerates variables that will be produced by earlier `save` entries, preserving unresolved `{{var}}` placeholders in the preview.
|
||||
- If a save path is missing during execution, the batch result records the error and obeys `--continue-on-error`.
|
||||
- Each result includes a `saved` map when variables were extracted successfully.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
GOPROXY=https://goproxy.cn,direct go test ./cmd/api
|
||||
go vet ./cmd/api
|
||||
GOPROXY=https://goproxy.cn,direct go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
|
@ -19,14 +19,18 @@
|
|||
"query": {
|
||||
"state": "open",
|
||||
"limit": 20
|
||||
},
|
||||
"save": {
|
||||
"issue": "issues.0.id",
|
||||
"subject": "data.issues.0.subject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "comment-issue",
|
||||
"name": "comment-issue-{{issue}}",
|
||||
"method": "POST",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues/{{issue}}/journals",
|
||||
"body": {
|
||||
"notes": "批处理自动评论"
|
||||
"notes": "批处理自动评论:{{subject}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -50,10 +54,21 @@ gitlink-cli api --batch-file plan.json --continue-on-error --format json
|
|||
| `requests[].path` | 是 | API 路径,可省略开头的 `/` |
|
||||
| `requests[].query` | 否 | 查询参数对象,值可为字符串、数字、布尔值或数组 |
|
||||
| `requests[].body` | 否 | JSON 请求体,字符串字段会做模板替换 |
|
||||
| `requests[].save` | 否 | 将响应字段保存为后续模板变量,格式为 `{ "变量名": "字段路径" }` |
|
||||
|
||||
## save 字段路径
|
||||
|
||||
`save` 支持简单点号路径:
|
||||
|
||||
- 对象字段:`issues.0.id`
|
||||
- 数组索引:`issues.0.id`
|
||||
- 可选 `data.` 前缀:`data.issues.0.id`
|
||||
|
||||
保存成功后,后续请求可以在 `name`、`path`、`query`、`body` 和 `save` 中使用 `{{变量名}}`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 默认遇到失败会停止;需要继续执行后续步骤时传 `--continue-on-error`。
|
||||
- 写入类操作先用 `--dry-run` 检查渲染后的路径、query 和 body。
|
||||
- 写入类操作先用 `--dry-run` 检查渲染后的路径、query 和 body;dry-run 会保留尚需由前序 `save` 产生的 `{{变量}}` 占位符。
|
||||
- `--var key=value` 可重复传入,并覆盖计划文件里的同名变量,适合在不同仓库或 Issue 上复用同一计划。
|
||||
- 批处理模式不能和单次请求的 `--body`、`--body-file`、`--body-stdin`、`--query`、`--header` 混用。
|
||||
|
|
|
|||
Loading…
Reference in New Issue