Skip to content

Commit 210fcdc

Browse files
Copilotpelikhan
andauthored
Skip hardcoded action pin fallback when GH_HOST is a non-github.qkg1.top host
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 7772632 commit 210fcdc

5 files changed

Lines changed: 97 additions & 0 deletions

File tree

.changeset/patch-skip-hardcoded-pins-on-ghe-host.md

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

pkg/actionpins/actionpins.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ type PinContext struct {
9898
Warnings map[string]bool
9999
// RecordResolutionFailure receives unresolved pinning failures for auditing.
100100
RecordResolutionFailure func(f ResolutionFailure)
101+
// SkipHardcodedFallback suppresses the non-strict hardcoded-pin fallback that
102+
// fires when dynamic resolution fails. Set this when GH_HOST is configured to a
103+
// non-github.qkg1.top host: the dynamic resolver will query the wrong host and fail,
104+
// so silently falling back to bundled pins would produce unverified SHA pins.
105+
SkipHardcodedFallback bool
101106
}
102107

103108
var (
@@ -376,6 +381,16 @@ func logDynamicResolutionSkipped(hasResolver, isAlreadySHA bool) {
376381

377382
func resolveActionPinFromHardcodedPins(actionRepo, version string, isAlreadySHA bool, ctx *PinContext) (string, bool) {
378383
actionPinsLog.Printf("Falling back to hardcoded pins for %s@%s", actionRepo, version)
384+
385+
// When the caller is targeting a non-github.qkg1.top host (e.g. GHES/GHEC), the
386+
// dynamic resolver already failed because it queried the wrong host. Silently
387+
// falling back to bundled pins in that case produces unverified SHA pins and
388+
// masks the real problem, so skip this fallback entirely.
389+
if ctx.SkipHardcodedFallback {
390+
actionPinsLog.Printf("SkipHardcodedFallback set, skipping hardcoded pin lookup for %s@%s", actionRepo, version)
391+
return "", false
392+
}
393+
379394
matchingPins := GetActionPinsByRepo(actionRepo)
380395
if len(matchingPins) == 0 {
381396
actionPinsLog.Printf("No hardcoded pins found for %s", actionRepo)

pkg/actionpins/actionpins_internal_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,25 @@ func TestResolveExactHardcodedPin_BySHA(t *testing.T) {
218218
require.True(t, ok, "Expected exact SHA match to resolve")
219219
assert.Contains(t, result, "sha-v5", "Expected result to include matched SHA")
220220
}
221+
222+
func TestResolveActionPinFromHardcodedPins_SkipHardcodedFallback(t *testing.T) {
223+
t.Run("returns false immediately when SkipHardcodedFallback is set", func(t *testing.T) {
224+
ctx := &PinContext{SkipHardcodedFallback: true, Warnings: make(map[string]bool)}
225+
226+
// actions/checkout has hardcoded pins, but SkipHardcodedFallback should prevent use
227+
result, ok := resolveActionPinFromHardcodedPins("actions/checkout", "v4", false, ctx)
228+
229+
assert.False(t, ok, "Expected SkipHardcodedFallback to prevent hardcoded pin lookup")
230+
assert.Empty(t, result, "Expected no pinned result when SkipHardcodedFallback is set")
231+
})
232+
233+
t.Run("allows hardcoded pins when SkipHardcodedFallback is not set", func(t *testing.T) {
234+
ctx := &PinContext{SkipHardcodedFallback: false, Warnings: make(map[string]bool)}
235+
236+
// actions/checkout has hardcoded pins and should resolve
237+
result, ok := resolveActionPinFromHardcodedPins("actions/checkout", "v4", false, ctx)
238+
239+
assert.True(t, ok, "Expected hardcoded pins to be consulted when SkipHardcodedFallback is false")
240+
assert.NotEmpty(t, result, "Expected a pinned result when SkipHardcodedFallback is not set")
241+
})
242+
}

pkg/workflow/compiler_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,13 @@ func (d *WorkflowData) PinContext() *actionpins.PinContext {
628628
if d.ActionResolver != nil {
629629
pinCtx.Resolver = d.ActionResolver
630630
}
631+
// When GH_HOST is set to a non-github.qkg1.top host (GHES/GHEC), the action
632+
// resolver targets that host and fails to resolve actions/* repos which live
633+
// on github.com. Silently falling back to bundled hardcoded pins in that
634+
// case produces unverified SHA pins, so disable the fallback.
635+
if ghHost := os.Getenv("GH_HOST"); ghHost != "" && ghHost != "github.com" {
636+
pinCtx.SkipHardcodedFallback = true
637+
}
631638
return pinCtx
632639
}
633640

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//go:build !integration
2+
3+
package workflow
4+
5+
import (
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestWorkflowData_PinContext_SkipHardcodedFallback(t *testing.T) {
14+
t.Run("sets SkipHardcodedFallback when GH_HOST is a non-github.qkg1.top host", func(t *testing.T) {
15+
t.Setenv("GH_HOST", "myorg.ghe.com")
16+
17+
d := &WorkflowData{}
18+
ctx := d.PinContext()
19+
20+
require.NotNil(t, ctx)
21+
assert.True(t, ctx.SkipHardcodedFallback, "Expected SkipHardcodedFallback to be true when GH_HOST is a GHE host")
22+
})
23+
24+
t.Run("does not set SkipHardcodedFallback when GH_HOST is github.com", func(t *testing.T) {
25+
t.Setenv("GH_HOST", "github.com")
26+
27+
d := &WorkflowData{}
28+
ctx := d.PinContext()
29+
30+
require.NotNil(t, ctx)
31+
assert.False(t, ctx.SkipHardcodedFallback, "Expected SkipHardcodedFallback to be false when GH_HOST is github.com")
32+
})
33+
34+
t.Run("does not set SkipHardcodedFallback when GH_HOST is not set", func(t *testing.T) {
35+
require.NoError(t, os.Unsetenv("GH_HOST"))
36+
37+
d := &WorkflowData{}
38+
ctx := d.PinContext()
39+
40+
require.NotNil(t, ctx)
41+
assert.False(t, ctx.SkipHardcodedFallback, "Expected SkipHardcodedFallback to be false when GH_HOST is not set")
42+
})
43+
44+
t.Run("returns nil for nil WorkflowData", func(t *testing.T) {
45+
var d *WorkflowData
46+
ctx := d.PinContext()
47+
assert.Nil(t, ctx)
48+
})
49+
}

0 commit comments

Comments
 (0)