feat: 扩展发布打包支持多组件多架构多格式产物
- 新增 web 组件独立发布为 nex-web_<version>.tar.gz
- server 新增 arm64 架构、macOS universal、Windows arm64 产物
- desktop 新增 arm64 架构支持(Linux/Windows)
- Linux desktop 新增 AppImage、deb、rpm 安装包格式
- macOS desktop 新增 unsigned DMG 安装包
- 统一发布资产命名为 {component}_{version}_{platform}_{arch}.{ext}
- 新增 SHA256SUMS 校验和清单覆盖全部发布资产
- versionctl 新增 asset-name CLI 支持按参数生成资产文件名
- Makefile release target 重构为组件/平台/架构参数化
- GitHub Actions release workflow 扩展多组件多架构构建矩阵
- 同步更新 openspec 主规范(desktop-app/release-pipeline/workspace-command-flows)
This commit is contained in:
@@ -106,8 +106,8 @@ func printMacOSPlist(root, minMacOSVersion string) error {
|
||||
}
|
||||
|
||||
func printAssetName(root string, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("asset-name 至少需要 kind 和 platform 参数")
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("asset-name 需要组件参数: server|web|desktop")
|
||||
}
|
||||
|
||||
version, err := projectversion.ReadString(root)
|
||||
@@ -115,30 +115,31 @@ func printAssetName(root string, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var platform, arch, format string
|
||||
switch args[0] {
|
||||
case "server":
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("server 资产命名需要 platform 和 arch 参数")
|
||||
case "server", "desktop":
|
||||
if len(args) != 4 {
|
||||
return fmt.Errorf("%s 资产命名需要 platform、arch 和 format 参数", args[0])
|
||||
}
|
||||
name, nameErr := projectversion.ServerAssetName(version, args[1], args[2])
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
fmt.Println(name)
|
||||
return nil
|
||||
case "desktop":
|
||||
platform = args[1]
|
||||
arch = args[2]
|
||||
format = args[3]
|
||||
case "web":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("desktop 资产命名只需要 platform 参数")
|
||||
return fmt.Errorf("web 资产命名只需要 format 参数")
|
||||
}
|
||||
name, nameErr := projectversion.DesktopAssetName(version, args[1])
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
fmt.Println(name)
|
||||
return nil
|
||||
format = args[1]
|
||||
default:
|
||||
return fmt.Errorf("不支持的资产类型 %q", args[0])
|
||||
return fmt.Errorf("不支持的资产组件 %q", args[0])
|
||||
}
|
||||
|
||||
name, nameErr := projectversion.ReleaseAssetName(version, args[0], platform, arch, format)
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
|
||||
fmt.Println(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustGetwd() string {
|
||||
|
||||
@@ -263,44 +263,86 @@ func ReadEnvVar(content, key string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ServerAssetName(version, goos, arch string) (string, error) {
|
||||
func ReleaseAssetName(version, component, platform, arch, format string) (string, error) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch goos {
|
||||
case "linux", "windows", "darwin":
|
||||
switch component {
|
||||
case "server":
|
||||
return serverAssetName(version, platform, arch, format)
|
||||
case "web":
|
||||
return webAssetName(version, platform, arch, format)
|
||||
case "desktop":
|
||||
return desktopAssetName(version, platform, arch, format)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的 server 平台 %q", goos)
|
||||
return "", fmt.Errorf("不支持的资产组件 %q", component)
|
||||
}
|
||||
|
||||
if arch == "" {
|
||||
return "", errors.New("server 资产命名缺少架构")
|
||||
}
|
||||
|
||||
ext := ".tar.gz"
|
||||
if goos == "windows" {
|
||||
ext = ".zip"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("nex-server_%s_%s_%s%s", version, goos, arch, ext), nil
|
||||
}
|
||||
|
||||
func DesktopAssetName(version, platform string) (string, error) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
func serverAssetName(version, platform, arch, format string) (string, error) {
|
||||
if !validCombination(platform, arch, format, []releaseAssetTarget{
|
||||
{platform: "linux", arch: "amd64", format: "tar.gz"},
|
||||
{platform: "linux", arch: "arm64", format: "tar.gz"},
|
||||
{platform: "macos", arch: "amd64", format: "tar.gz"},
|
||||
{platform: "macos", arch: "arm64", format: "tar.gz"},
|
||||
{platform: "macos", arch: "universal", format: "tar.gz"},
|
||||
{platform: "windows", arch: "amd64", format: "zip"},
|
||||
{platform: "windows", arch: "arm64", format: "zip"},
|
||||
}) {
|
||||
return "", fmt.Errorf("不支持的 server 资产目标 %s/%s/%s", platform, arch, format)
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case "linux":
|
||||
return fmt.Sprintf("Nex_%s_linux_amd64.tar.gz", version), nil
|
||||
case "windows":
|
||||
return fmt.Sprintf("Nex_%s_windows_amd64.zip", version), nil
|
||||
case "macos":
|
||||
return fmt.Sprintf("Nex_%s_macOS_universal.zip", version), nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的 desktop 平台 %q", platform)
|
||||
return fmt.Sprintf("nex-server_%s_%s_%s.%s", version, platform, arch, format), nil
|
||||
}
|
||||
|
||||
func webAssetName(version, platform, arch, format string) (string, error) {
|
||||
if platform != "" || arch != "" {
|
||||
return "", errors.New("web 资产命名不支持平台或架构参数")
|
||||
}
|
||||
|
||||
if format != "tar.gz" {
|
||||
return "", fmt.Errorf("不支持的 web 资产格式 %q", format)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("nex-web_%s.tar.gz", version), nil
|
||||
}
|
||||
|
||||
func desktopAssetName(version, platform, arch, format string) (string, error) {
|
||||
if !validCombination(platform, arch, format, []releaseAssetTarget{
|
||||
{platform: "linux", arch: "amd64", format: "tar.gz"},
|
||||
{platform: "linux", arch: "amd64", format: "AppImage"},
|
||||
{platform: "linux", arch: "amd64", format: "deb"},
|
||||
{platform: "linux", arch: "amd64", format: "rpm"},
|
||||
{platform: "linux", arch: "arm64", format: "tar.gz"},
|
||||
{platform: "linux", arch: "arm64", format: "AppImage"},
|
||||
{platform: "linux", arch: "arm64", format: "deb"},
|
||||
{platform: "linux", arch: "arm64", format: "rpm"},
|
||||
{platform: "macos", arch: "universal", format: "zip"},
|
||||
{platform: "macos", arch: "universal", format: "dmg"},
|
||||
{platform: "windows", arch: "amd64", format: "zip"},
|
||||
{platform: "windows", arch: "arm64", format: "zip"},
|
||||
}) {
|
||||
return "", fmt.Errorf("不支持的 desktop 资产目标 %s/%s/%s", platform, arch, format)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("nex-desktop_%s_%s_%s.%s", version, platform, arch, format), nil
|
||||
}
|
||||
|
||||
type releaseAssetTarget struct {
|
||||
platform string
|
||||
arch string
|
||||
format string
|
||||
}
|
||||
|
||||
func validCombination(platform, arch, format string, targets []releaseAssetTarget) bool {
|
||||
for _, target := range targets {
|
||||
if target.platform == platform && target.arch == arch && target.format == format {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func DesktopInfoPlist(version, minMacOSVersion string) (string, error) {
|
||||
|
||||
@@ -83,20 +83,72 @@ func TestVerifyTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssetNames(t *testing.T) {
|
||||
linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer)
|
||||
testCases := []struct {
|
||||
name string
|
||||
component string
|
||||
platform string
|
||||
arch string
|
||||
format string
|
||||
want string
|
||||
}{
|
||||
{"server linux amd64", "server", "linux", "amd64", "tar.gz", "nex-server_1.2.3_linux_amd64.tar.gz"},
|
||||
{"server linux arm64", "server", "linux", "arm64", "tar.gz", "nex-server_1.2.3_linux_arm64.tar.gz"},
|
||||
{"server macos amd64", "server", "macos", "amd64", "tar.gz", "nex-server_1.2.3_macos_amd64.tar.gz"},
|
||||
{"server macos arm64", "server", "macos", "arm64", "tar.gz", "nex-server_1.2.3_macos_arm64.tar.gz"},
|
||||
{"server macos universal", "server", "macos", "universal", "tar.gz", "nex-server_1.2.3_macos_universal.tar.gz"},
|
||||
{"server windows amd64", "server", "windows", "amd64", "zip", "nex-server_1.2.3_windows_amd64.zip"},
|
||||
{"server windows arm64", "server", "windows", "arm64", "zip", "nex-server_1.2.3_windows_arm64.zip"},
|
||||
{"web", "web", "", "", "tar.gz", "nex-web_1.2.3.tar.gz"},
|
||||
{"desktop linux amd64 tar", "desktop", "linux", "amd64", "tar.gz", "nex-desktop_1.2.3_linux_amd64.tar.gz"},
|
||||
{"desktop linux amd64 appimage", "desktop", "linux", "amd64", "AppImage", "nex-desktop_1.2.3_linux_amd64.AppImage"},
|
||||
{"desktop linux amd64 deb", "desktop", "linux", "amd64", "deb", "nex-desktop_1.2.3_linux_amd64.deb"},
|
||||
{"desktop linux amd64 rpm", "desktop", "linux", "amd64", "rpm", "nex-desktop_1.2.3_linux_amd64.rpm"},
|
||||
{"desktop linux arm64 tar", "desktop", "linux", "arm64", "tar.gz", "nex-desktop_1.2.3_linux_arm64.tar.gz"},
|
||||
{"desktop linux arm64 appimage", "desktop", "linux", "arm64", "AppImage", "nex-desktop_1.2.3_linux_arm64.AppImage"},
|
||||
{"desktop linux arm64 deb", "desktop", "linux", "arm64", "deb", "nex-desktop_1.2.3_linux_arm64.deb"},
|
||||
{"desktop linux arm64 rpm", "desktop", "linux", "arm64", "rpm", "nex-desktop_1.2.3_linux_arm64.rpm"},
|
||||
{"desktop macos zip", "desktop", "macos", "universal", "zip", "nex-desktop_1.2.3_macos_universal.zip"},
|
||||
{"desktop macos dmg", "desktop", "macos", "universal", "dmg", "nex-desktop_1.2.3_macos_universal.dmg"},
|
||||
{"desktop windows amd64", "desktop", "windows", "amd64", "zip", "nex-desktop_1.2.3_windows_amd64.zip"},
|
||||
{"desktop windows arm64", "desktop", "windows", "arm64", "zip", "nex-desktop_1.2.3_windows_arm64.zip"},
|
||||
}
|
||||
|
||||
macServer, err := ServerAssetName("1.2.3", "darwin", "arm64")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ReleaseAssetName("1.2.3", tc.component, tc.platform, tc.arch, tc.format)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
macDesktop, err := DesktopAssetName("1.2.3", "macos")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop)
|
||||
invalidCases := []struct {
|
||||
name string
|
||||
component string
|
||||
platform string
|
||||
arch string
|
||||
format string
|
||||
}{
|
||||
{"invalid version", "server", "linux", "amd64", "tar.gz"},
|
||||
{"invalid component", "mobile", "linux", "amd64", "tar.gz"},
|
||||
{"darwin platform", "server", "darwin", "arm64", "tar.gz"},
|
||||
{"server unsupported format", "server", "linux", "amd64", "zip"},
|
||||
{"server unsupported arch", "server", "windows", "universal", "zip"},
|
||||
{"web with platform", "web", "linux", "amd64", "tar.gz"},
|
||||
{"web unsupported format", "web", "", "", "zip"},
|
||||
{"desktop unsupported platform", "desktop", "ios", "arm64", "zip"},
|
||||
{"desktop unsupported format", "desktop", "macos", "universal", "tar.gz"},
|
||||
}
|
||||
|
||||
_, err = DesktopAssetName("1.2.3", "ios")
|
||||
assert.Error(t, err)
|
||||
for _, tc := range invalidCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
version := "1.2.3"
|
||||
if tc.name == "invalid version" {
|
||||
version = "1.2"
|
||||
}
|
||||
_, err := ReleaseAssetName(version, tc.component, tc.platform, tc.arch, tc.format)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopInfoPlist(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user