mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: migrate pmg to nx based release automation (#293)
* feat: Migrate release system to Nx with platform-specific npm packages * add go.work.sum * fix: CI deprecations, stale action pins, and signal propagation * fix: update e2e workflows to pnpm 11 and latest action SHAs * fix: update pmg-e2e.yml to Node 24 with Go and pnpm caching * fix: resolve E2E failures, remove goreleaser-test, update action SHAs * fix: restore goreleaser-test (required check) * fix: escape pnpm workspace detection for yarn/pnpx tests, update action versions
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
module github.com/safedep/pmg/scripts
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.30.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,4 @@
|
||||
# sync-binaries
|
||||
|
||||
Copies GoReleaser build artifacts into the npm platform packages under `packages/`.
|
||||
Run via Nx: `pnpm nx run sync-binaries:run` (snapshot) or `pnpm nx run sync-binaries:run-release` (release).
|
||||
@@ -0,0 +1,169 @@
|
||||
// Sync binaries to packages directory from goreleaser's dist/ directory.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type GoreleaserArtifact struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Goos string `json:"goos" validate:"required"`
|
||||
Goarch string `json:"goarch"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
}
|
||||
|
||||
var goArchToNodeArchMap = map[string]string{
|
||||
"amd64": "x64",
|
||||
"386": "x86",
|
||||
"arm64": "arm64",
|
||||
}
|
||||
|
||||
var goOsToNodeOsMap = map[string]string{
|
||||
"windows": "win32",
|
||||
}
|
||||
|
||||
func main() {
|
||||
artifactsPath := flag.String("artifacts-path", "dist/artifacts.json", "Path to goreleaser artifacts.json")
|
||||
packagesPath := flag.String("packages-path", "./packages", "Path to the npm packages directory")
|
||||
strict := flag.Bool("strict", true, "Fail if a package directory does not exist for a built artifact")
|
||||
setVersion := flag.String("set-version", "", "Semver x.y.z to write into all non-private package.json files under packages-path")
|
||||
verifyBins := flag.Bool("verify-bins", false, "Verify that each platform package has a non-empty bin/ directory after sync")
|
||||
flag.Parse()
|
||||
|
||||
artifactsBytes, err := os.ReadFile(*artifactsPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read artifacts.json (did you run goreleaser build?): %v", err)
|
||||
}
|
||||
|
||||
var artifacts []GoreleaserArtifact
|
||||
if err := json.Unmarshal(artifactsBytes, &artifacts); err != nil {
|
||||
log.Fatalf("failed to parse artifacts.json: %v", err)
|
||||
}
|
||||
|
||||
validate := validator.New(validator.WithRequiredStructEnabled())
|
||||
|
||||
for _, artifact := range artifacts {
|
||||
switch artifact.Type {
|
||||
case "Binary":
|
||||
if err := validate.Struct(artifact); err != nil {
|
||||
log.Printf("skipping invalid artifact: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// goreleaser v2 emits a universal macOS binary as type "Binary"
|
||||
// with goarch "all" when universal_binaries.replace is true.
|
||||
// Copy it to both darwin platform packages.
|
||||
if artifact.Goos == "darwin" && artifact.Goarch == "all" {
|
||||
for _, nodeArch := range []string{"x64", "arm64"} {
|
||||
packagePath := filepath.Join(*packagesPath, fmt.Sprintf("pmg-darwin-%s", nodeArch))
|
||||
if err := copyToBin(artifact.Path, packagePath, "pmg", *strict); err != nil {
|
||||
log.Fatalf("sync darwin universal -> %s: %v", nodeArch, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := syncBinary(artifact, *packagesPath, *strict); err != nil {
|
||||
log.Fatalf("sync: %v", err)
|
||||
}
|
||||
|
||||
case "Universal Binary":
|
||||
// Retained for compatibility with older goreleaser versions that
|
||||
// emitted a distinct type for universal binaries.
|
||||
if artifact.Goos != "darwin" {
|
||||
log.Printf("unexpected universal binary for goos=%s, skipping", artifact.Goos)
|
||||
continue
|
||||
}
|
||||
for _, nodeArch := range []string{"x64", "arm64"} {
|
||||
packagePath := filepath.Join(*packagesPath, fmt.Sprintf("pmg-darwin-%s", nodeArch))
|
||||
if err := copyToBin(artifact.Path, packagePath, "pmg", *strict); err != nil {
|
||||
log.Fatalf("sync darwin universal -> %s: %v", nodeArch, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *setVersion != "" {
|
||||
if err := setPackageVersions(*packagesPath, *setVersion); err != nil {
|
||||
log.Fatalf("set-version: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *verifyBins {
|
||||
if err := verifyPackageBins(*packagesPath); err != nil {
|
||||
log.Fatalf("verify-bins: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncBinary(artifact GoreleaserArtifact, packagesPath string, strict bool) error {
|
||||
nodeArch, ok := goArchToNodeArchMap[artifact.Goarch]
|
||||
if !ok {
|
||||
nodeArch = artifact.Goarch
|
||||
}
|
||||
nodeOs, ok := goOsToNodeOsMap[artifact.Goos]
|
||||
if !ok {
|
||||
nodeOs = artifact.Goos
|
||||
}
|
||||
|
||||
packagePath := filepath.Join(packagesPath, fmt.Sprintf("pmg-%s-%s", nodeOs, nodeArch))
|
||||
binName := "pmg"
|
||||
if artifact.Goos == "windows" {
|
||||
binName = "pmg.exe"
|
||||
}
|
||||
return copyToBin(artifact.Path, packagePath, binName, strict)
|
||||
}
|
||||
|
||||
func copyToBin(src, packagePath, binName string, strict bool) error {
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
if strict {
|
||||
return fmt.Errorf("package directory %s does not exist (add the platform package or remove the goreleaser target)", packagePath)
|
||||
}
|
||||
log.Printf("package directory %s does not exist, skipping", packagePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
binDir := filepath.Join(packagePath, "bin")
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil { //nolint:gosec // bin/ needs execute permission
|
||||
return fmt.Errorf("create bin dir %s: %w", binDir, err)
|
||||
}
|
||||
|
||||
dst := filepath.Join(binDir, binName)
|
||||
log.Printf("copying %s -> %s", src, dst)
|
||||
return copyFile(src, dst)
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
defer srcFile.Close() //nolint:errcheck // read-only; close error is negligible
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination: %w", err)
|
||||
}
|
||||
defer dstFile.Close() //nolint:errcheck // Sync() is called explicitly before return; deferred close is best-effort
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return fmt.Errorf("copy: %w", err)
|
||||
}
|
||||
if err := dstFile.Sync(); err != nil {
|
||||
return fmt.Errorf("sync: %w", err)
|
||||
}
|
||||
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat source: %w", err)
|
||||
}
|
||||
return os.Chmod(dst, srcInfo.Mode())
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "sync-binaries",
|
||||
"root": "scripts/sync-binaries",
|
||||
"targets": {
|
||||
"run": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [{"projects": "pmg", "target": "build-snapshot"}],
|
||||
"options": {
|
||||
"command": "go run ./scripts/sync-binaries/ --strict --verify-bins --artifacts-path dist/artifacts.json --packages-path ./packages",
|
||||
"cwd": "{workspaceRoot}"
|
||||
},
|
||||
"inputs": ["goSyncSources", "{workspaceRoot}/dist/artifacts.json"],
|
||||
"outputs": [
|
||||
"{workspaceRoot}/packages/pmg-linux-x64/bin",
|
||||
"{workspaceRoot}/packages/pmg-linux-arm64/bin",
|
||||
"{workspaceRoot}/packages/pmg-darwin-x64/bin",
|
||||
"{workspaceRoot}/packages/pmg-darwin-arm64/bin",
|
||||
"{workspaceRoot}/packages/pmg-win32-x64/bin"
|
||||
]
|
||||
},
|
||||
"run-release": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "go run ./scripts/sync-binaries/ --strict --verify-bins --artifacts-path dist/artifacts.json --packages-path ./packages --set-version $VERSION",
|
||||
"cwd": "{workspaceRoot}"
|
||||
},
|
||||
"inputs": ["goSyncSources", "{workspaceRoot}/dist/artifacts.json"],
|
||||
"outputs": [
|
||||
"{workspaceRoot}/packages/pmg-linux-x64/bin",
|
||||
"{workspaceRoot}/packages/pmg-linux-arm64/bin",
|
||||
"{workspaceRoot}/packages/pmg-darwin-x64/bin",
|
||||
"{workspaceRoot}/packages/pmg-darwin-arm64/bin",
|
||||
"{workspaceRoot}/packages/pmg-win32-x64/bin"
|
||||
],
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var semverRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
||||
|
||||
// packageMeta holds the fields from package.json that drive sync decisions.
|
||||
// Only the fields we actually branch on are declared; json.Unmarshal ignores
|
||||
// the rest, so the full file content is never disturbed.
|
||||
type packageMeta struct {
|
||||
Private bool `json:"private"`
|
||||
OS []string `json:"os"`
|
||||
}
|
||||
|
||||
func readPackageMeta(path string) (*packageMeta, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta packageMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return nil, fmt.Errorf("parse: %w", err)
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
// setPackageVersions scans every immediate subdirectory of packagesPath for a
|
||||
// package.json, skips those with "private": true, and writes version to the
|
||||
// rest. Returns an error on the first failure.
|
||||
func setPackageVersions(packagesPath, version string) error {
|
||||
if !semverRe.MatchString(version) {
|
||||
return fmt.Errorf("invalid version %q: must be x.y.z", version)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(packagesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packages dir: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(packagesPath, entry.Name(), "package.json")
|
||||
if err := setVersionInPackageJSON(path, version); err != nil {
|
||||
return fmt.Errorf("package %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// versionFieldRe is used to swap the "version" field in raw JSON bytes instead
|
||||
// of round-tripping through a Go struct, which would re-serialize arrays and
|
||||
// destroy inline formatting (e.g. "os": ["linux"] would expand to multi-line).
|
||||
// Anchoring to start-of-line prevents false matches inside string values of
|
||||
// other keys. Group 1 captures leading whitespace so indentation is unchanged.
|
||||
var versionFieldRe = regexp.MustCompile(`(?m)^(\s*)"version"\s*:\s*"[^"]*"`)
|
||||
|
||||
// setVersionInPackageJSON reads the file at path, sets "version" to version,
|
||||
// and writes it back. A missing file is silently skipped. Packages with
|
||||
// "private": true are skipped unchanged.
|
||||
//
|
||||
// The replacement is done on the raw bytes so all other formatting (key order,
|
||||
// inline arrays, whitespace) is preserved byte-for-byte.
|
||||
func setVersionInPackageJSON(path, version string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
var meta packageMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return fmt.Errorf("parse: %w", err)
|
||||
}
|
||||
if meta.Private {
|
||||
return nil
|
||||
}
|
||||
|
||||
repl := fmt.Appendf(nil, "${1}\"version\": \"%s\"", version)
|
||||
updated := versionFieldRe.ReplaceAll(data, repl)
|
||||
|
||||
// 0o644: package.json must be world-readable for npm tooling.
|
||||
return os.WriteFile(path, updated, 0o644) //nolint:gosec
|
||||
}
|
||||
|
||||
// verifyPackageBins checks that every platform package under packagesPath has a
|
||||
// non-empty bin/ directory. Platform packages are identified by the presence of
|
||||
// an "os" field in their package.json; packages without that field (e.g. the
|
||||
// meta/shim package) and private packages are skipped.
|
||||
func verifyPackageBins(packagesPath string) error {
|
||||
entries, err := os.ReadDir(packagesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packages dir: %w", err)
|
||||
}
|
||||
|
||||
var missing []string
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgJSONPath := filepath.Join(packagesPath, entry.Name(), "package.json")
|
||||
meta, err := readPackageMeta(pkgJSONPath)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
if meta.Private || len(meta.OS) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
binDir := filepath.Join(packagesPath, entry.Name(), "bin")
|
||||
binEntries, err := os.ReadDir(binDir)
|
||||
if err != nil || len(binEntries) == 0 {
|
||||
missing = append(missing, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("platform packages missing bin/: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// makeDir creates a directory for test fixtures.
|
||||
// 0o755 is the conventional permission for directories that need traversal.
|
||||
func makeDir(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.MkdirAll(path, 0o755)) //nolint:gosec
|
||||
}
|
||||
|
||||
// writeTestFile writes a file for test fixtures.
|
||||
// 0o644 is the conventional permission for config files like package.json.
|
||||
func writeTestFile(t *testing.T, path string, content []byte) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.WriteFile(path, content, 0o644)) //nolint:gosec
|
||||
}
|
||||
|
||||
func writePkgJSON(t *testing.T, dir, name string, content map[string]any) string {
|
||||
t.Helper()
|
||||
pkgDir := filepath.Join(dir, name)
|
||||
makeDir(t, pkgDir)
|
||||
data, err := json.MarshalIndent(content, "", " ")
|
||||
require.NoError(t, err)
|
||||
path := filepath.Join(pkgDir, "package.json")
|
||||
writeTestFile(t, path, append(data, '\n'))
|
||||
return path
|
||||
}
|
||||
|
||||
func readVersion(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
var pkg map[string]any
|
||||
require.NoError(t, json.Unmarshal(data, &pkg))
|
||||
v, _ := pkg["version"].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func TestSetPackageVersions(t *testing.T) {
|
||||
t.Run("sets version in all non-private packages", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pathA := writePkgJSON(t, dir, "pkg-a", map[string]any{"name": "pkg-a", "version": "0.0.0"})
|
||||
pathB := writePkgJSON(t, dir, "pkg-b", map[string]any{"name": "pkg-b", "version": "0.0.0"})
|
||||
|
||||
require.NoError(t, setPackageVersions(dir, "1.2.3"))
|
||||
|
||||
assert.Equal(t, "1.2.3", readVersion(t, pathA))
|
||||
assert.Equal(t, "1.2.3", readVersion(t, pathB))
|
||||
})
|
||||
|
||||
t.Run("skips private packages", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pathPriv := writePkgJSON(t, dir, "private-pkg", map[string]any{
|
||||
"name": "private-pkg",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
})
|
||||
pathPub := writePkgJSON(t, dir, "public-pkg", map[string]any{"name": "public-pkg", "version": "0.0.0"})
|
||||
|
||||
require.NoError(t, setPackageVersions(dir, "2.0.0"))
|
||||
|
||||
assert.Equal(t, "0.0.0", readVersion(t, pathPriv))
|
||||
assert.Equal(t, "2.0.0", readVersion(t, pathPub))
|
||||
})
|
||||
|
||||
t.Run("skips subdirectories without package.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
makeDir(t, filepath.Join(dir, "no-pkg-json"))
|
||||
pathA := writePkgJSON(t, dir, "pkg-a", map[string]any{"name": "pkg-a", "version": "0.0.0"})
|
||||
|
||||
require.NoError(t, setPackageVersions(dir, "3.0.0"))
|
||||
|
||||
assert.Equal(t, "3.0.0", readVersion(t, pathA))
|
||||
})
|
||||
|
||||
t.Run("rejects invalid semver", func(t *testing.T) {
|
||||
err := setPackageVersions(t.TempDir(), "not-a-version")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid version")
|
||||
})
|
||||
|
||||
t.Run("returns error when packages dir is missing", func(t *testing.T) {
|
||||
err := setPackageVersions("/nonexistent/path", "1.0.0")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func writeBinary(t *testing.T, dir, pkgName, binName string) {
|
||||
t.Helper()
|
||||
binDir := filepath.Join(dir, pkgName, "bin")
|
||||
makeDir(t, binDir)
|
||||
// 0o755: binary files need execute permission.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(binDir, binName), []byte("binary"), 0o755)) //nolint:gosec
|
||||
}
|
||||
|
||||
func TestVerifyPackageBins(t *testing.T) {
|
||||
t.Run("passes when all platform packages have binaries", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli-linux-x64", map[string]any{
|
||||
"name": "cli-linux-x64", "version": "0.0.0", "os": []string{"linux"},
|
||||
})
|
||||
writeBinary(t, dir, "cli-linux-x64", "pmg")
|
||||
|
||||
writePkgJSON(t, dir, "cli-darwin-arm64", map[string]any{
|
||||
"name": "cli-darwin-arm64", "version": "0.0.0", "os": []string{"darwin"},
|
||||
})
|
||||
writeBinary(t, dir, "cli-darwin-arm64", "pmg")
|
||||
|
||||
require.NoError(t, verifyPackageBins(dir))
|
||||
})
|
||||
|
||||
t.Run("fails when a platform package has an empty bin/", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli-linux-x64", map[string]any{
|
||||
"name": "cli-linux-x64", "version": "0.0.0", "os": []string{"linux"},
|
||||
})
|
||||
makeDir(t, filepath.Join(dir, "cli-linux-x64", "bin"))
|
||||
|
||||
err := verifyPackageBins(dir)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cli-linux-x64")
|
||||
})
|
||||
|
||||
t.Run("fails when a platform package has no bin/ directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli-linux-x64", map[string]any{
|
||||
"name": "cli-linux-x64", "version": "0.0.0", "os": []string{"linux"},
|
||||
})
|
||||
|
||||
err := verifyPackageBins(dir)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cli-linux-x64")
|
||||
})
|
||||
|
||||
t.Run("skips meta packages without os field", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli", map[string]any{
|
||||
"name": "cli", "version": "0.0.0",
|
||||
})
|
||||
|
||||
require.NoError(t, verifyPackageBins(dir))
|
||||
})
|
||||
|
||||
t.Run("skips private packages", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli-private", map[string]any{
|
||||
"name": "cli-private", "version": "0.0.0", "os": []string{"linux"}, "private": true,
|
||||
})
|
||||
|
||||
require.NoError(t, verifyPackageBins(dir))
|
||||
})
|
||||
|
||||
t.Run("reports multiple missing packages", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePkgJSON(t, dir, "cli-linux-x64", map[string]any{
|
||||
"name": "cli-linux-x64", "version": "0.0.0", "os": []string{"linux"},
|
||||
})
|
||||
writePkgJSON(t, dir, "cli-darwin-x64", map[string]any{
|
||||
"name": "cli-darwin-x64", "version": "0.0.0", "os": []string{"darwin"},
|
||||
})
|
||||
|
||||
err := verifyPackageBins(dir)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cli-linux-x64")
|
||||
assert.Contains(t, err.Error(), "cli-darwin-x64")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetVersionInPackageJSON(t *testing.T) {
|
||||
t.Run("writes version field", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writePkgJSON(t, dir, "pkg", map[string]any{"name": "pkg", "version": "0.0.0"})
|
||||
|
||||
require.NoError(t, setVersionInPackageJSON(filepath.Join(dir, "pkg", "package.json"), "4.5.6"))
|
||||
|
||||
assert.Equal(t, "4.5.6", readVersion(t, path))
|
||||
})
|
||||
|
||||
t.Run("skips private package", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writePkgJSON(t, dir, "pkg", map[string]any{"name": "pkg", "version": "0.0.0", "private": true})
|
||||
|
||||
require.NoError(t, setVersionInPackageJSON(filepath.Join(dir, "pkg", "package.json"), "4.5.6"))
|
||||
|
||||
assert.Equal(t, "0.0.0", readVersion(t, path))
|
||||
})
|
||||
|
||||
t.Run("missing file is a no-op", func(t *testing.T) {
|
||||
err := setVersionInPackageJSON("/nonexistent/package.json", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("preserves inline array formatting", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pkg", "package.json")
|
||||
makeDir(t, filepath.Dir(path))
|
||||
|
||||
original := `{
|
||||
"name": "pkg",
|
||||
"version": "0.0.0",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"],
|
||||
"files": ["bin/**"]
|
||||
}
|
||||
`
|
||||
writeTestFile(t, path, []byte(original))
|
||||
|
||||
require.NoError(t, setVersionInPackageJSON(path, "1.2.3"))
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
content := string(data)
|
||||
|
||||
assert.Contains(t, content, `"version": "1.2.3"`)
|
||||
assert.Contains(t, content, `"os": ["linux"]`)
|
||||
assert.Contains(t, content, `"cpu": ["x64"]`)
|
||||
assert.Contains(t, content, `"files": ["bin/**"]`)
|
||||
})
|
||||
|
||||
t.Run("does not match version inside a string value", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pkg", "package.json")
|
||||
makeDir(t, filepath.Dir(path))
|
||||
|
||||
original := "{\n \"name\": \"pkg\",\n \"version\": \"0.0.0\",\n \"description\": \"see \\\"version\\\": \\\"1.0.0\\\" in docs\"\n}\n"
|
||||
writeTestFile(t, path, []byte(original))
|
||||
|
||||
require.NoError(t, setVersionInPackageJSON(path, "2.0.0"))
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
content := string(data)
|
||||
|
||||
assert.Contains(t, content, `"version": "2.0.0"`)
|
||||
assert.Contains(t, content, "\"description\": \"see \\\"version\\\": \\\"1.0.0\\\" in docs\"")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user