Skip to content

fix(upgrade): extend rename+retry workaround to Windows#27271

Merged
pelikhan merged 5 commits intomainfrom
copilot/fix-gh-aw-upgrade-windows-issue
Apr 20, 2026
Merged

fix(upgrade): extend rename+retry workaround to Windows#27271
pelikhan merged 5 commits intomainfrom
copilot/fix-gh-aw-upgrade-windows-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 20, 2026

On Windows, gh aw upgrade fails with Access is denied when gh extension upgrade tries to overwrite the running gh-aw.exe — the same root cause as Linux ETXTBSY, just a different OS error.

Changes

  • needsRenameWorkaround() — new helper centralizing the linux || windows platform check, replacing scattered runtime.GOOS == "linux" guards
  • Rename+retry on Windows — the workaround (rename running binary to *.<pid>.bak, freeing the path for gh to write the new binary) now runs on Windows as well as Linux; PID-qualified backup names prevent collisions when cleanup fails (Windows cannot delete the running .exe)
  • Buffered first-attempt output on Windows — suppresses the confusing Access is denied message when the retry succeeds, consistent with existing Linux behavior
  • Windows fallback guidance — if rename+retry also fails and the error is a Windows file-lock symptom (Access is denied / sharing violation), prints copy-pasteable instructions using plain indented text (no prefix):
    ℹ On Windows, gh-aw cannot self-upgrade while it is running.
    ℹ Please upgrade manually by running one of the following:
      gh extension upgrade gh-aw
    ℹ If that does not work, try reinstalling:
      gh extension remove gh-aw
      gh extension install github/gh-aw
    
    Unrelated failures (network, auth, etc.) return the underlying error without Windows-specific messaging.
  • isWindowsLockError() — new helper that detects Windows binary-lock errors from the first-attempt output and retry error
  • Cross-platform upgrade integration test — new .github/workflows/upgrade-test.yml runs a matrix (ubuntu/macos/windows) that dynamically finds the two most recent stable releases, installs n-1, runs gh aw upgrade --no-fix (which calls upgradeExtensionIfOutdated — the running binary tries to upgrade itself, exercising the rename+retry path on Windows), and verifies the installed version is now n
  • Unit tests — updated TestFirstAttemptWriter_NonLinuxNonLinuxNonWindows, added TestFirstAttemptWriter_Windows, TestNeedsRenameWorkaround, and TestIsWindowsLockError

[!WARNING]

Copilot AI linked an issue Apr 20, 2026 that may be closed by this pull request
On Windows, `gh extension upgrade` fails with "Access is denied" when
trying to overwrite the currently-running gh-aw.exe binary. This is
the same root cause as the Linux ETXTBSY error, solved the same way:
rename the running binary away before running the upgrade.

Changes:
- Add needsRenameWorkaround() helper (Linux or Windows)
- Buffer first-attempt output on Windows (same as Linux) to suppress
  the confusing "Access is denied" message when retry succeeds
- Attempt rename+retry on Windows, not just Linux
- When rename+retry fails on Windows, print clear manual upgrade
  instructions: `gh extension upgrade gh-aw` or reinstall
- Update renamePathForUpgrade log message to be platform-generic
- Update tests: rename TestFirstAttemptWriter_NonLinux, add
  TestFirstAttemptWriter_Windows and TestNeedsRenameWorkaround

Fixes #<windows-upgrade-issue>

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f1110ed2-0ef6-41a7-9076-b76acf864fb7

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix gh-aw upgrade failure on Windows fix(upgrade): extend rename+retry workaround to Windows Apr 20, 2026
Copilot AI requested a review from dsyme April 20, 2026 05:08
@dsyme dsyme marked this pull request as ready for review April 20, 2026 05:23
Copilot AI review requested due to automatic review settings April 20, 2026 05:23
@github-actions github-actions bot mentioned this pull request Apr 20, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the existing rename+retry self-upgrade workaround (previously Linux-only) to also cover Windows, and updates tests to reflect the new platform behavior.

Changes:

  • Centralized platform gating into a new needsRenameWorkaround() helper (Linux + Windows).
  • Buffered first-attempt upgrade output on Windows to suppress confusing “Access is denied” when retry succeeds.
  • Added Windows-specific fallback guidance plus updated/added unit tests.
Show a summary per file
File Description
pkg/cli/update_extension_check.go Applies rename+retry + buffered output behavior to Windows, adds needsRenameWorkaround(), and prints Windows fallback guidance on failure.
pkg/cli/update_extension_check_test.go Splits non-Linux test into Windows vs non-Linux/non-Windows, and adds coverage for needsRenameWorkaround().

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread pkg/cli/update_extension_check.go
Comment thread pkg/cli/update_extension_check.go Outdated
Comment on lines +203 to +207
@@ -181,7 +204,7 @@ func renamePathForUpgrade(exe string) (string, error) {
if err := os.Rename(exe, backup); err != nil {
return "", fmt.Errorf("could not rename %s → %s: %w", exe, backup, err)
}
updateExtensionCheckLog.Printf("Renamed %s → %s to avoid ETXTBSY on Linux", exe, backup)
updateExtensionCheckLog.Printf("Renamed %s → %s to free path for upgrade", exe, backup)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, the renamed ".bak" file is the currently-running executable, so cleanupExecutableBackup(installPath) will typically fail (cannot delete/replace a running .exe). That leaves a persistent *.bak, and future calls to renamePathForUpgrade will fail because the destination already exists. Consider using a unique backup filename (e.g., include PID/timestamp) and/or deferring cleanup to the relaunched process (or a next-run cleanup) so repeated upgrades don't get stuck.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 332839arenamePathForUpgrade now uses a PID-qualified backup path (exe.<pid>.bak). Each invocation gets a unique backup name, so a failed cleanup (e.g. Windows can't delete the running .exe) won't block a subsequent upgrade attempt. restoreExecutableBackup and cleanupExecutableBackup now take the backup path directly instead of deriving it from installPath.

Comment thread pkg/cli/update_extension_check.go Outdated
Comment on lines +155 to +160
if runtime.GOOS == "windows" {
// On Windows, self-upgrade may not be possible while the binary is
// running. Guide the user to upgrade manually from a separate shell.
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("On Windows, gh-aw cannot self-upgrade while it is running."))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Please upgrade manually by running one of the following:"))
fmt.Fprintln(os.Stderr, console.FormatCommandMessage("gh extension upgrade gh-aw"))
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows fallback guidance is printed for any retry failure, even when the error isn't related to the running-binary lock (e.g., network/auth failures). This can mislead users. Consider emitting this guidance only when the first-attempt output (or retry error) matches the expected Windows lock symptom (e.g., contains "Access is denied" / sharing violation), and otherwise return the underlying upgrade error without the Windows-specific message.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 332839a — added isWindowsLockError which checks for "Access is denied" or "The process cannot access the file" in both the first-attempt output and the retry error. The Windows self-upgrade guidance is now gated behind this check, so unrelated failures (network, auth, etc.) just return the underlying error without the Windows-specific message.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100

Excellent test quality

Metric Value
New/modified tests analyzed 3
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (67%)
Duplicate test clusters 0
Test inflation detected No (0.51:1 ratio)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
TestFirstAttemptWriter_Windows pkg/cli/update_extension_check_test.go ✅ Design None — verifies Windows buffer routing
TestFirstAttemptWriter_NonLinuxNonWindows pkg/cli/update_extension_check_test.go ✅ Design None — covers other-platform edge case
TestNeedsRenameWorkaround pkg/cli/update_extension_check_test.go ✅ Design None — verifies platform-detection invariant

Flagged Tests — Requires Review

None. All tests passed quality checks.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 3 tests — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All three tests enforce behavioral contracts for platform-specific logic. The rename was from TestFirstAttemptWriter_NonLinuxTestFirstAttemptWriter_Windows (now correctly testing the Windows path), with a new TestFirstAttemptWriter_NonLinuxNonWindows covering other platforms, and a clean invariant test for needsRenameWorkaround(). Assertion messages are present throughout, build tag is correct, and no mock libraries are used.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References:

🧪 Test quality analysis by Test Quality Sentinel · ● 481.9K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 90/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All three modified/added tests enforce behavioral contracts for platform-specific routing logic.

@github-actions
Copy link
Copy Markdown
Contributor

Hey @Copilot 👋 — great work extending the rename+retry workaround to Windows! Fixing the Access is denied self-upgrade failure is a meaningful quality-of-life improvement for Windows users, and the approach (centralizing the platform check into needsRenameWorkaround()) keeps the code clean and readable.

This PR looks well-structured and ready for maintainer review. ✅


Contribution check result: 🟢 Aligned

Check Result
On-topic ✅ yes
Follows process ✅ yes (core team agent PR)
Focused ✅ yes
New dependencies ✅ no
Has tests ✅ yes
Has description ✅ yes
Lines changed 90

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pypi.org"

See Network Configuration for more information.

Generated by Contribution Check · ● 1.8M ·

- Update upgradeExtensionIfOutdated doc comment: installPath is now
  captured on both Linux and Windows, not just Linux
- Use PID-qualified backup path (exe.<pid>.bak) in renamePathForUpgrade
  so a failed cleanup (Windows can't delete running .exe) does not block
  a subsequent upgrade attempt
- Thread backupPath through to restoreExecutableBackup/cleanupExecutableBackup
  instead of re-deriving it from installPath
- Add isWindowsLockError helper; gate Windows self-upgrade guidance behind
  it so the message is only shown for lock-related errors, not network/auth
- Update tests for new signatures; add TestIsWindowsLockError table test

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b19bd17d-d6cc-4706-a5d7-e9bf2bec4f79

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI requested a review from dsyme April 20, 2026 09:03
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot update rendering of instructions so that they can be copy pasted. See extra ⚡ below


ℹ On Windows, gh-aw cannot self-upgrade while it is running.
⚡ gh extension upgrade gh-aw
⚡ gh extension remove gh-aw
⚡ gh extension install github/gh-aw

…d text

Replace FormatCommandMessage (which prepends ⚡) with plain fmt.Fprintln
calls using two-space indentation, so the commands can be copy-pasted
directly from the terminal output.

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/a1aa13a5-ade7-48fb-a03f-f7ce17767d62

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 20, 2026

@copilot update rendering of instructions so that they can be copy pasted. See extra ⚡ below

-----...

Fixed in 66245e0 — replaced FormatCommandMessage (which adds ) with plain fmt.Fprintln calls using two-space indentation. The output now looks like:

ℹ On Windows, gh-aw cannot self-upgrade while it is running.
ℹ Please upgrade manually by running one of the following:
  gh extension upgrade gh-aw
ℹ If that does not work, try reinstalling:
  gh extension remove gh-aw
  gh extension install github/gh-aw

Copilot AI requested a review from pelikhan April 20, 2026 12:51
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot

  • fix command message to be copy paste able (indented)
  • add a cross-platform integration test (matrix) that finds the pre-releases, installs n-1, then upgrade to make sure upgrade path works

New upgrade-test.yml workflow runs a matrix (ubuntu/macos/windows) that:
- Finds the two most recent stable (non-prerelease) releases dynamically
- Installs n-1 with gh extension install --pin
- Runs `gh aw upgrade --no-fix` which triggers upgradeExtensionIfOutdated,
  exercising the rename+retry workaround on Windows
- Verifies the installed version matches n

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2cba2b36-2ef5-482f-bd58-834760f0ad7c

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 20, 2026

@copilot

  • fix command message to be copy paste able (indented)
  • add a cross-platform integration test (matrix) that finds the pre-releases, installs n-1, then upgrade to make sure upgrade path work...

Both items addressed in 80b9c8c:

  1. Copy-paste commands — already fixed in 66245e0 by replacing FormatCommandMessage (which adds ) with plain indented fmt.Fprintln lines.

  2. Cross-platform upgrade integration test — added .github/workflows/upgrade-test.yml. It runs a matrix over ubuntu-latest, macos-latest, and windows-latest and:

    • Finds the two most recent stable (non-prerelease) releases dynamically via the GitHub API
    • Installs n-1 with gh extension install --pin
    • Runs gh aw upgrade --no-fix, which calls upgradeExtensionIfOutdated — the binary is the running process at that point, so on Windows the rename+retry workaround is exercised
    • Verifies the installed version is now n

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh gh release list --repo github/gh-aw --limit 10 (http block)

If you need me to access, download, or install something from one of these locations, you can either:

name: Extension Upgrade Test

on:
workflow_dispatch: {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot run on release created

# --no-fix keeps the test focused: skips codemods, action updates, and compilation.
gh aw upgrade --no-fix

- name: Verify version after upgrade
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot file an issue if failed

@pelikhan pelikhan merged commit 4d620b3 into main Apr 20, 2026
84 checks passed
@pelikhan pelikhan deleted the copilot/fix-gh-aw-upgrade-windows-issue branch April 20, 2026 13:37
Copilot AI requested a review from pelikhan April 20, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On Windows gh-aw upgrade can't update gh-aw

4 participants