* ci: consolidate duplicate workflows and merge CodeQL configs Remove 17 duplicate workflow files that were split copies of the combined originals. Each family (CI, CodeQL, Coverage, PR Build, Alpha Release) had the same job duplicated across separate push/pull_request/schedule/manual trigger files. Merge codeql.yml and codescan.yml into a single codeql.yml with a language matrix covering go, javascript-typescript, python, and actions — matching the previous default setup coverage. Remaining workflows (one per family): - ci.yml (push + PR + manual) - codeql.yml (push + PR + schedule, all languages) - coverage.yml (push + PR + manual) - alpha-release.yml (push + manual) - pr-build.yml (PR + manual) - release.yml (tag push) - agent-verify.yml, auto-label.yml, auto-project.yml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add collect, config, crypt, plugin packages and fix all lint issues Add four new infrastructure packages with CLI commands: - pkg/config: layered configuration (defaults → file → env → flags) - pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums) - pkg/plugin: plugin system with GitHub-based install/update/remove - pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate) Fix all golangci-lint issues across the entire codebase (~100 errcheck, staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that `core go qa` passes with 0 issues. Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
7.4 KiB
Go
268 lines
7.4 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"
|
|
)
|
|
|
|
//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 func() { _ = 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{}
|