diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index cdefeea..102e06a 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -5,6 +5,7 @@ import ( "go.wpm.so/cli/cli/command" "go.wpm.so/cli/cli/command/auth" + "go.wpm.so/cli/cli/command/disttag" pmInit "go.wpm.so/cli/cli/command/init" "go.wpm.so/cli/cli/command/install" "go.wpm.so/cli/cli/command/ls" @@ -22,6 +23,7 @@ func AddCommands(cmd *cobra.Command, wpmCli command.Cli) { auth.NewAuthCommand(wpmCli), pmInit.NewInitCommand(wpmCli), whoami.NewWhoamiCommand(wpmCli), + disttag.NewDistTagCommand(wpmCli), publish.NewPublishCommand(wpmCli), install.NewInstallCommand(wpmCli), outdated.NewOutdatedCommand(wpmCli), diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index 8a6fe7f..77a771b 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -80,9 +80,9 @@ func PackageVisibility() cobra.CompletionFunc { ) } -// PublishTags suggests a non-exhaustive list of common dist-tags for -// `wpm publish --tag`. -func PublishTags() cobra.CompletionFunc { +// DistTags suggests a non-exhaustive list of common distribution tags, used by +// `wpm publish --tag` and `wpm dist-tag add`. +func DistTags() cobra.CompletionFunc { return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{"latest", "next", "beta", "alpha"}, cobra.ShellCompDirectiveNoFileComp } diff --git a/cli/command/disttag/add.go b/cli/command/disttag/add.go new file mode 100644 index 0000000..394ea5f --- /dev/null +++ b/cli/command/disttag/add.go @@ -0,0 +1,101 @@ +package disttag + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + + "go.wpm.so/cli/cli" + "go.wpm.so/cli/cli/command" + "go.wpm.so/cli/cli/command/completion" + "go.wpm.so/cli/pkg/pm/wpmjson/validator" +) + +const defaultDistTag = "latest" + +func newAddCommand(wpmCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "add PACKAGE@VERSION [TAG]", + Short: "Point a dist tag at a package version", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + return runAdd(cmd.Context(), wpmCli, args) + }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return completion.DistTags()(cmd, args, toComplete) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }, + } + + return cmd +} + +func runAdd(ctx context.Context, wpmCli command.Cli, args []string) error { + name, version, err := parsePackageVersion(args[0]) + if err != nil { + return err + } + + tag := defaultDistTag + if len(args) == 2 { + tag = args[1] + } + + if err := validator.IsValidDistTag(tag); err != nil { + return fmt.Errorf("invalid dist tag %q: %w", tag, err) + } + + if err := validateAuth(wpmCli); err != nil { + return err + } + + client, err := wpmCli.RegistryClient() + if err != nil { + return err + } + + if err := wpmCli.Progress().RunWithProgress( + "", + func() error { return client.AddDistTag(ctx, name, tag, version) }, + wpmCli.Err(), + ); err != nil { + return err + } + + wpmCli.Out().WriteString(fmt.Sprintf("+%s: %s@%s\n", tag, name, version)) + + return nil +} + +func validateAuth(wpmCli command.Cli) error { + cfg := wpmCli.ConfigFile() + if cfg.DefaultUser == "" || cfg.AuthToken == "" { + return errors.New("user must be logged in to perform this action") + } + return nil +} + +func parsePackageVersion(arg string) (name, version string, err error) { + lastAt := strings.LastIndex(arg, "@") + if lastAt <= 0 { + return "", "", fmt.Errorf("invalid package spec %q: expected @", arg) + } + + name = arg[:lastAt] + version = arg[lastAt+1:] + + if err := validator.IsValidPackageName(name); err != nil { + return "", "", fmt.Errorf("invalid package name %q: %w", name, err) + } + + if err := validator.IsValidVersion(version); err != nil { + return "", "", fmt.Errorf("invalid version %q: %w", version, err) + } + + return name, version, nil +} diff --git a/cli/command/disttag/cmd.go b/cli/command/disttag/cmd.go new file mode 100644 index 0000000..5f0ac1c --- /dev/null +++ b/cli/command/disttag/cmd.go @@ -0,0 +1,26 @@ +package disttag + +import ( + "github.com/spf13/cobra" + + "go.wpm.so/cli/cli" + "go.wpm.so/cli/cli/command" +) + +func NewDistTagCommand(wpmCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "dist-tag", + Short: "Manage package distribution tags", + Aliases: []string{"dist-tags"}, + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SetOut(wpmCli.Out()) + cmd.HelpFunc()(cmd, args) + return nil + }, + } + + cmd.AddCommand(newAddCommand(wpmCli)) + + return cmd +} diff --git a/cli/command/publish/publish.go b/cli/command/publish/publish.go index 43d3162..97fa921 100644 --- a/cli/command/publish/publish.go +++ b/cli/command/publish/publish.go @@ -59,7 +59,7 @@ func NewPublishCommand(wpmCli command.Cli) *cobra.Command { flags.StringVarP(&opts.access, "access", "a", "private", "Set the package access level to either public or private") flags.BoolVar(&opts.dryRun, "dry-run", false, "Perform a publish operation without actually publishing the package") - _ = cmd.RegisterFlagCompletionFunc("tag", completion.PublishTags()) + _ = cmd.RegisterFlagCompletionFunc("tag", completion.DistTags()) _ = cmd.RegisterFlagCompletionFunc("access", completion.PackageVisibility()) return cmd diff --git a/docs/reference/cli/dist-tag.md b/docs/reference/cli/dist-tag.md new file mode 100644 index 0000000..5f2b245 --- /dev/null +++ b/docs/reference/cli/dist-tag.md @@ -0,0 +1,33 @@ +# wpm dist-tag + + + +Manage package distribution tags + +### Aliases + +`wpm dist-tag`, `wpm dist-tags` + +### Subcommands + +| Name | Description | +|:-------------------------|:--------------------------------------| +| [`add`](dist-tag_add.md) | Point a dist tag at a package version | + + + + + + +## Description + +`wpm dist-tag` groups the subcommands that manage a package's distribution tags. + +A distribution tag is a human-friendly label, such as `latest` or `beta`, that +points at a specific published version. Tags give consumers a stable name to +install against instead of pinning an exact version: `wpm install acme-blocks` +resolves through the `latest` tag to whatever version it currently points at. + +The `latest` tag is special and it is what `wpm install ` uses when no +version or tag is requested. Any other tag (`beta`, `next`, `canary`, …) is a +convention you define for your own release workflow. diff --git a/docs/reference/cli/dist-tag_add.md b/docs/reference/cli/dist-tag_add.md new file mode 100644 index 0000000..207ee55 --- /dev/null +++ b/docs/reference/cli/dist-tag_add.md @@ -0,0 +1,45 @@ +# wpm dist-tag add + + + +Point a dist tag at a package version + + + + + +## Description + +Point a distribution tag at an already-published version of a package. + +The spec is `@`, where the version must be a concrete semantic +version that already exists in the registry and the tag always resolves to an +exact release, never to another tag. If you omit the tag, it defaults to +`latest`. + +You must be logged in (`wpm auth login`) or have `WPM_TOKEN` set, and you need +write access to the package. On success wpm prints a one-line summary: + +```console +$ wpm dist-tag add acme-blocks@1.4.0 beta ++beta: acme-blocks@1.4.0 +``` + +Moving an existing tag is the same operation as creating one: re-run `add` with +a different version and the tag is re-pointed. + +## Examples + +### Tag a version as `latest` + +```console +$ wpm dist-tag add acme-blocks@1.4.0 ++latest: acme-blocks@1.4.0 +``` + +### Create a pre-release tag + +```console +$ wpm dist-tag add acme-blocks@2.0.0-beta.1 beta ++beta: acme-blocks@2.0.0-beta.1 +``` diff --git a/docs/reference/cli/wpm.md b/docs/reference/cli/wpm.md index 9304eee..db7c0ee 100644 --- a/docs/reference/cli/wpm.md +++ b/docs/reference/cli/wpm.md @@ -9,6 +9,7 @@ Package Manager for WordPress ecosystem | Name | Description | |:----------------------------|:-------------------------------------------------------------------| | [`auth`](auth.md) | Authenticate with the wpm registry | +| [`dist-tag`](dist-tag.md) | Manage package distribution tags | | [`init`](init.md) | Initialize a new WordPress package or init wpm in existing project | | [`install`](install.md) | Install project dependencies and add new packages | | [`ls`](ls.md) | List installed dependencies | diff --git a/pkg/pm/registry/client.go b/pkg/pm/registry/client.go index 3593177..5262ee6 100644 --- a/pkg/pm/registry/client.go +++ b/pkg/pm/registry/client.go @@ -32,6 +32,7 @@ type Client interface { DownloadTarball(ctx context.Context, url string) (io.ReadCloser, error) PutPackage(ctx context.Context, data *manifest.Package, tarball io.Reader) error GetPackageManifest(ctx context.Context, packageName, versionOrTag string, force bool) (*manifest.Package, error) + AddDistTag(ctx context.Context, packageName, tag, version string) error } var _ Client = &client{} @@ -91,6 +92,26 @@ func (c *client) PutPackage(ctx context.Context, data *manifest.Package, tarball ) } +type distTagRequest struct { + Version string `json:"version"` +} + +// AddDistTag sets a distribution tag to point at a specific package version in the registry. +func (c *client) AddDistTag(ctx context.Context, packageName, tag, version string) error { + body, err := json.Marshal(distTagRequest{Version: version}) + if err != nil { + return err + } + + return c.restClient.DoWithContext( + ctx, + http.MethodPut, + "/-/dist-tags/"+packageName+"/"+tag, + bytes.NewReader(body), + nil, + ) +} + // GetPackageManifest retrieves a package manifest from the registry func (c *client) GetPackageManifest(ctx context.Context, packageName, versionOrTag string, force bool) (*manifest.Package, error) { var pkg *manifest.Package