Skip to content

fix: force GH_HOST=github.com for action SHA resolution in resolveFromGitHub#40031

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-resolve-from-github-host
Jun 18, 2026
Merged

fix: force GH_HOST=github.com for action SHA resolution in resolveFromGitHub#40031
pelikhan merged 6 commits into
mainfrom
copilot/fix-resolve-from-github-host

Conversation

Copilot AI commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

When GH_HOST is set to a GHE instance, resolveFromGitHub inherits that host in its gh api subprocesses, causing action SHA lookups (e.g. actions/checkout@v4) to query the wrong endpoint and silently fall back to bundled hardcoded pins.

Changes

  • pkg/workflow/github_cli.go — adds ForceGHHostEnv(cmd, host): strips any existing GH_HOST from the command env (including inherited process env) and sets the specified value. Unlike SetGHHostEnv, this always applies — including for "github.com" — to override a GHE host set by the caller.

  • pkg/workflow/github_cli_wasm.go — adds a no-op stub for ForceGHHostEnv so action_resolver.go (no build constraint) compiles cleanly for WASM targets.

  • pkg/workflow/action_resolver.go — calls ForceGHHostEnv(cmd, "github.com") immediately after each ExecGHContext in resolveFromGitHub: once for the initial tag ref lookup and once per annotated-tag peel iteration. ResolveGhAwRef is intentionally unchanged — it targets github/gh-aw, which may live on the user's configured host.

  • pkg/workflow/action_resolver_test.go — adds TestResolveFromGitHubForcesGitHubComHost covering: GH_HOST truly unset, set to a GHE host, and already set to github.com.

// Before — inherits GH_HOST from process env:
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", "...")

// After — always targets github.com:
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", "...")
ForceGHHostEnv(cmd, "github.com")

Copilot AI and others added 3 commits June 18, 2026 11:32
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
When GH_HOST is set to a GHE instance, action SHA resolution via `gh api`
was targeting the wrong host. Add ForceGHHostEnv helper that strips any
existing GH_HOST from the command environment and sets GH_HOST=github.com,
then call it in resolveFromGitHub (both the initial tag lookup and the
annotated-tag peel loop) so action SHAs are always resolved against github.com.

ResolveGhAwRef is left unchanged since it resolves refs in the github/gh-aw
repo, which may live on the user's configured host.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix resolveFromGitHub to force github.com host fix: force GH_HOST=github.com for action SHA resolution in resolveFromGitHub Jun 18, 2026
Copilot AI requested a review from pelikhan June 18, 2026 11:45
@pelikhan pelikhan marked this pull request as ready for review June 18, 2026 12:03
Copilot AI review requested due to automatic review settings June 18, 2026 12:03
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #40031 does not have the 'implementation' label and has 91 new lines of code in business logic directories, which is below the 100-line threshold.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot mentioned this pull request Jun 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to make ActionResolver.resolveFromGitHub reliably resolve action tag SHAs from github.com even when the caller has GH_HOST set to a GitHub Enterprise host, avoiding silent fallbacks to bundled pins caused by querying the wrong API endpoint.

Changes:

  • Adds ForceGHHostEnv to unconditionally set GH_HOST on gh subprocesses (overriding inherited env).
  • Uses ForceGHHostEnv(cmd, "github.com") for the gh api calls inside resolveFromGitHub (initial tag lookup + peel loop).
  • Updates several generated workflow .lock.yml files to include awk in the allowed-tools list (not described in the PR summary).
Show a summary per file
File Description
pkg/workflow/github_cli.go Adds ForceGHHostEnv to override inherited GH_HOST for subprocesses.
pkg/workflow/github_cli_wasm.go Adds a wasm-safe no-op stub for ForceGHHostEnv.
pkg/workflow/action_resolver.go Forces GH_HOST=github.com on gh api commands used for action SHA resolution.
pkg/workflow/action_resolver_test.go Adds a test for host-forcing behavior (but currently doesn’t exercise resolveFromGitHub itself).
actions/setup-cli/install.sh Updates script sync comment.
.github/workflows/spec-librarian.lock.yml Adds shell(awk) to allowed tools list.
.github/workflows/go-fan.lock.yml Adds Bash(awk) to allowed tools list.
.github/workflows/daily-testify-uber-super-expert.lock.yml Adds shell(awk) to allowed tools list.
.github/workflows/daily-file-diet.lock.yml Adds shell(awk) to allowed tools list.

Copilot's findings

Tip

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

  • Files reviewed: 9/9 changed files
  • Comments generated: 7

Comment thread pkg/workflow/action_resolver.go Outdated
Comment on lines +292 to +296
if err := os.Unsetenv("GH_HOST"); err != nil {
t.Fatalf("failed to unset GH_HOST: %v", err)
}
t.Cleanup(func() { os.Unsetenv("GH_HOST") }) //nolint:errcheck
}

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: the "GH_HOST unset" branch now uses os.LookupEnv to save the original value and t.Cleanup to restore it (or re-unset if it was never set), matching the pattern used in github_cli_test.go.

Comment thread pkg/workflow/action_resolver_test.go Outdated
Comment on lines +262 to +266
// TestResolveFromGitHubForcesGitHubComHost verifies that the commands created by
// resolveFromGitHub always have GH_HOST=github.com in their environment, regardless
// of the process-level GH_HOST setting. This ensures that action SHA lookups always
// target github.com even when the user has GH_HOST set to a GHE instance.
func TestResolveFromGitHubForcesGitHubComHost(t *testing.T) {

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: renamed to TestForceGHHostEnvSetsGitHubCom with an updated comment accurately describing what is tested (direct ForceGHHostEnv coverage, not resolveFromGitHub integration, since the latter requires a live gh api endpoint).

Comment on lines 825 to 829
# --allow-tool github
# --allow-tool safeoutputs
# --allow-tool serena
# --allow-tool shell(awk)
# --allow-tool shell(cat pkg/**/*.go)

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.

Reverted: the shell(awk) addition was unrelated to this fix and is not present in the markdown source; the lock file has been restored to its original state.

Comment on lines 825 to 829
# --allow-tool github
# --allow-tool safeoutputs
# --allow-tool serena
# --allow-tool shell(awk)
# --allow-tool shell(cat pkg/**/*.go)

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.

Reverted: the shell(awk) addition was unrelated to this fix and is not present in the markdown source; the lock file has been restored to its original state.

Comment on lines 851 to 855
# --allow-tool github
# --allow-tool safeoutputs
# --allow-tool serena
# --allow-tool shell(awk)
# --allow-tool shell(cat **/*_test.go)

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.

Reverted: the shell(awk) addition was unrelated to this fix and is not present in the markdown source; the lock file has been restored to its original state.

Comment on lines 844 to 848
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
# - Bash(awk)
# - Bash(cat /tmp/gh-aw/cache-memory/)

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.

Reverted: the Bash(awk) addition was unrelated to this fix and is not present in the markdown source; the lock file has been restored to its original state.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

Analyzed 3 test scenario(s) across 1 table-driven function: 3 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (3 tests analyzed)
Metric Value
New/modified tests analyzed 3 (table rows in 1 function)
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — 64 test lines vs 2 production lines in paired action_resolver.go (32:1; note github_cli.go adds 21 more lines for the helper, total 23 production lines → 2.8:1)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestResolveFromGitHubForcesGitHubComHost / GH_HOST unset pkg/workflow/action_resolver_test.go:265 ✅ Design
TestResolveFromGitHubForcesGitHubComHost / GH_HOST set to GHE host pkg/workflow/action_resolver_test.go:265 ✅ Design
TestResolveFromGitHubForcesGitHubComHost / GH_HOST already set to github.com pkg/workflow/action_resolver_test.go:265 ✅ Design

Go: 3 (*_test.go); JavaScript: 0 (*.test.cjs, *.test.js). No other languages detected.

Scoring breakdown:

  • Behavioral coverage: 40/40 (100% design tests)
  • Error/edge case coverage: 30/30 (all 3 scenarios include error paths and edge cases)
  • Duplication penalty: 0 → 20/20
  • Test inflation penalty: −10 (test lines 64 vs combined production 23 → 2.8:1 > 2:1 threshold)

Verdict

Check passed. 0% implementation tests (threshold: 30%). The single table-driven test covers all three meaningful GH_HOST states (unset, GHE instance, already github.com) with proper env-isolation assertions, a nil-guard, and stdlib t.Fatal/t.Errorf calls that all carry descriptive messages. No mocks, no missing build tags, no guideline violations. The inflation flag is metric-mechanical: the fix touched two production files whose combined 23 new lines still yield a ~2.8:1 ratio, but the coverage quality is high.

🧪 Test quality analysis by Test Quality Sentinel ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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%).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd and /diagnose — approving with non-blocking suggestions on test quality.

📋 Key Themes & Highlights

Key Themes

  • Test scope mismatch: TestResolveFromGitHubForcesGitHubComHost tests the ForceGHHostEnv helper in isolation rather than exercising resolveFromGitHub directly. The test name overpromises — if the two call sites in action_resolver.go were removed, the test would still pass.
  • Test isolation gap: The "GH_HOST unset" subtest doesn't restore a pre-existing GH_HOST value on cleanup; using os.LookupEnv to save/restore would fully mirror t.Setenv semantics.
  • Untested branch: The cmd.Env != nil branch of ForceGHHostEnv (the dedup loop) has no test coverage, since ExecGHContext always yields a nil-Env cmd at the call sites.

Positive Highlights

  • ✅ Root cause correctly addressed: both call sites in resolveFromGitHub (initial tag lookup + annotated-tag peel loop) now force GH_HOST=github.com
  • ResolveGhAwRef intentionally left unchanged — correctly preserving the user-configured host for self-referential lookups against github/gh-aw
  • ForceGHHostEnv correctly strips all existing GH_HOST= entries before appending — avoids the duplicate-entry risk present in the older SetGHHostEnv
  • ✅ WASM stub is the right call: maintains build-constraint parity with zero runtime impact
  • ✅ Three-boundary parametric test (unset / GHE / github.com) is a solid smoke test for the helper function

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

Comment thread pkg/workflow/action_resolver_test.go Outdated
// resolveFromGitHub always have GH_HOST=github.com in their environment, regardless
// of the process-level GH_HOST setting. This ensures that action SHA lookups always
// target github.com even when the user has GH_HOST set to a GHE instance.
func TestResolveFromGitHubForcesGitHubComHost(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The test name and comment say it verifies resolveFromGitHub commands have GH_HOST=github.com, but the body only exercises ForceGHHostEnv directly — never calling resolveFromGitHub. If the two ForceGHHostEnv calls were removed from action_resolver.go, this test would still pass.

💡 Suggested improvement

Consider renaming to TestForceGHHostEnvStripsProcessGHHost to accurately reflect what is tested, and add a comment explaining why resolveFromGitHub isn't exercised directly (live gh api endpoint required).

// TestForceGHHostEnvStripsProcessGHHost verifies that ForceGHHostEnv overrides
// any process-level GH_HOST, including GHE hosts.
// Note: resolveFromGitHub is not called directly here because it requires a live
// gh api endpoint. The integration contract is enforced by the ForceGHHostEnv call
// sites in action_resolver.go.
func TestForceGHHostEnvStripsProcessGHHost(t *testing.T) {

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: renamed to TestForceGHHostEnvSetsGitHubCom with a comment explaining that resolveFromGitHub isn't called directly because it requires a live gh api endpoint.

} else {
// Truly unset GH_HOST (t.Setenv("GH_HOST", "") sets to empty string,
// which is not the same as unset; use os.Unsetenv with cleanup instead).
if err := os.Unsetenv("GH_HOST"); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The cleanup for the "GH_HOST unset" case doesn't restore the original GH_HOST value if it was set in the environment before this test ran — the t.Cleanup just calls os.Unsetenv again instead of restoring what was there.

💡 Suggested fix using os.LookupEnv
} else {
    original, hadVal := os.LookupEnv("GH_HOST")
    if err := os.Unsetenv("GH_HOST"); err != nil {
        t.Fatalf("failed to unset GH_HOST: %v", err)
    }
    t.Cleanup(func() {
        if hadVal {
            os.Setenv("GH_HOST", original) (nolint/redacted):errcheck
        } else {
            os.Unsetenv("GH_HOST") (nolint/redacted):errcheck
        }
    })
}

This mirrors what t.Setenv does internally, ensuring the test doesn't permanently unset GH_HOST for other tests running in the same process (e.g. in CI where GH_HOST may be set globally).

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: the "GH_HOST unset" branch now saves the original value with os.LookupEnv and restores it (or re-unsets) in t.Cleanup, exactly as suggested.

return
}
base := cmd.Env
if base == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] ForceGHHostEnv has two branches: cmd.Env == nil (hydrates from os.Environ()) and cmd.Env != nil (uses the existing slice). The new tests only exercise the nil-Env branch because ExecGHContext always returns a cmd with nil Env. The non-nil branch — including its dedup logic — has no test coverage.

💡 Suggested test

Add a case to the existing table (or a separate TestForceGHHostEnvWithPresetCmdEnv) that pre-populates cmd.Env with a stale GH_HOST entry:

cmd := exec.Command("gh", "api", "/test")
cmd.Env = []string{"GH_HOST=stale.ghe.com", "OTHER=value"}
ForceGHHostEnv(cmd, "github.com")
// assert: exactly one GH_HOST=github.com, no stale entry, OTHER preserved

This would guard the dedup loop and ensure SetGHHostEnv + ForceGHHostEnv chaining (if it ever occurs) doesn't produce duplicate entries.

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: added TestForceGHHostEnvWithPresetCmdEnv which pre-populates cmd.Env with ["GH_HOST=stale.ghe.com", "OTHER=value"], calls ForceGHHostEnv, then asserts exactly one GH_HOST=github.com entry and that OTHER=value is preserved. This covers the non-nil branch and the dedup loop.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

REQUEST_CHANGES — two correctness gaps and two test quality issues

The fix logic in ForceGHHostEnv is correct in isolation, but there are two blocking issues before merging.

Blocking issues

1. The test doesn't exercise resolveFromGitHub (see inline comment on line 298 of the test file). TestResolveFromGitHubForcesGitHubComHost directly calls the helper primitives and would pass even if both ForceGHHostEnv call sites were removed from resolveFromGitHub. The regression this PR is meant to prevent has no automated guard.

2. ResolveGhAwRef is left with the same ambient-host problem (see inline comment on line 262 of action_resolver.go). apiPath is hardcoded to /repos/github/gh-aw/commits/..., so a GHE GH_HOST will silently route it to the wrong server. The PR description calls this intentional, but the stated reason (github/gh-aw may live on the user's host) is not convincing — this is the extension's own canonical repo on github.com. At minimum a code comment justifying the asymmetry is needed; more likely ForceGHHostEnv should be applied here too.

Non-blocking but should be addressed
  • Test cleanup for the "unset" case uses bare os.Unsetenv instead of the save-and-restore idiom, which breaks isolation when GH_HOST was set before the test ran.
  • The defaultGHHost path through setupGHCommand is not exercised by any subtest.

🔎 Code quality review by PR Code Quality Reviewer

Comments that could not be inline-anchored

pkg/workflow/action_resolver_test.go:298

Test never exercises resolveFromGitHub, so removing the fix passes silently: this test constructs a command and calls ForceGHHostEnv directly — it does not go through resolveFromGitHub. If either of the two ForceGHHostEnv(cmd...) lines in action_resolver.go are removed in a future refactor, this test still passes.

<details>
<summary>💡 Suggested approach</summary>

Replace the direct-primitive test with a spy/intercept pattern that actually invokes resolveFromGitHub. One lightw…

pkg/workflow/action_resolver.go:262

ResolveGhAwRef silently misroutes under a GHE GH_HOST: the PR applies ForceGHHostEnv to resolveFromGitHub but leaves ResolveGhAwRef inheriting the process GH_HOST. Since apiPath is hardcoded to /repos/github/gh-aw/commits/..., a GHE host in the environment will cause this call to hit the wrong endpoint and fail with a confusing error.

<details>
<summary>💡 Detail and suggested fix</summary>

The PR description says _"ResolveGhAwRef is intentionally unchanged — it targets gi…

pkg/workflow/action_resolver_test.go:295

Cleanup for the "unset" case doesn't restore GH_HOST to its pre-test value: if the test environment has GH_HOST set (e.g., github.com in CI), os.Unsetenv at cleanup permanently drops it for the rest of the test binary run instead of restoring the original.

<details>
<summary>💡 Suggested fix</summary>

Use os.LookupEnv to save-and-restore, the same way t.Setenv works internally:

// Before clearing:
if orig, existed := os.LookupEnv(&quot;GH_HOST&quot;); existed {
    t.Cleanup(func</details>

<details><summary>pkg/workflow/action_resolver_test.go:286</summary>

**Missing coverage for the `defaultGHHost` path**: `setupGHCommand` (called by `ExecGHContext`) has a second way a GHE host can land in `cmd.Env`via `SetGHHostEnv(cmd, getDefaultGHHost())` when `GH_HOST` is absent from the process env but `SetDefaultGHHost(&quot;myorg.ghe.com&quot;)` was called earlier. No subtest exercises this path.

&lt;details&gt;
&lt;summary&gt;💡 Why it matters&lt;/summary&gt;

This is a plausible real-world config: a user has set a default GHE host programmatically but has no process-level `GH_H…

</details>

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits June 18, 2026 12:46
… cleanup in tests, rename test, revert unrelated lock file changes

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

All review threads addressed. Summary of changes made:

  • Removed redundant defer peelCancel() in action_resolver.go
  • Fixed GH_HOST env save/restore in the "unset" test branch using os.LookupEnv + t.Cleanup
  • Renamed test to TestForceGHHostEnvSetsGitHubCom to accurately reflect its scope
  • Added TestForceGHHostEnvWithPresetCmdEnv to cover the non-nil cmd.Env branch of ForceGHHostEnv
  • Reverted unrelated awk additions from 4 workflow lock files

All unit tests pass locally. CI needs a human re-trigger after the agent's push.

@pelikhan pelikhan merged commit 81ff8ef into main Jun 18, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/fix-resolve-from-github-host branch June 18, 2026 13:16
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.

3 participants