Skip to content

Commit 41f11b9

Browse files
committed
Merge main branch into copilot/clarify-projectops-docs
2 parents c074665 + b42bd9e commit 41f11b9

14 files changed

Lines changed: 2105 additions & 760 deletions

.github/workflows/agent-performance-analyzer.lock.yml

Lines changed: 124 additions & 240 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/campaign-manager.lock.yml

Lines changed: 124 additions & 240 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/workflow-health-manager.lock.yml

Lines changed: 124 additions & 240 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/gh-aw/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,15 @@ Examples:
214214
jsonOutput, _ := cmd.Flags().GetBool("json")
215215
fix, _ := cmd.Flags().GetBool("fix")
216216
stats, _ := cmd.Flags().GetBool("stats")
217+
noCheckUpdate, _ := cmd.Flags().GetBool("no-check-update")
217218
verbose, _ := cmd.Flags().GetBool("verbose")
218219
if err := validateEngine(engineOverride); err != nil {
219220
return err
220221
}
221222

223+
// Check for updates (non-blocking, runs once per day)
224+
cli.CheckForUpdatesAsync(noCheckUpdate, verbose)
225+
222226
// If --fix is specified, run fix --write first
223227
if fix {
224228
fixConfig := cli.FixConfig{
@@ -468,6 +472,7 @@ Use "` + constants.CLIExtensionPrefix + ` help all" to show help for all command
468472
compileCmd.Flags().Bool("fix", false, "Apply automatic codemod fixes to workflows before compiling")
469473
compileCmd.Flags().Bool("json", false, "Output results in JSON format")
470474
compileCmd.Flags().Bool("stats", false, "Display statistics table sorted by file size (shows jobs, steps, scripts, and shells)")
475+
compileCmd.Flags().Bool("no-check-update", false, "Skip checking for gh-aw updates")
471476
compileCmd.MarkFlagsMutuallyExclusive("dir", "workflows-dir")
472477

473478
// Register completions for compile command

pkg/cli/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s
130130

131131
// Configure VSCode settings for YAML schema validation
132132
initLog.Print("Configuring VSCode YAML schema validation")
133-
133+
134134
// Write workflow schema to .github/aw/
135135
if err := ensureWorkflowSchema(verbose); err != nil {
136136
initLog.Printf("Failed to write workflow schema: %v", err)

pkg/cli/mcp_schema.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package cli
22

33
import (
4-
"fmt"
5-
"reflect"
6-
74
"github.com/google/jsonschema-go/jsonschema"
85
)
96

@@ -30,15 +27,5 @@ import (
3027
// OutputSchema: schema,
3128
// }
3229
func GenerateOutputSchema[T any]() (*jsonschema.Schema, error) {
33-
// Get the type of T
34-
var zero T
35-
typ := reflect.TypeOf(zero)
36-
37-
// Use jsonschema.ForType to generate schema from Go type
38-
schema, err := jsonschema.ForType(typ, &jsonschema.ForOptions{})
39-
if err != nil {
40-
return nil, fmt.Errorf("failed to generate schema: %w", err)
41-
}
42-
43-
return schema, nil
30+
return jsonschema.For[T](nil)
4431
}

pkg/cli/update_check.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"time"
9+
10+
"github.com/cli/go-gh/v2/pkg/api"
11+
"github.com/githubnext/gh-aw/pkg/console"
12+
"github.com/githubnext/gh-aw/pkg/logger"
13+
"github.com/githubnext/gh-aw/pkg/workflow"
14+
)
15+
16+
var updateCheckLog = logger.New("cli:update_check")
17+
18+
const (
19+
// lastCheckFileName is the name of the file that tracks the last update check timestamp
20+
lastCheckFileName = "gh-aw-last-update-check"
21+
// checkInterval is how often we check for updates (24 hours)
22+
checkInterval = 24 * time.Hour
23+
)
24+
25+
// Release represents a GitHub release
26+
type Release struct {
27+
TagName string `json:"tag_name"`
28+
Name string `json:"name"`
29+
HTMLURL string `json:"html_url"`
30+
}
31+
32+
// shouldCheckForUpdate determines if we should check for updates based on:
33+
// - CI mode (disabled)
34+
// - MCP server mode (disabled via parent command detection)
35+
// - Time since last check (once per day)
36+
// - --no-check-update flag
37+
func shouldCheckForUpdate(noCheckUpdate bool) bool {
38+
// Skip if explicitly disabled
39+
if noCheckUpdate {
40+
updateCheckLog.Print("Update check disabled via --no-check-update flag")
41+
return false
42+
}
43+
44+
// Skip in CI environments
45+
if IsRunningInCI() {
46+
updateCheckLog.Print("Update check disabled in CI environment")
47+
return false
48+
}
49+
50+
// Skip if running as MCP server (detected by checking if parent command is "mcp-server")
51+
// When gh aw is invoked from MCP server, it's spawned as a subprocess
52+
if isRunningAsMCPServer() {
53+
updateCheckLog.Print("Update check disabled in MCP server mode")
54+
return false
55+
}
56+
57+
// Check if we've already checked recently
58+
lastCheckFile := getLastCheckFilePath()
59+
if lastCheckFile == "" {
60+
updateCheckLog.Print("Could not determine last check file path")
61+
return false
62+
}
63+
64+
// Read last check time
65+
data, err := os.ReadFile(lastCheckFile)
66+
if err != nil {
67+
if !os.IsNotExist(err) {
68+
updateCheckLog.Printf("Error reading last check file: %v", err)
69+
}
70+
// File doesn't exist or error reading - perform check
71+
return true
72+
}
73+
74+
lastCheck, err := time.Parse(time.RFC3339, strings.TrimSpace(string(data)))
75+
if err != nil {
76+
updateCheckLog.Printf("Error parsing last check time: %v", err)
77+
// Invalid timestamp - perform check
78+
return true
79+
}
80+
81+
// Check if enough time has passed
82+
if time.Since(lastCheck) < checkInterval {
83+
updateCheckLog.Printf("Last check was %v ago, skipping", time.Since(lastCheck))
84+
return false
85+
}
86+
87+
updateCheckLog.Print("Last check was more than 24 hours ago, performing check")
88+
return true
89+
}
90+
91+
// isRunningAsMCPServer detects if we're running as a subprocess of mcp-server
92+
// This is a heuristic - we can't reliably detect this, so we're conservative
93+
func isRunningAsMCPServer() bool {
94+
// Check for MCP_SERVER environment variable that could be set by the MCP server
95+
if os.Getenv("GH_AW_MCP_SERVER") != "" {
96+
return true
97+
}
98+
// Additional heuristic: check if we're likely being invoked by MCP server
99+
// MCP server tools typically run with minimal environment
100+
return false
101+
}
102+
103+
var (
104+
// getLastCheckFilePathFunc allows overriding in tests
105+
getLastCheckFilePathFunc = getLastCheckFilePathImpl
106+
)
107+
108+
// getLastCheckFilePath returns the path to the last check timestamp file
109+
func getLastCheckFilePath() string {
110+
return getLastCheckFilePathFunc()
111+
}
112+
113+
// getLastCheckFilePathImpl is the actual implementation
114+
func getLastCheckFilePathImpl() string {
115+
// Use OS temp directory for cross-platform compatibility
116+
tmpDir := os.TempDir()
117+
if tmpDir == "" {
118+
updateCheckLog.Print("Could not determine temp directory")
119+
return ""
120+
}
121+
122+
// Create a gh-aw subdirectory in temp
123+
ghAwTmpDir := filepath.Join(tmpDir, "gh-aw")
124+
if err := os.MkdirAll(ghAwTmpDir, 0755); err != nil {
125+
updateCheckLog.Printf("Error creating gh-aw temp directory: %v", err)
126+
return ""
127+
}
128+
129+
return filepath.Join(ghAwTmpDir, lastCheckFileName)
130+
}
131+
132+
// updateLastCheckTime updates the timestamp of the last update check
133+
func updateLastCheckTime() {
134+
lastCheckFile := getLastCheckFilePath()
135+
if lastCheckFile == "" {
136+
return
137+
}
138+
139+
timestamp := time.Now().Format(time.RFC3339)
140+
if err := os.WriteFile(lastCheckFile, []byte(timestamp), 0644); err != nil {
141+
updateCheckLog.Printf("Error writing last check time: %v", err)
142+
}
143+
}
144+
145+
// checkForUpdates checks if a newer version of gh-aw is available
146+
// This function is non-blocking and ignores all errors (connectivity, API, etc.)
147+
func checkForUpdates(noCheckUpdate bool, verbose bool) {
148+
// Quick check if we should even attempt the update check
149+
if !shouldCheckForUpdate(noCheckUpdate) {
150+
return
151+
}
152+
153+
updateCheckLog.Print("Checking for gh-aw updates...")
154+
155+
// Update the last check time immediately to prevent concurrent checks
156+
updateLastCheckTime()
157+
158+
// Get current version
159+
currentVersion := GetVersion()
160+
if !workflow.IsReleasedVersion(currentVersion) {
161+
updateCheckLog.Print("Not a released version, skipping update check")
162+
return
163+
}
164+
165+
// Query GitHub API for latest release
166+
latestVersion, err := getLatestRelease()
167+
if err != nil {
168+
// Silently ignore errors - update check should never fail the command
169+
updateCheckLog.Printf("Error checking for updates (ignoring): %v", err)
170+
return
171+
}
172+
173+
if latestVersion == "" {
174+
updateCheckLog.Print("Could not determine latest version")
175+
return
176+
}
177+
178+
// Compare versions
179+
if latestVersion == currentVersion {
180+
if verbose {
181+
updateCheckLog.Print("gh-aw is up to date")
182+
}
183+
return
184+
}
185+
186+
// Normalize versions for comparison (remove 'v' prefix)
187+
currentVersionNormalized := strings.TrimPrefix(currentVersion, "v")
188+
latestVersionNormalized := strings.TrimPrefix(latestVersion, "v")
189+
190+
if currentVersionNormalized == latestVersionNormalized {
191+
if verbose {
192+
updateCheckLog.Print("gh-aw is up to date (version format differs)")
193+
}
194+
return
195+
}
196+
197+
// Check if we're on a newer version (development/prerelease)
198+
// Simple heuristic: if current version sorts after latest, we might be on a dev version
199+
if currentVersionNormalized > latestVersionNormalized {
200+
updateCheckLog.Printf("Current version (%s) appears newer than latest release (%s), skipping notification", currentVersion, latestVersion)
201+
return
202+
}
203+
204+
// A newer version is available - display update message
205+
updateCheckLog.Printf("Newer version available: %s (current: %s)", latestVersion, currentVersion)
206+
fmt.Fprintln(os.Stderr, "")
207+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("A new version of gh-aw is available: %s (current: %s)", latestVersion, currentVersion)))
208+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Update with: gh extension upgrade githubnext/gh-aw"))
209+
fmt.Fprintln(os.Stderr, "")
210+
}
211+
212+
// getLatestRelease queries GitHub API for the latest release of gh-aw
213+
func getLatestRelease() (string, error) {
214+
updateCheckLog.Print("Querying GitHub API for latest release...")
215+
216+
// Create GitHub REST client using go-gh
217+
client, err := api.NewRESTClient(api.ClientOptions{})
218+
if err != nil {
219+
return "", fmt.Errorf("failed to create GitHub client: %w", err)
220+
}
221+
222+
// Query the latest release
223+
var release Release
224+
err = client.Get("repos/githubnext/gh-aw/releases/latest", &release)
225+
if err != nil {
226+
return "", fmt.Errorf("failed to query latest release: %w", err)
227+
}
228+
229+
updateCheckLog.Printf("Latest release: %s", release.TagName)
230+
return release.TagName, nil
231+
}
232+
233+
// CheckForUpdatesAsync performs update check in background (best effort)
234+
// This is called from compile command and should never block or fail the compilation
235+
func CheckForUpdatesAsync(noCheckUpdate bool, verbose bool) {
236+
// Run check in goroutine to avoid blocking compilation
237+
go func() {
238+
// Recover from any panics in the update check
239+
defer func() {
240+
if r := recover(); r != nil {
241+
updateCheckLog.Printf("Panic in update check (recovered): %v", r)
242+
}
243+
}()
244+
245+
checkForUpdates(noCheckUpdate, verbose)
246+
}()
247+
248+
// Give the goroutine a small window to complete quickly
249+
// This allows the message to appear before compilation starts
250+
// but doesn't block if the check takes longer
251+
time.Sleep(100 * time.Millisecond)
252+
}

0 commit comments

Comments
 (0)