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:
146
cmd/stamp/cmd_add.go
Normal file
146
cmd/stamp/cmd_add.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/thokra/stamp/internal/changeset"
|
||||
"github.com/thokra/stamp/internal/config"
|
||||
)
|
||||
|
||||
func addCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add",
|
||||
Usage: "create a new stamp (changeset) file",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "project",
|
||||
Usage: "project name to include (can be specified multiple times)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "bump",
|
||||
Usage: "bump type: major, minor, patch, premajor, preminor, prepatch, prerelease",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "message",
|
||||
Usage: "description of the change",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-interactive",
|
||||
Usage: "disable interactive prompts (requires --project, --bump, --message)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "slug",
|
||||
Usage: "custom filename slug (default: auto-generated)",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
repoRoot, err := findRepoRoot(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(repoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stampDir := filepath.Join(repoRoot, cfg.ChangesetDir())
|
||||
|
||||
noInteractive := cmd.Bool("no-interactive")
|
||||
|
||||
var projects []string
|
||||
var bumpType changeset.BumpType
|
||||
var message string
|
||||
|
||||
if noInteractive || cmd.IsSet("project") {
|
||||
projects = cmd.StringSlice("project")
|
||||
if len(projects) == 0 {
|
||||
return fmt.Errorf("--project is required in non-interactive mode")
|
||||
}
|
||||
bumpType = changeset.BumpType(cmd.String("bump"))
|
||||
if bumpType == "" {
|
||||
return fmt.Errorf("--bump is required in non-interactive mode")
|
||||
}
|
||||
message = cmd.String("message")
|
||||
if message == "" {
|
||||
return fmt.Errorf("--message is required in non-interactive mode")
|
||||
}
|
||||
} else {
|
||||
projects, bumpType, message, err = promptAdd(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate projects exist in config.
|
||||
for _, p := range projects {
|
||||
if cfg.FindProject(p) == nil {
|
||||
return fmt.Errorf("project %q not found in stamp.toml", p)
|
||||
}
|
||||
}
|
||||
|
||||
bumps := make(map[string]changeset.BumpType, len(projects))
|
||||
for _, p := range projects {
|
||||
bumps[p] = bumpType
|
||||
}
|
||||
|
||||
slug := cmd.String("slug")
|
||||
if slug == "" {
|
||||
slug = changeset.GenerateSlug()
|
||||
}
|
||||
|
||||
if err := changeset.Write(stampDir, slug, bumps, message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Created .stamp/%s.md\n", slug)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// promptAdd runs an interactive prompt to collect add parameters.
|
||||
func promptAdd(cfg *config.Config) ([]string, changeset.BumpType, string, error) {
|
||||
projectNames := cfg.ProjectNames()
|
||||
|
||||
fmt.Println("Which projects are affected? (enter comma-separated names)")
|
||||
for _, name := range projectNames {
|
||||
fmt.Printf(" - %s\n", name)
|
||||
}
|
||||
fmt.Print("> ")
|
||||
|
||||
var input string
|
||||
if _, err := fmt.Scanln(&input); err != nil {
|
||||
return nil, "", "", fmt.Errorf("reading input: %w", err)
|
||||
}
|
||||
|
||||
var projects []string
|
||||
for _, p := range splitComma(input) {
|
||||
if cfg.FindProject(p) == nil {
|
||||
return nil, "", "", fmt.Errorf("project %q not found in stamp.toml", p)
|
||||
}
|
||||
projects = append(projects, p)
|
||||
}
|
||||
if len(projects) == 0 {
|
||||
return nil, "", "", fmt.Errorf("at least one project must be specified")
|
||||
}
|
||||
|
||||
fmt.Println("Bump type? (major, minor, patch, premajor, preminor, prepatch, prerelease)")
|
||||
fmt.Print("> ")
|
||||
var bumpStr string
|
||||
if _, err := fmt.Scanln(&bumpStr); err != nil {
|
||||
return nil, "", "", fmt.Errorf("reading input: %w", err)
|
||||
}
|
||||
bumpType := changeset.BumpType(bumpStr)
|
||||
|
||||
fmt.Println("Description of the change:")
|
||||
fmt.Print("> ")
|
||||
var message string
|
||||
if _, err := fmt.Scanln(&message); err != nil {
|
||||
return nil, "", "", fmt.Errorf("reading input: %w", err)
|
||||
}
|
||||
|
||||
return projects, bumpType, message, nil
|
||||
}
|
||||
108
cmd/stamp/cmd_comment.go
Normal file
108
cmd/stamp/cmd_comment.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/thokra/stamp/internal/changeset"
|
||||
"github.com/thokra/stamp/internal/config"
|
||||
"github.com/thokra/stamp/internal/semver"
|
||||
ghClient "github.com/thokra/stamp/internal/github"
|
||||
giteaClient "github.com/thokra/stamp/internal/gitea"
|
||||
)
|
||||
|
||||
func commentCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "comment",
|
||||
Usage: "post or update a PR comment summarising pending stamps",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "pr",
|
||||
Usage: "pull request number",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo",
|
||||
Usage: "repository slug owner/repo (defaults to STAMP_REPO or GITHUB_REPOSITORY env vars)",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
repoRoot, err := findRepoRoot(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(repoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stampDir := filepath.Join(repoRoot, cfg.ChangesetDir())
|
||||
|
||||
changesets, err := changeset.ReadAll(stampDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
body := buildCommentBody(cfg, changesets)
|
||||
prNumber := int(cmd.Int("pr"))
|
||||
|
||||
repoSlug := cmd.String("repo")
|
||||
if repoSlug == "" {
|
||||
repoSlug = os.Getenv("STAMP_REPO")
|
||||
}
|
||||
if repoSlug == "" {
|
||||
repoSlug = os.Getenv("GITHUB_REPOSITORY")
|
||||
}
|
||||
if repoSlug == "" {
|
||||
return fmt.Errorf("--repo or STAMP_REPO / GITHUB_REPOSITORY env var must be set")
|
||||
}
|
||||
|
||||
if os.Getenv("GITEA_BASE_URL") != "" {
|
||||
client, err := giteaClient.NewClient(repoSlug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.UpsertPRComment(prNumber, body)
|
||||
}
|
||||
|
||||
client, err := ghClient.NewClient(repoSlug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.UpsertPRComment(ctx, prNumber, body)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildCommentBody(cfg *config.Config, changesets []*changeset.Changeset) string {
|
||||
if len(changesets) == 0 {
|
||||
return "## ⚠️ No stamps found\n\n" +
|
||||
"This PR does not include a stamp file. If this change affects a versioned project, " +
|
||||
"please add a stamp by running:\n\n" +
|
||||
"```sh\nstamp add\n```\n\n" +
|
||||
"or creating a `.stamp/<name>.md` file manually."
|
||||
}
|
||||
|
||||
projectBumps := semver.ProjectBumps(changesets)
|
||||
body := fmt.Sprintf("## ✅ Stamps detected (%d)\n\n", len(changesets))
|
||||
body += "| Project | Current | Next | Bump |\n"
|
||||
body += "|---------|---------|------|------|\n"
|
||||
|
||||
for _, project := range cfg.Projects {
|
||||
bumps := projectBumps[project.Name]
|
||||
if len(bumps) == 0 {
|
||||
continue
|
||||
}
|
||||
highest := semver.HighestBump(bumps)
|
||||
next, err := semver.Bump(project.Version, highest, "")
|
||||
if err != nil {
|
||||
next = "?"
|
||||
}
|
||||
body += fmt.Sprintf("| %s | %s | %s | %s |\n", project.Name, project.Version, next, highest)
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
73
cmd/stamp/cmd_publish.go
Normal file
73
cmd/stamp/cmd_publish.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/thokra/stamp/internal/config"
|
||||
"github.com/thokra/stamp/internal/publish"
|
||||
)
|
||||
|
||||
func publishCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "publish",
|
||||
Usage: "create git tags, releases, and upload artifacts",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "print what would be done without executing",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "project",
|
||||
Usage: "only publish a specific project",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
repoRoot, err := findRepoRoot(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(repoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoSlug := os.Getenv("STAMP_REPO")
|
||||
opts := publish.Options{
|
||||
DryRun: cmd.Bool("dry-run"),
|
||||
RepoSlug: repoSlug,
|
||||
RepoRoot: repoRoot,
|
||||
}
|
||||
|
||||
filterProject := cmd.String("project")
|
||||
published := 0
|
||||
|
||||
for i := range cfg.Projects {
|
||||
project := &cfg.Projects[i]
|
||||
if filterProject != "" && project.Name != filterProject {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Publishing %s@%s...\n", project.Name, project.Version)
|
||||
if err := publish.Publish(cfg, project, opts); err != nil {
|
||||
return fmt.Errorf("publishing %s: %w", project.Name, err)
|
||||
}
|
||||
published++
|
||||
}
|
||||
|
||||
if published == 0 {
|
||||
if filterProject != "" {
|
||||
return fmt.Errorf("project %q not found in stamp.toml", filterProject)
|
||||
}
|
||||
fmt.Println("No projects to publish.")
|
||||
} else {
|
||||
fmt.Printf("\n✓ Published %d project(s).\n", published)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
119
cmd/stamp/cmd_status.go
Normal file
119
cmd/stamp/cmd_status.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/thokra/stamp/internal/changeset"
|
||||
"github.com/thokra/stamp/internal/config"
|
||||
"github.com/thokra/stamp/internal/semver"
|
||||
)
|
||||
|
||||
// statusResult is the machine-readable form of stamp status.
|
||||
type statusResult struct {
|
||||
Pending int `json:"pending"`
|
||||
Projects []projectStatus `json:"projects"`
|
||||
}
|
||||
|
||||
type projectStatus struct {
|
||||
Name string `json:"name"`
|
||||
Current string `json:"current"`
|
||||
Next string `json:"next"`
|
||||
Bump string `json:"bump"`
|
||||
Changesets int `json:"changesets"`
|
||||
NoBump bool `json:"no_bump"`
|
||||
}
|
||||
|
||||
func statusCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "status",
|
||||
Usage: "show pending stamps and projected next versions",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "output as JSON",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
repoRoot, err := findRepoRoot(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(repoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stampDir := filepath.Join(repoRoot, cfg.ChangesetDir())
|
||||
|
||||
changesets, err := changeset.ReadAll(stampDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
projectBumps := semver.ProjectBumps(changesets)
|
||||
|
||||
result := statusResult{
|
||||
Pending: len(changesets),
|
||||
}
|
||||
|
||||
for _, project := range cfg.Projects {
|
||||
bumps := projectBumps[project.Name]
|
||||
ps := projectStatus{
|
||||
Name: project.Name,
|
||||
Current: project.Version,
|
||||
Changesets: len(bumps),
|
||||
NoBump: len(bumps) == 0,
|
||||
}
|
||||
|
||||
if len(bumps) > 0 {
|
||||
highest := semver.HighestBump(bumps)
|
||||
ps.Bump = string(highest)
|
||||
next, err := semver.Bump(project.Version, highest, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing next version for %s: %w", project.Name, err)
|
||||
}
|
||||
ps.Next = next
|
||||
} else {
|
||||
ps.Next = project.Version
|
||||
}
|
||||
|
||||
result.Projects = append(result.Projects, ps)
|
||||
}
|
||||
|
||||
if cmd.Bool("json") {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
return printStatus(result)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printStatus(result statusResult) error {
|
||||
if result.Pending == 0 {
|
||||
fmt.Println("⚠ No pending stamps found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("📦 Pending stamps: %d\n\n", result.Pending)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, " PROJECT\tCURRENT\tNEXT\tBUMP\tSTAMPS")
|
||||
fmt.Fprintln(w, " -------\t-------\t----\t----\t------")
|
||||
for _, p := range result.Projects {
|
||||
if p.NoBump {
|
||||
fmt.Fprintf(w, " %s\t%s\t%s\t—\t0\n", p.Name, p.Current, p.Current)
|
||||
} else {
|
||||
fmt.Fprintf(w, " %s\t%s\t%s\t%s\t%d\n", p.Name, p.Current, p.Next, p.Bump, p.Changesets)
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
138
cmd/stamp/cmd_version.go
Normal file
138
cmd/stamp/cmd_version.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/thokra/stamp/internal/changeset"
|
||||
"github.com/thokra/stamp/internal/changelog"
|
||||
"github.com/thokra/stamp/internal/config"
|
||||
"github.com/thokra/stamp/internal/git"
|
||||
"github.com/thokra/stamp/internal/semver"
|
||||
)
|
||||
|
||||
func versionCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "consume stamps, bump versions, and update changelogs",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "snapshot",
|
||||
Usage: "pre-release identifier (e.g. alpha, beta, rc) for snapshot releases",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-commit",
|
||||
Usage: "skip the git commit after versioning",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
repoRoot, err := findRepoRoot(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(repoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stampDir := filepath.Join(repoRoot, cfg.ChangesetDir())
|
||||
|
||||
changesets, err := changeset.ReadAll(stampDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if len(changesets) == 0 {
|
||||
fmt.Println("No pending stamps found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
preID := cmd.String("snapshot")
|
||||
projectBumps := semver.ProjectBumps(changesets)
|
||||
now := time.Now()
|
||||
|
||||
var changedFiles []string
|
||||
var bumped []string
|
||||
|
||||
for i := range cfg.Projects {
|
||||
project := &cfg.Projects[i]
|
||||
bumps := projectBumps[project.Name]
|
||||
if len(bumps) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
highest := semver.HighestBump(bumps)
|
||||
if preID != "" {
|
||||
highest = changeset.BumpPreRelease
|
||||
}
|
||||
|
||||
nextVer, err := semver.Bump(project.Version, highest, preID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bumping %s: %w", project.Name, err)
|
||||
}
|
||||
|
||||
// Build changelog body from all matching changesets.
|
||||
var body string
|
||||
for _, cs := range changesets {
|
||||
if _, ok := cs.Bumps[project.Name]; ok {
|
||||
if cs.Description != "" {
|
||||
body += "- " + cs.Description + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clPath := project.ChangelogPath(repoRoot)
|
||||
if err := changelog.Append(clPath, changelog.Entry{
|
||||
Version: nextVer,
|
||||
Date: now,
|
||||
Description: body,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("updating changelog for %s: %w", project.Name, err)
|
||||
}
|
||||
changedFiles = append(changedFiles, clPath)
|
||||
|
||||
fmt.Printf(" %s: %s → %s\n", project.Name, project.Version, nextVer)
|
||||
project.Version = nextVer
|
||||
bumped = append(bumped, project.Name)
|
||||
}
|
||||
|
||||
if len(bumped) == 0 {
|
||||
fmt.Println("No projects were bumped.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update stamp.toml with new versions.
|
||||
cfgPath := filepath.Join(repoRoot, config.ConfigFileName)
|
||||
if err := config.Save(repoRoot, cfg); err != nil {
|
||||
return fmt.Errorf("saving stamp.toml: %w", err)
|
||||
}
|
||||
changedFiles = append(changedFiles, cfgPath)
|
||||
|
||||
// Delete consumed stamp files.
|
||||
if err := changeset.DeleteAll(stampDir); err != nil {
|
||||
return fmt.Errorf("deleting stamp files: %w", err)
|
||||
}
|
||||
changedFiles = append(changedFiles, stampDir)
|
||||
|
||||
if cmd.Bool("no-commit") {
|
||||
fmt.Println("\nVersioning complete (no commit).")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stage and commit.
|
||||
if err := git.Add(repoRoot, changedFiles...); err != nil {
|
||||
return fmt.Errorf("staging files: %w", err)
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("chore: version packages\n\nBumped: %v", bumped)
|
||||
if err := git.Commit(repoRoot, commitMsg); err != nil {
|
||||
return fmt.Errorf("committing: %w", err)
|
||||
}
|
||||
fmt.Println("\n✓ Committed version bump.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
31
cmd/stamp/helpers.go
Normal file
31
cmd/stamp/helpers.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// findRepoRoot walks up from dir to find the git repository root.
|
||||
func findRepoRoot(dir string) (string, error) {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not find git repository root: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// splitComma splits a comma-separated string and trims each part.
|
||||
func splitComma(s string) []string {
|
||||
parts := strings.Split(s, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
29
cmd/stamp/main.go
Normal file
29
cmd/stamp/main.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.Command{
|
||||
Name: "stamp",
|
||||
Usage: "language-agnostic changesets-style versioning and changelog tool",
|
||||
Version: "0.1.0",
|
||||
Commands: []*cli.Command{
|
||||
addCmd(),
|
||||
statusCmd(),
|
||||
versionCmd(),
|
||||
publishCmd(),
|
||||
commentCmd(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user