diff --git a/README.md b/README.md index 289eb16..29c8e42 100644 --- a/README.md +++ b/README.md @@ -628,22 +628,22 @@ paper content, license, owning project). # List datasets for one or more projects (by numeric project ID) gitlink-cli dataset +list --ids 5988 -# View a repository's dataset and attachments -gitlink-cli dataset +view --owner Gitlink --repo forgeplus +# Preview a repository dataset request; live backend is guarded until Issue #21 is resolved +gitlink-cli dataset +view --owner Gitlink --repo forgeplus --dry-run -# Create / update a repository's dataset (preview first with --dry-run) +# Create / update preview only until the backend routes are deployed gitlink-cli dataset +create --owner me --repo proj -t "My dataset" -d "..." --license-id 359 --dry-run -gitlink-cli dataset +update --owner me --repo proj -t "My dataset" -d "updated" +gitlink-cli dataset +update --owner me --repo proj -t "My dataset" -d "updated" --dry-run -# Delete a dataset attachment (destructive: preview, then confirm with --yes) +# Delete attachment preview only until the backend route is deployed gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --dry-run -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --yes ``` -> Note: `dataset +list` (platform dataset query) is verified on production -> gitlink.org.cn. The per-repo `+view`/`+create`/`+update` routes follow the -> published OpenAPI contract but are not yet deployed on production (they return -> 404 there); they will work once the platform enables them. +> Note: `dataset +list` (platform dataset query) is verified on production. +> The per-repo `+view`/`+create`/`+update` and attachment deletion routes are +> guarded in gitlink-cli until GitLink Issue #21 is resolved, because the +> corresponding backend routes currently return 404 on production. Use `--dry-run` +> to inspect the request contract without calling the unavailable endpoints. ### Raw API diff --git a/README.zh-CN.md b/README.zh-CN.md index ce6f964..c238121 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -506,19 +506,18 @@ gitlink-cli profile +contribution --user zhangsan --year 2025 # 按数字项目 ID 列出一个或多个项目的数据集 gitlink-cli dataset +list --ids 5988 -# 查看仓库的数据集及其附件 -gitlink-cli dataset +view --owner Gitlink --repo forgeplus +# 预览仓库数据集请求;Issue #21 解决前不会调用未部署后端 +gitlink-cli dataset +view --owner Gitlink --repo forgeplus --dry-run -# 创建 / 更新仓库数据集(先用 --dry-run 预览) +# 创建 / 更新仓库数据集当前仅支持预览,等待后端路由部署 gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run -gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新" +gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新" --dry-run -# 删除数据集附件(破坏性:先预览,再用 --yes 确认) +# 删除数据集附件当前仅支持预览,等待后端路由部署 gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --dry-run -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --yes ``` -> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。 +> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 和附件删除接口在 Issue #21 解决前会被 CLI 保护,不再直接调用当前生产环境会 404 的后端路由;可用 `--dry-run` 查看请求契约。 ### Raw API Shortcuts 未覆盖的接口可通过 Raw API 直接调用: diff --git a/doc/changes/dataset-backend-guard.md b/doc/changes/dataset-backend-guard.md new file mode 100644 index 0000000..2d31fa9 --- /dev/null +++ b/doc/changes/dataset-backend-guard.md @@ -0,0 +1,38 @@ +# Dataset Backend Guard for Issue #21 + +## Background + +GitLink Issue #21 reports that PR #243 added dataset shortcuts for five endpoints, but production currently deploys only `GET /api/v1/project_datasets`. The repository-scoped dataset endpoints still return 404: + +- `GET /api/v1/{owner}/{repo}/dataset` +- `POST /api/v1/{owner}/{repo}/dataset` +- `PUT /api/v1/{owner}/{repo}/dataset` +- `DELETE /api/attachments/{uuid}` + +## What Changed + +This change keeps the verified command available: + +- `dataset +list --ids ...` + +And guards the unavailable backend routes: + +- `dataset +view` now supports `--dry-run` to preview the request and otherwise returns a clear Issue #21 error without calling the 404 endpoint. +- `dataset +create` and `dataset +update` keep `--dry-run` previews but return the same guard error for live writes. +- `dataset +delete-attachment` keeps `--dry-run` previews and `--yes` validation, then returns the guard error instead of calling the unavailable delete endpoint. + +## Why + +This prevents users and Agents from repeatedly hitting known 404 production routes while preserving the OpenAPI request contract for review and future backend rollout. + +## Validation + +```bash +git diff --check +GOPROXY=https://goproxy.cn,direct go test ./shortcuts/dataset ./shortcuts +go vet ./shortcuts/dataset ./shortcuts +go run . dataset +view --owner Gitlink --repo forgeplus --dry-run --format json +go run . dataset +create --owner Gitlink --repo forgeplus -t demo -d demo --dry-run --format json +GOPROXY=https://goproxy.cn,direct go test ./... +go vet ./... +``` diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..e79ef1c 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -133,6 +133,7 @@ "flag.dataset.description": "Dataset description", "flag.dataset.dry_run": "Preview the request without writing the dataset", "flag.dataset.dry_run_delete": "Preview the request without deleting the attachment", + "flag.dataset.dry_run_view": "Preview the repository dataset request without calling the guarded backend endpoint", "flag.dataset.ids": "Comma-separated project IDs to query datasets for", "flag.dataset.license_id": "License ID", "flag.dataset.limit": "Attachment page size", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..dccd9d9 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -133,6 +133,7 @@ "flag.dataset.description": "数据集描述", "flag.dataset.dry_run": "预览请求,不写入数据集", "flag.dataset.dry_run_delete": "预览请求,不删除附件", + "flag.dataset.dry_run_view": "预览仓库数据集请求,不调用受保护的后端接口", "flag.dataset.ids": "用于查询数据集的项目 ID,逗号分隔", "flag.dataset.license_id": "许可证 ID", "flag.dataset.limit": "附件每页数量", diff --git a/shortcuts/dataset/dataset.go b/shortcuts/dataset/dataset.go index aedc40c..409ea9e 100644 --- a/shortcuts/dataset/dataset.go +++ b/shortcuts/dataset/dataset.go @@ -18,6 +18,8 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) +const datasetBackendIssue = "GitLink dataset repository endpoints are not deployed yet; see https://www.gitlink.org.cn/Gitlink/gitlink-cli/issues/21" + // Shortcuts returns dataset management and query shortcuts. func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { tr := shortcutTranslator(translators...) @@ -38,6 +40,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { Flags: []common.Flag{ {Name: "page", Short: "p", Usage: tr.T("flag.dataset.page"), Default: "1"}, {Name: "limit", Short: "l", Usage: tr.T("flag.dataset.limit"), Default: "20"}, + {Name: "dry-run", Usage: tr.T("flag.dataset.dry_run_view"), Bool: true, Default: "false"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -46,11 +49,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { q := url.Values{} setIfPresent(q, "page", ctx.Arg("page")) setIfPresent(q, "limit", ctx.Arg("limit")) - env, err := ctx.CallAPIWithQuery("GET", repoDatasetPath(ctx), q) - if err != nil { - return err + path := repoDatasetPath(ctx) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, "action": "view_dataset", "method": "GET", "path": path, "query": q.Encode(), + }) } - return ctx.Output(env) + return datasetBackendUnavailable("dataset +view") }, }, { @@ -122,11 +127,7 @@ func runWrite(method, action string) func(ctx *common.RuntimeContext) error { "dry_run": true, "action": action, "method": method, "path": path, "body": body, }) } - env, err := ctx.CallAPI(method, path, body) - if err != nil { - return err - } - return ctx.Output(env) + return datasetBackendUnavailable("dataset +" + strings.TrimSuffix(action, "_dataset")) } } @@ -145,11 +146,11 @@ func runDeleteAttachment(ctx *common.RuntimeContext) error { if ctx.Arg("yes") != "true" { return fmt.Errorf("%s", ctx.Tr.T("error.dataset.delete_confirm")) } - env, err := ctx.CallAPI("DELETE", path, nil) - if err != nil { - return err - } - return ctx.Output(env) + return datasetBackendUnavailable("dataset +delete-attachment") +} + +func datasetBackendUnavailable(command string) error { + return fmt.Errorf("%s is guarded because %s", command, datasetBackendIssue) } // datasetBody builds the create/update request body and validates inputs. diff --git a/shortcuts/dataset/dataset_test.go b/shortcuts/dataset/dataset_test.go index 03e1674..3df6098 100644 --- a/shortcuts/dataset/dataset_test.go +++ b/shortcuts/dataset/dataset_test.go @@ -52,23 +52,25 @@ func decodeBody(t *testing.T, r *http.Request) map[string]interface{} { // --- view --- -func TestDatasetView(t *testing.T) { +func TestDatasetViewDryRunDoesNotCallGuardedBackend(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v1/alice/demo/dataset.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if got := r.URL.Query().Get("page"); got != "2" { - t.Fatalf("page = %q, want 2", got) - } - if got := r.URL.Query().Get("limit"); got != "5" { - t.Fatalf("limit = %q, want 5", got) - } - writeJSON(w, map[string]interface{}{"id": float64(1), "attachments": []interface{}{}}) + t.Fatal("no API call expected in dry-run") })) defer server.Close() - if err := runShortcut(t, server, "view", map[string]string{"page": "2", "limit": "5"}); err != nil { - t.Fatalf("view failed: %v", err) + if err := runShortcut(t, server, "view", map[string]string{"page": "2", "limit": "5", "dry-run": "true"}); err != nil { + t.Fatalf("view dry-run failed: %v", err) + } +} + +func TestDatasetViewGuardedUntilBackendIsAvailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("guarded backend endpoint should not be called") + })) + defer server.Close() + + if err := runShortcut(t, server, "view", map[string]string{"page": "2", "limit": "5"}); err == nil { + t.Fatal("expected backend guard error") } } @@ -104,28 +106,15 @@ func TestDatasetListInvalidIDs(t *testing.T) { // --- create --- -func TestDatasetCreate(t *testing.T) { +func TestDatasetCreateGuardedUntilBackendIsAvailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v1/alice/demo/dataset.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if r.Method != http.MethodPost { - t.Fatalf("method = %s, want POST", r.Method) - } - body := decodeBody(t, r) - if body["title"] != "DS" || body["description"] != "desc" { - t.Fatalf("unexpected body: %v", body) - } - if body["license_id"] != float64(359) { - t.Fatalf("license_id = %v, want 359", body["license_id"]) - } - writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + t.Fatal("guarded backend endpoint should not be called") })) defer server.Close() args := map[string]string{"title": "DS", "description": "desc", "license-id": "359", "paper-content": "x"} - if err := runShortcut(t, server, "create", args); err != nil { - t.Fatalf("create failed: %v", err) + if err := runShortcut(t, server, "create", args); err == nil { + t.Fatal("expected backend guard error") } } @@ -166,21 +155,15 @@ func TestDatasetCreateDryRun(t *testing.T) { // --- update --- -func TestDatasetUpdate(t *testing.T) { +func TestDatasetUpdateGuardedUntilBackendIsAvailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v1/alice/demo/dataset.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if r.Method != http.MethodPut { - t.Fatalf("method = %s, want PUT", r.Method) - } - writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + t.Fatal("guarded backend endpoint should not be called") })) defer server.Close() args := map[string]string{"title": "DS2", "description": "desc2"} - if err := runShortcut(t, server, "update", args); err != nil { - t.Fatalf("update failed: %v", err) + if err := runShortcut(t, server, "update", args); err == nil { + t.Fatal("expected backend guard error") } } @@ -209,20 +192,14 @@ func TestDatasetDeleteAttachmentRequiresConfirm(t *testing.T) { } } -func TestDatasetDeleteAttachmentConfirmed(t *testing.T) { +func TestDatasetDeleteAttachmentConfirmedIsGuardedUntilBackendIsAvailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/attachments/abc-123.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if r.Method != http.MethodDelete { - t.Fatalf("method = %s, want DELETE", r.Method) - } - writeJSON(w, map[string]interface{}{"status": float64(0), "message": "删除成功"}) + t.Fatal("guarded backend endpoint should not be called") })) defer server.Close() args := map[string]string{"uuid": "abc-123", "yes": "true"} - if err := runShortcut(t, server, "delete-attachment", args); err != nil { - t.Fatalf("delete failed: %v", err) + if err := runShortcut(t, server, "delete-attachment", args); err == nil { + t.Fatal("expected backend guard error") } } diff --git a/skills/gitlink-dataset/SKILL.md b/skills/gitlink-dataset/SKILL.md new file mode 100644 index 0000000..980c473 --- /dev/null +++ b/skills/gitlink-dataset/SKILL.md @@ -0,0 +1,37 @@ +--- +name: gitlink-dataset +version: 1.0.0 +description: "GitLink dataset query and guarded repository dataset request previews. Trigger when users need dataset +list or need to inspect repository dataset request contracts while Issue #21 backend routes are pending." +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli dataset --help" +--- + +# GitLink Dataset Skill + +Use this skill for GitLink research dataset operations. + +## Available Production Command + +```bash +# Query deployed platform dataset endpoint by numeric project IDs +gitlink-cli dataset +list --ids 5988 --format json +``` + +## Guarded Commands Pending Issue #21 + +The following repository-scoped dataset endpoints are documented by OpenAPI but are not deployed on production yet. Use `--dry-run` only until Issue #21 is resolved. + +```bash +gitlink-cli dataset +view --owner OWNER --repo REPO --dry-run --format json +gitlink-cli dataset +create --owner OWNER --repo REPO -t "Dataset" -d "Description" --dry-run --format json +gitlink-cli dataset +update --owner OWNER --repo REPO -t "Dataset" -d "Description" --dry-run --format json +gitlink-cli dataset +delete-attachment --owner OWNER --repo REPO --uuid UUID --dry-run --format json +``` + +## Safety Rules + +- Prefer `dataset +list` for production usage today. +- Do not run live `+view`, `+create`, `+update`, or `+delete-attachment` until GitLink Issue #21 is resolved. +- The CLI intentionally guards these live calls to avoid known production 404 responses.