mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946)
Fix #23703 When Gitea starts, it reads GITEA_RUNNER_REGISTRATION_TOKEN or GITEA_RUNNER_REGISTRATION_TOKEN_FILE to add registration token.
This commit is contained in:
		@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	} else if !has {
 | 
						} else if !has {
 | 
				
			||||||
		return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
 | 
							return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &runnerToken, nil
 | 
						return &runnerToken, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
 | 
				
			|||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewRunnerToken creates a new active runner token and invalidate all old tokens
 | 
					// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
 | 
				
			||||||
// ownerID will be ignored and treated as 0 if repoID is non-zero.
 | 
					// ownerID will be ignored and treated as 0 if repoID is non-zero.
 | 
				
			||||||
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
 | 
					func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
 | 
				
			||||||
	if ownerID != 0 && repoID != 0 {
 | 
						if ownerID != 0 && repoID != 0 {
 | 
				
			||||||
		// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
 | 
							// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
 | 
				
			||||||
		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 | 
							// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 | 
				
			||||||
		ownerID = 0
 | 
							ownerID = 0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	token, err := util.CryptoRandomString(40)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	runnerToken := &ActionRunnerToken{
 | 
						runnerToken := &ActionRunnerToken{
 | 
				
			||||||
		OwnerID:  ownerID,
 | 
							OwnerID:  ownerID,
 | 
				
			||||||
		RepoID:   repoID,
 | 
							RepoID:   repoID,
 | 
				
			||||||
@@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
 | 
				
			|||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		_, err = db.GetEngine(ctx).Insert(runnerToken)
 | 
							_, err := db.GetEngine(ctx).Insert(runnerToken)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
 | 
				
			||||||
 | 
						token, err := util.CryptoRandomString(40)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetLatestRunnerToken returns the latest runner token
 | 
					// GetLatestRunnerToken returns the latest runner token
 | 
				
			||||||
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
 | 
					func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
 | 
				
			||||||
	if ownerID != 0 && repoID != 0 {
 | 
						if ownerID != 0 && repoID != 0 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3722,6 +3722,7 @@ runners.status.active = Active
 | 
				
			|||||||
runners.status.offline = Offline
 | 
					runners.status.offline = Offline
 | 
				
			||||||
runners.version = Version
 | 
					runners.version = Version
 | 
				
			||||||
runners.reset_registration_token = Reset registration token
 | 
					runners.reset_registration_token = Reset registration token
 | 
				
			||||||
 | 
					runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one?
 | 
				
			||||||
runners.reset_registration_token_success = Runner registration token reset successfully
 | 
					runners.reset_registration_token_success = Runner registration token reset successfully
 | 
				
			||||||
 | 
					
 | 
				
			||||||
runs.all_workflows = All Workflows
 | 
					runs.all_workflows = All Workflows
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) {
 | 
				
			|||||||
	auth.Init()
 | 
						auth.Init()
 | 
				
			||||||
	mustInit(svg.Init)
 | 
						mustInit(svg.Init)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions_service.Init()
 | 
						mustInitCtx(ctx, actions_service.Init)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mustInit(repo_service.InitLicenseClassifier)
 | 
						mustInit(repo_service.InitLicenseClassifier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
 | 
				
			|||||||
		ctx.ServerError("ResetRunnerRegistrationToken", err)
 | 
							ctx.ServerError("ResetRunnerRegistrationToken", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
 | 
						ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
 | 
				
			||||||
	ctx.Redirect(redirectTo)
 | 
						ctx.JSONRedirect(redirectTo)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RunnerDeletePost response for deleting a runner
 | 
					// RunnerDeletePost response for deleting a runner
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) {
 | 
				
			|||||||
			m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
 | 
								m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
 | 
				
			||||||
				Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
 | 
									Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
 | 
				
			||||||
			m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
 | 
								m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
 | 
				
			||||||
			m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
 | 
								m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,23 +4,68 @@
 | 
				
			|||||||
package actions
 | 
					package actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						actions_model "code.gitea.io/gitea/models/actions"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/graceful"
 | 
						"code.gitea.io/gitea/modules/graceful"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/queue"
 | 
						"code.gitea.io/gitea/modules/queue"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
						notify_service "code.gitea.io/gitea/services/notify"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Init() {
 | 
					func initGlobalRunnerToken(ctx context.Context) error {
 | 
				
			||||||
 | 
						// use the same env name as the runner, for consistency
 | 
				
			||||||
 | 
						token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN")
 | 
				
			||||||
 | 
						tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE")
 | 
				
			||||||
 | 
						if token != "" && tokenFile != "" {
 | 
				
			||||||
 | 
							return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if tokenFile != "" {
 | 
				
			||||||
 | 
							file, err := os.ReadFile(tokenFile)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							token = strings.TrimSpace(string(file))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if token == "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(token) < 32 {
 | 
				
			||||||
 | 
							return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						existing, err := actions_model.GetRunnerToken(ctx, token)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
				
			||||||
 | 
							return fmt.Errorf("unable to check existing token: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if existing != nil {
 | 
				
			||||||
 | 
							if !existing.IsActive {
 | 
				
			||||||
 | 
								log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Init(ctx context.Context) error {
 | 
				
			||||||
	if !setting.Actions.Enabled {
 | 
						if !setting.Actions.Enabled {
 | 
				
			||||||
		return
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
 | 
						jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
 | 
				
			||||||
	if jobEmitterQueue == nil {
 | 
						if jobEmitterQueue == nil {
 | 
				
			||||||
		log.Fatal("Unable to create actions_ready_job queue")
 | 
							return errors.New("unable to create actions_ready_job queue")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	go graceful.GetManager().RunWithCancel(jobEmitterQueue)
 | 
						go graceful.GetManager().RunWithCancel(jobEmitterQueue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	notify_service.RegisterNotifier(NewNotifier())
 | 
						notify_service.RegisterNotifier(NewNotifier())
 | 
				
			||||||
 | 
						return initGlobalRunnerToken(ctx)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								services/actions/init_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								services/actions/init_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						actions_model "code.gitea.io/gitea/models/actions"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMain(m *testing.M) {
 | 
				
			||||||
 | 
						unittest.MainTest(m, &unittest.TestOptions{
 | 
				
			||||||
 | 
							FixtureFiles: []string{"action_runner_token.yml"},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						os.Exit(m.Run())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestInitToken(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("NoToken", func(t *testing.T) {
 | 
				
			||||||
 | 
							_, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token")
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
 | 
				
			||||||
 | 
							err := initGlobalRunnerToken(db.DefaultContext)
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{})
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							assert.False(t, notEmpty)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("EnvToken", func(t *testing.T) {
 | 
				
			||||||
 | 
							tokenValue, _ := util.CryptoRandomString(32)
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue)
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
 | 
				
			||||||
 | 
							err := initGlobalRunnerToken(db.DefaultContext)
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
 | 
				
			||||||
 | 
							assert.True(t, token.IsActive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// init with the same token again, should not create a new token
 | 
				
			||||||
 | 
							err = initGlobalRunnerToken(db.DefaultContext)
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
 | 
				
			||||||
 | 
							assert.Equal(t, token.ID, token2.ID)
 | 
				
			||||||
 | 
							assert.True(t, token.IsActive)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("EnvFileToken", func(t *testing.T) {
 | 
				
			||||||
 | 
							tokenValue, _ := util.CryptoRandomString(32)
 | 
				
			||||||
 | 
							f := t.TempDir() + "/token"
 | 
				
			||||||
 | 
							_ = os.WriteFile(f, []byte(tokenValue), 0o644)
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f)
 | 
				
			||||||
 | 
							err := initGlobalRunnerToken(db.DefaultContext)
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
 | 
				
			||||||
 | 
							assert.True(t, token.IsActive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if the env token is invalidated by another new token, then it shouldn't be active anymore
 | 
				
			||||||
 | 
							_, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0)
 | 
				
			||||||
 | 
							require.NoError(t, err)
 | 
				
			||||||
 | 
							token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
 | 
				
			||||||
 | 
							assert.False(t, token.IsActive)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("InvalidToken", func(t *testing.T) {
 | 
				
			||||||
 | 
							t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc")
 | 
				
			||||||
 | 
							err := initGlobalRunnerToken(db.DefaultContext)
 | 
				
			||||||
 | 
							assert.ErrorContains(t, err, "must be at least")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
	<h4 class="ui top attached header">
 | 
						<h4 class="ui top attached header">
 | 
				
			||||||
		{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 | 
							{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 | 
				
			||||||
		<div class="ui right">
 | 
							<div class="ui right">
 | 
				
			||||||
			<div class="ui top right pointing dropdown">
 | 
								<div class="ui top right pointing dropdown jump">
 | 
				
			||||||
				<button class="ui primary tiny button">
 | 
									<button class="ui primary tiny button">
 | 
				
			||||||
					{{ctx.Locale.Tr "actions.runners.new"}}
 | 
										{{ctx.Locale.Tr "actions.runners.new"}}
 | 
				
			||||||
					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
										{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
				
			||||||
@@ -17,14 +17,18 @@
 | 
				
			|||||||
						Registration Token
 | 
											Registration Token
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<div class="ui input">
 | 
										<div class="ui input">
 | 
				
			||||||
						<input type="text" value="{{.RegistrationToken}}">
 | 
											<input type="text" value="{{.RegistrationToken}}" readonly>
 | 
				
			||||||
						<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
 | 
											<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
 | 
				
			||||||
							{{svg "octicon-copy" 14}}
 | 
												{{svg "octicon-copy" 14}}
 | 
				
			||||||
						</button>
 | 
											</button>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<div class="divider"></div>
 | 
										<div class="divider"></div>
 | 
				
			||||||
					<div class="item">
 | 
										<div class="item">
 | 
				
			||||||
						<a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a>
 | 
											<a class="link-action" data-url="{{$.Link}}/reset_registration_token"
 | 
				
			||||||
 | 
												data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
 | 
				
			||||||
 | 
											</a>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user