diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..907cb29 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,44 @@ +name: Go +on: + workflow_dispatch: + pull_request: + branches: + - master + - main + push: + branches: + - master + - main +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ 1.26.5 ] + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + - name: Check gofmt + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "not gofmt-clean:" + echo "$unformatted" + gofmt -d . + exit 1 + fi + shell: bash + - name: Build + run: go build ./... + shell: bash + - name: Vet + run: go vet ./... + shell: bash + - name: Test + run: go test -v ./... + shell: bash diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index 4bf2a22..83bb547 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -1,5 +1,6 @@ name: VulnCheck on: + workflow_dispatch: pull_request: branches: - master diff --git a/CLAUDE.md b/CLAUDE.md index dd4c8a1..4dbec97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -pkger is a packaging tool for MinIO projects that generates DEB, RPM, and APK packages along with installation metadata JSON files. It's built in Go as a single-file application (`main.go`) that uses the nfpm library for package generation. +pkger is a packaging tool for MinIO/AIStor projects that generates DEB, RPM, and APK packages along with the download metadata JSON consumed by min.io/download. It's built in Go as a single-file application (`main.go`) that uses the nfpm library for package generation. ## Building and Running -Build the project: ```bash -go build -o pkger main.go +go build -o pkger . ``` The binary is self-contained and uses command-line flags for all configuration. @@ -18,167 +17,178 @@ The binary is self-contained and uses command-line flags for all configuration. ## Core Architecture ### Single-file Design -The entire application is in `main.go` (~1000 lines). Key components: -- **Command-line parsing**: Uses `kingpin` for flag handling -- **Package generation**: Uses `goreleaser/nfpm/v2` library with support for deb, rpm, and apk formats -- **Template system**: Uses Go's `text/template` for generating nfpm config (lines 88-119) -- **JSON generation**: Creates download metadata files for different applications and platforms -### Application Types -pkger supports multiple MinIO applications, each with different versioning and architecture requirements: +The entire application is in `main.go`. Key components: -1. **minio/mc**: Date-based releases (e.g., `RELEASE.2025-03-12T00-00-00Z`) - - Supports: amd64, arm64, ppc64le - - Generates packages and cross-platform download metadata +- **Command-line parsing**: `kingpin` for flag handling +- **Package generation**: `goreleaser/nfpm/v2`, supporting deb, rpm and apk +- **Template system**: Go `text/template` renders an nfpm config per architecture (`const tmpl`) +- **JSON generation**: download metadata for min.io/download -2. **minio-enterprise/mc-enterprise**: Enterprise variants (date-based) - - Supports: amd64, arm64 only - - Generates AIStor-branded download URLs +### Supported Apps -3. **sidekick**: Load balancer (date-based releases) - - Supports: amd64, arm64 only - - Generates package-only metadata (no binary downloads) +Every app packages for **`linux/amd64` and `linux/arm64` only** — see `pkgArches`. There is no ppc64le, and no community `minio`/`mc` app; those were removed along with the last ppc64le build. -4. **warp**: Benchmarking tool (semantic versioning, e.g., `v0.4.3`) - - Supports: amd64, arm64 only - - Strips 'v' prefix from package filenames - - Cross-platform: Linux, macOS (arm64), Windows (amd64) +| `--appName` | Release dir | Binary read | Package name | Versioning | Metadata | +| ----------- | ------------------ | ----------- | ------------ | ---------- | -------------------------------------------- | +| `aistor` | `minio-release` | `minio` | `minio` | date-based | `downloads-aistor.json` (AIStor + MinIO KMS) | +| `ac` | `mc-release` | `mc` | `mcli` | date-based | `downloads-ac.json` (AIStor Client) | +| `sidekick` | `sidekick-release` | `sidekick` | `sidekick` | date-based | `downloads-sidekick.json` (Linux + Windows) | +| `warp` | `warp-release` | `warp` | `warp` | semver | `downloads-warp.json` (cross-platform) | +| `memkv` | `memkv-release` | `memkv` | `memkv` | date-based | empty document | +| `aimem` | `aimem-release` | `aimem` | `aimem` | date-based | empty document | +| `minfs` | `minfs-release` | `minfs` | `minfs` | date-based | empty document | + +`aistor` and `ac` were renamed from `minio-enterprise` and `mc-enterprise`. **Only the app name changed.** The release directory, the binary read from it, the package name, and every `dl.min.io` path segment are deliberately unchanged, so published artifacts are byte-identical across the rename. When touching `releaseDirName`, `defaultPkgName` or `defaultBinarySrcName`, re-key the switch on the new app name but keep the returned on-disk name. + +`memkv`, `aimem` and `minfs` fall through to `generateDownloadsJSON`, which intentionally returns an empty document — the packages ship, but these apps have no dl.min.io download page to link. ### Version Handling -- **Date-based** (`RELEASE.2025-03-12T00-00-00Z`): Converted to semver format `20250312000000.0.0` via `semVerRelease()` (lines 793-806) -- **Semantic** (`v0.4.3` for warp): Validated and 'v' prefix stripped (lines 731-744) + +- **Date-based** (`RELEASE.2025-03-12T00-00-00Z`): converted to semver `20250312000000.0.0` via `semVerRelease()` +- **Semantic** (`v0.4.3`, warp only): validated against `vX.Y.Z` and the `v` prefix stripped for package filenames ### Package Generation Flow -1. Parse release tag and convert to appropriate version format -2. For each architecture (filtered by app requirements): - - Generate nfpm config from template (lines 865-920) - - Create packages in `{appName}-release/linux-{arch}/` directory - - Generate SHA256 checksums for each package - - Create symlinks for latest package -3. Generate `downloads-{appName}.json` metadata file + +1. Parse the release tag and convert it to the appropriate version format +2. For each architecture in `pkgArches`: + - Render the nfpm config from the template + - Write packages to `{releaseDir}/linux-{arch}/` + - Write a `.sha256sum` beside each package + - Symlink a stable "latest" alias (e.g. `minio.deb`), plus the legacy filenames when `--package-name` renamed the package +3. Write `downloads-{appName}.json` (or `downloads-{appName}-edge.json` with `--edge`) ## Common Commands -Run pkger for minio (date-based release): ```bash -# Requires: minio.service file and binaries in dist/linux-{arch}/ -pkger -r RELEASE.2025-03-12T00-00-00Z --appName minio --releaseDir=dist -``` +# aistor (date-based); needs minio.service and binaries in dist/linux-{arch}/ +pkger -r RELEASE.2025-03-12T00-00-00Z --appName aistor --releaseDir=dist -Run pkger for sidekick: -```bash -# Requires: binaries in sidekick-release/linux-{arch}/ +# ac client +pkger -r RELEASE.2025-03-12T00-00-00Z --appName ac + +# sidekick pkger -r RELEASE.2025-03-12T00-00-00Z --appName sidekick -``` -Run pkger for warp (semantic versioning): -```bash -# Requires: binaries in warp-release/linux-{arch}/ +# warp (semantic versioning) pkger -r v0.4.3 --appName warp -``` -Build specific package formats only: -```bash +# specific package formats only pkger -r --appName --packager deb,rpm -``` -Ignore missing architectures (continue on errors): -```bash +# continue past missing architectures pkger -r --appName --ignore -``` -Skip package building (only generate JSON): -```bash +# JSON metadata only pkger -r --appName --no-pkg -``` -Generate EDGE release (uses /edge/ path instead of /release/): -```bash -pkger -r RELEASE.2025-03-12T00-00-00Z --appName minio-enterprise --edge --no-pkg +# EDGE release (uses /edge/ instead of /release/) +pkger -r EDGE.2025-03-12T00-00-00Z --appName aistor --edge --no-pkg + +# rename the installed binary/package (see README) +pkger -r --appName aistor --binary-name aistor --package-name aistor ``` ## Key Flags -- `-r, --release`: Release tag (required). Format depends on app type -- `-a, --appName`: Application name (default: "minio") -- `-d, --releaseDir`: Directory containing binaries (default: "{appName}-release") -- `-p, --packager`: Package formats to build (default: "deb,rpm,apk") -- `-i, --ignore`: Ignore missing architecture errors -- `-n, --no-pkg`: Skip package generation -- `-e, --edge`: Generate EDGE release URLs (uses /edge/ path) -- `-s, --scriptsDir`: Directory with package scripts (preinstall.sh, postinstall.sh, etc.) -- `-l, --license`: Package license (default: "AGPLv3") -- `--deps`: JSON file with package dependencies +- `-r, --release`: release tag (required); format depends on the app +- `-a, --appName`: application name (default: `aistor`) +- `-d, --releaseDir`: directory containing binaries (default: per-app, see table) +- `-p, --packager`: formats to build (default: `deb,rpm,apk`) +- `-i, --ignore`: ignore missing architecture errors +- `-n, --no-pkg`: skip package generation +- `-j, --no-json`: skip JSON metadata generation +- `-e, --edge`: EDGE release URLs (`/edge/` path) +- `-s, --scriptsDir`: directory with package scripts (preinstall.sh, postinstall.sh, preremove.sh, postremove.sh) +- `-l, --license`: package license (default: `AGPLv3`) +- `-c, --contents`: YAML file with extra nfpm content entries (supports `${ARCH}`) +- `--deps`: JSON file with per-format package dependencies +- `--binary-name` / `--package-name`: override the source binary base name and the package/installed-command name; see README ## Important Implementation Details ### Architecture Mapping -- RPM uses x86_64/aarch64 (see `rpmArchMap`, lines 150-153) -- DEB uses amd64/arm64 (see `debArchMap`, lines 155-158) + +- RPM uses x86_64/aarch64 (`rpmArchMap`) +- DEB uses amd64/arm64 (`debArchMap`) ### Download URL Patterns -The JSON generators create download metadata with different URL structures: -- **Community**: `dl.min.io/{server|client}/{appName}/release/...` -- **Enterprise Release**: `dl.min.io/aistor/{appName}/release/...` -- **Enterprise EDGE**: `dl.min.io/aistor/{appName}/edge/...` (with `--edge` flag) -- **Sidekick/Warp**: Always use `dl.min.io/aistor/` path + +`dl.min.io` path segments are **hardcoded literals**, never interpolated from `--appName`. Renaming an app must not change a published URL. + +- **AIStor server**: `dl.min.io/aistor/minio/{release,edge}/...` +- **AIStor client**: `dl.min.io/aistor/mc/{release,edge}/...` +- **MinIO KMS**: `dl.min.io/aistor/minkms/{release,edge}/...` (emitted alongside aistor) +- **Sidekick / warp**: `dl.min.io/aistor/{sidekick,warp}/release/...` +- One macOS Homebrew entry still points at `dl.min.io/server/minio/...` ### EDGE Release Support -- Use `--edge` flag to generate EDGE release metadata -- Changes URL path from `/release/` to `/edge/` -- Generates separate JSON file: `downloads-{appName}-edge.json` -- Docker/Podman instructions use release tag (not `:latest`) -- Package building (RPM/DEB) works the same for both release and EDGE + +- `--edge` switches the URL path from `/release/` to `/edge/` and writes `downloads-{appName}-edge.json` +- An `EDGE.`-prefixed release tag requires `--edge`, and `--edge` rejects a `RELEASE.`-prefixed tag +- Package building works identically for release and EDGE +- Docker/Podman instructions use the actual release tag, not `:latest` ### Special Cases -- **minio/aistor**: Includes `minio.service` systemd file in packages (lines 103-106) -- **mc packages**: Binary named "mc" but package name is "mcli" (lines 870-872) -- **warp**: Version validation enforces `vX.Y.Z` format (lines 734-742) -- **Docker tags**: All Docker/Podman instructions now use actual release tags instead of `:latest` -## Testing Changes +- **aistor**: includes a `minio.service` systemd unit, gated on the _binary_ name being `minio` or `aistor`. The file is **not** in this repo — supply it in the working directory (q downloads it via the `service_file` config). +- **sidekick**: includes `sidekick.service` from this repo +- **ac**: binary `mc`, package `mcli` +- **warp**: enforces `vX.Y.Z` +- **`--package-name` rename**: when it differs from the app default, the old name becomes a legacy name. Inside the package that means a `/usr/local/bin/ -> ` symlink plus `provides`/`replaces`/`conflicts`. In the release dir it also means the old package filename, the old "latest" alias and the old `.sha256sum` are symlinked onto the new package, so already-published download URLs keep resolving. `TestPackageRenameKeepsOldLinks` pins this; `TestPackageDefaultsEmitNoLegacyLinks` pins that the non-rename path emits none of it. + +## Testing + +```bash +go test ./... +``` + +`main_test.go` covers: + +- Version conversion (date-based and semantic) +- The nfpm template under both default and renamed (`--binary-name`/`--package-name`) paths +- JSON generation for aistor, ac, sidekick and warp, plus the empty fallback document +- EDGE URL structure and Docker tag usage +- Architecture mapping and the `pkgArches` set +- Per-app release directory resolution When modifying version handling or JSON generation: -1. Test with all app types: minio, sidekick, warp, minio-enterprise -2. Verify package filenames match conventions (no 'v' prefix for warp) -3. Check generated JSON URLs point to correct dl.min.io paths -4. Ensure architecture filtering works correctly for each app + +1. Test every app in the table above +2. Verify package filenames match convention (no `v` prefix for warp; `minio`/`mcli` for aistor/ac) +3. Check generated JSON URLs still point at the correct dl.min.io paths +4. Confirm no new architecture leaked into `pkgArches` + +## Downstream Consumers + +`--appName`, the release directory layout, and the `downloads-{appName}.json` filename are a contract with the `q` release automation repo: + +- `q/v3/internal/pipeline/stage_build.go` builds the pkger argv from each product's `.qreleaser.yml` `packaging.app_name` +- `q/lib/copy-aistor-release-assets.sh` and `q/community/binary-releases/copy-release-assets.sh` look the JSON up by filename +- The product repos' `.qreleaser.yml` (`aistor`, `ac`, …) set `app_name` + +Renaming an app or changing the JSON filename requires coordinated edits there. `q/legacy-v1/` is archived and intentionally left on the old names. ## File Structure -``` +```text pkger/ ├── main.go # Single-file application -├── go.mod # Go 1.25+ required -├── minio.service # Systemd service file (included in minio packages) +├── main_test.go # Unit tests +├── go.mod # Go 1.26+ required +├── sidekick.service # Systemd unit shipped in sidekick packages ├── dist/ # GoReleaser output for pkger itself └── {app}-release/ # Input/output directories for packaging └── linux-{arch}/ ├── {binary}.{release} # Input binary - ├── {package}.rpm # Output package - ├── {package}.deb - ├── {package}.apk + ├── {package}.{rpm,deb,apk} # Output packages + ├── {alias}.{rpm,deb,apk} # "Latest" symlink └── *.sha256sum # Checksums ``` -## Testing - -Run unit tests: -```bash -go test -v -``` - -The test suite (`main_test.go`) covers: -- Version conversion (date-based and semantic) -- JSON generation for all app types -- EDGE release URL validation -- Docker tag usage verification -- Architecture mapping correctness -- URL path structure validation - ## Development Notes -- Comprehensive unit tests exist in `main_test.go` covering all JSON generation functions - Package scripts (preinstall.sh, etc.) are optional and loaded from `--scriptsDir` - The template system expects specific directory structures; paths are not validated upfront - JSON generation happens regardless of package build success/failure +- Avoid citing `main.go` line numbers in docs — they rot; name the function or variable instead diff --git a/README.md b/README.md index 6613f69..3b3b307 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,25 @@ pkger is a packaging tool for MinIO projects that generates DEB, RPM, and APK packages along with download metadata JSON files consumed by min.io/download. -## Packaging minio during development +## Supported apps -For testing minio packages during development, first install pkger so it's available in your PATH. Then prepare a release directory (such as `dist`) with architecture-specific subdirectories. For example, create `./dist/linux-amd64` and move your compiled minio binary there, renaming it to include the release version like `minio.RELEASE.2025-03-12T00-00-00Z.debug.GIT_TAG`. Make sure to replace the timestamp and git tag with your actual values. +Every app packages for `linux/amd64` and `linux/arm64` only. + +| `--appName` | Release dir | Binary read | Package name | Versioning | +| ----------- | ------------------ | ----------- | ------------ | ---------- | +| `aistor` | `minio-release` | `minio` | `minio` | date-based | +| `ac` | `mc-release` | `mc` | `mcli` | date-based | +| `sidekick` | `sidekick-release` | `sidekick` | `sidekick` | date-based | +| `warp` | `warp-release` | `warp` | `warp` | semver | +| `memkv` | `memkv-release` | `memkv` | `memkv` | date-based | +| `aimem` | `aimem-release` | `aimem` | `aimem` | date-based | +| `minfs` | `minfs-release` | `minfs` | `minfs` | date-based | + +`aistor` and `ac` keep the historical on-disk layout (`minio-release/`, `mc-release/`, `minio`/`mc` binaries, `minio`/`mcli` packages) — only the app name is rebranded. Override any of it with `--releaseDir`, `--binary-name` and `--package-name`. + +## Packaging aistor during development + +For testing aistor packages during development, first install pkger so it's available in your PATH. Then prepare a release directory (such as `dist`) with architecture-specific subdirectories. For example, create `./dist/linux-amd64` and move your compiled binary there, renaming it to include the release version like `minio.RELEASE.2025-03-12T00-00-00Z.debug.GIT_TAG`. Make sure to replace the timestamp and git tag with your actual values. You'll also need the minio.service systemd file, which you can download from the minio-service repository: @@ -12,17 +28,17 @@ You'll also need the minio.service systemd file, which you can download from the wget -O minio.service "https://raw.githubusercontent.com/minio/minio-service/refs/heads/master/linux-systemd/minio.service" ``` -Then run pkger with the release version, specifying minio as the app name and using the `--ignore` flag to continue even if some architectures are missing: +Then run pkger with the release version, specifying aistor as the app name and using the `--ignore` flag to continue even if some architectures are missing: ```shell -pkger -r RELEASE.2025-03-12T00-00-00Z.debug.GIT_TAG --appName minio --ignore --releaseDir=dist +pkger -r RELEASE.2025-03-12T00-00-00Z.debug.GIT_TAG --appName aistor --ignore --releaseDir=dist ``` -The packaged files (rpm, deb, apk) along with the downloads JSON metadata will be generated in the `./dist` directory. +The packaged files (rpm, deb, apk) along with `downloads-aistor.json` will be generated in the `./dist` directory. ## Packaging sidekick releases -Sidekick releases follow a similar workflow. Create the release directory structure with subdirectories for each supported architecture (amd64 and arm64 only). Place your compiled sidekick binaries in these directories with the release version appended to the filename. Note that sidekick only supports amd64 and arm64 architectures—ppc64le is not included. +Sidekick releases follow a similar workflow. Create the release directory structure with a subdirectory per architecture, then place your compiled sidekick binaries in them with the release version appended to the filename. ```shell mkdir -p ./sidekick-release/linux-amd64 ./sidekick-release/linux-arm64 @@ -42,7 +58,7 @@ The generated packages will appear in the architecture-specific directories alon Warp uses semantic versioning (e.g., v0.4.3) instead of date-based release tags. The version must include the `v` prefix when you run pkger, but this prefix is automatically stripped in the generated package filenames to follow standard RPM and DEB naming conventions. -Set up the release directories for amd64 and arm64 (warp doesn't support ppc64le): +Set up the release directories for amd64 and arm64: ```shell mkdir -p ./warp-release/linux-amd64 ./warp-release/linux-arm64 @@ -67,20 +83,34 @@ Two optional flags let you rename what a package installs without breaking exist When `--package-name` differs from the app's default package name, that old name is treated as a legacy name: the package installs a back-compat symlink `/usr/local/bin/ -> ` and declares `provides`/`replaces`/`conflicts` on the old name so the previous package is superseded on install. -For example, packaging the enterprise minio server as `aistor` while keeping the `minio` command working: +For example, shipping the aistor server's packages as `aistor` while leaving the built binary — and the `minio` command customers already invoke — alone: ```shell -pkger -r RELEASE.2025-03-12T00-00-00Z --appName minio-enterprise \ - --binary-name aistor --package-name aistor +pkger -r RELEASE.2025-03-12T00-00-00Z --appName aistor --package-name aistor ``` -and the enterprise `mc` client whose binary is `ac` but whose package/command is `acli`: +and the client, whose packages become `acli` while the installed command stays `mcli`: ```shell -pkger -r RELEASE.2025-03-12T00-00-00Z --appName mc-enterprise \ - --binary-name ac --package-name acli +pkger -r RELEASE.2025-03-12T00-00-00Z --appName ac --package-name acli +``` + +Note that neither example passes `--binary-name`: the binary read out of the release directory stays `minio`/`mc`, so no new binary has to be built for the rename. + +### Existing download links keep working + +A rename changes the package filename, which would break every already-published URL built from the old name. pkger therefore symlinks the old names onto the new package, so both resolve: + +```text +aistor--1.x86_64.rpm # the real package +aistor.rpm -> aistor--1.x86_64.rpm +minio.rpm -> aistor--1.x86_64.rpm +minio--1.x86_64.rpm -> aistor--1.x86_64.rpm +minio--1.x86_64.rpm.sha256sum -> aistor--1.x86_64.rpm.sha256sum ``` +The same applies to DEB and APK. The downloads metadata JSON points at the new (real) filenames; the old ones remain reachable as symlinks. Without `--package-name` no legacy links are emitted, since there is nothing to alias. + ### Upgrading across the rename On DEB and RPM the renamed package supersedes the old one automatically via the standard install commands — `dpkg -i` handles the `Replaces`+`Conflicts` takeover and `dnf`/`rpm` handles `Obsoletes` — removing the old package and taking over its files. diff --git a/main.go b/main.go index 367b46f..199a4ac 100644 --- a/main.go +++ b/main.go @@ -50,8 +50,8 @@ var ( releaseMatcher = regexp.MustCompile(`[0-9]`) app = kingpin.New("pkger", "Debian, RPMs and APKs for MinIO") - appName = app.Flag("appName", "Application name for the package"). - Default("minio"). + appName = app.Flag("appName", "Application name for the package: aistor, ac, sidekick, warp, memkv, aimem or minfs"). + Default("aistor"). Short('a'). String() @@ -258,6 +258,11 @@ var debArchMap = map[string]string{ "arm64": "arm64", } +// pkgArches is every architecture pkger packages for. Every supported app +// (aistor, ac, sidekick, warp, memkv, aimem, minfs) builds for exactly these +// two; ppc64le went away with the community minio/mc packages. +var pkgArches = []string{"amd64", "arm64"} + // generateEnterpriseDownloadsJSON builds the downloads metadata. binFile is the // raw downloadable binary filename (e.g. "aistor", "ac") and pkgFile is the // package filename base / rpm-deb name (e.g. "aistor", "acli"). dl paths are @@ -280,7 +285,7 @@ func generateEnterpriseDownloadsJSON(semVerTag, appName, releaseTag, binFile, pk MacOS: make(map[string]map[string]downloadJSON), } for subscription := range d.Subscriptions { - if appName == "minio-enterprise" { + if appName == "aistor" { // Linux d.Subscriptions[subscription].Linux["AIStor Server"] = map[string]downloadJSON{} d.Subscriptions[subscription].Linux["MinIO KMS"] = map[string]downloadJSON{} @@ -293,7 +298,7 @@ func generateEnterpriseDownloadsJSON(semVerTag, appName, releaseTag, binFile, pk // MacOS d.Subscriptions[subscription].MacOS["AIStor Server"] = map[string]downloadJSON{} } - if appName == "mc-enterprise" { + if appName == "ac" { // Linux d.Subscriptions[subscription].Linux["AIStor Client"] = map[string]downloadJSON{} // Kubernetes @@ -312,7 +317,7 @@ func generateEnterpriseDownloadsJSON(semVerTag, appName, releaseTag, binFile, pk "amd64", "arm64", } { - if appName == "mc-enterprise" { + if appName == "ac" { d.Subscriptions[subscription].Linux["AIStor Client"][arch] = downloadJSON{ Bin: &dlInfo{ Download: fmt.Sprintf("https://dl.min.io/aistor/mc/%s/linux-%s/%s", pathSegment, arch, binFile), @@ -346,7 +351,7 @@ mc --version`, releaseTag), }, } } - if appName == "minio-enterprise" { + if appName == "aistor" { d.Subscriptions[subscription].Kubernetes["AIStor Server"][arch] = downloadJSON{ Text: ``, } @@ -397,7 +402,7 @@ podman run minio/aistor/minio --version`, releaseTag), for _, arch := range []string{ "arm64", } { - if appName == "mc-enterprise" { + if appName == "ac" { d.Subscriptions[subscription].MacOS["AIStor Client"][arch] = downloadJSON{ Homebrew: &dlInfo{ Download: fmt.Sprintf("https://dl.min.io/aistor/mc/%s/darwin-%s/%s", pathSegment, arch, binFile), @@ -413,7 +418,7 @@ chmod +x %s }, } } - if appName == "minio-enterprise" { + if appName == "aistor" { d.Subscriptions[subscription].MacOS["AIStor Server"][arch] = downloadJSON{ Homebrew: &dlInfo{ Download: fmt.Sprintf("https://dl.min.io/server/minio/%s/darwin-%s/%s", pathSegment, arch, binFile), @@ -435,7 +440,7 @@ chmod +x %s for _, arch := range []string{ "amd64", } { - if appName == "mc-enterprise" { + if appName == "ac" { d.Subscriptions[subscription].Windows["AIStor Client"][arch] = downloadJSON{ Bin: &dlInfo{ Download: fmt.Sprintf("https://dl.min.io/aistor/mc/%s/windows-%s/%s.exe", pathSegment, arch, binFile), @@ -447,7 +452,7 @@ chmod +x %s } } - if appName == "minio-enterprise" { + if appName == "aistor" { d.Subscriptions[subscription].Windows["AIStor Server"][arch] = downloadJSON{ Bin: &dlInfo{ Download: fmt.Sprintf("https://dl.min.io/aistor/minio/%s/windows-%s/%s.exe", pathSegment, arch, binFile), @@ -462,172 +467,17 @@ chmod +x %s return d } -func generateDownloadsJSON(semVerTag string, appName string) downloadsJSON { - d := downloadsJSON{ +// generateDownloadsJSON is the fallback metadata generator for apps that have no +// dl.min.io download page of their own (memkv, aimem, minfs). It intentionally +// produces an empty document: the packages ship, but there is nothing to link. +func generateDownloadsJSON() downloadsJSON { + return downloadsJSON{ Linux: make(map[string]map[string]downloadJSON), MacOS: make(map[string]map[string]downloadJSON), Windows: make(map[string]map[string]downloadJSON), Docker: make(map[string]map[string]downloadJSON), Kubernetes: make(map[string]map[string]downloadJSON), } - - if appName == "minio" { - d.Linux["MinIO Server"] = map[string]downloadJSON{} - d.MacOS["MinIO Server"] = map[string]downloadJSON{} - d.Windows["MinIO Server"] = map[string]downloadJSON{} - d.Docker["MinIO Server"] = map[string]downloadJSON{} - d.Kubernetes["MinIO Server"] = map[string]downloadJSON{} - } - - if appName == "mc" { - d.Linux["MinIO Client"] = map[string]downloadJSON{} - d.MacOS["MinIO Client"] = map[string]downloadJSON{} - d.Windows["MinIO Client"] = map[string]downloadJSON{} - d.Docker["MinIO Client"] = map[string]downloadJSON{} - d.Kubernetes["MinIO Client"] = map[string]downloadJSON{} - } - - for _, linuxArch := range []string{ - "amd64", - "arm64", - "ppc64le", - } { - if appName == "minio" { - d.Kubernetes["MinIO Server"][linuxArch] = downloadJSON{ - Kubectl: &dlInfo{ - Text: `kubectl apply -k github.com/minio/operator`, - }, - } - d.Docker["MinIO Server"][linuxArch] = downloadJSON{ - Podman: &dlInfo{ - Text: `podman pull quay.io/minio/minio:latest -podman run minio/minio --version`, - }, - } - d.Linux["MinIO Server"][linuxArch] = downloadJSON{ - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio", linuxArch), - Text: fmt.Sprintf(`wget https://dl.min.io/server/minio/release/linux-%s/minio -chmod +x minio -./minio --version`, linuxArch), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio.sha256sum", linuxArch), - }, - RPM: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio-%s-1.%s.rpm", linuxArch, semVerTag, rpmArchMap[linuxArch]), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio-%s-1.%s.rpm.sha256sum", linuxArch, semVerTag, rpmArchMap[linuxArch]), - Text: fmt.Sprintf(`dnf install https://dl.min.io/server/minio/release/linux-%s/minio-%s-1.%s.rpm -minio --version`, linuxArch, semVerTag, rpmArchMap[linuxArch]), - }, - Deb: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio_%s_%s.deb", linuxArch, semVerTag, debArchMap[linuxArch]), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/linux-%s/minio_%s_%s.deb.sha256sum", linuxArch, semVerTag, debArchMap[linuxArch]), - Text: fmt.Sprintf(`wget https://dl.min.io/server/minio/release/linux-%s/minio_%s_%s.deb -dpkg -i minio_%s_%s.deb -minio --version`, linuxArch, semVerTag, debArchMap[linuxArch], semVerTag, debArchMap[linuxArch]), - }, - } - } - if appName == "mc" { - d.Kubernetes["MinIO Client"][linuxArch] = downloadJSON{ - Kubectl: &dlInfo{ - Text: `kubectl run my-mc -i --tty --image minio/mc:latest --command -- bash -mc --version`, - }, - } - d.Docker["MinIO Client"][linuxArch] = downloadJSON{ - Podman: &dlInfo{ - Text: `podman pull quay.io/minio/mc:latest -podman run --name my-mc --hostname my-mc -it --entrypoint /bin/bash --rm minio/mc -mc --version`, - }, - } - d.Linux["MinIO Client"][linuxArch] = downloadJSON{ - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mc", linuxArch), - Text: fmt.Sprintf(`wget https://dl.min.io/client/mc/release/linux-%s/mc -chmod +x mc -./mc --version`, linuxArch), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mc.sha256sum", linuxArch), - }, - RPM: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mcli-%s-1.%s.rpm", linuxArch, semVerTag, rpmArchMap[linuxArch]), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mcli-%s-1.%s.rpm.sha256sum", linuxArch, semVerTag, rpmArchMap[linuxArch]), - Text: fmt.Sprintf(`dnf install https://dl.min.io/client/mc/release/linux-%s/mcli-%s-1.%s.rpm -mcli --version`, linuxArch, semVerTag, rpmArchMap[linuxArch]), - }, - Deb: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mcli_%s_%s.deb", linuxArch, semVerTag, debArchMap[linuxArch]), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/linux-%s/mcli_%s_%s.deb.sha256sum", linuxArch, semVerTag, debArchMap[linuxArch]), - Text: fmt.Sprintf(`wget https://dl.min.io/client/mc/release/linux-%s/mcli_%s_%s.deb -dpkg -i mcli_%s_%s.deb -mcli --version`, linuxArch, semVerTag, debArchMap[linuxArch], semVerTag, debArchMap[linuxArch]), - }, - } - } - } - - for _, macArch := range []string{ - "amd64", - "arm64", - } { - if appName == "minio" { - d.MacOS["MinIO Server"][macArch] = downloadJSON{ - Homebrew: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/darwin-%s/minio", macArch), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/darwin-%s/minio.sha256sum", macArch), - Text: `brew install minio/stable/minio`, - }, - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/darwin-%s/minio", macArch), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/darwin-%s/minio.sha256sum", macArch), - Text: fmt.Sprintf(`curl --progress-bar -O https://dl.min.io/server/minio/release/darwin-%s/minio -chmod +x minio -./minio --version`, macArch), - }, - } - } - if appName == "mc" { - d.MacOS["MinIO Client"][macArch] = downloadJSON{ - Homebrew: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/darwin-%s/mc", macArch), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/darwin-%s/mc.sha256sum", macArch), - Text: `brew install minio/stable/mc`, - }, - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/darwin-%s/mc", macArch), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/darwin-%s/mc.sha256sum", macArch), - Text: fmt.Sprintf(`curl --progress-bar -O https://dl.min.io/client/mc/release/darwin-%s/mc -chmod +x mc -./minio --version`, macArch), - }, - } - } - } - for _, winArch := range []string{ - "amd64", - } { - if appName == "minio" { - d.Windows["MinIO Server"][winArch] = downloadJSON{ - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/server/minio/release/windows-%s/minio.exe", winArch), - Text: fmt.Sprintf(`Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-%s/minio.exe" -OutFile "minio.exe" -minio.exe --version`, winArch), - Checksum: fmt.Sprintf("https://dl.min.io/server/minio/release/windows-%s/minio.exe.sha256sum", winArch), - }, - } - } - if appName == "mc" { - d.Windows["MinIO Client"][winArch] = downloadJSON{ - Bin: &dlInfo{ - Download: fmt.Sprintf("https://dl.min.io/client/mc/release/windows-%s/mc.exe", winArch), - Text: fmt.Sprintf(`Invoke-WebRequest -Uri "https://dl.min.io/client/mc/release/windows-%s/mc.exe" -OutFile "mc.exe" -mc.exe --version`, winArch), - Checksum: fmt.Sprintf("https://dl.min.io/client/mc/release/windows-%s/mc.exe.sha256sum", winArch), - }, - } - } - } - return d } func generateSidekickDownloadsJSON(semVerTag, releaseTag string) downloadsJSON { @@ -776,9 +626,11 @@ func releaseDirName() string { } name := *appName switch name { - case "minio-enterprise": + // The built artifacts still land in minio-release/ and mc-release/; only the + // app name was rebranded. + case "aistor": name = "minio" - case "mc-enterprise": + case "ac": name = "mc" } return name + "-release" @@ -827,7 +679,7 @@ func main() { outputFilename += ".json" switch *appName { - case "minio-enterprise", "mc-enterprise": + case "aistor", "ac": semVerTag := semVerRelease(*release) d = generateEnterpriseDownloadsJSON(semVerTag, *appName, *release, binarySrcName(*appName, *binaryName), pkgName(*appName, *packageName), *edge) case "sidekick": @@ -848,8 +700,7 @@ func main() { // Strip 'v' prefix for package naming conventions d = generateWarpDownloadsJSON(versionWithoutV, *release) default: - semVerTag := semVerRelease(*release) - d = generateDownloadsJSON(semVerTag, *appName) + d = generateDownloadsJSON() } buf, err := json.Marshal(&d) @@ -885,9 +736,9 @@ type releaseTmpl struct { // legacy name (compat symlink + provides/replaces/conflicts). func defaultPkgName(appName string) string { switch appName { - case "minio-enterprise": + case "aistor": return "minio" - case "mc", "mc-enterprise": + case "ac": return "mcli" } return appName @@ -898,9 +749,9 @@ func defaultPkgName(appName string) string { // it. Independent of the package/install name. func defaultBinarySrcName(appName string) string { switch appName { - case "minio-enterprise": + case "aistor": return "minio" - case "mc-enterprise": + case "ac": return "mc" } return appName @@ -1014,36 +865,7 @@ func doPackage(appName, license, release, packager, deps, scriptsDir, binaryName semVerTag = semVerRelease(release) } - for _, arch := range []string{ - "amd64", - "arm64", - "ppc64le", - } { - if appName == "minio-enterprise" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "mc-enterprise" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "sidekick" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "warp" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "memkv" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "aimem" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "minfs" && arch != "amd64" && arch != "arm64" { - continue - } - if appName == "minfs-cache-server" && arch != "amd64" && arch != "arm64" { - continue - } - + for _, arch := range pkgArches { var buf bytes.Buffer err = mtmpl.Execute(&buf, releaseTmpl{ App: pkgName(appName, packageName), @@ -1054,13 +876,13 @@ func doPackage(appName, license, release, packager, deps, scriptsDir, binaryName Binary: binarySrcName(appName, binaryName), LegacyName: legacyName(appName, packageName), Description: func() string { - if appName == "minio-enterprise" { + if appName == "aistor" { return `MinIO is a High Performance Object Store. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.` } - if appName == "mc" || appName == "mc-enterprise" { + if appName == "ac" { return `MinIO Client for cloud storage and filesystems` } if appName == "memkv" { @@ -1077,11 +899,6 @@ func doPackage(appName, license, release, packager, deps, scriptsDir, binaryName return `minfs is the cache-tier mount for MinIO AIStor. It serves MinIO AIStor reads through a local on-disk or distributed cache cluster.` - } - if appName == "minfs-cache-server" { - return `minfs-cache-server is an NVMe-backed distributed cache daemon - for minfs. It clusters via SWIM gossip and places blocks via - rendezvous hashing.` } return `MinIO is a High Performance Object Storage released under AGPLv3. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build @@ -1155,11 +972,6 @@ func doPackage(appName, license, release, packager, deps, scriptsDir, binaryName } { - curDir, err := os.Getwd() - if err != nil { - return err - } - // Stable "latest" alias filename in the release dir. On the // default (non-rename) path this intentionally keeps the // historical alias name (minio.deb, mc.deb, ...) even where it @@ -1171,16 +983,50 @@ func doPackage(appName, license, release, packager, deps, scriptsDir, binaryName if packageName != "" { return packageName } - if appName == "minio-enterprise" { + if appName == "aistor" { return "minio" } return appName }() - _ = os.Chdir(filepath.Dir(tgtPath)) - _ = os.Remove(aliasBase + filepath.Ext(tgtPath)) - _ = os.Symlink(releasePkg, aliasBase+filepath.Ext(tgtPath)) - _ = os.Chdir(curDir) + // target stays a bare filename so the symlink is relative to + // the release dir and survives being copied or served from + // elsewhere; only the link path is absolute. + dir := filepath.Dir(tgtPath) + link := func(target, name string) error { + path := filepath.Join(dir, name) + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return os.Symlink(target, path) + } + + if err := link(releasePkg, aliasBase+filepath.Ext(tgtPath)); err != nil { + return err + } + + // Under a --package-name rename the package filename changes + // (minio-*.rpm -> aistor-*.rpm), which would break every + // published URL built from the old name. Symlink the old alias + // and the old versioned filename (plus its checksum) onto the + // new package so existing links keep resolving. + if legacy := legacyName(appName, packageName); legacy != "" { + legacyInfo := *info + legacyInfo.Name = legacy + legacyPkg := pkg.ConventionalFileName(&legacyInfo) + + if err := link(releasePkg, legacy+filepath.Ext(tgtPath)); err != nil { + return err + } + if legacyPkg != releasePkg { + if err := link(releasePkg, legacyPkg); err != nil { + return err + } + if err := link(releasePkg+".sha256sum", legacyPkg+".sha256sum"); err != nil { + return err + } + } + } } sh := sha256.New() diff --git a/main_test.go b/main_test.go index 7604c55..8657d7c 100644 --- a/main_test.go +++ b/main_test.go @@ -19,11 +19,14 @@ package main import ( "bytes" + "os" + "path/filepath" "strings" "testing" "text/template" "github.com/goreleaser/nfpm/v2" + jsoniter "github.com/json-iterator/go" ) // renderPackageYAML renders the nfpm package template for an app the same way @@ -73,13 +76,13 @@ func TestPackageTemplateRename(t *testing.T) { wantService bool }{ { - name: "minio-enterprise -> aistor", appName: "minio-enterprise", + name: "aistor -> aistor", appName: "aistor", binaryName: "aistor", packageName: "aistor", wantPkg: "aistor", wantInstall: "/usr/local/bin/aistor", wantSrcFile: "aistor", wantLegacy: "minio", wantService: true, }, { - name: "mc-enterprise -> acli (binary ac)", appName: "mc-enterprise", + name: "ac -> acli (binary ac)", appName: "ac", binaryName: "ac", packageName: "acli", wantPkg: "acli", wantInstall: "/usr/local/bin/acli", wantSrcFile: "ac", wantLegacy: "mcli", wantService: false, @@ -129,10 +132,8 @@ func TestPackageTemplateNoRenameDefaults(t *testing.T) { wantSrc string wantSvc bool }{ - {"minio-enterprise", "minio-enterprise", "minio", "/usr/local/bin/minio", "minio", true}, - {"minio community", "minio", "minio", "/usr/local/bin/minio", "minio", true}, - {"mc community", "mc", "mcli", "/usr/local/bin/mcli", "mc", false}, - {"mc-enterprise", "mc-enterprise", "mcli", "/usr/local/bin/mcli", "mc", false}, + {"aistor", "aistor", "minio", "/usr/local/bin/minio", "minio", true}, + {"ac", "ac", "mcli", "/usr/local/bin/mcli", "mc", false}, {"sidekick", "sidekick", "sidekick", "/usr/local/bin/sidekick", "sidekick", false}, } for _, tt := range tests { @@ -163,14 +164,178 @@ func TestPackageTemplateNoRenameDefaults(t *testing.T) { } } +// runDoPackage builds real packages in a temp dir. It stages a source binary for +// every arch in pkgArches, so no arch is silently skipped, and returns the +// per-arch release dirs keyed by arch. +func runDoPackage(t *testing.T, app, pkgOverride string) map[string]string { + t.Helper() + const rel = "RELEASE.2025-03-12T00-00-00Z" + + dir := t.TempDir() + t.Chdir(dir) + + oldApp, oldDir, oldIgnore := *appName, *releaseDir, *ignoreMissingArch + t.Cleanup(func() { *appName, *releaseDir, *ignoreMissingArch = oldApp, oldDir, oldIgnore }) + // No --ignore: every arch must package cleanly from the staged fixtures. + *appName, *releaseDir, *ignoreMissingArch = app, "", false + + // Stage into the same directory doPackage reads from. + relDir := releaseDirName() + out := make(map[string]string, len(pkgArches)) + for _, arch := range pkgArches { + archDir := filepath.Join(dir, relDir, "linux-"+arch) + if err := os.MkdirAll(archDir, 0o750); err != nil { + t.Fatal(err) + } + src := filepath.Join(archDir, defaultBinarySrcName(app)+"."+rel) + if err := os.WriteFile(src, []byte("binary"), 0o600); err != nil { + t.Fatal(err) + } + out[arch] = archDir + } + // aistor packages embed a systemd unit that is not vendored in this repo. + if err := os.WriteFile(filepath.Join(dir, "minio.service"), []byte("[Unit]\n"), 0o600); err != nil { + t.Fatal(err) + } + + if err := doPackage(app, "Test License", rel, "deb,rpm,apk", "", "./", "", pkgOverride); err != nil { + t.Fatalf("doPackage(%s, pkg=%q): %v", app, pkgOverride, err) + } + return out +} + +// pkgSuffix maps an arch to the per-format package filename suffixes nfpm +// produces, so the link assertions cover every arch pkgArches builds. +func pkgSuffix(arch string) map[string]string { + return map[string]string{ + "rpm": "-20250312000000.0.0-1." + rpmArchMap[arch] + ".rpm", + "deb": "_20250312000000.0.0_" + debArchMap[arch] + ".deb", + "apk": "_20250312000000.0.0_" + rpmArchMap[arch] + ".apk", + } +} + +// TestPackageRenameKeepsOldLinks pins the compatibility guarantee for a +// --package-name rename: the packages ship under the new name, and every +// filename an already-published URL could reference (the "latest" alias, the +// versioned package, its checksum) still resolves via a symlink. Checked for +// every arch, for both renamed apps. +func TestPackageRenameKeepsOldLinks(t *testing.T) { + for _, tt := range []struct { + app, pkgOverride, legacy string + }{ + {app: "aistor", pkgOverride: "aistor", legacy: "minio"}, + {app: "ac", pkgOverride: "acli", legacy: "mcli"}, + } { + t.Run(tt.app, func(t *testing.T) { + out := runDoPackage(t, tt.app, tt.pkgOverride) + + for arch, dir := range out { + sfx := pkgSuffix(arch) + + // The renamed package is the only real file. + for _, format := range []string{"rpm", "deb", "apk"} { + name := tt.pkgOverride + sfx[format] + fi, err := os.Lstat(filepath.Join(dir, name)) + if err != nil { + t.Fatalf("%s/%s missing: %v", arch, name, err) + } + if fi.Mode()&os.ModeSymlink != 0 { + t.Errorf("%s/%s should be a regular file, not a symlink", arch, name) + } + } + + var links []string + for _, format := range []string{"rpm", "deb", "apk"} { + links = append(links, + // New-style "latest" alias. + tt.pkgOverride+"."+format, + // Old-style URLs that must keep working. + tt.legacy+"."+format, + tt.legacy+sfx[format], + tt.legacy+sfx[format]+".sha256sum", + ) + } + for _, name := range links { + p := filepath.Join(dir, name) + fi, err := os.Lstat(p) + if err != nil { + t.Errorf("%s/%s missing: %v", arch, name, err) + continue + } + if fi.Mode()&os.ModeSymlink == 0 { + t.Errorf("%s/%s should be a symlink", arch, name) + } + // Relative target keeps the link valid if the dir moves. + if target, err := os.Readlink(p); err == nil && filepath.IsAbs(target) { + t.Errorf("%s/%s target should be relative, got %s", arch, name, target) + } + if _, err := os.Stat(p); err != nil { + t.Errorf("%s/%s does not resolve: %v", arch, name, err) + } + } + } + }) + } +} + +// TestPackageDefaultsEmitNoLegacyLinks is the other half of the contract: with +// no rename there is nothing to alias, so each arch dir must hold exactly the +// historical set of files. +func TestPackageDefaultsEmitNoLegacyLinks(t *testing.T) { + for _, tt := range []struct { + app, pkg, alias string + }{ + // aistor keeps the historical minio.* alias; ac's alias follows the + // app name, as it did pre-rename (mc-enterprise.* -> ac.*), while the + // package itself stays mcli. + {app: "aistor", pkg: "minio", alias: "minio"}, + {app: "ac", pkg: "mcli", alias: "ac"}, + } { + t.Run(tt.app, func(t *testing.T) { + out := runDoPackage(t, tt.app, "") + + for arch, dir := range out { + sfx := pkgSuffix(arch) + + want := map[string]bool{ + defaultBinarySrcName(tt.app) + ".RELEASE.2025-03-12T00-00-00Z": true, + } + for _, format := range []string{"rpm", "deb", "apk"} { + want[tt.pkg+sfx[format]] = true + want[tt.pkg+sfx[format]+".sha256sum"] = true + want[tt.alias+"."+format] = true + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + got := map[string]bool{} + for _, e := range entries { + got[e.Name()] = true + } + for name := range want { + if !got[name] { + t.Errorf("%s: missing %s", arch, name) + } + delete(got, name) + } + for name := range got { + t.Errorf("%s: unexpected extra file %s", arch, name) + } + } + }) + } +} + // TestEnterpriseDownloadsJSONRename checks the downloads metadata uses the // renamed filenames while keeping the dl paths unchanged. func TestEnterpriseDownloadsJSONRename(t *testing.T) { semVer := "20250312000000.0.0" rel := "RELEASE.2025-03-12T00-00-00Z" - t.Run("minio-enterprise", func(t *testing.T) { - d := generateEnterpriseDownloadsJSON(semVer, "minio-enterprise", rel, "aistor", "aistor", false) + t.Run("aistor", func(t *testing.T) { + d := generateEnterpriseDownloadsJSON(semVer, "aistor", rel, "aistor", "aistor", false) lin := d.Subscriptions["Enterprise"].Linux["AIStor Server"]["amd64"] if lin.Bin.Download != "https://dl.min.io/aistor/minio/release/linux-amd64/aistor" { t.Errorf("bin download: %s", lin.Bin.Download) @@ -183,8 +348,8 @@ func TestEnterpriseDownloadsJSONRename(t *testing.T) { } }) - t.Run("mc-enterprise", func(t *testing.T) { - d := generateEnterpriseDownloadsJSON(semVer, "mc-enterprise", rel, "ac", "acli", false) + t.Run("ac", func(t *testing.T) { + d := generateEnterpriseDownloadsJSON(semVer, "ac", rel, "ac", "acli", false) lin := d.Subscriptions["Enterprise"].Linux["AIStor Client"]["amd64"] if lin.Bin.Download != "https://dl.min.io/aistor/mc/release/linux-amd64/ac" { t.Errorf("bin download: %s", lin.Bin.Download) @@ -313,7 +478,7 @@ func TestGenerateEnterpriseDownloadsJSON(t *testing.T) { releaseTag := "RELEASE.2025-03-12T00-00-00Z" t.Run("MinIO Enterprise Release", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "minio-enterprise", releaseTag, "minio", "minio", false) + result := generateEnterpriseDownloadsJSON(semVerTag, "aistor", releaseTag, "minio", "minio", false) // Verify structure if result.Subscriptions == nil { @@ -342,7 +507,7 @@ func TestGenerateEnterpriseDownloadsJSON(t *testing.T) { }) t.Run("MinIO Enterprise EDGE", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "minio-enterprise", releaseTag, "minio", "minio", true) + result := generateEnterpriseDownloadsJSON(semVerTag, "aistor", releaseTag, "minio", "minio", true) // Verify EDGE path linuxData := result.Subscriptions["Enterprise"].Linux["AIStor Server"]["amd64"] @@ -358,7 +523,7 @@ func TestGenerateEnterpriseDownloadsJSON(t *testing.T) { }) t.Run("Docker tags use release version", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "minio-enterprise", releaseTag, "minio", "minio", false) + result := generateEnterpriseDownloadsJSON(semVerTag, "aistor", releaseTag, "minio", "minio", false) dockerData := result.Subscriptions["Enterprise"].Docker["AIStor Server"]["amd64"] if dockerData.Podman == nil { @@ -373,11 +538,11 @@ func TestGenerateEnterpriseDownloadsJSON(t *testing.T) { }) t.Run("MC Enterprise", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "mc-enterprise", releaseTag, "mc", "mcli", false) + result := generateEnterpriseDownloadsJSON(semVerTag, "ac", releaseTag, "mc", "mcli", false) linuxData := result.Subscriptions["Enterprise"].Linux["AIStor Client"]["amd64"] if linuxData.Bin == nil { - t.Error("Binary download info missing for mc-enterprise") + t.Error("Binary download info missing for ac") } // Verify mc paths @@ -387,46 +552,27 @@ func TestGenerateEnterpriseDownloadsJSON(t *testing.T) { }) } -// TestGenerateDownloadsJSON tests community JSON generation +// TestGenerateDownloadsJSON checks the fallback generator still emits a valid, +// empty document for apps without a dl.min.io download page (memkv, aimem, +// minfs) so their downloads-.json marshals cleanly. func TestGenerateDownloadsJSON(t *testing.T) { - semVerTag := "20250312000000.0.0" - - t.Run("MinIO Community", func(t *testing.T) { - result := generateDownloadsJSON(semVerTag, "minio") - - // Verify Linux has all architectures - if _, ok := result.Linux["MinIO Server"]["amd64"]; !ok { - t.Error("amd64 architecture missing") - } - if _, ok := result.Linux["MinIO Server"]["arm64"]; !ok { - t.Error("arm64 architecture missing") - } - if _, ok := result.Linux["MinIO Server"]["ppc64le"]; !ok { - t.Error("ppc64le architecture missing") - } - - // Verify RPM architecture mapping - rpmData := result.Linux["MinIO Server"]["amd64"].RPM - if !strings.Contains(rpmData.Download, "x86_64.rpm") { - t.Error("RPM should use x86_64 architecture for amd64") - } - - // Verify DEB architecture mapping - debData := result.Linux["MinIO Server"]["amd64"].Deb - if !strings.Contains(debData.Download, "_amd64.deb") { - t.Error("DEB should use amd64 architecture") - } - }) - - t.Run("MC Community", func(t *testing.T) { - result := generateDownloadsJSON(semVerTag, "mc") + result := generateDownloadsJSON() - // Verify package name is mcli not mc - rpmData := result.Linux["MinIO Client"]["amd64"].RPM - if !strings.Contains(rpmData.Download, "mcli-") { - t.Error("MC packages should be named 'mcli'") + buf, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(&result) + if err != nil { + t.Fatalf("marshal: %v", err) + } + for name, section := range map[string]map[string]map[string]downloadJSON{ + "Linux": result.Linux, + "MacOS": result.MacOS, + "Windows": result.Windows, + "Docker": result.Docker, + "Kubernetes": result.Kubernetes, + } { + if len(section) != 0 { + t.Errorf("expected %s to be empty, got %s", name, buf) } - }) + } } // TestGenerateSidekickDownloadsJSON tests sidekick JSON generation @@ -551,20 +697,15 @@ func TestReleaseDirName(t *testing.T) { expected string }{ { - name: "minio-enterprise", - appName: "minio-enterprise", + name: "aistor", + appName: "aistor", expected: "minio-release", }, { - name: "mc-enterprise", - appName: "mc-enterprise", + name: "ac", + appName: "ac", expected: "mc-release", }, - { - name: "minio", - appName: "minio", - expected: "minio-release", - }, { name: "sidekick", appName: "sidekick", @@ -580,6 +721,11 @@ func TestReleaseDirName(t *testing.T) { appName: "memkv", expected: "memkv-release", }, + { + name: "aimem", + appName: "aimem", + expected: "aimem-release", + }, } for _, tt := range tests { @@ -625,21 +771,28 @@ func TestArchitectureMappings(t *testing.T) { }) } +// TestPkgArches pins the packaged arch set. Every supported app (aistor, ac, +// sidekick, warp, memkv, aimem, minfs) is amd64+arm64 only; ppc64le went away +// with the community minio/mc packages and must not come back silently. +func TestPkgArches(t *testing.T) { + want := []string{"amd64", "arm64"} + if len(pkgArches) != len(want) { + t.Fatalf("pkgArches = %v, want %v", pkgArches, want) + } + for i, arch := range want { + if pkgArches[i] != arch { + t.Errorf("pkgArches[%d] = %q, want %q", i, pkgArches[i], arch) + } + } +} + // TestURLPathStructure validates URL structure for different release types func TestURLPathStructure(t *testing.T) { semVerTag := "20250312000000.0.0" releaseTag := "RELEASE.2025-03-12T00-00-00Z" - t.Run("Community MinIO uses /server/minio/release/", func(t *testing.T) { - result := generateDownloadsJSON(semVerTag, "minio") - binURL := result.Linux["MinIO Server"]["amd64"].Bin.Download - if !strings.HasPrefix(binURL, "https://dl.min.io/server/minio/release/") { - t.Errorf("Unexpected URL structure: %s", binURL) - } - }) - t.Run("Enterprise MinIO uses /aistor/minio/release/", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "minio-enterprise", releaseTag, "minio", "minio", false) + result := generateEnterpriseDownloadsJSON(semVerTag, "aistor", releaseTag, "minio", "minio", false) binURL := result.Subscriptions["Enterprise"].Linux["AIStor Server"]["amd64"].Bin.Download if !strings.HasPrefix(binURL, "https://dl.min.io/aistor/minio/release/") { t.Errorf("Unexpected URL structure: %s", binURL) @@ -647,7 +800,7 @@ func TestURLPathStructure(t *testing.T) { }) t.Run("Enterprise EDGE uses /aistor/minio/edge/", func(t *testing.T) { - result := generateEnterpriseDownloadsJSON(semVerTag, "minio-enterprise", releaseTag, "minio", "minio", true) + result := generateEnterpriseDownloadsJSON(semVerTag, "aistor", releaseTag, "minio", "minio", true) binURL := result.Subscriptions["Enterprise"].Linux["AIStor Server"]["amd64"].Bin.Download if !strings.HasPrefix(binURL, "https://dl.min.io/aistor/minio/edge/") { t.Errorf("Unexpected EDGE URL structure: %s", binURL)