Repository: pwntester/octo.nvim
Pull request #1000: feat: `gh run rerun|cancel`
Author: login: GustavEikaas
Labels: name: enhancement
description: New feature or request
Created: 2025-03-22T15:58:55Z | Merged: 2025-03-31T10:34:54Z

Issue: #449

- [x] Update readme

## Details buffer
- [x] Rerun
- [x] Rerun only failed jobs
- [x] Cancel

## Telescope
- [x] Rerun
- [x] Rerun only failed jobs
- [x] Cancel

### Keybindings Update  

Added the following keybindings to **buffer** and **telescope**:  

- **`<C-o>`** → Rerun workflow  
- **`<C-f>`** → Rerun failed workflow  
- **`<C-x>`** → Cancel workflow


Diff:
diff --git a/README.md b/README.md
index 6dec2de0b..a90a02f27 100644
--- a/README.md
+++ b/README.md
@@ -228,6 +228,9 @@ require"octo".setup({
       expand_step = { lhs = "o", desc = "expand workflow step" },
       open_in_browser = { lhs = "<C-b>", desc = "open workflow run in browser" },
       refresh = { lhs = "<C-r>", desc = "refresh workflow" },
+      rerun = { lhs = "<C-o>", desc = "rerun workflow" },
+      rerun_failed = { lhs = "<C-f>", desc = "rerun failed workflow" },
+      cancel = { lhs = "<C-x>", desc = "cancel workflow" },
       copy_url = { lhs = "<C-y>", desc = "copy url to system clipboard" },
     },
     issue = {
diff --git a/lua/octo/config.lua b/lua/octo/config.lua
index 44a898587..410b1cd4d 100644
--- a/lua/octo/config.lua
+++ b/lua/octo/config.lua
@@ -257,6 +257,9 @@ function M.get_default_values()
         expand_step = { lhs = "o", desc = "expand workflow step" },
         open_in_browser = { lhs = "<C-b>", desc = "open workflow run in browser" },
         refresh = { lhs = "<C-r>", desc = "refresh workflow" },
+        rerun = { lhs = "<C-o>", desc = "rerun workflow" },
+        rerun_failed = { lhs = "<C-f>", desc = "rerun failed workflow" },
+        cancel = { lhs = "<C-x>", desc = "cancel workflow" },
         copy_url = { lhs = "<C-y>", desc = "copy url to system clipboard" },
       },
       issue = {
diff --git a/lua/octo/pickers/telescope/provider.lua b/lua/octo/pickers/telescope/provider.lua
index abc68f061..7444da3f4 100644
--- a/lua/octo/pickers/telescope/provider.lua
+++ b/lua/octo/pickers/telescope/provider.lua
@@ -674,12 +674,32 @@ function M.workflow_runs(workflow_runs, title, on_select_cb)
       },
       sorter = conf.generic_sorter {},
       previewer = previewers.workflow_runs.new {},
-      attach_mappings = function()
+      attach_mappings = function(_, map)
         actions.select_default:replace(function(prompt_bufnr)
           local selection = action_state.get_selected_entry(prompt_bufnr)
           actions.close(prompt_bufnr)
           on_select_cb(selection.value)
         end)
+        local mappings = require("octo.config").values.mappings.runs
+
+        map("i", mappings.rerun.lhs, function(prompt_bufnr)
+          local selection = action_state.get_selected_entry(prompt_bufnr)
+          local id = selection.value.id
+          require("octo.workflow_runs").rerun { db_id = id }
+        end)
+
+        map("i", mappings.rerun_failed.lhs, function(prompt_bufnr)
+          local selection = action_state.get_selected_entry(prompt_bufnr)
+          local id = selection.value.id
+          require("octo.workflow_runs").rerun { db_id = id, failed = true }
+        end)
+
+        map("i", mappings.cancel.lhs, function(prompt_bufnr)
+          local selection = action_state.get_selected_entry(prompt_bufnr)
+          local id = selection.value.id
+          require("octo.workflow_runs").cancel(id)
+          actions.close(prompt_bufnr)
+        end)
         return true
       end,
     })
diff --git a/lua/octo/workflow_runs.lua b/lua/octo/workflow_runs.lua
index ffaedfd65..f9cc28693 100644
--- a/lua/octo/workflow_runs.lua
+++ b/lua/octo/workflow_runs.lua
@@ -56,6 +56,8 @@ local gh = require "octo.gh"
 ---@field wf_cache table<string,WorkflowRun>
 ---@field refresh function
 ---@field refetch function
+---@field cancel function
+---@field rerun function
 
 ---@class WorkflowNode
 ---@field id string
@@ -309,7 +311,6 @@ local function get_logs(id)
     local sanitized_job_id = node.job_id:gsub("/", ""):gsub(":", ""):gsub("%.+$", "*/")
     local file_name = string.format("%s_%s.txt", node.number, sanitized_name)
     local path = vim.fs.joinpath(sanitized_job_id, file_name)
-    print(path)
     local res = vim
       .system({
         "unzip",
@@ -353,9 +354,21 @@ end
 local keymaps = {
   ---@param api Handler
   [mappings.refresh.lhs] = function(api)
-    utils.info "refreshing..."
+    utils.info "Refreshing..."
     api.refetch()
   end,
+  [mappings.rerun.lhs] = function(api)
+    utils.info "Rerunning..."
+    api.rerun()
+  end,
+  [mappings.rerun_failed.lhs] = function(api)
+    utils.info "Rerunning failed jobs..."
+    api.rerun { failed = true }
+  end,
+  [mappings.cancel.lhs] = function(api)
+    utils.info "Cancelling..."
+    api.cancel()
+  end,
   [mappings.open_in_browser.lhs] = function(api)
     local id = api.current_wf.databaseId
     navigation.open_in_browser("workflow_run", nil, id)
@@ -723,4 +736,39 @@ M.refetch = function()
   populate_preview_buffer(id, M.buf)
 end
 
+---@param db_id number | nil
+M.cancel = function(db_id)
+  local id = db_id or M.current_wf.databaseId
+  local _, stderr = gh.run.cancel {
+    id,
+    opts = { mode = "sync" },
+  }
+  if stderr and not utils.is_blank(stderr) then
+    vim.api.nvim_err_writeln(stderr)
+    utils.error "Failed to cancel workflow run"
+  else
+    utils.info "Cancelled"
+  end
+  M.refetch()
+end
+
+---@param opts { db_id: number | nil, failed: boolean | nil }
+M.rerun = function(opts)
+  opts = opts or {}
+  local failed_jobs = opts.failed == true
+  local id = opts.db_id or (M.current_wf and M.current_wf.databaseId)
+  local _, stderr = gh.run.rerun {
+    id,
+    failed = failed_jobs,
+    opts = { mode = "sync" },
+  }
+  if stderr and not utils.is_blank(stderr) then
+    vim.api.nvim_err_writeln(stderr)
+    utils.error "Failed to rerun workflow run"
+  else
+    utils.info "Rerun queued"
+  end
+  M.refetch()
+end
+
 return M


Reviews:
user: login: williambdean
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T06:31:02Z
user: login: GustavEikaas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T07:18:41Z
user: login: GustavEikaas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T14:45:02Z
user: login: williambdean
body: Works great. Thanks!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-31T10:33:28Z

Review comments:
diff_hunk: @@ -723,4 +736,39 @@ M.refetch = function()
   populate_preview_buffer(id, M.buf)
 end
 
+---@param db_id number | nil
+M.cancel = function(db_id)
+  local id = db_id or M.current_wf.databaseId
+  local _, stderr = gh.run.cancel {
+    id,
+    opts = { mode = "sync" },
+  }
+  if stderr and not utils.is_blank(stderr) then
+    vim.api.nvim_err_writeln(stderr)
+    utils.error "Failed to cancel workflow run"
+  else
+    utils.info "Cancelled"
+  end
+  M.refetch()
+end
+
+---@param opts { db_id: number | nil, failed: boolean | nil }
+M.rerun = function(opts)
+  opts = opts or {}
+  local failed_jobs = opts.failed == true
+  local id = opts.db_id or (M.current_wf and M.current_wf.databaseId)
+  local _, stderr = gh.run.rerun {
+    id,
+    failed_jobs and "--failed" or "",
path: lua/octo/workflow_runs.lua
user: login: williambdean
body: I think this works. intended use was with a table { failed = true / false } which get translated to --failed. I'm not sure out the empty string behavior

{ "" } would lead to an empty string in plenary.Job table 

You haven't had an issue?
created_at: 2025-03-28T06:31:02Z
author_association: COLLABORATOR
diff_hunk: @@ -723,4 +736,39 @@ M.refetch = function()
   populate_preview_buffer(id, M.buf)
 end
 
+---@param db_id number | nil
+M.cancel = function(db_id)
+  local id = db_id or M.current_wf.databaseId
+  local _, stderr = gh.run.cancel {
+    id,
+    opts = { mode = "sync" },
+  }
+  if stderr and not utils.is_blank(stderr) then
+    vim.api.nvim_err_writeln(stderr)
+    utils.error "Failed to cancel workflow run"
+  else
+    utils.info "Cancelled"
+  end
+  M.refetch()
+end
+
+---@param opts { db_id: number | nil, failed: boolean | nil }
+M.rerun = function(opts)
+  opts = opts or {}
+  local failed_jobs = opts.failed == true
+  local id = opts.db_id or (M.current_wf and M.current_wf.databaseId)
+  local _, stderr = gh.run.rerun {
+    id,
+    failed_jobs and "--failed" or "",
path: lua/octo/workflow_runs.lua
user: login: GustavEikaas
body: Havent had an issue but will refactor. True false approach is cleaner. I didnt know it was supported
created_at: 2025-03-28T07:18:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -723,4 +736,39 @@ M.refetch = function()
   populate_preview_buffer(id, M.buf)
 end
 
+---@param db_id number | nil
+M.cancel = function(db_id)
+  local id = db_id or M.current_wf.databaseId
+  local _, stderr = gh.run.cancel {
+    id,
+    opts = { mode = "sync" },
+  }
+  if stderr and not utils.is_blank(stderr) then
+    vim.api.nvim_err_writeln(stderr)
+    utils.error "Failed to cancel workflow run"
+  else
+    utils.info "Cancelled"
+  end
+  M.refetch()
+end
+
+---@param opts { db_id: number | nil, failed: boolean | nil }
+M.rerun = function(opts)
+  opts = opts or {}
+  local failed_jobs = opts.failed == true
+  local id = opts.db_id or (M.current_wf and M.current_wf.databaseId)
+  local _, stderr = gh.run.rerun {
+    id,
+    failed_jobs and "--failed" or "",
path: lua/octo/workflow_runs.lua
user: login: GustavEikaas
body: Refactored

created_at: 2025-03-28T14:45:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: williambdean
created_at: 2025-03-22T16:10:09Z
body: Thanks for the PR! Looks awesome! 🎉 

How easy is it to make a `Octo run rerun` command? Do the pickers support callbacks on the selected item? Thinking something similar to `Octo issue list` and `Octo issue develop`
author_association: COLLABORATOR
user: login: GustavEikaas
created_at: 2025-03-22T16:16:15Z
body: You mean `Octo run rerun` like in?
1. `:Octo run rerun` 
2. Telescope picker listing all wf runs
3. Press enter to trigger a rerun
4. Go to details buffer of wf run
author_association: CONTRIBUTOR
user: login: williambdean
created_at: 2025-03-22T16:28:27Z
body: Yeah, exactly. Would that be useful as well? 
author_association: COLLABORATOR
user: login: GustavEikaas
created_at: 2025-03-23T07:15:32Z
body: I'm not convinced tbh, I'm struggling to see the use-case. 

The way I see it, the user flow would be something like this

1. User triggers a workflow
2. User notices the workflow has failed
3. User modifies and external system before rerunning the workflow (or the workflow is flaky af)
4. User triggers a rerun of the workflow

So to me I dont really understand how the user would know that the workflow failed. There are multiple cases I guess

1. `:Octo run list` reveals that the workflow failed -> use telescope binding to rerun?
2. The octo workflow run buffer reveals that the workflow failed -> use buffer binding to rerun
3. Github UI shows a failed workflow -> easily retrigger from github ui


I dont mind implementing it, just personally I dont see what the user flow is


## EDIT

I guess there are other motivations for rerunning a workflow rather than failed workflow. e.g workflow_dispatch etc
But I would still want to see the details of old run before rerunning
author_association: CONTRIBUTOR
user: login: GustavEikaas
created_at: 2025-03-23T07:23:47Z
body: Okay so basically, I have added keybindings for `rerun | cancel` in both telescope and buffer. I could need some good suggestions on which sensible keybindings to use
author_association: CONTRIBUTOR
user: login: williambdean
created_at: 2025-03-23T08:11:59Z
body: Maybe `<C-x>` for cancel?
author_association: COLLABORATOR
user: login: GustavEikaas
created_at: 2025-03-24T19:02:43Z
body: @williambdean What do you think about merging this PR, which adds `cancel | rerun | rerun failed` to telescope and octo wf buffer. 

If we want `Octo run cancel` and `Octo run rerun` it could be a separate pr? 

> How easy is it to make a Octo run rerun command? Do the pickers support callbacks on the selected item? Thinking something similar to Octo issue list and Octo issue develop

Not that hard to add but I think it merits some discussion about how the signature/syntax should be
author_association: CONTRIBUTOR
user: login: williambdean
created_at: 2025-03-26T15:51:32Z
body: Yeah, let's wait on that then! 

Did you want to update the README still?
author_association: COLLABORATOR
user: login: GustavEikaas
created_at: 2025-03-27T14:50:08Z
body: Readme updated✅
Some tests are failing but it seems unrelated to my changes?
author_association: CONTRIBUTOR
user: login: williambdean
created_at: 2025-03-28T05:46:38Z
body: Yeah, they seem unrelated. Let me take a look at the changes 
author_association: COLLABORATOR
user: login: GustavEikaas
created_at: 2025-03-30T19:18:41Z
body: @williambdean It's good to go I think
author_association: CONTRIBUTOR
user: login: williambdean
created_at: 2025-03-31T06:13:20Z
body: Great. Taking a look 
author_association: COLLABORATOR