From d598f8d657caad8f5c17ec83ab8ba16ffa54ae91 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Wed, 15 Jul 2026 04:35:27 +0530 Subject: [PATCH] feat: add --install-name flag with back-compat symlink Add an opt-in --install-name flag that overrides the name the binary is installed as under /usr/local/bin (default: the package name, i.e. current behavior). When the install name differs from the package name, the template also emits a back-compat symlink at the package name pointing to it. This is for the AIStor server rename minio -> aistor: callers pass --install-name aistor so the deb/rpm/apk packages install /usr/local/bin/aistor and /usr/local/bin/minio -> aistor. The shipped minio.service (which execs /usr/local/bin/minio) keeps working through the symlink, and existing deployments keep the minio command. Keeping this behind a flag (rather than hardcoding it for minio-enterprise) decouples the rename from the pkger version: bumping pkger for unrelated reasons no longer implies a rename, activation is explicit per release, and it can be reverted from the caller without a new pkger release. The flag is generic, so other packages can reuse it. Refactors the previously inlined per-app name logic into helpers: - pkgName - nfpm package name ("name:"); unchanged - binarySrcName - artifact read from the release dir; unchanged - resolveBinName - install path; = package name unless --install-name is set Default output (no flag) is byte-identical to before for every app. minio.service is shipped unchanged; package name is unchanged, so upgrades are in-place with no Conflicts/Replaces. Validated: with --install-name aistor the file<->symlink transition is clean in both directions on deb (dpkg), rpm, and apk, including the case of an older-style package (real minio file) landing over a renamed install. Also bump the Go toolchain to 1.26.5 and go-git to v5.19.1 (GO-2026-5496, GO-2026-4910, GO-2026-4909) to fix the failing VulnCheck CI. --- .github/workflows/vulncheck.yml | 2 +- go.mod | 15 ++-- go.sum | 36 ++++----- main.go | 71 ++++++++++++----- main_test.go | 132 +++++++++++++++++++++++++++++++- 5 files changed, 208 insertions(+), 48 deletions(-) diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index ec33aef..4bf2a22 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -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 diff --git a/go.mod b/go.mod index 5e72330..7de6403 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -45,7 +45,7 @@ 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 @@ -53,10 +53,9 @@ require ( 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 ) diff --git a/go.sum b/go.sum index d483516..cb9b8b7 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/main.go b/main.go index cde3cfe..2c770b8 100644 --- a/main.go +++ b/main.go @@ -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 { @@ -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 @@ -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 { @@ -845,6 +853,7 @@ type releaseTmpl struct { License string ReleaseDir string Binary string + BinName string Description string OS string Arch string @@ -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 "."). 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" @@ -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 @@ -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. diff --git a/main_test.go b/main_test.go index 93bd26a..5cdf719 100644 --- a/main_test.go +++ b/main_test.go @@ -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. artifact. + if !strings.Contains(out, "src: minio-release/linux-amd64/minio.RELEASE.2025-03-12T00-00-00Z") { + t.Errorf("expected src minio., 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 { @@ -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)