mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: archive template versions to hide them from the ui (#10179)
* api + cli implementation
This commit is contained in:
+4
-24
@@ -48,33 +48,13 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
|
||||
templates = append(templates, template)
|
||||
}
|
||||
} else {
|
||||
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
|
||||
template, err := selectTemplate(inv, client, organization)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get templates by organization: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(allTemplates) == 0 {
|
||||
return xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
|
||||
}
|
||||
|
||||
opts := make([]string, 0, len(allTemplates))
|
||||
for _, template := range allTemplates {
|
||||
opts = append(opts, template.Name)
|
||||
}
|
||||
|
||||
selection, err := cliui.Select(inv, cliui.SelectOptions{
|
||||
Options: opts,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("select template: %w", err)
|
||||
}
|
||||
|
||||
for _, template := range allTemplates {
|
||||
if template.Name == selection {
|
||||
templates = append(templates, template)
|
||||
templateNames = append(templateNames, template.Name)
|
||||
}
|
||||
}
|
||||
templates = append(templates, template)
|
||||
templateNames = append(templateNames, template.Name)
|
||||
}
|
||||
|
||||
// Confirm deletion of the template.
|
||||
|
||||
+35
-2
@@ -3,9 +3,9 @@ package cli
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
@@ -43,12 +43,45 @@ func (r *RootCmd) templates() *clibase.Cmd {
|
||||
r.templateVersions(),
|
||||
r.templateDelete(),
|
||||
r.templatePull(),
|
||||
r.archiveTemplateVersions(),
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func selectTemplate(inv *clibase.Invocation, client *codersdk.Client, organization codersdk.Organization) (codersdk.Template, error) {
|
||||
var empty codersdk.Template
|
||||
ctx := inv.Context()
|
||||
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
|
||||
if err != nil {
|
||||
return empty, xerrors.Errorf("get templates by organization: %w", err)
|
||||
}
|
||||
|
||||
if len(allTemplates) == 0 {
|
||||
return empty, xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
|
||||
}
|
||||
|
||||
opts := make([]string, 0, len(allTemplates))
|
||||
for _, template := range allTemplates {
|
||||
opts = append(opts, template.Name)
|
||||
}
|
||||
|
||||
selection, err := cliui.Select(inv, cliui.SelectOptions{
|
||||
Options: opts,
|
||||
})
|
||||
if err != nil {
|
||||
return empty, xerrors.Errorf("select template: %w", err)
|
||||
}
|
||||
|
||||
for _, template := range allTemplates {
|
||||
if template.Name == selection {
|
||||
return template, nil
|
||||
}
|
||||
}
|
||||
return empty, xerrors.Errorf("no template selected")
|
||||
}
|
||||
|
||||
type templateTableRow struct {
|
||||
// Used by json format:
|
||||
Template codersdk.Template
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func (r *RootCmd) unarchiveTemplateVersion() *clibase.Cmd {
|
||||
return r.setArchiveTemplateVersion(false)
|
||||
}
|
||||
|
||||
func (r *RootCmd) archiveTemplateVersion() *clibase.Cmd {
|
||||
return r.setArchiveTemplateVersion(true)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
|
||||
presentVerb := "archive"
|
||||
pastVerb := "archived"
|
||||
if !archive {
|
||||
presentVerb = "unarchive"
|
||||
pastVerb = "unarchived"
|
||||
}
|
||||
|
||||
client := new(codersdk.Client)
|
||||
cmd := &clibase.Cmd{
|
||||
Use: presentVerb + " <template-name> [template-version-names...] ",
|
||||
Short: strings.ToUpper(string(presentVerb[0])) + presentVerb[1:] + " a template version(s).",
|
||||
Middleware: clibase.Chain(
|
||||
r.InitClient(client),
|
||||
),
|
||||
Options: clibase.OptionSet{
|
||||
cliui.SkipPromptOption(),
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
var (
|
||||
ctx = inv.Context()
|
||||
versions []codersdk.TemplateVersion
|
||||
)
|
||||
|
||||
organization, err := CurrentOrganization(inv, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(inv.Args) == 0 {
|
||||
return xerrors.Errorf("missing template name")
|
||||
}
|
||||
if len(inv.Args) < 2 {
|
||||
return xerrors.Errorf("missing template version name(s)")
|
||||
}
|
||||
|
||||
templateName := inv.Args[0]
|
||||
template, err := client.TemplateByName(ctx, organization.ID, templateName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by name: %w", err)
|
||||
}
|
||||
for _, versionName := range inv.Args[1:] {
|
||||
version, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, template.Name, versionName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template version by name %q: %w", versionName, err)
|
||||
}
|
||||
versions = append(versions, version)
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
if version.Archived == archive {
|
||||
_, _ = fmt.Fprintln(
|
||||
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" already "+pastVerb),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
err := client.SetArchiveTemplateVersion(ctx, version.ID, archive)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("%s template version %q: %w", presentVerb, version.Name, err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(
|
||||
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" "+pastVerb+" at "+cliui.Timestamp(time.Now())),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
|
||||
var all clibase.Bool
|
||||
client := new(codersdk.Client)
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "archive [template-name...] ",
|
||||
Short: "Archive unused or failed template versions from a given template(s)",
|
||||
Middleware: clibase.Chain(
|
||||
r.InitClient(client),
|
||||
),
|
||||
Options: clibase.OptionSet{
|
||||
cliui.SkipPromptOption(),
|
||||
clibase.Option{
|
||||
Name: "all",
|
||||
Description: "Include all unused template versions. By default, only failed template versions are archived.",
|
||||
Flag: "all",
|
||||
Value: &all,
|
||||
},
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
var (
|
||||
ctx = inv.Context()
|
||||
templateNames = []string{}
|
||||
templates = []codersdk.Template{}
|
||||
)
|
||||
|
||||
organization, err := CurrentOrganization(inv, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(inv.Args) > 0 {
|
||||
templateNames = inv.Args
|
||||
|
||||
for _, templateName := range templateNames {
|
||||
template, err := client.TemplateByName(ctx, organization.ID, templateName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by name: %w", err)
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
} else {
|
||||
template, err := selectTemplate(inv, client, organization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templates = append(templates, template)
|
||||
templateNames = append(templateNames, template.Name)
|
||||
}
|
||||
|
||||
// Confirm archive of the template.
|
||||
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Archive template versions of these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(templateNames, ", "))),
|
||||
IsConfirm: true,
|
||||
Default: cliui.ConfirmNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, template := range templates {
|
||||
resp, err := client.ArchiveTemplateVersions(ctx, template.ID, all.Value())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("archive template %q: %w", template.Name, err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(
|
||||
inv.Stdout, fmt.Sprintf("Archived %d versions from "+pretty.Sprint(cliui.DefaultStyles.Keyword, template.Name)+" at "+cliui.Timestamp(time.Now()), len(resp.ArchivedIDs)),
|
||||
)
|
||||
|
||||
if ok, _ := inv.ParsedFlags().GetBool("verbose"); err == nil && ok {
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal verbose response: %w", err)
|
||||
}
|
||||
_, _ = fmt.Fprintln(
|
||||
inv.Stdout, string(data),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestTemplateVersionsArchive(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Archive-Unarchive", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
other := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
|
||||
request.TemplateID = template.ID
|
||||
})
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, other.ID)
|
||||
|
||||
// Archive
|
||||
inv, root := clitest.New(t, "templates", "versions", "archive", template.Name, other.Name, "-y")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
w.RequireSuccess()
|
||||
|
||||
// Verify archived
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
found, err := client.TemplateVersion(ctx, other.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, found.Archived, "expect archived")
|
||||
|
||||
// Unarchive
|
||||
inv, root = clitest.New(t, "templates", "versions", "unarchive", template.Name, other.Name, "-y")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
w = clitest.StartWithWaiter(t, inv)
|
||||
w.RequireSuccess()
|
||||
|
||||
// Verify unarchived
|
||||
ctx = testutil.Context(t, testutil.WaitMedium)
|
||||
found, err = client.TemplateVersion(ctx, other.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, found.Archived, "expect unarchived")
|
||||
})
|
||||
|
||||
t.Run("ArchiveMany", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
|
||||
// Add a failed
|
||||
expArchived := map[uuid.UUID]bool{}
|
||||
failed := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionApply: echo.ApplyFailed,
|
||||
ProvisionPlan: echo.PlanFailed,
|
||||
}, func(request *codersdk.CreateTemplateVersionRequest) {
|
||||
request.TemplateID = template.ID
|
||||
})
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, failed.ID)
|
||||
expArchived[failed.ID] = true
|
||||
// Add unused
|
||||
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
|
||||
request.TemplateID = template.ID
|
||||
})
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, unused.ID)
|
||||
expArchived[unused.ID] = true
|
||||
|
||||
// Archive all unused versions
|
||||
inv, root := clitest.New(t, "templates", "archive", template.Name, "-y", "--all")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
w.RequireSuccess()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
all, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
IncludeArchived: true,
|
||||
})
|
||||
require.NoError(t, err, "query all versions")
|
||||
for _, v := range all {
|
||||
if _, ok := expArchived[v.ID]; ok {
|
||||
require.True(t, v.Archived, "expect archived")
|
||||
delete(expArchived, v.ID)
|
||||
} else {
|
||||
require.False(t, v.Archived, "expect unarchived")
|
||||
}
|
||||
}
|
||||
require.Len(t, expArchived, 0, "expect all archived")
|
||||
})
|
||||
}
|
||||
+53
-2
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -29,6 +30,8 @@ func (r *RootCmd) templateVersions() *clibase.Cmd {
|
||||
},
|
||||
Children: []*clibase.Cmd{
|
||||
r.templateVersionsList(),
|
||||
r.archiveTemplateVersion(),
|
||||
r.unarchiveTemplateVersion(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,19 +39,59 @@ func (r *RootCmd) templateVersions() *clibase.Cmd {
|
||||
}
|
||||
|
||||
func (r *RootCmd) templateVersionsList() *clibase.Cmd {
|
||||
defaultColumns := []string{
|
||||
"Name",
|
||||
"Created At",
|
||||
"Created By",
|
||||
"Status",
|
||||
"Active",
|
||||
}
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.TableFormat([]templateVersionRow{}, nil),
|
||||
cliui.TableFormat([]templateVersionRow{}, defaultColumns),
|
||||
cliui.JSONFormat(),
|
||||
)
|
||||
client := new(codersdk.Client)
|
||||
|
||||
var includeArchived clibase.Bool
|
||||
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "list <template>",
|
||||
Middleware: clibase.Chain(
|
||||
clibase.RequireNArgs(1),
|
||||
r.InitClient(client),
|
||||
func(next clibase.HandlerFunc) clibase.HandlerFunc {
|
||||
return func(i *clibase.Invocation) error {
|
||||
// This is the only way to dynamically add the "archived"
|
||||
// column if '--include-archived' is true.
|
||||
// It does not make sense to show this column if the
|
||||
// flag is false.
|
||||
if includeArchived {
|
||||
for _, opt := range i.Command.Options {
|
||||
if opt.Flag == "column" {
|
||||
if opt.ValueSource == clibase.ValueSourceDefault {
|
||||
v, ok := opt.Value.(*clibase.StringArray)
|
||||
if ok {
|
||||
// Add the extra new default column.
|
||||
*v = append(*v, "Archived")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return next(i)
|
||||
}
|
||||
},
|
||||
),
|
||||
Short: "List all the versions of the specified template",
|
||||
Options: clibase.OptionSet{
|
||||
{
|
||||
Name: "include-archived",
|
||||
Description: "Include archived versions in the result list.",
|
||||
Flag: "include-archived",
|
||||
Value: &includeArchived,
|
||||
},
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
organization, err := CurrentOrganization(inv, client)
|
||||
if err != nil {
|
||||
@@ -59,7 +102,8 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
|
||||
return xerrors.Errorf("get template by name: %w", err)
|
||||
}
|
||||
req := codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
TemplateID: template.ID,
|
||||
IncludeArchived: includeArchived.Value(),
|
||||
}
|
||||
|
||||
versions, err := client.TemplateVersionsByTemplate(inv.Context(), req)
|
||||
@@ -92,6 +136,7 @@ type templateVersionRow struct {
|
||||
CreatedBy string `json:"-" table:"created by"`
|
||||
Status string `json:"-" table:"status"`
|
||||
Active string `json:"-" table:"active"`
|
||||
Archived string `json:"-" table:"archived"`
|
||||
}
|
||||
|
||||
// templateVersionsToRows converts a list of template versions to a list of rows
|
||||
@@ -104,6 +149,11 @@ func templateVersionsToRows(activeVersionID uuid.UUID, templateVersions ...coder
|
||||
activeStatus = cliui.Keyword("Active")
|
||||
}
|
||||
|
||||
archivedStatus := ""
|
||||
if templateVersion.Archived {
|
||||
archivedStatus = pretty.Sprint(cliui.DefaultStyles.Warn, "Archived")
|
||||
}
|
||||
|
||||
rows[i] = templateVersionRow{
|
||||
TemplateVersion: templateVersion,
|
||||
Name: templateVersion.Name,
|
||||
@@ -111,6 +161,7 @@ func templateVersionsToRows(activeVersionID uuid.UUID, templateVersions ...coder
|
||||
CreatedBy: templateVersion.CreatedBy.Username,
|
||||
Status: strings.Title(string(templateVersion.Job.Status)),
|
||||
Active: activeStatus,
|
||||
Archived: archivedStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -23,6 +23,8 @@ USAGE:
|
||||
$ coder templates push my-template
|
||||
|
||||
SUBCOMMANDS:
|
||||
archive Archive unused or failed template versions from a given
|
||||
template(s)
|
||||
create Create a template from the current directory or as specified by
|
||||
flag
|
||||
delete Delete templates
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder templates archive [flags] [template-name...]
|
||||
|
||||
Archive unused or failed template versions from a given template(s)
|
||||
|
||||
OPTIONS:
|
||||
--all bool
|
||||
Include all unused template versions. By default, only failed template
|
||||
versions are archived.
|
||||
|
||||
-y, --yes bool
|
||||
Bypass prompts.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
+3
-1
@@ -12,7 +12,9 @@ USAGE:
|
||||
$ coder templates versions list my-template
|
||||
|
||||
SUBCOMMANDS:
|
||||
list List all the versions of the specified template
|
||||
archive Archive a template version(s).
|
||||
list List all the versions of the specified template
|
||||
unarchive Unarchive a template version(s).
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder templates versions archive [flags] <template-name>
|
||||
[template-version-names...]
|
||||
|
||||
Archive a template version(s).
|
||||
|
||||
OPTIONS:
|
||||
-y, --yes bool
|
||||
Bypass prompts.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
@@ -6,9 +6,12 @@ USAGE:
|
||||
List all the versions of the specified template
|
||||
|
||||
OPTIONS:
|
||||
-c, --column string-array (default: name,created at,created by,status,active)
|
||||
-c, --column string-array (default: Name,Created At,Created By,Status,Active)
|
||||
Columns to display in table output. Available columns: name, created
|
||||
at, created by, status, active.
|
||||
at, created by, status, active, archived.
|
||||
|
||||
--include-archived bool
|
||||
Include archived versions in the result list.
|
||||
|
||||
-o, --output string (default: table)
|
||||
Output format. Available formats: table, json.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder templates versions unarchive [flags] <template-name>
|
||||
[template-version-names...]
|
||||
|
||||
Unarchive a template version(s).
|
||||
|
||||
OPTIONS:
|
||||
-y, --yes bool
|
||||
Bypass prompts.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
Reference in New Issue
Block a user