* fix(docs): respect workspace.yaml packages_dir setting (fixes #46) * fix(workspace): improve config loading logic (CR feedback) - Expand ~ before resolving relative paths in cmd_registry - Handle LoadWorkspaceConfig errors properly - Update Repo.Path when PackagesDir overrides default - Validate workspace config version - Add unit tests for workspace config loading * docs: add comments and increase test coverage (CR feedback) - Add docstrings to exported functions in pkg/cli - Add unit tests for Semantic Output (pkg/cli/output.go) - Add unit tests for CheckBuilder (pkg/cli/check.go) - Add unit tests for IPC Query/Perform (pkg/framework/core) * fix(test): fix panics and failures in php package tests - Fix panic in TestLookupLinuxKit_Bad by mocking paths - Fix assertion errors in TestGetSSLDir_Bad and TestGetPackageInfo_Bad - Fix formatting in test files * fix(test): correct syntax in services_extended_test.go * fix(ci): point coverage workflow to go.mod instead of go.work * fix(ci): build CLI before running coverage * fix(ci): run go generate for updater package in coverage workflow * fix(github): allow dry-run publish without gh CLI authentication Moves validation check after dry-run check so tests can verify dry-run behavior in CI environments.
233 lines
6.4 KiB
Go
233 lines
6.4 KiB
Go
// Package publishers provides release publishing implementations.
|
|
package publishers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// GitHubPublisher publishes releases to GitHub using the gh CLI.
|
|
type GitHubPublisher struct{}
|
|
|
|
// NewGitHubPublisher creates a new GitHub publisher.
|
|
func NewGitHubPublisher() *GitHubPublisher {
|
|
return &GitHubPublisher{}
|
|
}
|
|
|
|
// Name returns the publisher's identifier.
|
|
func (p *GitHubPublisher) Name() string {
|
|
return "github"
|
|
}
|
|
|
|
// Publish publishes the release to GitHub.
|
|
// Uses the gh CLI for creating releases and uploading assets.
|
|
func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
|
// Determine repository
|
|
repo := ""
|
|
if relCfg != nil {
|
|
repo = relCfg.GetRepository()
|
|
}
|
|
if repo == "" {
|
|
// Try to detect from git remote
|
|
detectedRepo, err := detectRepository(release.ProjectDir)
|
|
if err != nil {
|
|
return fmt.Errorf("github.Publish: could not determine repository: %w", err)
|
|
}
|
|
repo = detectedRepo
|
|
}
|
|
|
|
if dryRun {
|
|
return p.dryRunPublish(release, pubCfg, repo)
|
|
}
|
|
|
|
// Validate gh CLI is available and authenticated for actual publish
|
|
if err := validateGhCli(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return p.executePublish(ctx, release, pubCfg, repo)
|
|
}
|
|
|
|
// dryRunPublish shows what would be done without actually publishing.
|
|
func (p *GitHubPublisher) dryRunPublish(release *Release, pubCfg PublisherConfig, repo string) error {
|
|
fmt.Println()
|
|
fmt.Println("=== DRY RUN: GitHub Release ===")
|
|
fmt.Println()
|
|
fmt.Printf("Repository: %s\n", repo)
|
|
fmt.Printf("Version: %s\n", release.Version)
|
|
fmt.Printf("Draft: %t\n", pubCfg.Draft)
|
|
fmt.Printf("Prerelease: %t\n", pubCfg.Prerelease)
|
|
fmt.Println()
|
|
|
|
fmt.Println("Would create release with command:")
|
|
args := p.buildCreateArgs(release, pubCfg, repo)
|
|
fmt.Printf(" gh %s\n", strings.Join(args, " "))
|
|
fmt.Println()
|
|
|
|
if len(release.Artifacts) > 0 {
|
|
fmt.Println("Would upload artifacts:")
|
|
for _, artifact := range release.Artifacts {
|
|
fmt.Printf(" - %s\n", filepath.Base(artifact.Path))
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("Changelog:")
|
|
fmt.Println("---")
|
|
fmt.Println(release.Changelog)
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("=== END DRY RUN ===")
|
|
|
|
return nil
|
|
}
|
|
|
|
// executePublish actually creates the release and uploads artifacts.
|
|
func (p *GitHubPublisher) executePublish(ctx context.Context, release *Release, pubCfg PublisherConfig, repo string) error {
|
|
// Build the release create command
|
|
args := p.buildCreateArgs(release, pubCfg, repo)
|
|
|
|
// Add artifact paths to the command
|
|
for _, artifact := range release.Artifacts {
|
|
args = append(args, artifact.Path)
|
|
}
|
|
|
|
// Execute gh release create
|
|
cmd := exec.CommandContext(ctx, "gh", args...)
|
|
cmd.Dir = release.ProjectDir
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("github.Publish: gh release create failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildCreateArgs builds the arguments for gh release create.
|
|
func (p *GitHubPublisher) buildCreateArgs(release *Release, pubCfg PublisherConfig, repo string) []string {
|
|
args := []string{"release", "create", release.Version}
|
|
|
|
// Add repository flag
|
|
if repo != "" {
|
|
args = append(args, "--repo", repo)
|
|
}
|
|
|
|
// Add title
|
|
args = append(args, "--title", release.Version)
|
|
|
|
// Add notes (changelog)
|
|
if release.Changelog != "" {
|
|
args = append(args, "--notes", release.Changelog)
|
|
} else {
|
|
args = append(args, "--generate-notes")
|
|
}
|
|
|
|
// Add draft flag
|
|
if pubCfg.Draft {
|
|
args = append(args, "--draft")
|
|
}
|
|
|
|
// Add prerelease flag
|
|
if pubCfg.Prerelease {
|
|
args = append(args, "--prerelease")
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// validateGhCli checks if the gh CLI is available and authenticated.
|
|
func validateGhCli() error {
|
|
// Check if gh is installed
|
|
cmd := exec.Command("gh", "--version")
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("github: gh CLI not found. Install it from https://cli.github.com")
|
|
}
|
|
|
|
// Check if authenticated
|
|
cmd = exec.Command("gh", "auth", "status")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("github: not authenticated with gh CLI. Run 'gh auth login' first")
|
|
}
|
|
|
|
if !strings.Contains(string(output), "Logged in") {
|
|
return fmt.Errorf("github: not authenticated with gh CLI. Run 'gh auth login' first")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// detectRepository detects the GitHub repository from git remote.
|
|
func detectRepository(dir string) (string, error) {
|
|
cmd := exec.Command("git", "remote", "get-url", "origin")
|
|
cmd.Dir = dir
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get git remote: %w", err)
|
|
}
|
|
|
|
url := strings.TrimSpace(string(output))
|
|
return parseGitHubRepo(url)
|
|
}
|
|
|
|
// parseGitHubRepo extracts owner/repo from a GitHub URL.
|
|
// Supports:
|
|
// - git@github.com:owner/repo.git
|
|
// - https://github.com/owner/repo.git
|
|
// - https://github.com/owner/repo
|
|
func parseGitHubRepo(url string) (string, error) {
|
|
// SSH format
|
|
if strings.HasPrefix(url, "git@github.com:") {
|
|
repo := strings.TrimPrefix(url, "git@github.com:")
|
|
repo = strings.TrimSuffix(repo, ".git")
|
|
return repo, nil
|
|
}
|
|
|
|
// HTTPS format
|
|
if strings.HasPrefix(url, "https://github.com/") {
|
|
repo := strings.TrimPrefix(url, "https://github.com/")
|
|
repo = strings.TrimSuffix(repo, ".git")
|
|
return repo, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("not a GitHub URL: %s", url)
|
|
}
|
|
|
|
// UploadArtifact uploads a single artifact to an existing release.
|
|
// This can be used to add artifacts to a release after creation.
|
|
func UploadArtifact(ctx context.Context, repo, version, artifactPath string) error {
|
|
cmd := exec.CommandContext(ctx, "gh", "release", "upload", version, artifactPath, "--repo", repo)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("github.UploadArtifact: failed to upload %s: %w", artifactPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteRelease deletes a release by tag name.
|
|
func DeleteRelease(ctx context.Context, repo, version string) error {
|
|
cmd := exec.CommandContext(ctx, "gh", "release", "delete", version, "--repo", repo, "--yes")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("github.DeleteRelease: failed to delete %s: %w", version, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReleaseExists checks if a release exists for the given version.
|
|
func ReleaseExists(ctx context.Context, repo, version string) bool {
|
|
cmd := exec.CommandContext(ctx, "gh", "release", "view", version, "--repo", repo)
|
|
return cmd.Run() == nil
|
|
}
|