From 3c04513b4aeff78f14a22bf1382f90cb96aecdad Mon Sep 17 00:00:00 2001 From: 1os21ka23r9navae6mrro <1os21ka23r9navae6mrro@gmail.com> Date: Wed, 8 Jul 2026 14:18:51 +0000 Subject: [PATCH 1/3] feat(action): new command group for Gitea Actions workflows Wraps the v1 actions API (Api::V1::Projects::Actions): - action +list: workflow files under .gitea/workflows - action +runs -w : paginated run history of a workflow - action +run -w -r : trigger a run - action +rerun / +job-rerun: rerun a whole run or a single job - action +enable / +disable -w : per-workflow toggle (the server requires the workflow param; omitting it errors) Production-verified: list, runs, trigger (new run appeared in history), and disable/enable round-trip. Rerun endpoints surface the server message verbatim when gitea-hat rejects the rerun. 6 unit tests, README section, and bilingual i18n keys. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 21 ++++ internal/i18n/locales/en-US.json | 12 ++ internal/i18n/locales/zh-CN.json | 12 ++ shortcuts/action/action.go | 203 +++++++++++++++++++++++++++++++ shortcuts/action/action_test.go | 159 ++++++++++++++++++++++++ shortcuts/register.go | 3 + 6 files changed, 410 insertions(+) create mode 100644 shortcuts/action/action.go create mode 100644 shortcuts/action/action_test.go diff --git a/README.md b/README.md index e5e4318..21cd81f 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,27 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i ``` +### Gitea Actions + +```bash +# List workflow files (.gitea/workflows) +gitlink-cli action +list --owner Gitlink --repo forgeplus + +# List runs of a workflow +gitlink-cli action +runs --owner Gitlink --repo forgeplus -w ci.yml + +# Trigger a workflow run on a branch +gitlink-cli action +run --owner Gitlink --repo forgeplus -w ci.yml -r master + +# Rerun a whole run, or a single job +gitlink-cli action +rerun --owner Gitlink --repo forgeplus -i 6 +gitlink-cli action +job-rerun --owner Gitlink --repo forgeplus -i 6 -j build + +# Enable or disable a workflow +gitlink-cli action +disable --owner Gitlink --repo forgeplus -w ci.yml +gitlink-cli action +enable --owner Gitlink --repo forgeplus -w ci.yml +``` + ### Pipeline Operations ```bash diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..e255878 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -1,4 +1,12 @@ { + "cmd.action.disable.short": "Disable Actions for the repository", + "cmd.action.enable.short": "Enable Actions for the repository", + "cmd.action.job_rerun.short": "Rerun a single job of a run", + "cmd.action.list.short": "List workflow files (.gitea/workflows)", + "cmd.action.rerun.short": "Rerun all jobs of a run", + "cmd.action.run.short": "Trigger a workflow run", + "cmd.action.runs.short": "List runs of a workflow", + "cmd.action.short": "Gitea Actions (CI workflow) operations", "cmd.api.long": "Send arbitrary HTTP requests to the GitLink API. Authentication is injected automatically.", "cmd.api.short": "Make raw API requests to GitLink", "cmd.auth.login.short": "Login to GitLink", @@ -114,6 +122,10 @@ "error.missing_required_flag": "required flag --{name} is missing", "error.profile.user_required": "could not determine target user; pass --user or run gitlink-cli auth login", "error.unsupported_language": "unsupported language: {lang}", + "flag.action.job": "Job name within the run", + "flag.action.ref": "Branch or tag to run on", + "flag.action.run_id": "Run ID from action +runs", + "flag.action.workflow": "Workflow file name (e.g. ci.yml)", "flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure", "flag.api.batch_dry_run": "Preview batch requests without sending remote requests", "flag.api.batch_file": "Read an API batch plan from a JSON file", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..e3c7a5b 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -1,4 +1,12 @@ { + "cmd.action.disable.short": "为仓库停用 Actions", + "cmd.action.enable.short": "为仓库启用 Actions", + "cmd.action.job_rerun.short": "重跑运行中的单个任务", + "cmd.action.list.short": "列出工作流文件(.gitea/workflows)", + "cmd.action.rerun.short": "重跑一次运行的全部任务", + "cmd.action.run.short": "触发一次工作流运行", + "cmd.action.runs.short": "列出工作流的运行记录", + "cmd.action.short": "Gitea Actions(CI 工作流)操作", "cmd.api.long": "向 GitLink API 发送任意 HTTP 请求。认证信息会自动注入。", "cmd.api.short": "向 GitLink 发起原始 API 请求", "cmd.auth.login.short": "登录 GitLink", @@ -114,6 +122,10 @@ "error.missing_required_flag": "缺少必需参数 --{name}", "error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录", "error.unsupported_language": "不支持的语言:{lang}", + "flag.action.job": "运行中的任务名", + "flag.action.ref": "运行所在的分支或标签", + "flag.action.run_id": "运行 ID(来自 action +runs)", + "flag.action.workflow": "工作流文件名(如 ci.yml)", "flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求", "flag.api.batch_dry_run": "预览批处理请求,不发送远端请求", "flag.api.batch_file": "从 JSON 文件读取 API 批处理计划", diff --git a/shortcuts/action/action.go b/shortcuts/action/action.go new file mode 100644 index 0000000..53dfc21 --- /dev/null +++ b/shortcuts/action/action.go @@ -0,0 +1,203 @@ +package action + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func actionsPath(ctx *common.RuntimeContext) string { + return fmt.Sprintf("/v1/%s/%s/actions", ctx.Owner, ctx.Repo) +} + +func requireIntArg(ctx *common.RuntimeContext, name string) (string, error) { + value, err := ctx.RequireArg(name) + if err != nil { + return "", err + } + if _, err := strconv.Atoi(value); err != nil { + return "", fmt.Errorf("--%s must be an integer, got %q", name, value) + } + return value, nil +} + +// Shortcuts returns Gitea Actions (CI workflow) shortcuts. +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) + return []*common.Shortcut{ + { + Name: "list", + Description: tr.T("cmd.action.list.short"), + Flags: []common.Flag{}, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", actionsPath(ctx), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "runs", + Description: tr.T("cmd.action.runs.short"), + Flags: []common.Flag{ + {Name: "workflow", Short: "w", Usage: tr.T("flag.action.workflow"), Required: true}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + workflow, err := ctx.RequireArg("workflow") + if err != nil { + return err + } + q := url.Values{} + q.Set("workflow", workflow) + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", actionsPath(ctx)+"/runs", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "run", + Description: tr.T("cmd.action.run.short"), + Flags: []common.Flag{ + {Name: "workflow", Short: "w", Usage: tr.T("flag.action.workflow"), Required: true}, + {Name: "ref", Short: "r", Usage: tr.T("flag.action.ref"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + workflow, err := ctx.RequireArg("workflow") + if err != nil { + return err + } + ref, err := ctx.RequireArg("ref") + if err != nil { + return err + } + q := url.Values{} + q.Set("workflow", workflow) + q.Set("ref", ref) + env, err := ctx.CallAPIWithQuery("POST", actionsPath(ctx)+"/runs", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "rerun", + Description: tr.T("cmd.action.rerun.short"), + Flags: []common.Flag{ + {Name: "run-id", Short: "i", Usage: tr.T("flag.action.run_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + runID, err := requireIntArg(ctx, "run-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/runs/%s/rerun", actionsPath(ctx), runID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "job-rerun", + Description: tr.T("cmd.action.job_rerun.short"), + Flags: []common.Flag{ + {Name: "run-id", Short: "i", Usage: tr.T("flag.action.run_id"), Required: true}, + {Name: "job", Short: "j", Usage: tr.T("flag.action.job"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + runID, err := requireIntArg(ctx, "run-id") + if err != nil { + return err + } + job, err := ctx.RequireArg("job") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/runs/%s/jobs/%s/rerun", actionsPath(ctx), runID, url.PathEscape(job)), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "enable", + Description: tr.T("cmd.action.enable.short"), + Flags: []common.Flag{ + {Name: "workflow", Short: "w", Usage: tr.T("flag.action.workflow"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + workflow, err := ctx.RequireArg("workflow") + if err != nil { + return err + } + q := url.Values{} + q.Set("workflow", workflow) + env, err := ctx.CallAPIWithQuery("POST", actionsPath(ctx)+"/enable", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "disable", + Description: tr.T("cmd.action.disable.short"), + Flags: []common.Flag{ + {Name: "workflow", Short: "w", Usage: tr.T("flag.action.workflow"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + workflow, err := ctx.RequireArg("workflow") + if err != nil { + return err + } + q := url.Values{} + q.Set("workflow", workflow) + env, err := ctx.CallAPIWithQuery("POST", actionsPath(ctx)+"/disable", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/action/action_test.go b/shortcuts/action/action_test.go new file mode 100644 index 0000000..1d5813d --- /dev/null +++ b/shortcuts/action/action_test.go @@ -0,0 +1,159 @@ +package action + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "owner", + Repo: "repo", + Format: "json", + Args: args, + } + return shortcut.Run(ctx) +} + +func findShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, s := range Shortcuts() { + if s.Name == name { + return s + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + t.Fatalf("write response: %v", err) + } +} + +func TestActionListUsesActionsEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/actions.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"total_count": 1, "files": []interface{}{}}) + })) + defer server.Close() + + if err := runShortcut(t, server, "list", map[string]string{}); err != nil { + t.Fatalf("list failed: %v", err) + } +} + +func TestActionRunsRequiresWorkflowAndForwardsQuery(t *testing.T) { + var query string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/actions/runs.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + query = r.URL.RawQuery + writeJSON(t, w, map[string]interface{}{"total_data": 0, "runs": []interface{}{}}) + })) + defer server.Close() + + if err := runShortcut(t, server, "runs", map[string]string{"workflow": "ci.yml", "page": "1", "limit": "20"}); err != nil { + t.Fatalf("runs failed: %v", err) + } + if !strings.Contains(query, "workflow=ci.yml") { + t.Fatalf("expected workflow in query, got %q", query) + } + + if err := runShortcut(t, server, "runs", map[string]string{}); err == nil { + t.Fatal("expected error when --workflow missing") + } +} + +func TestActionRunPostsWorkflowAndRef(t *testing.T) { + var query string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/actions/runs.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + query = r.URL.RawQuery + writeJSON(t, w, map[string]interface{}{"status": float64(0)}) + })) + defer server.Close() + + if err := runShortcut(t, server, "run", map[string]string{"workflow": "ci.yml", "ref": "master"}); err != nil { + t.Fatalf("run failed: %v", err) + } + if !strings.Contains(query, "workflow=ci.yml") || !strings.Contains(query, "ref=master") { + t.Fatalf("unexpected query: %q", query) + } +} + +func TestActionRerunValidatesRunID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/actions/runs/7/rerun.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0)}) + })) + defer server.Close() + + if err := runShortcut(t, server, "rerun", map[string]string{"run-id": "7"}); err != nil { + t.Fatalf("rerun failed: %v", err) + } + if err := runShortcut(t, server, "rerun", map[string]string{"run-id": "abc"}); err == nil { + t.Fatal("expected error for non-integer --run-id") + } +} + +func TestActionJobRerunBuildsNestedPath(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/actions/runs/7/jobs/build/rerun.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0)}) + })) + defer server.Close() + + if err := runShortcut(t, server, "job-rerun", map[string]string{"run-id": "7", "job": "build"}); err != nil { + t.Fatalf("job-rerun failed: %v", err) + } +} + +func TestActionEnableDisableEndpoints(t *testing.T) { + var paths []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + paths = append(paths, r.URL.Path) + writeJSON(t, w, map[string]interface{}{"status": float64(0)}) + })) + defer server.Close() + + if err := runShortcut(t, server, "enable", map[string]string{"workflow": "ci.yml"}); err != nil { + t.Fatalf("enable failed: %v", err) + } + if err := runShortcut(t, server, "disable", map[string]string{"workflow": "ci.yml"}); err != nil { + t.Fatalf("disable failed: %v", err) + } + if err := runShortcut(t, server, "disable", map[string]string{}); err == nil { + t.Fatal("expected error when --workflow missing") + } + want := []string{"/v1/owner/repo/actions/enable.json", "/v1/owner/repo/actions/disable.json"} + for i, p := range want { + if paths[i] != p { + t.Fatalf("expected %s, got %s", p, paths[i]) + } + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 1fedc7e..570ccd9 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/action" "github.com/gitlink-org/gitlink-cli/shortcuts/branch" "github.com/gitlink-org/gitlink-cli/shortcuts/ci" "github.com/gitlink-org/gitlink-cli/shortcuts/common" @@ -36,6 +37,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { tr = translators[0] } groups := map[string][]*common.Shortcut{ + "action": action.Shortcuts(tr), "repo": repo.Shortcuts(tr), "issue": issue.Shortcuts(tr), "label": label.Shortcuts(), @@ -61,6 +63,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { } descriptions := map[string]string{ + "action": tr.T("cmd.action.short"), "repo": tr.T("cmd.repo.short"), "issue": tr.T("cmd.issue.short"), "label": "Issue label operations", From 05a1859c63d837b9a340d5aa758be5e7cdb41f25 Mon Sep 17 00:00:00 2001 From: 1os21ka23r9navae6mrro <1os21ka23r9navae6mrro@gmail.com> Date: Wed, 8 Jul 2026 14:19:19 +0000 Subject: [PATCH 2/3] test: include action group in RegisterAll expectations Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- shortcuts/register_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 00f4c57..8e6f24e 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) { "repo", "issue", "label", "license", "pr", "profile", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "dataset", "health", "ignore", "wiki", + "dataset", "health", "ignore", "wiki", "action", } groupSet := map[string]bool{} From a31c53484ed31b1513ecdf69a520a34968b4e32a Mon Sep 17 00:00:00 2001 From: 1os21ka23r9navae6mrro <1os21ka23r9navae6mrro@gmail.com> Date: Wed, 8 Jul 2026 14:48:50 +0000 Subject: [PATCH 3/3] feat(action): add +logs for raw workflow job logs Wraps GET /actions/runs/:run_id/jobs/:job/logs (runs#job_logs, which redirects to the log file). Non-JSON body is printed as-is so the output pipes cleanly into grep/less. Production-verified on gitlink.org.cn (real run's job log streamed). Unit test covers the nested path and the --run-id integer guard. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 3 +++ internal/i18n/locales/en-US.json | 1 + internal/i18n/locales/zh-CN.json | 1 + shortcuts/action/action.go | 30 ++++++++++++++++++++++++++++++ shortcuts/action/action_test.go | 20 ++++++++++++++++++++ 5 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 21cd81f..dd25d93 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,9 @@ gitlink-cli action +run --owner Gitlink --repo forgeplus -w ci.yml -r master gitlink-cli action +rerun --owner Gitlink --repo forgeplus -i 6 gitlink-cli action +job-rerun --owner Gitlink --repo forgeplus -i 6 -j build +# Raw logs of a workflow job +gitlink-cli action +logs --owner Gitlink --repo forgeplus -i 6 -j 0 + # Enable or disable a workflow gitlink-cli action +disable --owner Gitlink --repo forgeplus -w ci.yml gitlink-cli action +enable --owner Gitlink --repo forgeplus -w ci.yml diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index e255878..8a6510e 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -3,6 +3,7 @@ "cmd.action.enable.short": "Enable Actions for the repository", "cmd.action.job_rerun.short": "Rerun a single job of a run", "cmd.action.list.short": "List workflow files (.gitea/workflows)", + "cmd.action.logs.short": "Fetch the raw logs of a workflow job", "cmd.action.rerun.short": "Rerun all jobs of a run", "cmd.action.run.short": "Trigger a workflow run", "cmd.action.runs.short": "List runs of a workflow", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index e3c7a5b..328db62 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -3,6 +3,7 @@ "cmd.action.enable.short": "为仓库启用 Actions", "cmd.action.job_rerun.short": "重跑运行中的单个任务", "cmd.action.list.short": "列出工作流文件(.gitea/workflows)", + "cmd.action.logs.short": "获取流水线任务的原始日志", "cmd.action.rerun.short": "重跑一次运行的全部任务", "cmd.action.run.short": "触发一次工作流运行", "cmd.action.runs.short": "列出工作流的运行记录", diff --git a/shortcuts/action/action.go b/shortcuts/action/action.go index 53dfc21..9b8b26a 100644 --- a/shortcuts/action/action.go +++ b/shortcuts/action/action.go @@ -146,6 +146,36 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "logs", + Description: tr.T("cmd.action.logs.short"), + Flags: []common.Flag{ + {Name: "run-id", Short: "i", Usage: tr.T("flag.action.run_id"), Required: true}, + {Name: "job", Short: "j", Usage: tr.T("flag.action.job"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + runID, err := requireIntArg(ctx, "run-id") + if err != nil { + return err + } + job, err := ctx.RequireArg("job") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/runs/%s/jobs/%s/logs", actionsPath(ctx), runID, url.PathEscape(job)), nil) + if err != nil { + return err + } + if text, ok := env.Data.(string); ok { + fmt.Print(text) + return nil + } + return ctx.Output(env) + }, + }, { Name: "enable", Description: tr.T("cmd.action.enable.short"), diff --git a/shortcuts/action/action_test.go b/shortcuts/action/action_test.go index 1d5813d..a17032b 100644 --- a/shortcuts/action/action_test.go +++ b/shortcuts/action/action_test.go @@ -2,6 +2,7 @@ package action import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -157,3 +158,22 @@ func TestActionEnableDisableEndpoints(t *testing.T) { } } } + +func TestActionLogsBuildsNestedLogsPath(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/actions/runs/6/jobs/0/logs.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "text/plain") + fmt.Fprint(w, "log line 1\nlog line 2\n") + })) + defer server.Close() + + if err := runShortcut(t, server, "logs", map[string]string{"run-id": "6", "job": "0"}); err != nil { + t.Fatalf("logs failed: %v", err) + } + + if err := runShortcut(t, server, "logs", map[string]string{"run-id": "abc", "job": "0"}); err == nil { + t.Fatal("expected error for non-integer --run-id") + } +}