cli/pkg/release/publishers/aur.go
Snider acec997d18 Migrate pkg/release to io.Medium abstraction (#290)
* chore(io): migrate pkg/release to io.Medium abstraction

Migrated `pkg/release` and its subpackages to use the `io.Medium` abstraction for filesystem operations. This enables better testability and support for alternative storage backends.

Changes:
- Added `FS io.Medium` field to `release.Release` and `publishers.Release` structs.
- Updated `LoadConfig`, `ConfigExists`, and `WriteConfig` in `pkg/release/config.go` to accept `io.Medium`.
- Updated `Publish`, `Run`, `findArtifacts`, and `buildArtifacts` in `pkg/release/release.go` to use `io.Medium`.
- Migrated all publishers (`aur`, `chocolatey`, `docker`, `github`, `homebrew`, `linuxkit`, `npm`, `scoop`) to use `io.Medium` for file operations.
- Implemented custom template overrides in publishers by checking for templates in `.core/templates/<publisher>/` via `io.Medium`.
- Updated all relevant tests to provide `io.Medium`.

* chore(io): fix missing callers in pkg/release migration

Updated callers of `release` package functions that had their signatures changed during the `io.Medium` migration.

Fixed files:
- `internal/cmd/ci/cmd_init.go`
- `internal/cmd/ci/cmd_publish.go`
- `pkg/build/buildcmd/cmd_release.go`

These changes ensure the project compiles successfully by providing `io.Local` to `LoadConfig`, `WriteConfig`, and `ConfigExists`.

* chore(io): fix build errors in pkg/release migration

Fixed compilation errors by updating all callers of `release.LoadConfig`, `release.ConfigExists`, and `release.WriteConfig` to provide the required `io.Medium` argument.

Files updated:
- `internal/cmd/ci/cmd_init.go`
- `internal/cmd/ci/cmd_publish.go`
- `pkg/build/buildcmd/cmd_release.go`

These entry points now correctly pass `io.Local` to the `release` package functions.
2026-02-04 15:07:13 +00:00

313 lines
8.7 KiB
Go

// 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"
"github.com/host-uk/core/pkg/io"
)
//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(release.FS, data, cfg)
}
return p.executePublish(ctx, release.ProjectDir, data, cfg, release)
}
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(m io.Medium, 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(m, "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(m, "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, release *Release) error {
pkgbuild, err := p.renderTemplate(release.FS, "templates/aur/PKGBUILD.tmpl", data)
if err != nil {
return fmt.Errorf("aur.Publish: failed to render PKGBUILD: %w", err)
}
srcinfo, err := p.renderTemplate(release.FS, "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 := release.FS.EnsureDir(output); err != nil {
return fmt.Errorf("aur.Publish: failed to create output directory: %w", err)
}
pkgbuildPath := filepath.Join(output, "PKGBUILD")
if err := release.FS.Write(pkgbuildPath, pkgbuild); err != nil {
return fmt.Errorf("aur.Publish: failed to write PKGBUILD: %w", err)
}
srcinfoPath := filepath.Join(output, ".SRCINFO")
if err := release.FS.Write(srcinfoPath, srcinfo); 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 func() { _ = 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(m io.Medium, name string, data aurTemplateData) (string, error) {
var content []byte
var err error
// Try custom template from medium
customPath := filepath.Join(".core", name)
if m != nil && m.IsFile(customPath) {
customContent, err := m.Read(customPath)
if err == nil {
content = []byte(customContent)
}
}
// Fallback to embedded template
if content == nil {
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{}