- Remove `os` import from internal/ax/ax.go; replace os.Getwd() with
syscall.Getwd(), os.MkdirAll() with coreio.Local.EnsureDir(), and
os.Chmod() with syscall.Chmod()
- Remove `os` import from pkg/sdk/generators/typescript_test.go;
replace os.PathListSeparator and os.Getenv() with core.Env("PS")
and core.Env("PATH")
- Replace all "Usage example: call/declare ... from integrating code"
placeholder comments with concrete code examples across 45 files
covering build, release, sdk, signing, publishers, builders, and cmd
Co-Authored-By: Virgil <virgil@lethean.io>
265 lines
7.4 KiB
Go
265 lines
7.4 KiB
Go
// Package publishers provides release publishing implementations.
|
|
package publishers
|
|
|
|
import (
|
|
"context"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/core/build/internal/ax"
|
|
coreerr "dappco.re/go/core/log"
|
|
)
|
|
|
|
// GitHubPublisher publishes releases to GitHub using the gh CLI.
|
|
//
|
|
// pub := publishers.NewGitHubPublisher()
|
|
type GitHubPublisher struct{}
|
|
|
|
// NewGitHubPublisher creates a new GitHub publisher.
|
|
//
|
|
// pub := publishers.NewGitHubPublisher()
|
|
func NewGitHubPublisher() *GitHubPublisher {
|
|
return &GitHubPublisher{}
|
|
}
|
|
|
|
// Name returns the publisher's identifier.
|
|
//
|
|
// name := pub.Name() // → "github"
|
|
func (p *GitHubPublisher) Name() string {
|
|
return "github"
|
|
}
|
|
|
|
// Publish publishes the release to GitHub using the gh CLI.
|
|
//
|
|
// err := pub.Publish(ctx, rel, pubCfg, relCfg, false) // dryRun=true to preview
|
|
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(ctx, release.ProjectDir)
|
|
if err != nil {
|
|
return coreerr.E("github.Publish", "could not determine repository", err)
|
|
}
|
|
repo = detectedRepo
|
|
}
|
|
|
|
if dryRun {
|
|
return p.dryRunPublish(release, pubCfg, repo)
|
|
}
|
|
|
|
ghCommand, err := resolveGhCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate gh CLI is available and authenticated for actual publish
|
|
if err := validateGhAuth(ctx, ghCommand); err != nil {
|
|
return err
|
|
}
|
|
|
|
return p.executePublish(ctx, release, pubCfg, repo, ghCommand)
|
|
}
|
|
|
|
// dryRunPublish shows what would be done without actually publishing.
|
|
func (p *GitHubPublisher) dryRunPublish(release *Release, pubCfg PublisherConfig, repo string) error {
|
|
publisherPrintln()
|
|
publisherPrintln("=== DRY RUN: GitHub Release ===")
|
|
publisherPrintln()
|
|
publisherPrint("Repository: %s", repo)
|
|
publisherPrint("Version: %s", release.Version)
|
|
publisherPrint("Draft: %t", pubCfg.Draft)
|
|
publisherPrint("Prerelease: %t", pubCfg.Prerelease)
|
|
publisherPrintln()
|
|
|
|
publisherPrintln("Would create release with command:")
|
|
args := p.buildCreateArgs(release, pubCfg, repo)
|
|
publisherPrint(" gh %s", core.Join(" ", args...))
|
|
publisherPrintln()
|
|
|
|
if len(release.Artifacts) > 0 {
|
|
publisherPrintln("Would upload artifacts:")
|
|
for _, artifact := range release.Artifacts {
|
|
publisherPrint(" - %s", ax.Base(artifact.Path))
|
|
}
|
|
}
|
|
|
|
publisherPrintln()
|
|
publisherPrintln("Changelog:")
|
|
publisherPrintln("---")
|
|
publisherPrintln(release.Changelog)
|
|
publisherPrintln("---")
|
|
publisherPrintln()
|
|
publisherPrintln("=== 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, ghCommand 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
|
|
if err := publisherRun(ctx, release.ProjectDir, nil, ghCommand, args...); err != nil {
|
|
return coreerr.E("github.Publish", "gh release create failed", 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
|
|
}
|
|
|
|
func resolveGhCli(paths ...string) (string, error) {
|
|
if len(paths) == 0 {
|
|
paths = []string{
|
|
"/usr/local/bin/gh",
|
|
"/opt/homebrew/bin/gh",
|
|
}
|
|
}
|
|
|
|
command, err := ax.ResolveCommand("gh", paths...)
|
|
if err != nil {
|
|
return "", coreerr.E("github.resolveGhCli", "gh CLI not found. Install it from https://cli.github.com", err)
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
// validateGhCli checks if the gh CLI is available and authenticated.
|
|
func validateGhCli(ctx context.Context) error {
|
|
ghCommand, err := resolveGhCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return validateGhAuth(ctx, ghCommand)
|
|
}
|
|
|
|
func validateGhAuth(ctx context.Context, ghCommand string) error {
|
|
output, err := ax.CombinedOutput(ctx, "", nil, ghCommand, "auth", "status")
|
|
if err != nil {
|
|
return coreerr.E("github.validateGhCli", "not authenticated with gh CLI. Run 'gh auth login' first", err)
|
|
}
|
|
|
|
if !core.Contains(output, "Logged in") {
|
|
return coreerr.E("github.validateGhCli", "not authenticated with gh CLI. Run 'gh auth login' first", nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// detectRepository detects the GitHub repository from git remote.
|
|
func detectRepository(ctx context.Context, dir string) (string, error) {
|
|
output, err := ax.RunDir(ctx, dir, "git", "remote", "get-url", "origin")
|
|
if err != nil {
|
|
return "", coreerr.E("github.detectRepository", "failed to get git remote", err)
|
|
}
|
|
|
|
return parseGitHubRepo(core.Trim(output))
|
|
}
|
|
|
|
// 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 core.HasPrefix(url, "git@github.com:") {
|
|
repo := core.TrimPrefix(url, "git@github.com:")
|
|
repo = core.TrimSuffix(repo, ".git")
|
|
return repo, nil
|
|
}
|
|
|
|
// HTTPS format
|
|
if core.HasPrefix(url, "https://github.com/") {
|
|
repo := core.TrimPrefix(url, "https://github.com/")
|
|
repo = core.TrimSuffix(repo, ".git")
|
|
return repo, nil
|
|
}
|
|
|
|
return "", coreerr.E("github.parseGitHubRepo", "not a GitHub URL: "+url, nil)
|
|
}
|
|
|
|
// UploadArtifact uploads a single artifact to an existing release.
|
|
// This can be used to add artifacts to a release after creation.
|
|
//
|
|
// err := publishers.UploadArtifact(ctx, "host-uk/core-build", "v1.2.3", "dist/core-build_v1.2.3_linux_amd64.tar.gz")
|
|
func UploadArtifact(ctx context.Context, repo, version, artifactPath string) error {
|
|
ghCommand, err := resolveGhCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := publisherRun(ctx, "", nil, ghCommand, "release", "upload", version, artifactPath, "--repo", repo); err != nil {
|
|
return coreerr.E("github.UploadArtifact", "failed to upload "+artifactPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteRelease deletes a release by tag name.
|
|
//
|
|
// err := publishers.DeleteRelease(ctx, "host-uk/core-build", "v1.2.3")
|
|
func DeleteRelease(ctx context.Context, repo, version string) error {
|
|
ghCommand, err := resolveGhCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := publisherRun(ctx, "", nil, ghCommand, "release", "delete", version, "--repo", repo, "--yes"); err != nil {
|
|
return coreerr.E("github.DeleteRelease", "failed to delete "+version, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReleaseExists checks if a release exists for the given version.
|
|
//
|
|
// exists := publishers.ReleaseExists(ctx, "host-uk/core-build", "v1.2.3")
|
|
func ReleaseExists(ctx context.Context, repo, version string) bool {
|
|
ghCommand, err := resolveGhCli()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return ax.Exec(ctx, ghCommand, "release", "view", version, "--repo", repo) == nil
|
|
}
|