Skip to content

Commit 94b89bb

Browse files
fix: use PR head SHA instead of merge commit SHA in GitHub Actions (#192)
1 parent b76f492 commit 94b89bb

2 files changed

Lines changed: 204 additions & 1 deletion

File tree

cmd/run.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"context"
55
_ "embed"
6+
"encoding/json"
67
"fmt"
78
"os"
89
"os/exec"
@@ -965,7 +966,11 @@ func validateCIMetadata(metadata CIMetadata) (CIMetadata, error) {
965966
if inCI {
966967
if metadata.CommitSha == "" {
967968
if isGitHub {
968-
metadata.CommitSha = os.Getenv("GITHUB_SHA")
969+
if sha := getGitHubPRHeadSHA(); sha != "" {
970+
metadata.CommitSha = sha
971+
} else {
972+
metadata.CommitSha = os.Getenv("GITHUB_SHA")
973+
}
969974
} else if isGitLab {
970975
metadata.CommitSha = os.Getenv("CI_COMMIT_SHA")
971976
}
@@ -1023,6 +1028,38 @@ func validateCIMetadata(metadata CIMetadata) (CIMetadata, error) {
10231028
return metadata, nil
10241029
}
10251030

1031+
// getGitHubPRHeadSHA reads the actual PR head commit SHA from the GitHub Actions
1032+
// event payload. Returns "" if not a PR event or if the SHA cannot be determined.
1033+
func getGitHubPRHeadSHA() string {
1034+
eventName := os.Getenv("GITHUB_EVENT_NAME")
1035+
if eventName != "pull_request" && eventName != "pull_request_target" {
1036+
return ""
1037+
}
1038+
1039+
eventPath := os.Getenv("GITHUB_EVENT_PATH")
1040+
if eventPath == "" {
1041+
return ""
1042+
}
1043+
1044+
data, err := os.ReadFile(eventPath) //nolint:gosec // path comes from trusted GITHUB_EVENT_PATH env var set by GitHub Actions
1045+
if err != nil {
1046+
return ""
1047+
}
1048+
1049+
var event struct {
1050+
PullRequest struct {
1051+
Head struct {
1052+
SHA string `json:"sha"`
1053+
} `json:"head"`
1054+
} `json:"pull_request"`
1055+
}
1056+
if err := json.Unmarshal(data, &event); err != nil {
1057+
return ""
1058+
}
1059+
1060+
return event.PullRequest.Head.SHA
1061+
}
1062+
10261063
func stringPtr(s string) *string {
10271064
return &s
10281065
}

cmd/run_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func writeEventFile(t *testing.T, payload any) string {
13+
t.Helper()
14+
data, err := json.Marshal(payload)
15+
require.NoError(t, err)
16+
path := filepath.Join(t.TempDir(), "event.json")
17+
require.NoError(t, os.WriteFile(path, data, 0o600))
18+
return path
19+
}
20+
21+
func TestGetGitHubPRHeadSHA(t *testing.T) {
22+
tests := []struct {
23+
name string
24+
eventName string
25+
eventFile string // raw content; if empty, writeEventFile is used with payload
26+
payload any
27+
expected string
28+
}{
29+
{
30+
name: "pull_request event with valid JSON",
31+
eventName: "pull_request",
32+
payload: map[string]any{
33+
"pull_request": map[string]any{
34+
"head": map[string]any{
35+
"sha": "abc123def456",
36+
},
37+
},
38+
},
39+
expected: "abc123def456",
40+
},
41+
{
42+
name: "pull_request_target event",
43+
eventName: "pull_request_target",
44+
payload: map[string]any{
45+
"pull_request": map[string]any{
46+
"head": map[string]any{
47+
"sha": "target789",
48+
},
49+
},
50+
},
51+
expected: "target789",
52+
},
53+
{
54+
name: "non-PR event returns empty",
55+
eventName: "push",
56+
expected: "",
57+
},
58+
{
59+
name: "no event name returns empty",
60+
eventName: "",
61+
expected: "",
62+
},
63+
{
64+
name: "malformed JSON returns empty",
65+
eventName: "pull_request",
66+
eventFile: "{invalid json",
67+
expected: "",
68+
},
69+
{
70+
name: "missing sha field returns empty",
71+
eventName: "pull_request",
72+
payload: map[string]any{
73+
"pull_request": map[string]any{
74+
"head": map[string]any{},
75+
},
76+
},
77+
expected: "",
78+
},
79+
{
80+
name: "missing event file returns empty",
81+
eventName: "pull_request",
82+
eventFile: "__nonexistent__",
83+
expected: "",
84+
},
85+
{
86+
name: "empty event path returns empty",
87+
eventName: "pull_request",
88+
eventFile: "",
89+
expected: "",
90+
},
91+
}
92+
93+
for _, tt := range tests {
94+
t.Run(tt.name, func(t *testing.T) {
95+
t.Setenv("GITHUB_EVENT_NAME", tt.eventName)
96+
97+
switch {
98+
case tt.eventFile == "__nonexistent__":
99+
t.Setenv("GITHUB_EVENT_PATH", "/nonexistent/path/event.json")
100+
case tt.eventFile != "":
101+
path := filepath.Join(t.TempDir(), "event.json")
102+
require.NoError(t, os.WriteFile(path, []byte(tt.eventFile), 0o600))
103+
t.Setenv("GITHUB_EVENT_PATH", path)
104+
case tt.payload != nil:
105+
t.Setenv("GITHUB_EVENT_PATH", writeEventFile(t, tt.payload))
106+
default:
107+
t.Setenv("GITHUB_EVENT_PATH", "")
108+
}
109+
110+
got := getGitHubPRHeadSHA()
111+
require.Equal(t, tt.expected, got)
112+
})
113+
}
114+
}
115+
116+
func TestValidateCIMetadata_GitHubPRSHA(t *testing.T) {
117+
t.Run("uses PR head SHA over GITHUB_SHA", func(t *testing.T) {
118+
t.Setenv("GITHUB_ACTIONS", "true")
119+
t.Setenv("GITHUB_SHA", "merge-commit-sha")
120+
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
121+
t.Setenv("GITHUB_EVENT_PATH", writeEventFile(t, map[string]any{
122+
"pull_request": map[string]any{
123+
"head": map[string]any{
124+
"sha": "real-head-sha",
125+
},
126+
},
127+
}))
128+
t.Setenv("GITHUB_REF", "refs/pull/42/merge")
129+
t.Setenv("GITHUB_HEAD_REF", "feature-branch")
130+
131+
meta, err := validateCIMetadata(CIMetadata{})
132+
require.NoError(t, err)
133+
require.Equal(t, "real-head-sha", meta.CommitSha)
134+
})
135+
136+
t.Run("falls back to GITHUB_SHA on non-PR event", func(t *testing.T) {
137+
t.Setenv("GITHUB_ACTIONS", "true")
138+
t.Setenv("GITHUB_SHA", "push-sha")
139+
t.Setenv("GITHUB_EVENT_NAME", "push")
140+
t.Setenv("GITHUB_REF", "refs/pull/42/merge")
141+
t.Setenv("GITHUB_HEAD_REF", "feature-branch")
142+
143+
meta, err := validateCIMetadata(CIMetadata{})
144+
require.NoError(t, err)
145+
require.Equal(t, "push-sha", meta.CommitSha)
146+
})
147+
148+
t.Run("explicit flag takes precedence", func(t *testing.T) {
149+
t.Setenv("GITHUB_ACTIONS", "true")
150+
t.Setenv("GITHUB_SHA", "merge-commit-sha")
151+
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
152+
t.Setenv("GITHUB_EVENT_PATH", writeEventFile(t, map[string]any{
153+
"pull_request": map[string]any{
154+
"head": map[string]any{
155+
"sha": "real-head-sha",
156+
},
157+
},
158+
}))
159+
t.Setenv("GITHUB_REF", "refs/pull/42/merge")
160+
t.Setenv("GITHUB_HEAD_REF", "feature-branch")
161+
162+
meta, err := validateCIMetadata(CIMetadata{CommitSha: "flag-sha"})
163+
require.NoError(t, err)
164+
require.Equal(t, "flag-sha", meta.CommitSha)
165+
})
166+
}

0 commit comments

Comments
 (0)