-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrelease_workflow_security_test.go
More file actions
151 lines (141 loc) · 4.92 KB
/
Copy pathrelease_workflow_security_test.go
File metadata and controls
151 lines (141 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package main
import (
"os"
"regexp"
"strings"
"testing"
)
var (
fullActionSHA = regexp.MustCompile(`^[0-9a-f]{40}$`)
versionComment = regexp.MustCompile(`^# v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$`)
unreleasedHeading = regexp.MustCompile(`(?mi)^##[ \t]+\[unreleased\][ \t]*\r?$`)
generatedVersionHeader = regexp.MustCompile(`(?m)^## \[[0-9]+\.[0-9]+\.[0-9]+\]\(`)
)
// TestChangelogHasNoUnreleasedLedger pins #422's first migration invariant.
// Release Please derives versioned sections from squash commits (or their
// PR-body commit overrides); it never consumes a hand-maintained Unreleased
// section.
func TestChangelogHasNoUnreleasedLedger(t *testing.T) {
data, err := os.ReadFile("CHANGELOG.md")
if err != nil {
t.Fatalf("read changelog: %v", err)
}
changelog := string(data)
if unreleasedHeading.MatchString(changelog) {
t.Error("CHANGELOG.md contains an Unreleased section; add consumer detail to the PR-body commit override described in AGENTS.md")
}
if !generatedVersionHeader.MatchString(changelog) {
t.Fatal("CHANGELOG.md contains no generated version section; the ownership guard matched no Release Please output")
}
}
// TestReleaseWorkflowActionsAreImmutable is the regression guard for #208.
// The release job handles signing material and write-scoped tokens, so a
// movable tag on any third-party action is a credential-execution boundary,
// not merely dependency-update convenience.
func TestReleaseWorkflowActionsAreImmutable(t *testing.T) {
lines := releaseWorkflowLines(t)
pinned := 0
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "uses:") {
continue
}
value := strings.TrimSpace(strings.TrimPrefix(trimmed, "uses:"))
fields := strings.Fields(value)
if len(fields) == 0 {
t.Fatalf("release.yml:%d: empty action reference", i+1)
}
ref := fields[0]
if strings.HasPrefix(ref, "./") {
continue // Repository-local actions are reviewed with this repository.
}
at := strings.LastIndexByte(ref, '@')
if at < 1 || !fullActionSHA.MatchString(ref[at+1:]) {
t.Errorf("release.yml:%d: third-party action %q must use a full commit SHA", i+1, ref)
continue
}
comment := strings.TrimSpace(value[len(ref):])
if !versionComment.MatchString(comment) {
t.Errorf("release.yml:%d: pinned action %q must retain an exact version comment such as # v1.2.3", i+1, ref)
}
pinned++
}
if pinned == 0 {
t.Fatal("release.yml contains no pinned third-party actions; the guard matched nothing")
}
}
// TestReleaseWorkflowCheckoutDoesNotPersistCredentials pins the other half of
// #208: no checkout in the credential-bearing job may leave its token in the
// repository's git configuration for later steps to inherit.
func TestReleaseWorkflowCheckoutDoesNotPersistCredentials(t *testing.T) {
lines := releaseWorkflowLines(t)
checkouts := 0
for i, line := range lines {
if !strings.Contains(strings.TrimSpace(line), "uses: actions/checkout@") {
continue
}
checkouts++
usesIndent := leadingWhitespace(line)
found := false
for j := i + 1; j < len(lines); j++ {
trimmed := strings.TrimSpace(lines[j])
if strings.HasPrefix(trimmed, "- ") && leadingWhitespace(lines[j]) < usesIndent {
break
}
if trimmed == "persist-credentials: false" {
found = true
break
}
}
if !found {
t.Errorf("release.yml:%d: checkout must set persist-credentials: false in the same step", i+1)
}
}
if checkouts == 0 {
t.Fatal("release.yml contains no checkout step; the credential guard matched nothing")
}
}
// TestReleaseWorkflowBuildsWithLatestGoPatch pins check-latest on every
// setup-go step. go-version '1.26' alone lets setup-go use whatever 1.26 patch
// the runner image cached, so a release binary could ship on a patch older than
// the one the Vulncheck workflow scanned — with standard-library fixes that
// scan assumed were present.
func TestReleaseWorkflowBuildsWithLatestGoPatch(t *testing.T) {
lines := releaseWorkflowLines(t)
setups := 0
for i, line := range lines {
if !strings.Contains(strings.TrimSpace(line), "uses: actions/setup-go@") {
continue
}
setups++
usesIndent := leadingWhitespace(line)
found := false
for j := i + 1; j < len(lines); j++ {
trimmed := strings.TrimSpace(lines[j])
if strings.HasPrefix(trimmed, "- ") && leadingWhitespace(lines[j]) < usesIndent {
break
}
if trimmed == "check-latest: true" {
found = true
break
}
}
if !found {
t.Errorf("release.yml:%d: setup-go must set check-latest: true in the same step", i+1)
}
}
if setups == 0 {
t.Fatal("release.yml contains no setup-go step; the guard matched nothing")
}
}
func releaseWorkflowLines(t *testing.T) []string {
t.Helper()
data, err := os.ReadFile(".github/workflows/release.yml")
if err != nil {
t.Fatalf("read release workflow: %v", err)
}
return strings.Split(string(data), "\n")
}
func leadingWhitespace(s string) int {
return len(s) - len(strings.TrimLeft(s, " \t"))
}