Skip to content
Snippets Groups Projects
Commit 470df3b4 authored by Jay McCure's avatar Jay McCure
Browse files

Merge branch 'origin/feat/token-support-step-3-rotate' into 'main'

feat: rotate personal, project or group access tokens

See merge request !1643



Merged-by: default avatarJay McCure <jmccure@gitlab.com>
Approved-by: default avatarGreg Alfaro <galfaro@gitlab.com>
Approved-by: default avatarAmy Qualls <aqualls@gitlab.com>
Approved-by: default avatarJay McCure <jmccure@gitlab.com>
Reviewed-by: default avatarAmy Qualls <aqualls@gitlab.com>
Reviewed-by: default avatarJay McCure <jmccure@gitlab.com>
Co-authored-by: default avatarMark van Holsteijn <mark.van.holsteijn@gmail.com>
parents 8528b60e 8c159845
No related branches found
No related tags found
1 merge request!1643feat: rotate personal, project or group access tokens
Pipeline #1504592797 passed with warnings
......@@ -126,3 +126,27 @@ var RevokePersonalAccessToken = func(client *gitlab.Client, id int) error {
_, err := client.PersonalAccessTokens.RevokePersonalAccessToken(id)
return err
}
var RotateProjectAccessToken = func(client *gitlab.Client, pid interface{}, id int, opts *gitlab.RotateProjectAccessTokenOptions) (*gitlab.ProjectAccessToken, error) {
if client == nil {
client = apiClient.Lab()
}
token, _, err := client.ProjectAccessTokens.RotateProjectAccessToken(pid, id, opts)
return token, err
}
var RotateGroupAccessToken = func(client *gitlab.Client, gid interface{}, id int, opts *gitlab.RotateGroupAccessTokenOptions) (*gitlab.GroupAccessToken, error) {
if client == nil {
client = apiClient.Lab()
}
token, _, err := client.GroupAccessTokens.RotateGroupAccessToken(gid, id, opts)
return token, err
}
var RotatePersonalAccessToken = func(client *gitlab.Client, id int, opts *gitlab.RotatePersonalAccessTokenOptions) (*gitlab.PersonalAccessToken, error) {
if client == nil {
client = apiClient.Lab()
}
token, _, err := client.PersonalAccessTokens.RotatePersonalAccessToken(id, opts)
return token, err
}
......@@ -251,7 +251,7 @@ func createTokenRun(opts *CreateOptions) error {
return err
}
} else {
if _, err := opts.IO.StdOut.Write([]byte(outputTokenValue)); err != nil {
if _, err := fmt.Fprintf(opts.IO.StdOut, "%s\n", outputTokenValue); err != nil {
return err
}
}
......
......@@ -194,7 +194,7 @@ func TestCreateOwnPersonalAccessTokenAsText(t *testing.T) {
t.Error(err)
return
}
assert.Equal(t, "glpat-jRHatYQ8Fs77771111ps", output.String())
assert.Equal(t, "glpat-jRHatYQ8Fs77771111ps\n", output.String())
}
func TestCreateOtherPersonalAccessTokenAsJSON(t *testing.T) {
......@@ -248,7 +248,7 @@ func TestCreateOtherPersonalAccessTokenAsText(t *testing.T) {
t.Error(err)
return
}
assert.Equal(t, "glpat-jRHatYQ8Fs77771111ps", output.String())
assert.Equal(t, "glpat-jRHatYQ8Fs77771111ps\n", output.String())
}
var groupAccessTokenResponse = heredoc.Doc(`
......@@ -313,7 +313,7 @@ func TestCreateGroupAccessTokenAsText(t *testing.T) {
return
}
assert.Equal(t, "glpat-yz2791KMU-xxxxxxxxx", output.String())
assert.Equal(t, "glpat-yz2791KMU-xxxxxxxxx\n", output.String())
}
var projectAccessTokenResponse = heredoc.Doc(`
......@@ -378,5 +378,5 @@ func TestCreateProjectAccessTokenAsText(t *testing.T) {
return
}
assert.Equal(t, "glpat-dfsdfjksjdfslkdfjsd", output.String())
assert.Equal(t, "glpat-dfsdfjksjdfslkdfjsd\n", output.String())
}
package rotate
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"gitlab.com/gitlab-org/cli/commands/token/expirationdate"
"gitlab.com/gitlab-org/cli/commands/token/filter"
"gitlab.com/gitlab-org/cli/commands/flag"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/glrepo"
)
type RotateOptions struct {
HTTPClient func() (*gitlab.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (glrepo.Interface, error)
User string
Group string
Name interface{}
Duration time.Duration
ExpireAt expirationdate.ExpirationDate
OutputFormat string
}
func NewCmdRotate(f *cmdutils.Factory, runE func(opts *RotateOptions) error) *cobra.Command {
opts := &RotateOptions{
IO: f.IO,
}
cmd := &cobra.Command{
Use: "rotate <token-name|token-id>",
Short: "Rotate user, group, or project access tokens",
Aliases: []string{"rotate", "rot"},
Args: cobra.RangeArgs(1, 1),
Long: heredoc.Doc(`
Rotate user, group, or project access token, then print the new token on stdout. If multiple tokens with
the same name exist, you can specify the ID of the token.
The expiration date of the token will be calculated by adding the duration (default 30 days) to the
current date. Alternatively you can specify a different duration or an explicit end date.
The output format can be either "JSON" or "text". The JSON output will show the meta information of the
rotated token.
Administrators can rotate personal access tokens belonging to other users.
`),
Example: heredoc.Doc(`
# Rotate project access token of current project
glab token rotate my-project-token
# Rotate project access token of another project, set to expiration date
glab token rotate --repo user/repo my-project-token --expires-at 2024-08-08
# Rotate group access token
glab token rotate --group group/sub-group my-group-token
# Rotate personal access token and extend duration to 7 days
glab token rotate --user @me --duration $((7 * 24))h my-personal-token
# Rotate a personal access token of another user (administrator only)
glab token rotate --user johndoe johns-personal-token
`),
RunE: func(cmd *cobra.Command, args []string) (err error) {
// Supports repo override
opts.HTTPClient = f.HttpClient
opts.BaseRepo = f.BaseRepo
if opts.Name, err = strconv.Atoi(args[0]); err != nil {
opts.Name = args[0]
}
if opts.Group, err = flag.GroupOverride(cmd); err != nil {
return
}
if opts.Group != "" && opts.User != "" {
return cmdutils.FlagError{Err: errors.New("'--group' and '--user' are mutually exclusive.")}
}
if opts.Duration.Truncate(24*time.Hour) != opts.Duration {
return cmdutils.FlagError{Err: errors.New("duration must be in days.")}
}
if opts.Duration < 24*time.Hour || opts.Duration > 365*24*time.Hour {
return cmdutils.FlagError{Err: errors.New("duration in days must be between 1 and 365.")}
}
if cmd.Flags().Changed("expires-at") && cmd.Flags().Changed("duration") {
return cmdutils.FlagError{Err: errors.New("'--expires-at' and '--duration' are mutually exclusive.")}
}
if time.Time(opts.ExpireAt).IsZero() {
opts.ExpireAt = expirationdate.ExpirationDate(time.Now().Add(opts.Duration).Truncate(time.Hour * 24))
}
if runE != nil {
return runE(opts)
}
return rotateTokenRun(opts)
},
}
cmdutils.EnableRepoOverride(cmd, f)
cmd.Flags().StringVarP(&opts.Group, "group", "g", "", "Rotate group access token. Ignored if a user or repository argument is set.")
cmd.Flags().StringVarP(&opts.User, "user", "U", "", "Rotate personal access token. Use @me for the current user.")
cmd.Flags().DurationVarP(&opts.Duration, "duration", "D", time.Duration(30*24*time.Hour), "Sets the token duration, in hours. Maximum of 8760. Examples: 24h, 168h, 504h.")
cmd.Flags().VarP(&opts.ExpireAt, "expires-at", "E", "Sets the token's expiration date and time, in YYYY-MM-DD format. If not specified, --duration is used.")
cmd.Flags().StringVarP(&opts.OutputFormat, "output", "F", "text", "Format output as: text, json. 'text' provides the new token value; 'json' outputs the token with metadata.")
return cmd
}
func rotateTokenRun(opts *RotateOptions) error {
httpClient, err := opts.HTTPClient()
if err != nil {
return err
}
expirationDate := gitlab.ISOTime(opts.ExpireAt)
var outputToken interface{}
var outputTokenValue string
if opts.User != "" {
user, err := api.UserByName(httpClient, opts.User)
if err != nil {
return cmdutils.FlagError{Err: err}
}
options := &gitlab.ListPersonalAccessTokensOptions{
ListOptions: gitlab.ListOptions{PerPage: 100},
UserID: &user.ID,
}
tokens, err := api.ListPersonalAccessTokens(httpClient, options)
if err != nil {
return err
}
var token *gitlab.PersonalAccessToken
tokens = filter.Filter(tokens, func(t *gitlab.PersonalAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v'. Use the ID instead.", opts.Name)}
}
rotateOptions := &gitlab.RotatePersonalAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotatePersonalAccessToken(httpClient, token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
} else {
if opts.Group != "" {
options := &gitlab.ListGroupAccessTokensOptions{PerPage: 100}
tokens, err := api.ListGroupAccessTokens(httpClient, opts.Group, options)
if err != nil {
return err
}
var token *gitlab.GroupAccessToken
tokens = filter.Filter(tokens, func(t *gitlab.GroupAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v', use the ID instead", opts.Name)}
}
rotateOptions := &gitlab.RotateGroupAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotateGroupAccessToken(httpClient, opts.Group, token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
} else {
repo, err := opts.BaseRepo()
if err != nil {
return err
}
options := &gitlab.ListProjectAccessTokensOptions{PerPage: 100}
tokens, err := api.ListProjectAccessTokens(httpClient, repo.FullName(), options)
if err != nil {
return err
}
tokens = filter.Filter(tokens, func(t *gitlab.ProjectAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
var token *gitlab.ProjectAccessToken
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v', use the ID instead", opts.Name)}
}
rotateOptions := &gitlab.RotateProjectAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotateProjectAccessToken(httpClient, repo.FullName(), token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
}
}
if opts.OutputFormat == "json" {
encoder := json.NewEncoder(opts.IO.StdOut)
if err := encoder.Encode(outputToken); err != nil {
return err
}
} else {
if _, err := fmt.Fprintf(opts.IO.StdOut, "%s\n", outputTokenValue); err != nil {
return err
}
}
return nil
}
package rotate
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc/v2"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/commands/cmdtest"
"gitlab.com/gitlab-org/cli/pkg/httpmock"
"gitlab.com/gitlab-org/cli/test"
)
func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) {
ios, _, stdout, stderr := cmdtest.InitIOStreams(true, "")
factory := cmdtest.InitFactory(ios, rt)
// TODO: shouldn't be there but the stub doesn't work without it
_, _ = factory.HttpClient()
cmd := NewCmdRotate(factory, nil)
if out, err := cmdtest.ExecuteCommand(cmd, cli, stdout, stderr); err != nil {
return nil, fmt.Errorf("error running command %s '%s', %s", cmd.Aliases[0], cli, err)
} else {
return out, nil
}
}
var userResponse = heredoc.Doc(`
{
"id": 1,
"username": "johndoe",
"name": "John Doe",
"state": "active",
"locked": false,
"avatar_url": "https://secure.gravatar.com/avatar/johndoe?s=80&d=identicon",
"web_url": "https://gitlab.com/johndoe",
"created_at": "2017-01-05T08:36:01.368Z",
"bio": "",
"location": "",
"public_email": "",
"skype": "",
"linkedin": "",
"twitter": "",
"discord": "",
"website_url": "",
"organization": "",
"job_title": "",
"pronouns": null,
"bot": false,
"work_information": null,
"local_time": null,
"last_sign_in_at": "2024-07-07T06:57:16.562Z",
"confirmed_at": "2017-01-05T08:36:24.701Z",
"last_activity_on": "2024-07-07",
"email": "john.doe@acme.com",
"theme_id": null,
"color_scheme_id": 1,
"projects_limit": 100000,
"current_sign_in_at": "2024-07-07T07:57:57.858Z",
"identities": [
{
"provider": "google_oauth2",
"extern_uid": "102139960402025821780",
"saml_provider_id": null
}
],
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
"private_profile": false,
"commit_email": "john.doe@acme.com",
"shared_runners_minutes_limit": 2000,
"extra_shared_runners_minutes_limit": null,
"scim_identities": []
}
`)
var personalAccessTokenResponse = heredoc.Doc(`
{
"id": 10183862,
"name": "my-pat",
"revoked": false,
"created_at": "2024-07-08T01:23:04.311Z",
"scopes": [
"k8s_proxy"
],
"user_id": 926857,
"active": true,
"expires_at": "2024-08-07",
"token": "glpat-jRHatYQ8Fs77771111ps"
}
`)
func TestRotatePersonalAccessTokenAsJSON(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/user",
httpmock.NewStringResponse(http.StatusOK, userResponse))
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/personal_access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", personalAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/personal_access_tokens/10183862/rotate",
httpmock.NewStringResponse(http.StatusOK, personalAccessTokenResponse))
output, err := runCommand(fakeHTTP, "--user @me --output json my-pat")
if err != nil {
t.Error(err)
return
}
var expect interface{}
var actual interface{}
if err := json.Unmarshal([]byte(personalAccessTokenResponse), &expect); err != nil {
t.Error(err)
}
if err := json.Unmarshal([]byte(output.String()), &actual); err != nil {
t.Error(err)
}
assert.Equal(t, expect, actual)
assert.Empty(t, output.Stderr())
}
func TestRotatePersonalAccessTokenAsText(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/user",
httpmock.NewStringResponse(http.StatusOK, userResponse))
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/personal_access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", personalAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/personal_access_tokens/10183862/rotate",
httpmock.NewStringResponse(http.StatusOK, personalAccessTokenResponse))
output, err := runCommand(fakeHTTP, "--user @me my-pat")
if err != nil {
t.Error(err)
return
}
assert.Equal(t, "glpat-jRHatYQ8Fs77771111ps\n", output.String())
}
var groupAccessTokenResponse = heredoc.Doc(`
{
"id": 10190772,
"user_id": 21989300,
"name": "my-group-token",
"scopes": [
"read_registry",
"read_repository"
],
"created_at": "2024-07-08T17:33:34.829Z",
"expires_at": "2024-08-07",
"last_used_at": null,
"active": true,
"revoked": false,
"token": "glpat-yz2791KMU-xxxxxxxxx",
"access_level": 30
}`)
func TestRotateGroupAccessTokenAsJSON(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/groups/GROUP/access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", groupAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/groups/GROUP/access_tokens/10190772/rotate",
httpmock.NewStringResponse(http.StatusOK, groupAccessTokenResponse))
output, err := runCommand(fakeHTTP, "--group GROUP my-group-token --output json")
if err != nil {
t.Error(err)
return
}
var expect interface{}
var actual interface{}
if err := json.Unmarshal([]byte(groupAccessTokenResponse), &expect); err != nil {
t.Error(err)
}
if err := json.Unmarshal([]byte(output.String()), &actual); err != nil {
t.Error(err)
}
assert.Equal(t, expect, actual)
assert.Empty(t, output.Stderr())
}
func TestRotateGroupAccessTokenAsText(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/groups/GROUP/access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", groupAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/groups/GROUP/access_tokens/10190772/rotate",
httpmock.NewStringResponse(http.StatusOK, groupAccessTokenResponse))
output, err := runCommand(fakeHTTP, "--group GROUP my-group-token")
if err != nil {
t.Error(err)
return
}
assert.Equal(t, "glpat-yz2791KMU-xxxxxxxxx\n", output.String())
}
var projectAccessTokenResponse = heredoc.Doc(`
{
"id": 10191548,
"user_id": 21990679,
"name": "my-project-token",
"scopes": [
"api",
"read_repository"
],
"created_at": "2024-07-08T19:47:14.727Z",
"last_used_at": null,
"expires_at": "2024-08-07",
"active": true,
"revoked": false,
"token": "glpat-dfsdfjksjdfslkdfjsd",
"access_level": 30
}`)
func TestRotateProjectAccessTokenAsJSON(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/projects/OWNER/REPO/access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", projectAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/access_tokens/10191548/rotate",
httpmock.NewStringResponse(http.StatusOK, projectAccessTokenResponse))
output, err := runCommand(fakeHTTP, "--output json my-project-token")
if err != nil {
t.Error(err)
return
}
var expect interface{}
var actual interface{}
if err := json.Unmarshal([]byte(projectAccessTokenResponse), &expect); err != nil {
t.Error(err)
}
if err := json.Unmarshal([]byte(output.String()), &actual); err != nil {
t.Error(err)
}
assert.Equal(t, expect, actual)
assert.Empty(t, output.Stderr())
}
func TestRotateProjectAccessTokenAsText(t *testing.T) {
fakeHTTP := &httpmock.Mocker{}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/projects/OWNER/REPO/access_tokens",
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf("[%s]", projectAccessTokenResponse)))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/access_tokens/10191548/rotate",
httpmock.NewStringResponse(http.StatusOK, projectAccessTokenResponse))
output, err := runCommand(fakeHTTP, "my-project-token")
if err != nil {
t.Error(err)
return
}
assert.Equal(t, "glpat-dfsdfjksjdfslkdfjsd\n", output.String())
}
......@@ -5,6 +5,7 @@ import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/token/create"
"gitlab.com/gitlab-org/cli/commands/token/revoke"
"gitlab.com/gitlab-org/cli/commands/token/rotate"
)
func NewTokenCmd(f *cmdutils.Factory) *cobra.Command {
......@@ -17,5 +18,6 @@ func NewTokenCmd(f *cmdutils.Factory) *cobra.Command {
cmdutils.EnableRepoOverride(cmd, f)
cmd.AddCommand(create.NewCmdCreate(f, nil))
cmd.AddCommand(revoke.NewCmdRevoke(f, nil))
cmd.AddCommand(rotate.NewCmdRotate(f, nil))
return cmd
}
......@@ -35,3 +35,4 @@ token
- [`create`](create.md)
- [`revoke`](revoke.md)
- [`rotate`](rotate.md)
---
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab token rotate`
Rotate user, group, or project access tokens
## Synopsis
Rotate user, group, or project access token, then print the new token on stdout. If multiple tokens with
the same name exist, you can specify the ID of the token.
The expiration date of the token will be calculated by adding the duration (default 30 days) to the
current date. Alternatively you can specify a different duration or an explicit end date.
The output format can be either "JSON" or "text". The JSON output will show the meta information of the
rotated token.
Administrators can rotate personal access tokens belonging to other users.
```plaintext
glab token rotate <token-name|token-id> [flags]
```
## Aliases
```plaintext
rotate
rot
```
## Examples
```plaintext
# Rotate project access token of current project
glab token rotate my-project-token
# Rotate project access token of another project, set to expiration date
glab token rotate --repo user/repo my-project-token --expires-at 2024-08-08
# Rotate group access token
glab token rotate --group group/sub-group my-group-token
# Rotate personal access token and extend duration to 7 days
glab token rotate --user @me --duration $((7 * 24))h my-personal-token
# Rotate a personal access token of another user (administrator only)
glab token rotate --user johndoe johns-personal-token
```
## Options
```plaintext
-D, --duration duration Sets the token duration, in hours. Maximum of 8760. Examples: 24h, 168h, 504h. (default 720h0m0s)
-E, --expires-at DATE Sets the token's expiration date and time, in YYYY-MM-DD format. If not specified, --duration is used. (default 0001-01-01)
-g, --group string Rotate group access token. Ignored if a user or repository argument is set.
-F, --output string Format output as: text, json. 'text' provides the new token value; 'json' outputs the token with metadata. (default "text")
-R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL.
-U, --user string Rotate personal access token. Use @me for the current user.
```
## Options inherited from parent commands
```plaintext
--help Show help for this command.
```
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment