packaging/naming_test.go
1
package packaging_test
3
import (
4
"fmt"
5
"os"
6
"path/filepath"
7
"regexp"
8
"sort"
9
"strings"
10
"testing"
11
)
13
const versionPlaceholder = "VERSION"
15
var (
16
buildMatrix = regexp.MustCompile(`for target in ([^;]+); do`)
17
archiveName = regexp.MustCompile(`name="koment_` + versionPlaceholder + `_\$\{os\}_\$\{arch\}"`)
18
archiveMentions = regexp.MustCompile(`koment_` + versionPlaceholder + `_([a-z0-9]+)_([a-z0-9]+)\.(tar\.gz|zip)`)
19
checksumMention = regexp.MustCompile(`koment_` + versionPlaceholder + `_checksums\.txt`)
20
)
22
var versionTokens = strings.NewReplacer(
23
"${VERSION}", versionPlaceholder,
24
"{{VERSION}}", versionPlaceholder,
25
"${version}", versionPlaceholder,
26
"$version", versionPlaceholder,
27
)
29
func repositoryFile(t *testing.T, path string) string {
30
t.Helper()
31
content, err := os.ReadFile(filepath.Join("..", filepath.FromSlash(path)))
32
if err != nil {
33
t.Fatalf("read %s: %v", path, err)
34
}
35
return versionTokens.Replace(string(content))
36
}
38
type archive struct {
39
os, arch, extension string
40
}
42
func (a archive) String() string {
43
return fmt.Sprintf("koment_%s_%s_%s.%s", versionPlaceholder, a.os, a.arch, a.extension)
44
}
46
func (a archive) binary() string {
47
if a.os == "windows" {
48
return "koment.exe"
49
}
50
return "koment"
51
}
53
const releaseWorkflow = ".github/workflows/release.yml"
55
func published(t *testing.T) map[archive]bool {
56
t.Helper()
57
workflow := repositoryFile(t, releaseWorkflow)
59
if !archiveName.MatchString(workflow) {
60
t.Fatalf("%s no longer builds koment_%s_<os>_<arch>; every consumer below is now guessing",
61
releaseWorkflow, versionPlaceholder)
62
}
63
matrix := buildMatrix.FindStringSubmatch(workflow)
64
if matrix == nil {
65
t.Fatalf("%s has no recognisable build matrix", releaseWorkflow)
66
}
68
archives := map[archive]bool{}
69
for _, target := range strings.Fields(matrix[1]) {
70
platform, architecture, found := strings.Cut(target, "/")
71
if !found {
72
t.Fatalf("build target %q is not os/arch", target)
73
}
74
archives[archive{os: platform, arch: architecture, extension: extensionFor(platform)}] = true
75
}
76
if len(archives) == 0 {
77
t.Fatal("the release workflow builds nothing")
78
}
79
return archives
80
}
82
func extensionFor(platform string) string {
83
if platform == "windows" {
84
return "zip"
85
}
86
return "tar.gz"
87
}
89
var consumers = map[string]string{
90
"the Homebrew tap": "packaging/homebrew/koment.rb.tmpl",
91
"the Scoop bucket": "packaging/scoop/koment.json.tmpl",
92
"the WinGet bundle": "packaging/winget/JanPuc.Koment.installer.yaml.tmpl",
93
}
95
const setupAction = "action.yml"
97
var (
98
actionArchive = regexp.MustCompile(`archive="koment_` + versionPlaceholder + `_\$\{platform\}_\$\{arch\}\.(tar\.gz|zip)"`)
99
actionPlatform = regexp.MustCompile(`platform=([a-z0-9]+)\s*;;`)
100
actionArchitecure = regexp.MustCompile(`arch=([a-z0-9]+)\s*;;`)
101
)
103
func requestedByTheSetupAction(t *testing.T) map[archive]bool {
104
t.Helper()
105
content := repositoryFile(t, setupAction)
107
template := actionArchive.FindStringSubmatch(content)
108
if template == nil {
109
t.Fatalf("%s no longer builds its download name from the runner's platform and architecture", setupAction)
110
}
111
platforms := actionPlatform.FindAllStringSubmatch(content, -1)
112
architectures := actionArchitecure.FindAllStringSubmatch(content, -1)
113
if platforms == nil || architectures == nil {
114
t.Fatalf("%s has no recognisable runner mapping", setupAction)
115
}
117
requested := map[archive]bool{}
118
for _, platform := range platforms {
119
for _, architecture := range architectures {
120
requested[archive{os: platform[1], arch: architecture[1], extension: template[1]}] = true
121
}
122
}
123
return requested
124
}
126
func TestEveryPackagedArchiveIsOneTheReleaseWorkflowBuilds(t *testing.T) {
127
archives := published(t)
129
for consumer, path := range consumers {
130
content := repositoryFile(t, path)
131
mentions := archiveMentions.FindAllStringSubmatch(content, -1)
132
if mentions == nil {
133
t.Errorf("%s (%s) references no koment archive at all", consumer, path)
134
continue
135
}
136
for _, mention := range mentions {
137
referenced := archive{os: mention[1], arch: mention[2], extension: mention[3]}
138
if !archives[referenced] {
139
t.Errorf("%s (%s) downloads %s, which the release workflow never builds; buildable: %s",
140
consumer, path, referenced, sortedNames(archives))
141
}
142
}
143
}
145
for requested := range requestedByTheSetupAction(t) {
146
if !archives[requested] {
147
t.Errorf("the setup action (%s) resolves runners to %s, which the release workflow never builds; buildable: %s",
148
setupAction, requested, sortedNames(archives))
149
}
150
}
151
}
153
// An archive nothing installs is either a wasted build or a platform whose
154
// install path was forgotten. Both are worth failing over.
155
func TestEveryPublishedArchiveHasSomethingThatInstallsIt(t *testing.T) {
156
archives := published(t)
158
installed := map[archive]string{}
159
for consumer, path := range consumers {
160
content := repositoryFile(t, path)
161
for _, mention := range archiveMentions.FindAllStringSubmatch(content, -1) {
162
installed[archive{os: mention[1], arch: mention[2], extension: mention[3]}] = consumer
163
}
164
}
165
for requested := range requestedByTheSetupAction(t) {
166
installed[requested] = "the setup action"
167
}
169
for built := range archives {
170
if installed[built] == "" {
171
t.Errorf("the release workflow builds %s but no packaging channel installs it", built)
172
}
173
}
174
}
176
// The setup action verifies its download against the manifest the release
177
// workflow writes, so the two must agree on that file's name too.
178
func TestTheSetupActionAsksForTheChecksumManifestByItsPublishedName(t *testing.T) {
179
if !checksumMention.MatchString(repositoryFile(t, releaseWorkflow)) {
180
t.Fatalf("%s no longer writes koment_%s_checksums.txt", releaseWorkflow, versionPlaceholder)
181
}
182
if !checksumMention.MatchString(repositoryFile(t, "action.yml")) {
183
t.Error("action.yml would download a checksum manifest the release never publishes")
184
}
185
}
187
// Package managers put the extracted binary on PATH by name, so the name inside
188
// the archive is as much a public interface as the archive itself.
189
func TestPackagedBinaryNamesMatchWhatTheArchivesContain(t *testing.T) {
190
for _, expectation := range []struct {
191
consumer, path, needle string
192
platform string
193
}{
194
{"the Homebrew tap", "packaging/homebrew/koment.rb.tmpl", `bin.install "koment"`, "darwin"},
195
{"the WinGet bundle", "packaging/winget/JanPuc.Koment.installer.yaml.tmpl", "RelativeFilePath: koment.exe", "windows"},
196
{"the Scoop bucket", "packaging/scoop/koment.json.tmpl", `"bin": "koment.exe"`, "windows"},
197
{"the setup action", "action.yml", `-C "$target" koment`, "linux"},
198
} {
199
want := archive{os: expectation.platform}.binary()
200
if !strings.Contains(expectation.needle, want) {
201
t.Fatalf("test is inconsistent: %q does not name %q", expectation.needle, want)
202
}
203
if !strings.Contains(repositoryFile(t, expectation.path), expectation.needle) {
204
t.Errorf("%s (%s) no longer installs %q; the release workflow packages that name for %s",
205
expectation.consumer, expectation.path, want, expectation.platform)
206
}
207
}
208
}
210
// Homebrew installs from the archive root and the setup action extracts a bare
211
// member name, so nesting the binary in a directory would break both without
212
// changing a single file name (ADR 0109).
213
func TestBothArchiveShapesCarryTheBinaryAndItsLicenceAtTheRoot(t *testing.T) {
214
workflow := repositoryFile(t, releaseWorkflow)
216
for shape, packaging := range map[string]string{
217
"the Windows zip": `zip -q "${name}.zip" "$binary" LICENSE README.md`,
218
"the POSIX tarball": `tar -czf "${name}.tar.gz" "$binary" LICENSE README.md`,
219
"the Windows binary": `[ "$os" = windows ] && binary=koment.exe`,
220
"the extracted entry": `tar -xzf "${work}/${archive}" -C "$target" koment`,
221
} {
222
source := releaseWorkflow
223
if shape == "the extracted entry" {
224
source = setupAction
225
}
226
if !strings.Contains(repositoryFile(t, source), packaging) {
227
t.Errorf("%s no longer packages the binary, LICENSE and README at the archive root: %s expected %q",
228
shape, source, packaging)
229
}
230
}
232
if strings.Contains(workflow, `tar -czf "${name}.tar.gz" -C`) {
233
t.Error("the tarball now nests its contents; Homebrew's bin.install and the setup action both read the archive root")
234
}
235
}
237
func sortedNames(archives map[archive]bool) string {
238
names := make([]string, 0, len(archives))
239
for a := range archives {
240
names = append(names, a.String())
241
}
242
sort.Strings(names)
243
return strings.Join(names, ", ")
244
}