Files
stamp/internal/publish/publish.go
Thomas 77462f5e8a feat: initial stamp implementation
- stamp add: create .stamp/*.md changeset files (interactive + --no-interactive)
- stamp status: show pending stamps and projected next versions (plain + --json)
- stamp version: consume stamps, bump semver, update CHANGELOGs, commit
- stamp publish: create git tags, GitHub/Gitea releases, upload artifacts
- stamp comment: post/update idempotent PR comments via GitHub/Gitea API
- Supports monorepos via stamp.toml [[projects]] blocks
- Changeset files support YAML (---) and TOML (+++) frontmatter
- Semver + pre-release support (alpha, beta, rc)
- Examples and workflow documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-08 20:56:23 +01:00

142 lines
3.6 KiB
Go

package publish
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/thokra/stamp/internal/config"
"github.com/thokra/stamp/internal/git"
ghClient "github.com/thokra/stamp/internal/github"
giteaClient "github.com/thokra/stamp/internal/gitea"
)
// Options controls publish behaviour.
type Options struct {
DryRun bool
RepoSlug string // "owner/repo" — required for releases
RepoRoot string
}
// Publish creates tags, releases, and uploads artifacts for a project.
func Publish(cfg *config.Config, project *config.Project, opts Options) error {
tag := git.TagName(project.Name, project.Version)
// --- Git tag ---
if project.Publish.PublishTags() {
if opts.DryRun {
fmt.Printf("[dry-run] would create git tag %s\n", tag)
} else {
exists, err := git.TagExists(opts.RepoRoot, tag)
if err != nil {
return err
}
if exists {
fmt.Printf("tag %s already exists, skipping\n", tag)
} else {
if err := git.Tag(opts.RepoRoot, tag, fmt.Sprintf("release %s %s", project.Name, project.Version)); err != nil {
return fmt.Errorf("creating tag %s: %w", tag, err)
}
if err := git.PushTag(opts.RepoRoot, tag); err != nil {
return fmt.Errorf("pushing tag %s: %w", tag, err)
}
fmt.Printf("created and pushed tag %s\n", tag)
}
}
}
if !project.Publish.PublishReleases() {
return nil
}
if opts.RepoSlug == "" {
return fmt.Errorf("STAMP_REPO (owner/repo) must be set to publish releases")
}
// --- Collect artifact paths ---
var artifacts []string
for _, pattern := range project.Publish.Artifacts {
matches, err := filepath.Glob(filepath.Join(opts.RepoRoot, pattern))
if err != nil {
return fmt.Errorf("glob %q: %w", pattern, err)
}
artifacts = append(artifacts, matches...)
}
releaseTitle := fmt.Sprintf("%s v%s", project.Name, project.Version)
releaseBody := fmt.Sprintf("Release of %s version %s", project.Name, project.Version)
// Detect host: GitHub vs Gitea
if isGitea() {
if err := publishGitea(opts, tag, releaseTitle, releaseBody, artifacts); err != nil {
return err
}
} else {
if err := publishGitHub(opts, tag, releaseTitle, releaseBody, artifacts); err != nil {
return err
}
}
return nil
}
func publishGitHub(opts Options, tag, title, body string, artifacts []string) error {
client, err := ghClient.NewClient(opts.RepoSlug)
if err != nil {
return err
}
ctx := context.Background()
if opts.DryRun {
fmt.Printf("[dry-run] would create GitHub release %s with %d artifact(s)\n", tag, len(artifacts))
return nil
}
releaseID, err := client.CreateRelease(ctx, tag, title, body, false, false)
if err != nil {
return err
}
fmt.Printf("created GitHub release %s (id=%d)\n", tag, releaseID)
for _, a := range artifacts {
if err := client.UploadAsset(ctx, releaseID, a); err != nil {
return err
}
fmt.Printf("uploaded artifact: %s\n", a)
}
return nil
}
func publishGitea(opts Options, tag, title, body string, artifacts []string) error {
client, err := giteaClient.NewClient(opts.RepoSlug)
if err != nil {
return err
}
if opts.DryRun {
fmt.Printf("[dry-run] would create Gitea release %s with %d artifact(s)\n", tag, len(artifacts))
return nil
}
releaseID, err := client.CreateRelease(tag, title, body, false, false)
if err != nil {
return err
}
fmt.Printf("created Gitea release %s (id=%d)\n", tag, releaseID)
for _, a := range artifacts {
if err := client.UploadAsset(releaseID, a); err != nil {
return err
}
fmt.Printf("uploaded artifact: %s\n", a)
}
return nil
}
// isGitea returns true when Gitea environment variables are configured.
func isGitea() bool {
return os.Getenv("GITEA_BASE_URL") != ""
}