Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/vulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ 1.26.1 ]
go-version: [ 1.26.5 ]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
Expand Down
15 changes: 7 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/minio/pkger/v2

go 1.26

toolchain go1.26.1
toolchain go1.26.5

require (
github.com/alecthomas/kingpin v2.2.6+incompatible
Expand All @@ -27,8 +27,8 @@ require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect
github.com/go-git/go-git/v5 v5.16.5 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/rpmpack v0.7.1 // indirect
Expand All @@ -45,18 +45,17 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
36 changes: 18 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
Expand Down Expand Up @@ -113,8 +113,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -158,27 +158,27 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
71 changes: 51 additions & 20 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ var (
contentsFile = app.Flag("contents", "YAML file with additional nfpm content entries (src/dst/type), supports ${ARCH} expansion").
Short('c').
String()

installName = app.Flag("install-name", "Install the binary under /usr/local/bin using this name instead of the package name; when it differs, a back-compat symlink at the package name is also created (e.g. --install-name aistor => /usr/local/bin/aistor + /usr/local/bin/minio -> aistor)").
String()
)

type extraContent struct {
Expand Down Expand Up @@ -170,7 +173,12 @@ rpm:
group: Applications/File
contents:
- src: {{ .ReleaseDir }}/{{ .OS }}-{{ .Arch }}/{{ .Binary }}.{{ .Release }}
dst: /usr/local/bin/{{ .BinName }}
{{if ne .BinName .App}}
- src: {{ .BinName }}
dst: /usr/local/bin/{{ .App }}
type: symlink
{{end}}
{{if or (eq .Binary "minio") (eq .Binary "aistor")}}
- src: minio.service
dst: /lib/systemd/system/minio.service
Expand Down Expand Up @@ -778,7 +786,7 @@ func main() {
}

if !*noPackages {
if err := doPackage(*appName, *license, *release, *packager, *deps, *scriptsDir); err != nil {
if err := doPackage(*appName, *license, *release, *packager, *deps, *scriptsDir, *installName); err != nil {
if !*ignoreMissingArch {
kingpin.Fatalf(err.Error())
} else {
Expand Down Expand Up @@ -845,6 +853,7 @@ type releaseTmpl struct {
License string
ReleaseDir string
Binary string
BinName string
Description string
OS string
Arch string
Expand All @@ -856,6 +865,43 @@ type releaseTmpl struct {
ExtraContents []extraContent
}

// pkgName is the nfpm package name (the "name:" field). Kept stable across the
// aistor rename so existing installs upgrade in place rather than treating the
// package as new.
func pkgName(appName string) string {
switch appName {
case "minio-enterprise":
return "minio"
case "mc", "mc-enterprise":
return "mcli"
}
return appName
}

// binarySrcName is the base name of the built binary picked up from the release
// dir (src is "<binarySrcName>.<release>"). Independent of the installed path.
func binarySrcName(appName string) string {
switch appName {
case "minio-enterprise":
return "minio"
case "mc-enterprise":
return "mc"
}
return appName
}

// resolveBinName is the name the binary is installed as under /usr/local/bin.
// It defaults to the package name (current behavior); the --install-name flag
// overrides it (e.g. "aistor"). When the result differs from the package name
// the template also emits a back-compat symlink at the package name
// (e.g. /usr/local/bin/minio -> aistor).
func resolveBinName(appName, installName string) string {
if installName != "" {
return installName
}
return pkgName(appName)
}

const (
minioReleaseTagTimeLayout = "2006-01-02T15-04-05Z"
minioPkgReleaseTagTimeLayout = "20060102150405"
Expand Down Expand Up @@ -903,7 +949,7 @@ func parseDepsFile(path string) (map[string][]string, error) {
}

// nolint:funlen
func doPackage(appName, license, release, packager, deps, scriptsDir string) error {
func doPackage(appName, license, release, packager, deps, scriptsDir, installName string) error {
var pkgDeps map[string][]string
if deps != "" {
var err error
Expand Down Expand Up @@ -968,28 +1014,13 @@ func doPackage(appName, license, release, packager, deps, scriptsDir string) err

var buf bytes.Buffer
err = mtmpl.Execute(&buf, releaseTmpl{
App: func() string {
if appName == "minio-enterprise" {
return "minio"
}
if appName == "mc" || appName == "mc-enterprise" {
return "mcli"
}
return appName
}(),
App: pkgName(appName),
License: func() string {
return license
}(),
ReleaseDir: releaseDirName(),
Binary: func() string {
if appName == "minio-enterprise" {
return "minio"
}
if appName == "mc-enterprise" {
return "mc"
}
return appName
}(),
Binary: binarySrcName(appName),
BinName: resolveBinName(appName, installName),
Description: func() string {
if appName == "minio-enterprise" {
return `MinIO is a High Performance Object Store.
Expand Down
132 changes: 131 additions & 1 deletion main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,140 @@
package main

import (
"bytes"
"strings"
"testing"
"text/template"

"github.com/goreleaser/nfpm/v2"
)

// renderPackageYAML renders the nfpm package template for an app the same way
// doPackage does, so tests exercise the real name/symlink logic.
func renderPackageYAML(t *testing.T, appName, installName string) string {
t.Helper()
mtmpl, err := template.New("minio").Parse(tmpl)
if err != nil {
t.Fatalf("parse template: %v", err)
}
var buf bytes.Buffer
if err := mtmpl.Execute(&buf, releaseTmpl{
App: pkgName(appName),
License: "Test License",
ReleaseDir: pkgName(appName) + "-release",
Binary: binarySrcName(appName),
BinName: resolveBinName(appName, installName),
Description: "Test package",
OS: "linux",
Arch: "amd64",
Release: "RELEASE.2025-03-12T00-00-00Z",
SemVerRelease: "20250312000000.0.0",
}); err != nil {
t.Fatalf("execute template: %v", err)
}
// The rendered document must be valid nfpm config.
rendered := buf.Bytes()
if _, err := nfpm.Parse(bytes.NewReader(rendered)); err != nil {
t.Fatalf("nfpm.Parse failed for %s: %v\n---\n%s", appName, err, rendered)
}
return buf.String()
}

// TestPackageTemplateMinioEnterprise verifies the aistor rename + back-compat
// symlink for the enterprise minio package when --install-name aistor is set.
func TestPackageTemplateMinioEnterprise(t *testing.T) {
out := renderPackageYAML(t, "minio-enterprise", "aistor")

// Package name stays "minio" for in-place upgrades.
if !strings.Contains(out, `name: "minio"`) {
t.Errorf("expected package name minio, got:\n%s", out)
}
// Real binary installs as aistor.
if !strings.Contains(out, "dst: /usr/local/bin/aistor") {
t.Errorf("expected binary installed at /usr/local/bin/aistor, got:\n%s", out)
}
// Source still comes from the minio.<release> artifact.
if !strings.Contains(out, "src: minio-release/linux-amd64/minio.RELEASE.2025-03-12T00-00-00Z") {
t.Errorf("expected src minio.<release>, got:\n%s", out)
}
// Back-compat symlink /usr/local/bin/minio -> aistor.
if !containsSymlink(out, "aistor", "/usr/local/bin/minio") {
t.Errorf("expected symlink /usr/local/bin/minio -> aistor, got:\n%s", out)
}
// Service file still shipped unchanged.
if !strings.Contains(out, "dst: /lib/systemd/system/minio.service") {
t.Errorf("expected minio.service, got:\n%s", out)
}
}

// TestPackageTemplateNoSymlinkForOthers ensures that without --install-name the
// output is unchanged: the binary installs under the package name and no compat
// symlink is emitted. The minio-enterprise/default row proves the rename is
// strictly opt-in (no flag => old behavior).
func TestPackageTemplateNoSymlinkForOthers(t *testing.T) {
tests := []struct {
name string
appName string
installName string
wantName string
wantBinDst string
wantSvc bool // minio.service shipped?
}{
{"minio-enterprise default", "minio-enterprise", "", "minio", "/usr/local/bin/minio", true},
{"minio community", "minio", "", "minio", "/usr/local/bin/minio", true},
{"mc-enterprise", "mc-enterprise", "", "mcli", "/usr/local/bin/mcli", false},
{"sidekick", "sidekick", "", "sidekick", "/usr/local/bin/sidekick", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := renderPackageYAML(t, tt.appName, tt.installName)
if !strings.Contains(out, `name: "`+tt.wantName+`"`) {
t.Errorf("expected name %s, got:\n%s", tt.wantName, out)
}
if !strings.Contains(out, "dst: "+tt.wantBinDst) {
t.Errorf("expected binary at %s, got:\n%s", tt.wantBinDst, out)
}
if strings.Contains(out, "type: symlink") {
t.Errorf("%s should not emit a compat symlink, got:\n%s", tt.appName, out)
}
if strings.Contains(out, "aistor") {
t.Errorf("%s should not mention aistor, got:\n%s", tt.appName, out)
}
hasSvc := strings.Contains(out, "dst: /lib/systemd/system/minio.service")
if hasSvc != tt.wantSvc {
t.Errorf("%s minio.service presence = %v, want %v", tt.appName, hasSvc, tt.wantSvc)
}
})
}
}

// containsSymlink checks the rendered YAML has a content entry that is a symlink
// with the given src (target) and dst (link path).
func containsSymlink(out, src, dst string) bool {
lines := strings.Split(out, "\n")
for i, l := range lines {
// src entries are YAML list items ("- src: ...").
if strings.TrimSpace(l) != "- src: "+src {
continue
}
// Look at the next two lines for dst + type: symlink.
var haveDst, haveType bool
for j := i + 1; j < len(lines) && j <= i+2; j++ {
s := strings.TrimSpace(lines[j])
if s == "dst: "+dst {
haveDst = true
}
if s == "type: symlink" {
haveType = true
}
}
if haveDst && haveType {
return true
}
}
return false
}

// TestSemVerRelease tests the conversion from release tags to semver format
func TestSemVerRelease(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -266,7 +396,7 @@ func TestGenerateSidekickDownloadsJSON(t *testing.T) {

// TestGenerateWarpDownloadsJSON tests warp JSON generation
func TestGenerateWarpDownloadsJSON(t *testing.T) {
version := "0.4.3" // Without 'v' prefix
version := "0.4.3" // Without 'v' prefix
releaseTag := "v0.4.3" // With 'v' prefix

result := generateWarpDownloadsJSON(version, releaseTag)
Expand Down
Loading