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>
This commit is contained in:
141
internal/publish/publish.go
Normal file
141
internal/publish/publish.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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") != ""
|
||||
}
|
||||
Reference in New Issue
Block a user