Skip to content

Commit 7e100f2

Browse files
committed
new release update checker
1 parent 2bd699a commit 7e100f2

8 files changed

Lines changed: 1261 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@ gh extension install github/gh-stack
1212

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

15+
### Updating
16+
17+
```sh
18+
gh extension upgrade stack
19+
```
20+
21+
Official, unpinned stable release installations check for a newer
22+
[latest release](https://github.com/github/gh-stack/releases/latest) in the
23+
background, at most once every 24 hours. When an update is available, successful
24+
commands can append an upgrade notice to stderr, also at most once every 24
25+
hours. This includes non-interactive use; stdout and JSON output are unchanged.
26+
The check and reminder timestamps are shared across repositories for your user.
27+
They are stored as YAML in `gh-stack/state.yml` beneath GitHub CLI's state
28+
directory (`~/.local/state/gh` by default on macOS/Linux).
29+
30+
Commands never wait for a release check. Short commands may finish before a
31+
notice is ready, and failed or interrupted checks still count toward the daily
32+
check limit. Completed checks are cached so a later command can show the notice.
33+
No upgrades happen automatically, and development, locally linked, prerelease,
34+
and pinned installations are excluded. Help, version, and completion commands
35+
do not run the notifier.
36+
37+
Set `GH_STACK_NO_UPDATE_NOTIFIER=1` to disable these checks and notices, including
38+
in CI or scripts. Any non-empty value disables the notifier. Optional check
39+
failures do not affect command exit codes; set `GH_DEBUG=1` for diagnostics.
40+
Install a release containing this feature to receive notices for future releases.
41+
1542
## AI agent integration
1643

1744
Install the gh-stack skill so your AI coding agent knows how to work with stacked PRs and the `gh stack` CLI:

‎cmd/root.go‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"os"
78

89
"github.com/github/gh-stack/internal/config"
910
"github.com/github/gh-stack/internal/theme"
11+
"github.com/github/gh-stack/internal/update"
1012
"github.com/spf13/cobra"
1113
)
1214

15+
type updateResult struct {
16+
notify update.Notification
17+
err error
18+
}
19+
1320
func RootCmd() *cobra.Command {
14-
cfg := config.New()
21+
return newRootCmd(config.New(), startUpdateCheck)
22+
}
1523

24+
func newRootCmd(cfg *config.Config, startCheck func(context.Context, string) <-chan updateResult) *cobra.Command {
1625
root := &cobra.Command{
1726
Use: "stack <command>",
1827
Short: "Manage stacked branches and pull requests",
@@ -36,10 +45,31 @@ locally, then push to GitHub to create your stack of PRs.`,
3645
Version: Version,
3746
SilenceUsage: true,
3847
SilenceErrors: true,
39-
// Honor GH_STACK_THEME (auto|light|dark) before any command renders
40-
PersistentPreRun: func(_ *cobra.Command, _ []string) {
41-
theme.ApplyOverride()
42-
},
48+
}
49+
50+
var updates <-chan updateResult
51+
root.PersistentPreRun = func(cmd *cobra.Command, _ []string) {
52+
theme.ApplyOverride()
53+
updates = nil
54+
if update.Enabled(root.Version) && !isHelpOrCompletionCommand(cmd) {
55+
updates = startCheck(cmd.Context(), root.Version)
56+
}
57+
}
58+
root.PersistentPostRun = func(cmd *cobra.Command, _ []string) {
59+
if cmd.Context().Err() != nil {
60+
return
61+
}
62+
select {
63+
case result := <-updates:
64+
if result.notify != nil {
65+
result.err = errors.Join(result.err, result.notify(cmd.ErrOrStderr()))
66+
}
67+
if result.err != nil && os.Getenv("GH_DEBUG") != "" {
68+
fmt.Fprintf(cmd.ErrOrStderr(), "debug: gh-stack update notification: %v\n", result.err)
69+
}
70+
default:
71+
// Never wait for a release check if the command finishes quickly
72+
}
4373
}
4474

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

156-
func Execute() {
157-
cmd := RootCmd()
186+
func startUpdateCheck(ctx context.Context, version string) <-chan updateResult {
187+
results := make(chan updateResult, 1)
188+
go func() {
189+
notify, err := update.Check(ctx, version)
190+
results <- updateResult{notify: notify, err: err}
191+
}()
192+
return results
193+
}
194+
195+
func isHelpOrCompletionCommand(cmd *cobra.Command) bool {
196+
for c := cmd; c != nil; c = c.Parent() {
197+
switch c.Name() {
198+
case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
199+
return true
200+
}
201+
}
202+
return false
203+
}
204+
205+
func execute(cmd *cobra.Command, args []string) error {
206+
ctx, cancel := context.WithCancel(context.Background())
207+
defer cancel()
158208

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

164-
if err := wrapCmd.Execute(); err != nil {
216+
func Execute() {
217+
cmd := RootCmd()
218+
if err := execute(cmd, os.Args[1:]); err != nil {
165219
var exitErr *ExitError
166220
if errors.As(err, &exitErr) {
167221
os.Exit(exitErr.Code)

0 commit comments

Comments
 (0)