Skip to content

Commit 03be7f5

Browse files
Copilotpelikhan
andauthored
Add --pre-releases support to gh aw upgrade and fix duplicate success symbol (#27401)
* feat: add upgrade pre-releases flag and fix success output Agent-Logs-Url: https://github.com/github/gh-aw/sessions/96178ed7-9483-4099-a0b0-98e95a0dad05 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore: refine prerelease release selection helper Agent-Logs-Url: https://github.com/github/gh-aw/sessions/96178ed7-9483-4099-a0b0-98e95a0dad05 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent c88b278 commit 03be7f5

7 files changed

Lines changed: 98 additions & 13 deletions

File tree

.github/workflows/upgrade-test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ jobs:
8484
# 2. Runs `gh extension upgrade github/gh-aw`
8585
# 3. On Windows: binary is in use → rename+retry workaround is triggered
8686
# 4. Re-launches the freshly installed binary with --skip-extension-upgrade
87+
# --pre-releases ensures the upgrade check considers prereleases.
8788
# --no-fix keeps the test focused: skips codemods, action updates, and compilation.
88-
gh aw upgrade --no-fix
89+
gh aw upgrade --pre-releases --no-fix
8990
9091
- name: Verify version after upgrade
9192
shell: bash

pkg/cli/update_check.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const (
2121
lastCheckFileName = "gh-aw-last-update-check"
2222
// checkInterval is how often we check for updates (24 hours)
2323
checkInterval = 24 * time.Hour
24+
// maxReleasesToQuery is the maximum number of releases queried when prereleases are included.
25+
maxReleasesToQuery = 50
2426
)
2527

2628
// Release represents a GitHub release
@@ -29,6 +31,7 @@ type Release struct {
2931
Name string `json:"name"`
3032
HTMLURL string `json:"html_url"`
3133
Prerelease bool `json:"prerelease"`
34+
Draft bool `json:"draft"`
3235
}
3336

3437
// shouldCheckForUpdate determines if we should check for updates based on:
@@ -160,7 +163,7 @@ func checkForUpdates(noCheckUpdate bool, verbose bool) {
160163
}
161164

162165
// Query GitHub API for latest release
163-
latestVersion, err := getLatestRelease()
166+
latestVersion, err := getLatestRelease(false)
164167
if err != nil {
165168
// Silently ignore errors - update check should never fail the command
166169
updateCheckLog.Printf("Error checking for updates (ignoring): %v", err)
@@ -207,7 +210,7 @@ func checkForUpdates(noCheckUpdate bool, verbose bool) {
207210
}
208211

209212
// getLatestRelease queries GitHub API for the latest release of gh-aw
210-
func getLatestRelease() (string, error) {
213+
func getLatestRelease(includePrereleases bool) (string, error) {
211214
updateCheckLog.Print("Querying GitHub API for latest release...")
212215

213216
// Create GitHub REST client using go-gh
@@ -216,7 +219,19 @@ func getLatestRelease() (string, error) {
216219
return "", fmt.Errorf("failed to create GitHub client: %w", err)
217220
}
218221

219-
// Query the latest release
222+
if includePrereleases {
223+
var releases []Release
224+
err = client.Get(fmt.Sprintf("repos/github/gh-aw/releases?per_page=%d", maxReleasesToQuery), &releases)
225+
if err != nil {
226+
return "", fmt.Errorf("failed to query releases: %w", err)
227+
}
228+
229+
tag := findLatestPublishedReleaseTag(releases)
230+
updateCheckLog.Printf("Latest published release (pre-releases allowed): %s", tag)
231+
return tag, nil
232+
}
233+
234+
// Query the latest stable release
220235
var release Release
221236
err = client.Get("repos/github/gh-aw/releases/latest", &release)
222237
if err != nil {
@@ -234,6 +249,18 @@ func getLatestRelease() (string, error) {
234249
return release.TagName, nil
235250
}
236251

252+
// findLatestPublishedReleaseTag returns the first non-draft release tag from the
253+
// releases API response, skipping entries without tag names.
254+
func findLatestPublishedReleaseTag(releases []Release) string {
255+
for _, release := range releases {
256+
if release.Draft || release.TagName == "" {
257+
continue
258+
}
259+
return release.TagName
260+
}
261+
return ""
262+
}
263+
237264
// CheckForUpdatesAsync performs update check in background (best effort)
238265
// This is called from compile command and should never block or fail the compilation
239266
// The context can be used to cancel the update check if the program is shutting down

pkg/cli/update_check_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"path/filepath"
99
"testing"
1010
"time"
11+
12+
"github.com/stretchr/testify/assert"
1113
)
1214

1315
func TestShouldCheckForUpdate(t *testing.T) {
@@ -338,3 +340,51 @@ func TestCheckForUpdatesAsync_ContextCancellation(t *testing.T) {
338340
// Note: The check might still run if it started before cancellation,
339341
// so we just verify no panics occurred
340342
}
343+
344+
func TestFindLatestPublishedReleaseTag(t *testing.T) {
345+
tests := []struct {
346+
name string
347+
releases []Release
348+
want string
349+
}{
350+
{
351+
name: "returns first non-draft release tag",
352+
releases: []Release{
353+
{TagName: "v1.2.0-beta.1", Draft: false, Prerelease: true},
354+
{TagName: "v1.1.0", Draft: false, Prerelease: false},
355+
},
356+
want: "v1.2.0-beta.1",
357+
},
358+
{
359+
name: "skips draft releases",
360+
releases: []Release{
361+
{TagName: "v1.3.0", Draft: true},
362+
{TagName: "v1.2.0", Draft: false},
363+
},
364+
want: "v1.2.0",
365+
},
366+
{
367+
name: "skips empty tags",
368+
releases: []Release{
369+
{TagName: "", Draft: false},
370+
{TagName: "v1.0.0", Draft: false},
371+
},
372+
want: "v1.0.0",
373+
},
374+
{
375+
name: "returns empty when no published releases",
376+
releases: []Release{
377+
{TagName: "", Draft: true},
378+
{TagName: "", Draft: false},
379+
},
380+
want: "",
381+
},
382+
}
383+
384+
for _, tt := range tests {
385+
t.Run(tt.name, func(t *testing.T) {
386+
got := findLatestPublishedReleaseTag(tt.releases)
387+
assert.Equal(t, tt.want, got, "unexpected latest published release tag")
388+
})
389+
}
390+
}

pkg/cli/update_extension_check.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var updateExtensionCheckLog = logger.New("cli:update_extension_check")
3535
// baked in. The caller should re-launch the freshly-installed binary (at
3636
// installPath) so that subsequent work (e.g. lock-file compilation) uses the
3737
// correct new version string.
38-
func upgradeExtensionIfOutdated(verbose bool) (bool, string, error) {
38+
func upgradeExtensionIfOutdated(verbose bool, includePrereleases bool) (bool, string, error) {
3939
currentVersion := GetVersion()
4040
updateExtensionCheckLog.Printf("Checking if extension needs upgrade (current: %s)", currentVersion)
4141

@@ -49,7 +49,7 @@ func upgradeExtensionIfOutdated(verbose bool) (bool, string, error) {
4949
}
5050

5151
// Query GitHub API for latest release
52-
latestVersion, err := getLatestRelease()
52+
latestVersion, err := getLatestRelease(includePrereleases)
5353
if err != nil {
5454
// Fail silently - don't block the upgrade command if we can't reach GitHub
5555
updateExtensionCheckLog.Printf("Failed to check for latest release (silently ignoring): %v", err)

pkg/cli/update_extension_check_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestUpgradeExtensionIfOutdated_DevBuild(t *testing.T) {
2626
// Verify the function exits before making any API calls.
2727
// If it did make API calls we'd see a network error in test environments,
2828
// but the function must return (false, "", nil) immediately.
29-
upgraded, installPath, err := upgradeExtensionIfOutdated(false)
29+
upgraded, installPath, err := upgradeExtensionIfOutdated(false, false)
3030
require.NoError(t, err, "Should not return error for dev builds")
3131
assert.False(t, upgraded, "Should not report upgrade for dev builds")
3232
assert.Empty(t, installPath, "installPath should be empty for dev builds")
@@ -43,7 +43,7 @@ func TestUpgradeExtensionIfOutdated_SilentFailureOnAPIError(t *testing.T) {
4343
// Use a release version so the API call is attempted
4444
SetVersionInfo("v0.1.0")
4545

46-
upgraded, installPath, err := upgradeExtensionIfOutdated(false)
46+
upgraded, installPath, err := upgradeExtensionIfOutdated(false, false)
4747
require.NoError(t, err, "Should fail silently on API errors")
4848
assert.False(t, upgraded, "Should not report upgrade when API is unreachable")
4949
assert.Empty(t, installPath, "installPath should be empty when API is unreachable")

pkg/cli/upgrade_command.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ Examples:
6565
` + string(constants.CLIExtensionPrefix) + ` upgrade --create-pull-request # Upgrade and open a pull request
6666
` + string(constants.CLIExtensionPrefix) + ` upgrade --dir custom/workflows # Upgrade workflows in custom directory
6767
` + string(constants.CLIExtensionPrefix) + ` upgrade --audit # Check dependency health without upgrading
68-
` + string(constants.CLIExtensionPrefix) + ` upgrade --audit --json # Output audit results in JSON format`,
68+
` + string(constants.CLIExtensionPrefix) + ` upgrade --audit --json # Output audit results in JSON format
69+
` + string(constants.CLIExtensionPrefix) + ` upgrade --pre-releases # Include prerelease versions when self-upgrading the extension`,
6970
Args: cobra.NoArgs,
7071
RunE: func(cmd *cobra.Command, args []string) error {
7172
verbose, _ := cmd.Flags().GetBool("verbose")
@@ -80,6 +81,7 @@ Examples:
8081
jsonOutput, _ := cmd.Flags().GetBool("json")
8182
skipExtensionUpgrade, _ := cmd.Flags().GetBool("skip-extension-upgrade")
8283
approveUpgrade, _ := cmd.Flags().GetBool("approve")
84+
preReleases, _ := cmd.Flags().GetBool("pre-releases")
8385

8486
// Handle audit mode
8587
if auditFlag {
@@ -92,7 +94,7 @@ Examples:
9294
}
9395
}
9496

95-
if err := runUpgradeCommand(cmd.Context(), verbose, dir, noFix, noCompile, noActions, skipExtensionUpgrade, approveUpgrade); err != nil {
97+
if err := runUpgradeCommand(cmd.Context(), verbose, dir, noFix, noCompile, noActions, skipExtensionUpgrade, approveUpgrade, preReleases); err != nil {
9698
return err
9799
}
98100

@@ -115,6 +117,7 @@ Examples:
115117
cmd.Flags().Bool("pr", false, "Alias for --create-pull-request")
116118
_ = cmd.Flags().MarkHidden("pr") // Hide the short alias from help output
117119
cmd.Flags().Bool("audit", false, "Check dependency health without performing upgrades")
120+
cmd.Flags().Bool("pre-releases", false, "Include pre-release versions when checking for extension upgrades")
118121
cmd.Flags().Bool("approve", false, "Approve all safe update changes. When strict mode is active (the default), the compiler emits warnings for new restricted secrets or unapproved action additions/removals not present in the existing gh-aw-manifest. Use this flag to approve and skip safe update enforcement")
119122
cmd.Flags().Bool("skip-extension-upgrade", false, "Skip automatic extension upgrade (used internally to prevent recursion after upgrade)")
120123
_ = cmd.Flags().MarkHidden("skip-extension-upgrade")
@@ -146,7 +149,7 @@ func runDependencyAudit(verbose bool, jsonOutput bool) error {
146149
}
147150

148151
// runUpgradeCommand executes the upgrade process
149-
func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, noFix bool, noCompile bool, noActions bool, skipExtensionUpgrade bool, approve bool) error {
152+
func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, noFix bool, noCompile bool, noActions bool, skipExtensionUpgrade bool, approve bool, preReleases bool) error {
150153
upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, skipExtensionUpgrade=%v",
151154
verbose, workflowDir, noFix, noCompile, noActions, skipExtensionUpgrade)
152155

@@ -157,7 +160,7 @@ func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, no
157160
// prevents the re-launched process from entering this branch again.
158161
if !skipExtensionUpgrade {
159162
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking gh-aw extension version..."))
160-
upgraded, installPath, err := upgradeExtensionIfOutdated(verbose)
163+
upgraded, installPath, err := upgradeExtensionIfOutdated(verbose, preReleases)
161164
if err != nil {
162165
upgradeLog.Printf("Extension upgrade failed: %v", err)
163166
return err
@@ -303,7 +306,7 @@ func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, no
303306

304307
// Print success message
305308
fmt.Fprintln(os.Stderr, "")
306-
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Upgrade complete"))
309+
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Upgrade complete"))
307310

308311
return nil
309312
}

pkg/cli/upgrade_command_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ func TestUpgradeCommandHelpTextConsistency(t *testing.T) {
1818
approveFlag := cmd.Flags().Lookup("approve")
1919
require.NotNil(t, approveFlag, "--approve flag should exist")
2020
assert.Contains(t, approveFlag.Usage, "When strict mode is active", "--approve description should match compile semantics")
21+
22+
preReleasesFlag := cmd.Flags().Lookup("pre-releases")
23+
require.NotNil(t, preReleasesFlag, "--pre-releases flag should exist")
24+
assert.Contains(t, preReleasesFlag.Usage, "Include pre-release versions", "--pre-releases description should mention pre-release upgrades")
2125
}

0 commit comments

Comments
 (0)