Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
new release update checker
  • Loading branch information
skarim committed Oct 2, 2026
commit 0b05b273c01ead9176343581aa1bbd8f27b6aaa5
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ gh extension install github/gh-stack

Requires the [GitHub CLI](https://cli.github.com/) (`gh`) v2.0+ and Git 2.36+.

### Updating

```sh
gh extension upgrade stack
```

Official, unpinned stable release installations check for a newer
[latest release](https://github.com/github/gh-stack/releases/latest) in the
background, at most once every 24 hours. When an update is available, successful
commands can append an upgrade notice to stderr, also at most once every 24
hours. This includes non-interactive use; stdout and JSON output are unchanged.
The check and reminder timestamps are shared across repositories for your user.
They are stored as YAML in `gh-stack/state.yml` beneath GitHub CLI's state
directory (`~/.local/state/gh` by default on macOS/Linux).

Commands never wait for a release check. Short commands may finish before a
notice is ready, and failed or interrupted checks still count toward the daily
check limit. Completed checks are cached so a later command can show the notice.
No upgrades happen automatically, and development, locally linked, prerelease,
and pinned installations are excluded. Help, version, and completion commands
do not run the notifier.

Set `GH_STACK_NO_UPDATE_NOTIFIER=1` to disable these checks and notices, including
in CI or scripts. Any non-empty value disables the notifier. Optional check
failures do not affect command exit codes; set `GH_DEBUG=1` for diagnostics.
Install a release containing this feature to receive notices for future releases.

## AI agent integration

Install the gh-stack skill so your AI coding agent knows how to work with stacked PRs and the `gh stack` CLI:
Expand Down
72 changes: 63 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"

"github.com/github/gh-stack/internal/config"
"github.com/github/gh-stack/internal/theme"
"github.com/github/gh-stack/internal/update"
"github.com/spf13/cobra"
)

type updateResult struct {
notify update.Notification
err error
}

func RootCmd() *cobra.Command {
cfg := config.New()
return newRootCmd(config.New(), startUpdateCheck)
}

func newRootCmd(cfg *config.Config, startCheck func(context.Context, string) <-chan updateResult) *cobra.Command {
root := &cobra.Command{
Use: "stack <command>",
Short: "Manage stacked branches and pull requests",
Expand All @@ -36,10 +45,31 @@ locally, then push to GitHub to create your stack of PRs.`,
Version: Version,
SilenceUsage: true,
SilenceErrors: true,
// Honor GH_STACK_THEME (auto|light|dark) before any command renders
PersistentPreRun: func(_ *cobra.Command, _ []string) {
theme.ApplyOverride()
},
}

var updates <-chan updateResult
root.PersistentPreRun = func(cmd *cobra.Command, _ []string) {
theme.ApplyOverride()
updates = nil
if update.Enabled(root.Version) && !isHelpOrCompletionCommand(cmd) {
updates = startCheck(cmd.Context(), root.Version)
}
}
root.PersistentPostRun = func(cmd *cobra.Command, _ []string) {
if cmd.Context().Err() != nil {
return
}
select {
case result := <-updates:
if result.notify != nil {
result.err = errors.Join(result.err, result.notify(cmd.ErrOrStderr()))
}
if result.err != nil && os.Getenv("GH_DEBUG") != "" {
fmt.Fprintf(cmd.ErrOrStderr(), "debug: gh-stack update notification: %v\n", result.err)
}
default:
// Never wait for a release check if the command finishes quickly
}
}

root.SetVersionTemplate("gh stack version {{.Version}}\n")
Expand Down Expand Up @@ -153,15 +183,39 @@ locally, then push to GitHub to create your stack of PRs.`,
return root
}

func Execute() {
cmd := RootCmd()
func startUpdateCheck(ctx context.Context, version string) <-chan updateResult {
results := make(chan updateResult, 1)
go func() {
notify, err := update.Check(ctx, version)
results <- updateResult{notify: notify, err: err}
}()
return results
}

func isHelpOrCompletionCommand(cmd *cobra.Command) bool {
for c := cmd; c != nil; c = c.Parent() {
switch c.Name() {
case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
return true
}
}
return false
}

func execute(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Wrap in a "gh" parent so help output shows "gh stack" instead of just "stack".
wrapCmd := &cobra.Command{Use: "gh", SilenceUsage: true, SilenceErrors: true}
wrapCmd.AddCommand(cmd)
wrapCmd.SetArgs(append([]string{"stack"}, os.Args[1:]...))
wrapCmd.SetArgs(append([]string{"stack"}, args...))
return wrapCmd.ExecuteContext(ctx)
}

if err := wrapCmd.Execute(); err != nil {
func Execute() {
cmd := RootCmd()
if err := execute(cmd, os.Args[1:]); err != nil {
var exitErr *ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.Code)
Expand Down
Loading
Loading