feat(release): add package manager publishers for S3.2
Add publishers for distributing CLI binaries to package managers: - npm: binary wrapper pattern with postinstall download - Homebrew: formula generation + tap auto-commit - Scoop: JSON manifest + bucket auto-commit - AUR: PKGBUILD + .SRCINFO + AUR push - Chocolatey: NuSpec + install script + optional push Each publisher supports: - Dry-run mode for previewing changes - Auto-commit to own repos (tap/bucket/AUR) - Generate files for PRs to official repos via `official` config Also includes Docker and LinuxKit build helpers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
20f25ca062
commit
513a241c1b
21 changed files with 2678 additions and 11 deletions
|
|
@ -51,8 +51,13 @@ var guiTemplate embed.FS
|
|||
func AddBuildCommand(app *clir.Cli) {
|
||||
buildCmd := app.NewSubCommand("build", "Build projects with auto-detection and cross-compilation")
|
||||
buildCmd.LongDescription("Builds the current project with automatic type detection.\n" +
|
||||
"Supports Go, Wails, Node.js, and PHP projects.\n" +
|
||||
"Configuration can be provided via .core/build.yaml or command-line flags.")
|
||||
"Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.\n" +
|
||||
"Configuration can be provided via .core/build.yaml or command-line flags.\n\n" +
|
||||
"Examples:\n" +
|
||||
" core build # Auto-detect and build\n" +
|
||||
" core build --type docker # Build Docker image\n" +
|
||||
" core build --type linuxkit # Build LinuxKit image\n" +
|
||||
" core build --type linuxkit --config linuxkit.yml --format qcow2-bios")
|
||||
|
||||
// Flags for the main build command
|
||||
var buildType string
|
||||
|
|
@ -62,20 +67,32 @@ func AddBuildCommand(app *clir.Cli) {
|
|||
var doArchive bool
|
||||
var doChecksum bool
|
||||
|
||||
buildCmd.StringFlag("type", "Builder type (go, wails, node, php) - auto-detected if not specified", &buildType)
|
||||
// Docker/LinuxKit specific flags
|
||||
var configPath string
|
||||
var format string
|
||||
var push bool
|
||||
var imageName string
|
||||
|
||||
buildCmd.StringFlag("type", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified", &buildType)
|
||||
buildCmd.BoolFlag("ci", "CI mode - minimal output with JSON artifact list at the end", &ciMode)
|
||||
buildCmd.StringFlag("targets", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)", &targets)
|
||||
buildCmd.StringFlag("output", "Output directory for artifacts (default: dist)", &outputDir)
|
||||
buildCmd.BoolFlag("archive", "Create archives (tar.gz for linux/darwin, zip for windows) - default: true", &doArchive)
|
||||
buildCmd.BoolFlag("checksum", "Generate SHA256 checksums and CHECKSUMS.txt - default: true", &doChecksum)
|
||||
|
||||
// Docker/LinuxKit specific
|
||||
buildCmd.StringFlag("config", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)", &configPath)
|
||||
buildCmd.StringFlag("format", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)", &format)
|
||||
buildCmd.BoolFlag("push", "Push Docker image after build (default: false)", &push)
|
||||
buildCmd.StringFlag("image", "Docker image name (e.g., host-uk/core-devops)", &imageName)
|
||||
|
||||
// Set defaults for archive and checksum (true by default)
|
||||
doArchive = true
|
||||
doChecksum = true
|
||||
|
||||
// Default action for `core build` (no subcommand)
|
||||
buildCmd.Action(func() error {
|
||||
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum)
|
||||
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName)
|
||||
})
|
||||
|
||||
// --- `build from-path` command (legacy PWA/GUI build) ---
|
||||
|
|
@ -102,7 +119,7 @@ func AddBuildCommand(app *clir.Cli) {
|
|||
}
|
||||
|
||||
// runProjectBuild handles the main `core build` command with auto-detection.
|
||||
func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDir string, doArchive bool, doChecksum bool) error {
|
||||
func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDir string, doArchive bool, doChecksum bool, configPath string, format string, push bool, imageName string) error {
|
||||
// Get current working directory as project root
|
||||
projectDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
|
@ -185,6 +202,16 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
|||
Name: binaryName,
|
||||
Version: buildCfg.Project.Name, // Could be enhanced with git describe
|
||||
LDFlags: buildCfg.Build.LDFlags,
|
||||
// Docker/LinuxKit specific
|
||||
Dockerfile: configPath, // Reuse for Dockerfile path
|
||||
LinuxKitConfig: configPath,
|
||||
Push: push,
|
||||
Image: imageName,
|
||||
}
|
||||
|
||||
// Parse formats for LinuxKit
|
||||
if format != "" {
|
||||
cfg.Formats = strings.Split(format, ",")
|
||||
}
|
||||
|
||||
// Execute build
|
||||
|
|
@ -407,6 +434,12 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
|
|||
return builders.NewWailsBuilder(), nil
|
||||
case build.ProjectTypeGo:
|
||||
return builders.NewGoBuilder(), nil
|
||||
case build.ProjectTypeDocker:
|
||||
return builders.NewDockerBuilder(), nil
|
||||
case build.ProjectTypeLinuxKit:
|
||||
return builders.NewLinuxKitBuilder(), nil
|
||||
case build.ProjectTypeTaskfile:
|
||||
return builders.NewTaskfileBuilder(), nil
|
||||
case build.ProjectTypeNode:
|
||||
return nil, fmt.Errorf("Node.js builder not yet implemented")
|
||||
case build.ProjectTypePHP:
|
||||
|
|
@ -631,8 +664,8 @@ func runBuild(fromPath string) error {
|
|||
return fmt.Errorf("failed to anchor template filesystem: %w", err)
|
||||
}
|
||||
sod := gosod.New(templateFS)
|
||||
if sod != nil {
|
||||
return fmt.Errorf("failed to create new sod instance: %w", sod)
|
||||
if sod == nil {
|
||||
return fmt.Errorf("failed to create new sod instance")
|
||||
}
|
||||
|
||||
templateData := map[string]string{"AppName": appName}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ import (
|
|||
type ProjectType string
|
||||
|
||||
const (
|
||||
ProjectTypeGo ProjectType = "go"
|
||||
ProjectTypeWails ProjectType = "wails"
|
||||
ProjectTypeNode ProjectType = "node"
|
||||
ProjectTypePHP ProjectType = "php"
|
||||
ProjectTypeGo ProjectType = "go"
|
||||
ProjectTypeWails ProjectType = "wails"
|
||||
ProjectTypeNode ProjectType = "node"
|
||||
ProjectTypePHP ProjectType = "php"
|
||||
ProjectTypeDocker ProjectType = "docker"
|
||||
ProjectTypeLinuxKit ProjectType = "linuxkit"
|
||||
ProjectTypeTaskfile ProjectType = "taskfile"
|
||||
)
|
||||
|
||||
// Target represents a build target platform.
|
||||
|
|
@ -48,6 +51,18 @@ type Config struct {
|
|||
Version string
|
||||
// LDFlags are additional linker flags.
|
||||
LDFlags []string
|
||||
|
||||
// Docker-specific config
|
||||
Dockerfile string // Path to Dockerfile (default: Dockerfile)
|
||||
Registry string // Container registry (default: ghcr.io)
|
||||
Image string // Image name (owner/repo format)
|
||||
Tags []string // Additional tags to apply
|
||||
BuildArgs map[string]string // Docker build arguments
|
||||
Push bool // Whether to push after build
|
||||
|
||||
// LinuxKit-specific config
|
||||
LinuxKitConfig string // Path to LinuxKit YAML config
|
||||
Formats []string // Output formats (iso, qcow2, raw, vmdk)
|
||||
}
|
||||
|
||||
// Builder defines the interface for project-specific build implementations.
|
||||
|
|
|
|||
214
pkg/build/builders/docker.go
Normal file
214
pkg/build/builders/docker.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// Package builders provides build implementations for different project types.
|
||||
package builders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
// DockerBuilder builds Docker images.
|
||||
type DockerBuilder struct{}
|
||||
|
||||
// NewDockerBuilder creates a new Docker builder.
|
||||
func NewDockerBuilder() *DockerBuilder {
|
||||
return &DockerBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
func (b *DockerBuilder) Name() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Detect checks if a Dockerfile exists in the directory.
|
||||
func (b *DockerBuilder) Detect(dir string) (bool, error) {
|
||||
dockerfilePath := filepath.Join(dir, "Dockerfile")
|
||||
if _, err := os.Stat(dockerfilePath); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Build builds Docker images for the specified targets.
|
||||
func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate docker CLI is available
|
||||
if err := b.validateDockerCli(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure buildx is available
|
||||
if err := b.ensureBuildx(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine Dockerfile path
|
||||
dockerfile := cfg.Dockerfile
|
||||
if dockerfile == "" {
|
||||
dockerfile = filepath.Join(cfg.ProjectDir, "Dockerfile")
|
||||
}
|
||||
|
||||
// Validate Dockerfile exists
|
||||
if _, err := os.Stat(dockerfile); err != nil {
|
||||
return nil, fmt.Errorf("docker.Build: Dockerfile not found: %s", dockerfile)
|
||||
}
|
||||
|
||||
// Determine image name
|
||||
imageName := cfg.Image
|
||||
if imageName == "" {
|
||||
imageName = cfg.Name
|
||||
}
|
||||
if imageName == "" {
|
||||
imageName = filepath.Base(cfg.ProjectDir)
|
||||
}
|
||||
|
||||
// Build platform string from targets
|
||||
var platforms []string
|
||||
for _, t := range targets {
|
||||
platforms = append(platforms, fmt.Sprintf("%s/%s", t.OS, t.Arch))
|
||||
}
|
||||
|
||||
// If no targets specified, use current platform
|
||||
if len(platforms) == 0 {
|
||||
platforms = []string{"linux/amd64"}
|
||||
}
|
||||
|
||||
// Determine registry
|
||||
registry := cfg.Registry
|
||||
if registry == "" {
|
||||
registry = "ghcr.io"
|
||||
}
|
||||
|
||||
// Determine tags
|
||||
tags := cfg.Tags
|
||||
if len(tags) == 0 {
|
||||
tags = []string{"latest"}
|
||||
if cfg.Version != "" {
|
||||
tags = append(tags, cfg.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// Build full image references
|
||||
var imageRefs []string
|
||||
for _, tag := range tags {
|
||||
// Expand version template
|
||||
expandedTag := strings.ReplaceAll(tag, "{{.Version}}", cfg.Version)
|
||||
expandedTag = strings.ReplaceAll(expandedTag, "{{Version}}", cfg.Version)
|
||||
|
||||
if registry != "" {
|
||||
imageRefs = append(imageRefs, fmt.Sprintf("%s/%s:%s", registry, imageName, expandedTag))
|
||||
} else {
|
||||
imageRefs = append(imageRefs, fmt.Sprintf("%s:%s", imageName, expandedTag))
|
||||
}
|
||||
}
|
||||
|
||||
// Build the docker buildx command
|
||||
args := []string{"buildx", "build"}
|
||||
|
||||
// Multi-platform support
|
||||
args = append(args, "--platform", strings.Join(platforms, ","))
|
||||
|
||||
// Add all tags
|
||||
for _, ref := range imageRefs {
|
||||
args = append(args, "-t", ref)
|
||||
}
|
||||
|
||||
// Dockerfile path
|
||||
args = append(args, "-f", dockerfile)
|
||||
|
||||
// Build arguments
|
||||
for k, v := range cfg.BuildArgs {
|
||||
expandedValue := strings.ReplaceAll(v, "{{.Version}}", cfg.Version)
|
||||
expandedValue = strings.ReplaceAll(expandedValue, "{{Version}}", cfg.Version)
|
||||
args = append(args, "--build-arg", fmt.Sprintf("%s=%s", k, expandedValue))
|
||||
}
|
||||
|
||||
// Always add VERSION build arg if version is set
|
||||
if cfg.Version != "" {
|
||||
args = append(args, "--build-arg", fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
}
|
||||
|
||||
// Output to local docker images or push
|
||||
if cfg.Push {
|
||||
args = append(args, "--push")
|
||||
} else {
|
||||
// For multi-platform builds without push, we need to load or output somewhere
|
||||
if len(platforms) == 1 {
|
||||
args = append(args, "--load")
|
||||
} else {
|
||||
// Multi-platform builds can't use --load, output to tarball
|
||||
outputPath := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s.tar", imageName))
|
||||
args = append(args, "--output", fmt.Sprintf("type=oci,dest=%s", outputPath))
|
||||
}
|
||||
}
|
||||
|
||||
// Build context (project directory)
|
||||
args = append(args, cfg.ProjectDir)
|
||||
|
||||
// Create output directory
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("docker.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Execute build
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building Docker image: %s\n", imageName)
|
||||
fmt.Printf(" Platforms: %s\n", strings.Join(platforms, ", "))
|
||||
fmt.Printf(" Tags: %s\n", strings.Join(imageRefs, ", "))
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("docker.Build: buildx build failed: %w", err)
|
||||
}
|
||||
|
||||
// Create artifacts for each platform
|
||||
var artifacts []build.Artifact
|
||||
for _, t := range targets {
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: imageRefs[0], // Primary image reference
|
||||
OS: t.OS,
|
||||
Arch: t.Arch,
|
||||
})
|
||||
}
|
||||
|
||||
return artifacts, nil
|
||||
}
|
||||
|
||||
// validateDockerCli checks if the docker CLI is available.
|
||||
func (b *DockerBuilder) validateDockerCli() error {
|
||||
cmd := exec.Command("docker", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: docker CLI not found. Install it from https://docs.docker.com/get-docker/")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureBuildx ensures docker buildx is available and has a builder.
|
||||
func (b *DockerBuilder) ensureBuildx(ctx context.Context) error {
|
||||
// Check if buildx is available
|
||||
cmd := exec.CommandContext(ctx, "docker", "buildx", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/")
|
||||
}
|
||||
|
||||
// Check if we have a builder, create one if not
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "inspect", "--bootstrap")
|
||||
if err := cmd.Run(); err != nil {
|
||||
// Try to create a builder
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "create", "--use", "--bootstrap")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: failed to create buildx builder: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
248
pkg/build/builders/linuxkit.go
Normal file
248
pkg/build/builders/linuxkit.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
// Package builders provides build implementations for different project types.
|
||||
package builders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
// LinuxKitBuilder builds LinuxKit images.
|
||||
type LinuxKitBuilder struct{}
|
||||
|
||||
// NewLinuxKitBuilder creates a new LinuxKit builder.
|
||||
func NewLinuxKitBuilder() *LinuxKitBuilder {
|
||||
return &LinuxKitBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
func (b *LinuxKitBuilder) Name() string {
|
||||
return "linuxkit"
|
||||
}
|
||||
|
||||
// Detect checks if a linuxkit.yml or .yml config exists in the directory.
|
||||
func (b *LinuxKitBuilder) Detect(dir string) (bool, error) {
|
||||
// Check for linuxkit.yml
|
||||
if _, err := os.Stat(filepath.Join(dir, "linuxkit.yml")); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
// Check for .core/linuxkit/*.yml
|
||||
if matches, _ := filepath.Glob(filepath.Join(dir, ".core", "linuxkit", "*.yml")); len(matches) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Build builds LinuxKit images for the specified targets.
|
||||
func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate linuxkit CLI is available
|
||||
if err := b.validateLinuxKitCli(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine config file path
|
||||
configPath := cfg.LinuxKitConfig
|
||||
if configPath == "" {
|
||||
// Auto-detect
|
||||
if _, err := os.Stat(filepath.Join(cfg.ProjectDir, "linuxkit.yml")); err == nil {
|
||||
configPath = filepath.Join(cfg.ProjectDir, "linuxkit.yml")
|
||||
} else {
|
||||
// Look in .core/linuxkit/
|
||||
matches, _ := filepath.Glob(filepath.Join(cfg.ProjectDir, ".core", "linuxkit", "*.yml"))
|
||||
if len(matches) > 0 {
|
||||
configPath = matches[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return nil, fmt.Errorf("linuxkit.Build: no LinuxKit config file found. Specify with --config or create linuxkit.yml")
|
||||
}
|
||||
|
||||
// Validate config file exists
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return nil, fmt.Errorf("linuxkit.Build: config file not found: %s", configPath)
|
||||
}
|
||||
|
||||
// Determine output formats
|
||||
formats := cfg.Formats
|
||||
if len(formats) == 0 {
|
||||
formats = []string{"qcow2-bios"} // Default to QEMU-compatible format
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
outputDir := cfg.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("linuxkit.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Determine base name from config file or project name
|
||||
baseName := cfg.Name
|
||||
if baseName == "" {
|
||||
baseName = strings.TrimSuffix(filepath.Base(configPath), ".yml")
|
||||
}
|
||||
|
||||
// If no targets, default to linux/amd64
|
||||
if len(targets) == 0 {
|
||||
targets = []build.Target{{OS: "linux", Arch: "amd64"}}
|
||||
}
|
||||
|
||||
var artifacts []build.Artifact
|
||||
|
||||
// Build for each target and format
|
||||
for _, target := range targets {
|
||||
// LinuxKit only supports Linux
|
||||
if target.OS != "linux" {
|
||||
fmt.Printf("Skipping %s/%s (LinuxKit only supports Linux)\n", target.OS, target.Arch)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, target.Arch)
|
||||
|
||||
args := b.buildLinuxKitArgs(configPath, format, outputName, outputDir, target.Arch)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "linuxkit", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building LinuxKit image: %s (%s, %s)\n", outputName, format, target.Arch)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("linuxkit.Build: build failed for %s/%s: %w", target.Arch, format, err)
|
||||
}
|
||||
|
||||
// Determine the actual output file path
|
||||
artifactPath := b.getArtifactPath(outputDir, outputName, format)
|
||||
|
||||
// Verify the artifact was created
|
||||
if _, err := os.Stat(artifactPath); err != nil {
|
||||
// Try alternate naming conventions
|
||||
artifactPath = b.findArtifact(outputDir, outputName, format)
|
||||
if artifactPath == "" {
|
||||
return nil, fmt.Errorf("linuxkit.Build: artifact not found after build: expected %s", b.getArtifactPath(outputDir, outputName, format))
|
||||
}
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: artifactPath,
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts, nil
|
||||
}
|
||||
|
||||
// buildLinuxKitArgs builds the arguments for linuxkit build command.
|
||||
func (b *LinuxKitBuilder) buildLinuxKitArgs(configPath, format, outputName, outputDir, arch string) []string {
|
||||
args := []string{"build"}
|
||||
|
||||
// Output format
|
||||
args = append(args, "-format", format)
|
||||
|
||||
// Output name
|
||||
args = append(args, "-name", outputName)
|
||||
|
||||
// Output directory
|
||||
args = append(args, "-dir", outputDir)
|
||||
|
||||
// Architecture (if not amd64)
|
||||
if arch != "amd64" {
|
||||
args = append(args, "-arch", arch)
|
||||
}
|
||||
|
||||
// Config file
|
||||
args = append(args, configPath)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// getArtifactPath returns the expected path of the built artifact.
|
||||
func (b *LinuxKitBuilder) getArtifactPath(outputDir, outputName, format string) string {
|
||||
ext := b.getFormatExtension(format)
|
||||
return filepath.Join(outputDir, outputName+ext)
|
||||
}
|
||||
|
||||
// findArtifact searches for the built artifact with various naming conventions.
|
||||
func (b *LinuxKitBuilder) findArtifact(outputDir, outputName, format string) string {
|
||||
// LinuxKit can create files with different suffixes
|
||||
extensions := []string{
|
||||
b.getFormatExtension(format),
|
||||
"-bios" + b.getFormatExtension(format),
|
||||
"-efi" + b.getFormatExtension(format),
|
||||
}
|
||||
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(outputDir, outputName+ext)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find any file matching the output name
|
||||
matches, _ := filepath.Glob(filepath.Join(outputDir, outputName+"*"))
|
||||
for _, match := range matches {
|
||||
// Return first match that looks like an image
|
||||
ext := filepath.Ext(match)
|
||||
if ext == ".iso" || ext == ".qcow2" || ext == ".raw" || ext == ".vmdk" || ext == ".vhd" {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getFormatExtension returns the file extension for a LinuxKit output format.
|
||||
func (b *LinuxKitBuilder) getFormatExtension(format string) string {
|
||||
switch format {
|
||||
case "iso", "iso-bios", "iso-efi":
|
||||
return ".iso"
|
||||
case "raw", "raw-bios", "raw-efi":
|
||||
return ".raw"
|
||||
case "qcow2", "qcow2-bios", "qcow2-efi":
|
||||
return ".qcow2"
|
||||
case "vmdk":
|
||||
return ".vmdk"
|
||||
case "vhd":
|
||||
return ".vhd"
|
||||
case "gcp":
|
||||
return ".img.tar.gz"
|
||||
case "aws":
|
||||
return ".raw"
|
||||
default:
|
||||
return "." + strings.TrimSuffix(format, "-bios")
|
||||
}
|
||||
}
|
||||
|
||||
// validateLinuxKitCli checks if the linuxkit CLI is available.
|
||||
func (b *LinuxKitBuilder) validateLinuxKitCli() error {
|
||||
// Check PATH first
|
||||
if _, err := exec.LookPath("linuxkit"); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
paths := []string{
|
||||
"/usr/local/bin/linuxkit",
|
||||
"/opt/homebrew/bin/linuxkit",
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("linuxkit: linuxkit CLI not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit")
|
||||
}
|
||||
233
pkg/build/builders/taskfile.go
Normal file
233
pkg/build/builders/taskfile.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// Package builders provides build implementations for different project types.
|
||||
package builders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
// TaskfileBuilder builds projects using Taskfile (https://taskfile.dev/).
|
||||
// This is a generic builder that can handle any project type that has a Taskfile.
|
||||
type TaskfileBuilder struct{}
|
||||
|
||||
// NewTaskfileBuilder creates a new Taskfile builder.
|
||||
func NewTaskfileBuilder() *TaskfileBuilder {
|
||||
return &TaskfileBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
func (b *TaskfileBuilder) Name() string {
|
||||
return "taskfile"
|
||||
}
|
||||
|
||||
// Detect checks if a Taskfile exists in the directory.
|
||||
func (b *TaskfileBuilder) Detect(dir string) (bool, error) {
|
||||
// Check for Taskfile.yml, Taskfile.yaml, or Taskfile
|
||||
taskfiles := []string{
|
||||
"Taskfile.yml",
|
||||
"Taskfile.yaml",
|
||||
"Taskfile",
|
||||
"taskfile.yml",
|
||||
"taskfile.yaml",
|
||||
}
|
||||
|
||||
for _, tf := range taskfiles {
|
||||
if _, err := os.Stat(filepath.Join(dir, tf)); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Build runs the Taskfile build task for each target platform.
|
||||
func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate task CLI is available
|
||||
if err := b.validateTaskCli(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
outputDir := cfg.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("taskfile.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
var artifacts []build.Artifact
|
||||
|
||||
// If no targets specified, just run the build task once
|
||||
if len(targets) == 0 {
|
||||
if err := b.runTask(ctx, cfg, "", ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to find artifacts in output directory
|
||||
found := b.findArtifacts(outputDir)
|
||||
artifacts = append(artifacts, found...)
|
||||
} else {
|
||||
// Run build task for each target
|
||||
for _, target := range targets {
|
||||
if err := b.runTask(ctx, cfg, target.OS, target.Arch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to find artifacts for this target
|
||||
found := b.findArtifactsForTarget(outputDir, target)
|
||||
artifacts = append(artifacts, found...)
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts, nil
|
||||
}
|
||||
|
||||
// runTask executes the Taskfile build task.
|
||||
func (b *TaskfileBuilder) runTask(ctx context.Context, cfg *build.Config, goos, goarch string) error {
|
||||
// Build task command
|
||||
args := []string{"build"}
|
||||
|
||||
// Pass variables if targets are specified
|
||||
if goos != "" {
|
||||
args = append(args, fmt.Sprintf("GOOS=%s", goos))
|
||||
}
|
||||
if goarch != "" {
|
||||
args = append(args, fmt.Sprintf("GOARCH=%s", goarch))
|
||||
}
|
||||
if cfg.OutputDir != "" {
|
||||
args = append(args, fmt.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir))
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
args = append(args, fmt.Sprintf("NAME=%s", cfg.Name))
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
args = append(args, fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "task", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Set environment variables
|
||||
cmd.Env = os.Environ()
|
||||
if goos != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOOS=%s", goos))
|
||||
}
|
||||
if goarch != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOARCH=%s", goarch))
|
||||
}
|
||||
if cfg.OutputDir != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir))
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("NAME=%s", cfg.Name))
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
}
|
||||
|
||||
if goos != "" && goarch != "" {
|
||||
fmt.Printf("Running task build for %s/%s\n", goos, goarch)
|
||||
} else {
|
||||
fmt.Println("Running task build")
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("taskfile.Build: task build failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findArtifacts searches for built artifacts in the output directory.
|
||||
func (b *TaskfileBuilder) findArtifacts(outputDir string) []build.Artifact {
|
||||
var artifacts []build.Artifact
|
||||
|
||||
entries, err := os.ReadDir(outputDir)
|
||||
if err != nil {
|
||||
return artifacts
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip common non-artifact files
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
|
||||
continue
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: filepath.Join(outputDir, name),
|
||||
OS: "",
|
||||
Arch: "",
|
||||
})
|
||||
}
|
||||
|
||||
return artifacts
|
||||
}
|
||||
|
||||
// findArtifactsForTarget searches for built artifacts for a specific target.
|
||||
func (b *TaskfileBuilder) findArtifactsForTarget(outputDir string, target build.Target) []build.Artifact {
|
||||
var artifacts []build.Artifact
|
||||
|
||||
// Look for files matching the target pattern
|
||||
patterns := []string{
|
||||
fmt.Sprintf("*-%s-%s*", target.OS, target.Arch),
|
||||
fmt.Sprintf("*_%s_%s*", target.OS, target.Arch),
|
||||
fmt.Sprintf("*-%s*", target.Arch),
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(outputDir, pattern))
|
||||
for _, match := range matches {
|
||||
info, err := os.Stat(match)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: match,
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
}
|
||||
|
||||
if len(artifacts) > 0 {
|
||||
break // Found matches, stop looking
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts
|
||||
}
|
||||
|
||||
// validateTaskCli checks if the task CLI is available.
|
||||
func (b *TaskfileBuilder) validateTaskCli() error {
|
||||
// Check PATH first
|
||||
if _, err := exec.LookPath("task"); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
paths := []string{
|
||||
"/usr/local/bin/task",
|
||||
"/opt/homebrew/bin/task",
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("taskfile: task CLI not found. Install with: brew install go-task (macOS), go install github.com/go-task/task/v3/cmd/task@latest, or see https://taskfile.dev/installation/")
|
||||
}
|
||||
|
|
@ -83,6 +83,42 @@ type PublisherConfig struct {
|
|||
Tags []string `yaml:"tags,omitempty"`
|
||||
// BuildArgs are additional Docker build arguments.
|
||||
BuildArgs map[string]string `yaml:"build_args,omitempty"`
|
||||
|
||||
// npm-specific configuration
|
||||
// Package is the npm package name (e.g., "@host-uk/core").
|
||||
Package string `yaml:"package,omitempty"`
|
||||
// Access is the npm access level: "public" or "restricted".
|
||||
Access string `yaml:"access,omitempty"`
|
||||
|
||||
// Homebrew-specific configuration
|
||||
// Tap is the Homebrew tap repository (e.g., "host-uk/homebrew-tap").
|
||||
Tap string `yaml:"tap,omitempty"`
|
||||
// Formula is the formula name (defaults to project name).
|
||||
Formula string `yaml:"formula,omitempty"`
|
||||
|
||||
// Scoop-specific configuration
|
||||
// Bucket is the Scoop bucket repository (e.g., "host-uk/scoop-bucket").
|
||||
Bucket string `yaml:"bucket,omitempty"`
|
||||
|
||||
// AUR-specific configuration
|
||||
// Maintainer is the AUR package maintainer (e.g., "Name <email>").
|
||||
Maintainer string `yaml:"maintainer,omitempty"`
|
||||
|
||||
// Chocolatey-specific configuration
|
||||
// Push determines whether to push to Chocolatey (false = generate only).
|
||||
Push bool `yaml:"push,omitempty"`
|
||||
|
||||
// Official repo configuration (for Homebrew, Scoop)
|
||||
// When enabled, generates files for PR to official repos.
|
||||
Official *OfficialConfig `yaml:"official,omitempty"`
|
||||
}
|
||||
|
||||
// OfficialConfig holds configuration for generating files for official repo PRs.
|
||||
type OfficialConfig struct {
|
||||
// Enabled determines whether to generate files for official repos.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Output is the directory to write generated files.
|
||||
Output string `yaml:"output,omitempty"`
|
||||
}
|
||||
|
||||
// ChangelogConfig holds changelog generation settings.
|
||||
|
|
|
|||
297
pkg/release/publishers/aur.go
Normal file
297
pkg/release/publishers/aur.go
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
//go:embed templates/aur/*.tmpl
|
||||
var aurTemplates embed.FS
|
||||
|
||||
// AURConfig holds AUR-specific configuration.
|
||||
type AURConfig struct {
|
||||
// Package is the AUR package name.
|
||||
Package string
|
||||
// Maintainer is the package maintainer (e.g., "Name <email>").
|
||||
Maintainer string
|
||||
// Official config for generating files for official repo PRs.
|
||||
Official *OfficialConfig
|
||||
}
|
||||
|
||||
// AURPublisher publishes releases to AUR.
|
||||
type AURPublisher struct{}
|
||||
|
||||
// NewAURPublisher creates a new AUR publisher.
|
||||
func NewAURPublisher() *AURPublisher {
|
||||
return &AURPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *AURPublisher) Name() string {
|
||||
return "aur"
|
||||
}
|
||||
|
||||
// Publish publishes the release to AUR.
|
||||
func (p *AURPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
if cfg.Maintainer == "" {
|
||||
return fmt.Errorf("aur.Publish: maintainer is required (set publish.aur.maintainer in config)")
|
||||
}
|
||||
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
projectName := ""
|
||||
if relCfg != nil {
|
||||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
packageName := cfg.Package
|
||||
if packageName == "" {
|
||||
packageName = projectName
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
data := aurTemplateData{
|
||||
PackageName: packageName,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
BinaryName: projectName,
|
||||
Maintainer: cfg.Maintainer,
|
||||
Checksums: checksums,
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(data, cfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release.ProjectDir, data, cfg)
|
||||
}
|
||||
|
||||
type aurTemplateData struct {
|
||||
PackageName string
|
||||
Description string
|
||||
Repository string
|
||||
Version string
|
||||
License string
|
||||
BinaryName string
|
||||
Maintainer string
|
||||
Checksums ChecksumMap
|
||||
}
|
||||
|
||||
func (p *AURPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig) AURConfig {
|
||||
cfg := AURConfig{}
|
||||
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if pkg, ok := ext["package"].(string); ok && pkg != "" {
|
||||
cfg.Package = pkg
|
||||
}
|
||||
if maintainer, ok := ext["maintainer"].(string); ok && maintainer != "" {
|
||||
cfg.Maintainer = maintainer
|
||||
}
|
||||
if official, ok := ext["official"].(map[string]any); ok {
|
||||
cfg.Official = &OfficialConfig{}
|
||||
if enabled, ok := official["enabled"].(bool); ok {
|
||||
cfg.Official.Enabled = enabled
|
||||
}
|
||||
if output, ok := official["output"].(string); ok {
|
||||
cfg.Official.Output = output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *AURPublisher) dryRunPublish(data aurTemplateData, cfg AURConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: AUR Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s-bin\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Maintainer: %s\n", data.Maintainer)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
|
||||
pkgbuild, err := p.renderTemplate("templates/aur/PKGBUILD.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated PKGBUILD:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(pkgbuild)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
srcinfo, err := p.renderTemplate("templates/aur/.SRCINFO.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated .SRCINFO:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(srcinfo)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Printf("Would push to AUR: ssh://aur@aur.archlinux.org/%s-bin.git\n", data.PackageName)
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AURPublisher) executePublish(ctx context.Context, projectDir string, data aurTemplateData, cfg AURConfig) error {
|
||||
pkgbuild, err := p.renderTemplate("templates/aur/PKGBUILD.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to render PKGBUILD: %w", err)
|
||||
}
|
||||
|
||||
srcinfo, err := p.renderTemplate("templates/aur/.SRCINFO.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to render .SRCINFO: %w", err)
|
||||
}
|
||||
|
||||
// If official config is enabled, write to output directory
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "aur")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(output, 0755); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
pkgbuildPath := filepath.Join(output, "PKGBUILD")
|
||||
if err := os.WriteFile(pkgbuildPath, []byte(pkgbuild), 0644); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to write PKGBUILD: %w", err)
|
||||
}
|
||||
|
||||
srcinfoPath := filepath.Join(output, ".SRCINFO")
|
||||
if err := os.WriteFile(srcinfoPath, []byte(srcinfo), 0644); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to write .SRCINFO: %w", err)
|
||||
}
|
||||
fmt.Printf("Wrote AUR files: %s\n", output)
|
||||
}
|
||||
|
||||
// Push to AUR if not in official-only mode
|
||||
if cfg.Official == nil || !cfg.Official.Enabled {
|
||||
if err := p.pushToAUR(ctx, data, pkgbuild, srcinfo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AURPublisher) pushToAUR(ctx context.Context, data aurTemplateData, pkgbuild, srcinfo string) error {
|
||||
aurURL := fmt.Sprintf("ssh://aur@aur.archlinux.org/%s-bin.git", data.PackageName)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "aur-package-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Clone existing AUR repo (or initialize new one)
|
||||
fmt.Printf("Cloning AUR package %s-bin...\n", data.PackageName)
|
||||
cmd := exec.CommandContext(ctx, "git", "clone", aurURL, tmpDir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
// If clone fails, init a new repo
|
||||
cmd = exec.CommandContext(ctx, "git", "init", tmpDir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to initialize repo: %w", err)
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, "git", "-C", tmpDir, "remote", "add", "origin", aurURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to add remote: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write files
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "PKGBUILD"), []byte(pkgbuild), 0644); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to write PKGBUILD: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, ".SRCINFO"), []byte(srcinfo), 0644); err != nil {
|
||||
return fmt.Errorf("aur.Publish: failed to write .SRCINFO: %w", err)
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("Update to %s", data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur.Publish: git add failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur.Publish: git commit failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push", "origin", "master")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur.Publish: git push failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published to AUR: https://aur.archlinux.org/packages/%s-bin\n", data.PackageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AURPublisher) renderTemplate(name string, data aurTemplateData) (string, error) {
|
||||
content, err := aurTemplates.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", name, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Ensure build package is used
|
||||
var _ = build.Artifact{}
|
||||
277
pkg/release/publishers/chocolatey.go
Normal file
277
pkg/release/publishers/chocolatey.go
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
//go:embed templates/chocolatey/*.tmpl templates/chocolatey/tools/*.tmpl
|
||||
var chocolateyTemplates embed.FS
|
||||
|
||||
// ChocolateyConfig holds Chocolatey-specific configuration.
|
||||
type ChocolateyConfig struct {
|
||||
// Package is the Chocolatey package name.
|
||||
Package string
|
||||
// Push determines whether to push to Chocolatey (false = generate only).
|
||||
Push bool
|
||||
// Official config for generating files for official repo PRs.
|
||||
Official *OfficialConfig
|
||||
}
|
||||
|
||||
// ChocolateyPublisher publishes releases to Chocolatey.
|
||||
type ChocolateyPublisher struct{}
|
||||
|
||||
// NewChocolateyPublisher creates a new Chocolatey publisher.
|
||||
func NewChocolateyPublisher() *ChocolateyPublisher {
|
||||
return &ChocolateyPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *ChocolateyPublisher) Name() string {
|
||||
return "chocolatey"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Chocolatey.
|
||||
func (p *ChocolateyPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
projectName := ""
|
||||
if relCfg != nil {
|
||||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
packageName := cfg.Package
|
||||
if packageName == "" {
|
||||
packageName = projectName
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
// Extract authors from repository
|
||||
authors := strings.Split(repo, "/")[0]
|
||||
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: packageName,
|
||||
Title: fmt.Sprintf("%s CLI", strings.Title(projectName)),
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
BinaryName: projectName,
|
||||
Authors: authors,
|
||||
Tags: fmt.Sprintf("cli %s", projectName),
|
||||
Checksums: checksums,
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(data, cfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release.ProjectDir, data, cfg)
|
||||
}
|
||||
|
||||
type chocolateyTemplateData struct {
|
||||
PackageName string
|
||||
Title string
|
||||
Description string
|
||||
Repository string
|
||||
Version string
|
||||
License string
|
||||
BinaryName string
|
||||
Authors string
|
||||
Tags string
|
||||
Checksums ChecksumMap
|
||||
}
|
||||
|
||||
func (p *ChocolateyPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig) ChocolateyConfig {
|
||||
cfg := ChocolateyConfig{
|
||||
Push: false, // Default to generate only
|
||||
}
|
||||
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if pkg, ok := ext["package"].(string); ok && pkg != "" {
|
||||
cfg.Package = pkg
|
||||
}
|
||||
if push, ok := ext["push"].(bool); ok {
|
||||
cfg.Push = push
|
||||
}
|
||||
if official, ok := ext["official"].(map[string]any); ok {
|
||||
cfg.Official = &OfficialConfig{}
|
||||
if enabled, ok := official["enabled"].(bool); ok {
|
||||
cfg.Official.Enabled = enabled
|
||||
}
|
||||
if output, ok := official["output"].(string); ok {
|
||||
cfg.Official.Output = output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *ChocolateyPublisher) dryRunPublish(data chocolateyTemplateData, cfg ChocolateyConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Chocolatey Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Push: %t\n", cfg.Push)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
|
||||
nuspec, err := p.renderTemplate("templates/chocolatey/package.nuspec.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chocolatey.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated package.nuspec:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(nuspec)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
install, err := p.renderTemplate("templates/chocolatey/tools/chocolateyinstall.ps1.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chocolatey.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated chocolateyinstall.ps1:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(install)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
if cfg.Push {
|
||||
fmt.Println("Would push to Chocolatey community repo")
|
||||
} else {
|
||||
fmt.Println("Would generate package files only (push=false)")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ChocolateyPublisher) executePublish(ctx context.Context, projectDir string, data chocolateyTemplateData, cfg ChocolateyConfig) error {
|
||||
nuspec, err := p.renderTemplate("templates/chocolatey/package.nuspec.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: failed to render nuspec: %w", err)
|
||||
}
|
||||
|
||||
install, err := p.renderTemplate("templates/chocolatey/tools/chocolateyinstall.ps1.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: failed to render install script: %w", err)
|
||||
}
|
||||
|
||||
// Create package directory
|
||||
output := filepath.Join(projectDir, "dist", "chocolatey")
|
||||
if cfg.Official != nil && cfg.Official.Enabled && cfg.Official.Output != "" {
|
||||
output = cfg.Official.Output
|
||||
if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
}
|
||||
}
|
||||
|
||||
toolsDir := filepath.Join(output, "tools")
|
||||
if err := os.MkdirAll(toolsDir, 0755); err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Write files
|
||||
nuspecPath := filepath.Join(output, fmt.Sprintf("%s.nuspec", data.PackageName))
|
||||
if err := os.WriteFile(nuspecPath, []byte(nuspec), 0644); err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: failed to write nuspec: %w", err)
|
||||
}
|
||||
|
||||
installPath := filepath.Join(toolsDir, "chocolateyinstall.ps1")
|
||||
if err := os.WriteFile(installPath, []byte(install), 0644); err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: failed to write install script: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Wrote Chocolatey package files: %s\n", output)
|
||||
|
||||
// Push to Chocolatey if configured
|
||||
if cfg.Push {
|
||||
if err := p.pushToChocolatey(ctx, output, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ChocolateyPublisher) pushToChocolatey(ctx context.Context, packageDir string, data chocolateyTemplateData) error {
|
||||
// Check for CHOCOLATEY_API_KEY
|
||||
apiKey := os.Getenv("CHOCOLATEY_API_KEY")
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("chocolatey.Publish: CHOCOLATEY_API_KEY environment variable is required for push")
|
||||
}
|
||||
|
||||
// Pack the package
|
||||
nupkgPath := filepath.Join(packageDir, fmt.Sprintf("%s.%s.nupkg", data.PackageName, data.Version))
|
||||
|
||||
cmd := exec.CommandContext(ctx, "choco", "pack", filepath.Join(packageDir, fmt.Sprintf("%s.nuspec", data.PackageName)), "-OutputDirectory", packageDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: choco pack failed: %w", err)
|
||||
}
|
||||
|
||||
// Push the package
|
||||
cmd = exec.CommandContext(ctx, "choco", "push", nupkgPath, "--source", "https://push.chocolatey.org/", "--api-key", apiKey)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("chocolatey.Publish: choco push failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published to Chocolatey: https://community.chocolatey.org/packages/%s\n", data.PackageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ChocolateyPublisher) renderTemplate(name string, data chocolateyTemplateData) (string, error) {
|
||||
content, err := chocolateyTemplates.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", name, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Ensure build package is used
|
||||
var _ = build.Artifact{}
|
||||
355
pkg/release/publishers/homebrew.go
Normal file
355
pkg/release/publishers/homebrew.go
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
//go:embed templates/homebrew/*.tmpl
|
||||
var homebrewTemplates embed.FS
|
||||
|
||||
// HomebrewConfig holds Homebrew-specific configuration.
|
||||
type HomebrewConfig struct {
|
||||
// Tap is the Homebrew tap repository (e.g., "host-uk/homebrew-tap").
|
||||
Tap string
|
||||
// Formula is the formula name (defaults to project name).
|
||||
Formula string
|
||||
// Official config for generating files for official repo PRs.
|
||||
Official *OfficialConfig
|
||||
}
|
||||
|
||||
// OfficialConfig holds configuration for generating files for official repo PRs.
|
||||
type OfficialConfig struct {
|
||||
// Enabled determines whether to generate files for official repos.
|
||||
Enabled bool
|
||||
// Output is the directory to write generated files.
|
||||
Output string
|
||||
}
|
||||
|
||||
// HomebrewPublisher publishes releases to Homebrew.
|
||||
type HomebrewPublisher struct{}
|
||||
|
||||
// NewHomebrewPublisher creates a new Homebrew publisher.
|
||||
func NewHomebrewPublisher() *HomebrewPublisher {
|
||||
return &HomebrewPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *HomebrewPublisher) Name() string {
|
||||
return "homebrew"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Homebrew.
|
||||
func (p *HomebrewPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Parse config
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
// Validate configuration
|
||||
if cfg.Tap == "" && (cfg.Official == nil || !cfg.Official.Enabled) {
|
||||
return fmt.Errorf("homebrew.Publish: tap is required (set publish.homebrew.tap in config)")
|
||||
}
|
||||
|
||||
// Get repository and project info
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
projectName := ""
|
||||
if relCfg != nil {
|
||||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
formulaName := cfg.Formula
|
||||
if formulaName == "" {
|
||||
formulaName = projectName
|
||||
}
|
||||
|
||||
// Strip leading 'v' from version
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
|
||||
// Build checksums map from artifacts
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
// Template data
|
||||
data := homebrewTemplateData{
|
||||
FormulaClass: toFormulaClass(formulaName),
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
BinaryName: projectName,
|
||||
Checksums: checksums,
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(data, cfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release.ProjectDir, data, cfg)
|
||||
}
|
||||
|
||||
// homebrewTemplateData holds data for Homebrew templates.
|
||||
type homebrewTemplateData struct {
|
||||
FormulaClass string
|
||||
Description string
|
||||
Repository string
|
||||
Version string
|
||||
License string
|
||||
BinaryName string
|
||||
Checksums ChecksumMap
|
||||
}
|
||||
|
||||
// ChecksumMap holds checksums for different platform/arch combinations.
|
||||
type ChecksumMap struct {
|
||||
DarwinAmd64 string
|
||||
DarwinArm64 string
|
||||
LinuxAmd64 string
|
||||
LinuxArm64 string
|
||||
WindowsAmd64 string
|
||||
WindowsArm64 string
|
||||
}
|
||||
|
||||
// parseConfig extracts Homebrew-specific configuration.
|
||||
func (p *HomebrewPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig) HomebrewConfig {
|
||||
cfg := HomebrewConfig{
|
||||
Tap: "",
|
||||
Formula: "",
|
||||
}
|
||||
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if tap, ok := ext["tap"].(string); ok && tap != "" {
|
||||
cfg.Tap = tap
|
||||
}
|
||||
if formula, ok := ext["formula"].(string); ok && formula != "" {
|
||||
cfg.Formula = formula
|
||||
}
|
||||
if official, ok := ext["official"].(map[string]any); ok {
|
||||
cfg.Official = &OfficialConfig{}
|
||||
if enabled, ok := official["enabled"].(bool); ok {
|
||||
cfg.Official.Enabled = enabled
|
||||
}
|
||||
if output, ok := official["output"].(string); ok {
|
||||
cfg.Official.Output = output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// dryRunPublish shows what would be done.
|
||||
func (p *HomebrewPublisher) dryRunPublish(data homebrewTemplateData, cfg HomebrewConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Homebrew Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Formula: %s\n", data.FormulaClass)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Tap: %s\n", cfg.Tap)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
|
||||
// Generate and show formula
|
||||
formula, err := p.renderTemplate("templates/homebrew/formula.rb.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("homebrew.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated formula.rb:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(formula)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
if cfg.Tap != "" {
|
||||
fmt.Printf("Would commit to tap: %s\n", cfg.Tap)
|
||||
}
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = "dist/homebrew"
|
||||
}
|
||||
fmt.Printf("Would write files for official PR to: %s\n", output)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePublish creates the formula and commits to tap.
|
||||
func (p *HomebrewPublisher) executePublish(ctx context.Context, projectDir string, data homebrewTemplateData, cfg HomebrewConfig) error {
|
||||
// Generate formula
|
||||
formula, err := p.renderTemplate("templates/homebrew/formula.rb.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to render formula: %w", err)
|
||||
}
|
||||
|
||||
// If official config is enabled, write to output directory
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "homebrew")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(output, 0755); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
formulaPath := filepath.Join(output, fmt.Sprintf("%s.rb", strings.ToLower(data.FormulaClass)))
|
||||
if err := os.WriteFile(formulaPath, []byte(formula), 0644); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to write formula: %w", err)
|
||||
}
|
||||
fmt.Printf("Wrote Homebrew formula for official PR: %s\n", formulaPath)
|
||||
}
|
||||
|
||||
// If tap is configured, commit to it
|
||||
if cfg.Tap != "" {
|
||||
if err := p.commitToTap(ctx, cfg.Tap, data, formula); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitToTap commits the formula to the tap repository.
|
||||
func (p *HomebrewPublisher) commitToTap(ctx context.Context, tap string, data homebrewTemplateData, formula string) error {
|
||||
// Clone tap repo to temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "homebrew-tap-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Clone the tap
|
||||
fmt.Printf("Cloning tap %s...\n", tap)
|
||||
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", tap, tmpDir, "--", "--depth=1")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to clone tap: %w", err)
|
||||
}
|
||||
|
||||
// Ensure Formula directory exists
|
||||
formulaDir := filepath.Join(tmpDir, "Formula")
|
||||
if err := os.MkdirAll(formulaDir, 0755); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to create Formula directory: %w", err)
|
||||
}
|
||||
|
||||
// Write formula
|
||||
formulaPath := filepath.Join(formulaDir, fmt.Sprintf("%s.rb", strings.ToLower(data.FormulaClass)))
|
||||
if err := os.WriteFile(formulaPath, []byte(formula), 0644); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: failed to write formula: %w", err)
|
||||
}
|
||||
|
||||
// Git add, commit, push
|
||||
commitMsg := fmt.Sprintf("Update %s to %s", data.FormulaClass, data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: git add failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: git commit failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("homebrew.Publish: git push failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated Homebrew tap: %s\n", tap)
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderTemplate renders an embedded template with the given data.
|
||||
func (p *HomebrewPublisher) renderTemplate(name string, data homebrewTemplateData) (string, error) {
|
||||
content, err := homebrewTemplates.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", name, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// toFormulaClass converts a package name to a Ruby class name.
|
||||
func toFormulaClass(name string) string {
|
||||
// Convert kebab-case to PascalCase
|
||||
parts := strings.Split(name, "-")
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// buildChecksumMap extracts checksums from artifacts into a structured map.
|
||||
func buildChecksumMap(artifacts []build.Artifact) ChecksumMap {
|
||||
checksums := ChecksumMap{}
|
||||
|
||||
for _, a := range artifacts {
|
||||
// Parse artifact name to determine platform
|
||||
name := filepath.Base(a.Path)
|
||||
checksum := a.Checksum
|
||||
|
||||
switch {
|
||||
case strings.Contains(name, "darwin-amd64"):
|
||||
checksums.DarwinAmd64 = checksum
|
||||
case strings.Contains(name, "darwin-arm64"):
|
||||
checksums.DarwinArm64 = checksum
|
||||
case strings.Contains(name, "linux-amd64"):
|
||||
checksums.LinuxAmd64 = checksum
|
||||
case strings.Contains(name, "linux-arm64"):
|
||||
checksums.LinuxArm64 = checksum
|
||||
case strings.Contains(name, "windows-amd64"):
|
||||
checksums.WindowsAmd64 = checksum
|
||||
case strings.Contains(name, "windows-arm64"):
|
||||
checksums.WindowsArm64 = checksum
|
||||
}
|
||||
}
|
||||
|
||||
return checksums
|
||||
}
|
||||
248
pkg/release/publishers/npm.go
Normal file
248
pkg/release/publishers/npm.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed templates/npm/*.tmpl
|
||||
var npmTemplates embed.FS
|
||||
|
||||
// NpmConfig holds npm-specific configuration.
|
||||
type NpmConfig struct {
|
||||
// Package is the npm package name (e.g., "@host-uk/core").
|
||||
Package string
|
||||
// Access is the npm access level: "public" or "restricted".
|
||||
Access string
|
||||
}
|
||||
|
||||
// NpmPublisher publishes releases to npm using the binary wrapper pattern.
|
||||
type NpmPublisher struct{}
|
||||
|
||||
// NewNpmPublisher creates a new npm publisher.
|
||||
func NewNpmPublisher() *NpmPublisher {
|
||||
return &NpmPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *NpmPublisher) Name() string {
|
||||
return "npm"
|
||||
}
|
||||
|
||||
// Publish publishes the release to npm.
|
||||
// It generates a binary wrapper package that downloads the correct platform binary on postinstall.
|
||||
func (p *NpmPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Parse npm config
|
||||
npmCfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
// Validate configuration
|
||||
if npmCfg.Package == "" {
|
||||
return fmt.Errorf("npm.Publish: package name is required (set publish.npm.package in config)")
|
||||
}
|
||||
|
||||
// Get repository
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
// Get project name (binary name)
|
||||
projectName := ""
|
||||
if relCfg != nil {
|
||||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
// Try to infer from package name
|
||||
parts := strings.Split(npmCfg.Package, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// Strip leading 'v' from version for npm
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
|
||||
// Template data
|
||||
data := npmTemplateData{
|
||||
Package: npmCfg.Package,
|
||||
Version: version,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
License: "MIT",
|
||||
Repository: repo,
|
||||
BinaryName: projectName,
|
||||
ProjectName: projectName,
|
||||
Access: npmCfg.Access,
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(data, &npmCfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, data, &npmCfg)
|
||||
}
|
||||
|
||||
// parseConfig extracts npm-specific configuration from the publisher config.
|
||||
func (p *NpmPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig) NpmConfig {
|
||||
cfg := NpmConfig{
|
||||
Package: "",
|
||||
Access: "public",
|
||||
}
|
||||
|
||||
// Override from extended config if present
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if pkg, ok := ext["package"].(string); ok && pkg != "" {
|
||||
cfg.Package = pkg
|
||||
}
|
||||
if access, ok := ext["access"].(string); ok && access != "" {
|
||||
cfg.Access = access
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// npmTemplateData holds data for npm templates.
|
||||
type npmTemplateData struct {
|
||||
Package string
|
||||
Version string
|
||||
Description string
|
||||
License string
|
||||
Repository string
|
||||
BinaryName string
|
||||
ProjectName string
|
||||
Access string
|
||||
}
|
||||
|
||||
// dryRunPublish shows what would be done without actually publishing.
|
||||
func (p *NpmPublisher) dryRunPublish(data npmTemplateData, cfg *NpmConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: npm Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.Package)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Access: %s\n", data.Access)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Printf("Binary: %s\n", data.BinaryName)
|
||||
fmt.Println()
|
||||
|
||||
// Generate and show package.json
|
||||
pkgJSON, err := p.renderTemplate("templates/npm/package.json.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated package.json:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(pkgJSON)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Would run: npm publish --access", data.Access)
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePublish actually creates and publishes the npm package.
|
||||
func (p *NpmPublisher) executePublish(ctx context.Context, data npmTemplateData, cfg *NpmConfig) error {
|
||||
// Check for NPM_TOKEN
|
||||
if os.Getenv("NPM_TOKEN") == "" {
|
||||
return fmt.Errorf("npm.Publish: NPM_TOKEN environment variable is required")
|
||||
}
|
||||
|
||||
// Create temp directory for package
|
||||
tmpDir, err := os.MkdirTemp("", "npm-publish-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create bin directory
|
||||
binDir := filepath.Join(tmpDir, "bin")
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to create bin directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate package.json
|
||||
pkgJSON, err := p.renderTemplate("templates/npm/package.json.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to render package.json: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(pkgJSON), 0644); err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to write package.json: %w", err)
|
||||
}
|
||||
|
||||
// Generate install.js
|
||||
installJS, err := p.renderTemplate("templates/npm/install.js.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to render install.js: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "install.js"), []byte(installJS), 0644); err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to write install.js: %w", err)
|
||||
}
|
||||
|
||||
// Generate run.js
|
||||
runJS, err := p.renderTemplate("templates/npm/run.js.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to render run.js: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(binDir, "run.js"), []byte(runJS), 0755); err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to write run.js: %w", err)
|
||||
}
|
||||
|
||||
// Create .npmrc with token
|
||||
npmrc := fmt.Sprintf("//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n")
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, ".npmrc"), []byte(npmrc), 0600); err != nil {
|
||||
return fmt.Errorf("npm.Publish: failed to write .npmrc: %w", err)
|
||||
}
|
||||
|
||||
// Run npm publish
|
||||
cmd := exec.CommandContext(ctx, "npm", "publish", "--access", data.Access)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "NPM_TOKEN="+os.Getenv("NPM_TOKEN"))
|
||||
|
||||
fmt.Printf("Publishing %s@%s to npm...\n", data.Package, data.Version)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("npm.Publish: npm publish failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published %s@%s to npm\n", data.Package, data.Version)
|
||||
fmt.Printf(" https://www.npmjs.com/package/%s\n", data.Package)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderTemplate renders an embedded template with the given data.
|
||||
func (p *NpmPublisher) renderTemplate(name string, data npmTemplateData) (string, error) {
|
||||
content, err := npmTemplates.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", name, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
268
pkg/release/publishers/scoop.go
Normal file
268
pkg/release/publishers/scoop.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
)
|
||||
|
||||
//go:embed templates/scoop/*.tmpl
|
||||
var scoopTemplates embed.FS
|
||||
|
||||
// ScoopConfig holds Scoop-specific configuration.
|
||||
type ScoopConfig struct {
|
||||
// Bucket is the Scoop bucket repository (e.g., "host-uk/scoop-bucket").
|
||||
Bucket string
|
||||
// Official config for generating files for official repo PRs.
|
||||
Official *OfficialConfig
|
||||
}
|
||||
|
||||
// ScoopPublisher publishes releases to Scoop.
|
||||
type ScoopPublisher struct{}
|
||||
|
||||
// NewScoopPublisher creates a new Scoop publisher.
|
||||
func NewScoopPublisher() *ScoopPublisher {
|
||||
return &ScoopPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *ScoopPublisher) Name() string {
|
||||
return "scoop"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Scoop.
|
||||
func (p *ScoopPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
if cfg.Bucket == "" && (cfg.Official == nil || !cfg.Official.Enabled) {
|
||||
return fmt.Errorf("scoop.Publish: bucket is required (set publish.scoop.bucket in config)")
|
||||
}
|
||||
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scoop.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
projectName := ""
|
||||
if relCfg != nil {
|
||||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
data := scoopTemplateData{
|
||||
PackageName: projectName,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
BinaryName: projectName,
|
||||
Checksums: checksums,
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(data, cfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release.ProjectDir, data, cfg)
|
||||
}
|
||||
|
||||
type scoopTemplateData struct {
|
||||
PackageName string
|
||||
Description string
|
||||
Repository string
|
||||
Version string
|
||||
License string
|
||||
BinaryName string
|
||||
Checksums ChecksumMap
|
||||
}
|
||||
|
||||
func (p *ScoopPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig) ScoopConfig {
|
||||
cfg := ScoopConfig{}
|
||||
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if bucket, ok := ext["bucket"].(string); ok && bucket != "" {
|
||||
cfg.Bucket = bucket
|
||||
}
|
||||
if official, ok := ext["official"].(map[string]any); ok {
|
||||
cfg.Official = &OfficialConfig{}
|
||||
if enabled, ok := official["enabled"].(bool); ok {
|
||||
cfg.Official.Enabled = enabled
|
||||
}
|
||||
if output, ok := official["output"].(string); ok {
|
||||
cfg.Official.Output = output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *ScoopPublisher) dryRunPublish(data scoopTemplateData, cfg ScoopConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Scoop Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Bucket: %s\n", cfg.Bucket)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
|
||||
manifest, err := p.renderTemplate("templates/scoop/manifest.json.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scoop.dryRunPublish: %w", err)
|
||||
}
|
||||
fmt.Println("Generated manifest.json:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(manifest)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
|
||||
if cfg.Bucket != "" {
|
||||
fmt.Printf("Would commit to bucket: %s\n", cfg.Bucket)
|
||||
}
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = "dist/scoop"
|
||||
}
|
||||
fmt.Printf("Would write files for official PR to: %s\n", output)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ScoopPublisher) executePublish(ctx context.Context, projectDir string, data scoopTemplateData, cfg ScoopConfig) error {
|
||||
manifest, err := p.renderTemplate("templates/scoop/manifest.json.tmpl", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to render manifest: %w", err)
|
||||
}
|
||||
|
||||
// If official config is enabled, write to output directory
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "scoop")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(output, 0755); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(output, fmt.Sprintf("%s.json", data.PackageName))
|
||||
if err := os.WriteFile(manifestPath, []byte(manifest), 0644); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to write manifest: %w", err)
|
||||
}
|
||||
fmt.Printf("Wrote Scoop manifest for official PR: %s\n", manifestPath)
|
||||
}
|
||||
|
||||
// If bucket is configured, commit to it
|
||||
if cfg.Bucket != "" {
|
||||
if err := p.commitToBucket(ctx, cfg.Bucket, data, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ScoopPublisher) commitToBucket(ctx context.Context, bucket string, data scoopTemplateData, manifest string) error {
|
||||
tmpDir, err := os.MkdirTemp("", "scoop-bucket-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
fmt.Printf("Cloning bucket %s...\n", bucket)
|
||||
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", bucket, tmpDir, "--", "--depth=1")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to clone bucket: %w", err)
|
||||
}
|
||||
|
||||
// Ensure bucket directory exists
|
||||
bucketDir := filepath.Join(tmpDir, "bucket")
|
||||
if _, err := os.Stat(bucketDir); os.IsNotExist(err) {
|
||||
bucketDir = tmpDir // Some repos put manifests in root
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(bucketDir, fmt.Sprintf("%s.json", data.PackageName))
|
||||
if err := os.WriteFile(manifestPath, []byte(manifest), 0644); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: failed to write manifest: %w", err)
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("Update %s to %s", data.PackageName, data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: git add failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: git commit failed: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("scoop.Publish: git push failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated Scoop bucket: %s\n", bucket)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ScoopPublisher) renderTemplate(name string, data scoopTemplateData) (string, error) {
|
||||
content, err := scoopTemplates.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", name, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Ensure build package is used
|
||||
var _ = build.Artifact{}
|
||||
16
pkg/release/publishers/templates/aur/.SRCINFO.tmpl
Normal file
16
pkg/release/publishers/templates/aur/.SRCINFO.tmpl
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
pkgbase = {{.PackageName}}-bin
|
||||
pkgdesc = {{.Description}}
|
||||
pkgver = {{.Version}}
|
||||
pkgrel = 1
|
||||
url = https://github.com/{{.Repository}}
|
||||
arch = x86_64
|
||||
arch = aarch64
|
||||
license = {{.License}}
|
||||
provides = {{.PackageName}}
|
||||
conflicts = {{.PackageName}}
|
||||
source_x86_64 = {{.PackageName}}-bin-{{.Version}}-x86_64.tar.gz::https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-linux-amd64.tar.gz
|
||||
sha256sums_x86_64 = {{.Checksums.LinuxAmd64}}
|
||||
source_aarch64 = {{.PackageName}}-bin-{{.Version}}-aarch64.tar.gz::https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-linux-arm64.tar.gz
|
||||
sha256sums_aarch64 = {{.Checksums.LinuxArm64}}
|
||||
|
||||
pkgname = {{.PackageName}}-bin
|
||||
20
pkg/release/publishers/templates/aur/PKGBUILD.tmpl
Normal file
20
pkg/release/publishers/templates/aur/PKGBUILD.tmpl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Maintainer: {{.Maintainer}}
|
||||
pkgname={{.PackageName}}-bin
|
||||
pkgver={{.Version}}
|
||||
pkgrel=1
|
||||
pkgdesc="{{.Description}}"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/{{.Repository}}"
|
||||
license=('{{.License}}')
|
||||
provides=('{{.PackageName}}')
|
||||
conflicts=('{{.PackageName}}')
|
||||
|
||||
source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/{{.Repository}}/releases/download/v${pkgver}/{{.BinaryName}}-linux-amd64.tar.gz")
|
||||
source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/{{.Repository}}/releases/download/v${pkgver}/{{.BinaryName}}-linux-arm64.tar.gz")
|
||||
|
||||
sha256sums_x86_64=('{{.Checksums.LinuxAmd64}}')
|
||||
sha256sums_aarch64=('{{.Checksums.LinuxArm64}}')
|
||||
|
||||
package() {
|
||||
install -Dm755 {{.BinaryName}} "${pkgdir}/usr/bin/{{.BinaryName}}"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>{{.PackageName}}</id>
|
||||
<version>{{.Version}}</version>
|
||||
<title>{{.Title}}</title>
|
||||
<authors>{{.Authors}}</authors>
|
||||
<projectUrl>https://github.com/{{.Repository}}</projectUrl>
|
||||
<licenseUrl>https://github.com/{{.Repository}}/blob/main/LICENSE</licenseUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<description>{{.Description}}</description>
|
||||
<tags>{{.Tags}}</tags>
|
||||
<releaseNotes>https://github.com/{{.Repository}}/releases/tag/v{{.Version}}</releaseNotes>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools\**" target="tools" />
|
||||
</files>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
|
||||
$url64 = 'https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-windows-amd64.zip'
|
||||
|
||||
$packageArgs = @{
|
||||
packageName = '{{.PackageName}}'
|
||||
unzipLocation = $toolsDir
|
||||
url64bit = $url64
|
||||
checksum64 = '{{.Checksums.WindowsAmd64}}'
|
||||
checksumType64 = 'sha256'
|
||||
}
|
||||
|
||||
Install-ChocolateyZipPackage @packageArgs
|
||||
37
pkg/release/publishers/templates/homebrew/formula.rb.tmpl
Normal file
37
pkg/release/publishers/templates/homebrew/formula.rb.tmpl
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
class {{.FormulaClass}} < Formula
|
||||
desc "{{.Description}}"
|
||||
homepage "https://github.com/{{.Repository}}"
|
||||
version "{{.Version}}"
|
||||
license "{{.License}}"
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-darwin-arm64.tar.gz"
|
||||
sha256 "{{.Checksums.DarwinArm64}}"
|
||||
else
|
||||
url "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-darwin-amd64.tar.gz"
|
||||
sha256 "{{.Checksums.DarwinAmd64}}"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-linux-arm64.tar.gz"
|
||||
sha256 "{{.Checksums.LinuxArm64}}"
|
||||
else
|
||||
url "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-linux-amd64.tar.gz"
|
||||
sha256 "{{.Checksums.LinuxAmd64}}"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "{{.BinaryName}}"
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/{{.BinaryName}}", "--version"
|
||||
end
|
||||
end
|
||||
176
pkg/release/publishers/templates/npm/install.js.tmpl
Normal file
176
pkg/release/publishers/templates/npm/install.js.tmpl
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Binary installer for {{.Package}}
|
||||
* Downloads the correct binary for the current platform from GitHub releases.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const { spawnSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PACKAGE_VERSION = '{{.Version}}';
|
||||
const GITHUB_REPO = '{{.Repository}}';
|
||||
const BINARY_NAME = '{{.BinaryName}}';
|
||||
|
||||
// Platform/arch mapping
|
||||
const PLATFORM_MAP = {
|
||||
darwin: 'darwin',
|
||||
linux: 'linux',
|
||||
win32: 'windows',
|
||||
};
|
||||
|
||||
const ARCH_MAP = {
|
||||
x64: 'amd64',
|
||||
arm64: 'arm64',
|
||||
};
|
||||
|
||||
function getPlatformInfo() {
|
||||
const platform = PLATFORM_MAP[process.platform];
|
||||
const arch = ARCH_MAP[process.arch];
|
||||
|
||||
if (!platform || !arch) {
|
||||
console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { platform, arch };
|
||||
}
|
||||
|
||||
function getDownloadUrl(platform, arch) {
|
||||
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
|
||||
const name = `${BINARY_NAME}-${platform}-${arch}${ext}`;
|
||||
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${name}`;
|
||||
}
|
||||
|
||||
function getChecksumsUrl() {
|
||||
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/checksums.txt`;
|
||||
}
|
||||
|
||||
function download(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (url) => {
|
||||
https.get(url, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
// Follow redirect
|
||||
request(res.headers.location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download ${url}: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
res.on('error', reject);
|
||||
}).on('error', reject);
|
||||
};
|
||||
request(url);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchChecksums() {
|
||||
try {
|
||||
const data = await download(getChecksumsUrl());
|
||||
const checksums = {};
|
||||
data.toString().split('\n').forEach((line) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length === 2) {
|
||||
checksums[parts[1]] = parts[0];
|
||||
}
|
||||
});
|
||||
return checksums;
|
||||
} catch (err) {
|
||||
console.warn('Warning: Could not fetch checksums, skipping verification');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyChecksum(data, expectedHash) {
|
||||
const actualHash = crypto.createHash('sha256').update(data).digest('hex');
|
||||
return actualHash === expectedHash;
|
||||
}
|
||||
|
||||
function extract(data, destDir, platform) {
|
||||
const tempFile = path.join(destDir, platform === 'windows' ? 'temp.zip' : 'temp.tar.gz');
|
||||
fs.writeFileSync(tempFile, data);
|
||||
|
||||
try {
|
||||
if (platform === 'windows') {
|
||||
// Use PowerShell to extract zip
|
||||
const result = spawnSync('powershell', [
|
||||
'-command',
|
||||
`Expand-Archive -Path '${tempFile}' -DestinationPath '${destDir}' -Force`
|
||||
], { stdio: 'ignore' });
|
||||
if (result.status !== 0) {
|
||||
throw new Error('Failed to extract zip');
|
||||
}
|
||||
} else {
|
||||
const result = spawnSync('tar', ['-xzf', tempFile, '-C', destDir], { stdio: 'ignore' });
|
||||
if (result.status !== 0) {
|
||||
throw new Error('Failed to extract tar.gz');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { platform, arch } = getPlatformInfo();
|
||||
const binDir = path.join(__dirname, 'bin');
|
||||
const binaryPath = path.join(binDir, platform === 'windows' ? `${BINARY_NAME}.exe` : BINARY_NAME);
|
||||
|
||||
// Skip if binary already exists
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
console.log(`${BINARY_NAME} binary already installed`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Installing ${BINARY_NAME} v${PACKAGE_VERSION} for ${platform}/${arch}...`);
|
||||
|
||||
// Ensure bin directory exists
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Fetch checksums
|
||||
const checksums = await fetchChecksums();
|
||||
|
||||
// Download binary
|
||||
const url = getDownloadUrl(platform, arch);
|
||||
console.log(`Downloading from ${url}`);
|
||||
|
||||
const data = await download(url);
|
||||
|
||||
// Verify checksum if available
|
||||
if (checksums) {
|
||||
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
|
||||
const filename = `${BINARY_NAME}-${platform}-${arch}${ext}`;
|
||||
const expectedHash = checksums[filename];
|
||||
if (expectedHash && !verifyChecksum(data, expectedHash)) {
|
||||
console.error('Checksum verification failed!');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Checksum verified');
|
||||
}
|
||||
|
||||
// Extract
|
||||
extract(data, binDir, platform);
|
||||
|
||||
// Make executable on Unix
|
||||
if (platform !== 'windows') {
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
console.log(`${BINARY_NAME} installed successfully`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`Installation failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
34
pkg/release/publishers/templates/npm/package.json.tmpl
Normal file
34
pkg/release/publishers/templates/npm/package.json.tmpl
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "{{.Package}}",
|
||||
"version": "{{.Version}}",
|
||||
"description": "{{.Description}}",
|
||||
"license": "{{.License}}",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/{{.Repository}}.git"
|
||||
},
|
||||
"homepage": "https://github.com/{{.Repository}}",
|
||||
"bugs": {
|
||||
"url": "https://github.com/{{.Repository}}/issues"
|
||||
},
|
||||
"bin": {
|
||||
"{{.BinaryName}}": "./bin/run.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node ./install.js"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"install.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
"{{.ProjectName}}"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "{{.Access}}"
|
||||
}
|
||||
}
|
||||
48
pkg/release/publishers/templates/npm/run.js.tmpl
Normal file
48
pkg/release/publishers/templates/npm/run.js.tmpl
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Binary wrapper for {{.Package}}
|
||||
* Executes the platform-specific binary.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const BINARY_NAME = '{{.BinaryName}}';
|
||||
|
||||
function getBinaryPath() {
|
||||
const binDir = path.join(__dirname);
|
||||
const isWindows = process.platform === 'win32';
|
||||
const binaryName = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
||||
return path.join(binDir, binaryName);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const binaryPath = getBinaryPath();
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
console.error(`Binary not found at ${binaryPath}`);
|
||||
console.error('Try reinstalling the package: npm install -g {{.Package}}');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Failed to start ${BINARY_NAME}: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
} else {
|
||||
process.exit(code ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
30
pkg/release/publishers/templates/scoop/manifest.json.tmpl
Normal file
30
pkg/release/publishers/templates/scoop/manifest.json.tmpl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"version": "{{.Version}}",
|
||||
"description": "{{.Description}}",
|
||||
"homepage": "https://github.com/{{.Repository}}",
|
||||
"license": "{{.License}}",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-windows-amd64.zip",
|
||||
"hash": "{{.Checksums.WindowsAmd64}}"
|
||||
},
|
||||
"arm64": {
|
||||
"url": "https://github.com/{{.Repository}}/releases/download/v{{.Version}}/{{.BinaryName}}-windows-arm64.zip",
|
||||
"hash": "{{.Checksums.WindowsArm64}}"
|
||||
}
|
||||
},
|
||||
"bin": "{{.BinaryName}}.exe",
|
||||
"checkver": {
|
||||
"github": "https://github.com/{{.Repository}}"
|
||||
},
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/{{.Repository}}/releases/download/v$version/{{.BinaryName}}-windows-amd64.zip"
|
||||
},
|
||||
"arm64": {
|
||||
"url": "https://github.com/{{.Repository}}/releases/download/v$version/{{.BinaryName}}-windows-arm64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +216,16 @@ func getPublisher(pubType string) (publishers.Publisher, error) {
|
|||
return publishers.NewLinuxKitPublisher(), nil
|
||||
case "docker":
|
||||
return publishers.NewDockerPublisher(), nil
|
||||
case "npm":
|
||||
return publishers.NewNpmPublisher(), nil
|
||||
case "homebrew":
|
||||
return publishers.NewHomebrewPublisher(), nil
|
||||
case "scoop":
|
||||
return publishers.NewScoopPublisher(), nil
|
||||
case "aur":
|
||||
return publishers.NewAURPublisher(), nil
|
||||
case "chocolatey":
|
||||
return publishers.NewChocolateyPublisher(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported publisher type: %s", pubType)
|
||||
}
|
||||
|
|
@ -257,6 +267,47 @@ func buildExtendedConfig(pubCfg PublisherConfig) map[string]any {
|
|||
ext["build_args"] = args
|
||||
}
|
||||
|
||||
// npm-specific config
|
||||
if pubCfg.Package != "" {
|
||||
ext["package"] = pubCfg.Package
|
||||
}
|
||||
if pubCfg.Access != "" {
|
||||
ext["access"] = pubCfg.Access
|
||||
}
|
||||
|
||||
// Homebrew-specific config
|
||||
if pubCfg.Tap != "" {
|
||||
ext["tap"] = pubCfg.Tap
|
||||
}
|
||||
if pubCfg.Formula != "" {
|
||||
ext["formula"] = pubCfg.Formula
|
||||
}
|
||||
|
||||
// Scoop-specific config
|
||||
if pubCfg.Bucket != "" {
|
||||
ext["bucket"] = pubCfg.Bucket
|
||||
}
|
||||
|
||||
// AUR-specific config
|
||||
if pubCfg.Maintainer != "" {
|
||||
ext["maintainer"] = pubCfg.Maintainer
|
||||
}
|
||||
|
||||
// Chocolatey-specific config
|
||||
if pubCfg.Push {
|
||||
ext["push"] = pubCfg.Push
|
||||
}
|
||||
|
||||
// Official repo config (shared by multiple publishers)
|
||||
if pubCfg.Official != nil {
|
||||
official := make(map[string]any)
|
||||
official["enabled"] = pubCfg.Official.Enabled
|
||||
if pubCfg.Official.Output != "" {
|
||||
official["output"] = pubCfg.Official.Output
|
||||
}
|
||||
ext["official"] = official
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue