[CLI] implement forgejo-cli

(cherry picked from commit 2555e315f7561302484b15576d34c5da0d4cdb12)
(cherry picked from commit 51b9c9092e21a451695ee0154e7d49753574f525)

[CLI] implement forgejo-cli (squash) support initDB

(cherry picked from commit 5c31ae602a45f1d9a90b86bece5393bc9faddf25)
(cherry picked from commit bbf76489a73bad83d68ca7c8e7a75cf8e27b2198)

Conflicts:
	because of d0dbe52e76
	upgrade to https://pkg.go.dev/github.com/urfave/cli/v2
(cherry picked from commit b6c1bcc008fcff0e297d570a0069bf41bc74e53d)

[CLI] implement forgejo-cli actions

(cherry picked from commit 08be2b226e46d9f41e08f66e936b317bcfb4a257)
(cherry picked from commit b6cfa88c6e2ae00e30c832ce4cf93c9e3f2cd6e4)
(cherry picked from commit 59704200de59b65a4f37c39569a3b43e1ee38862)

[CLI] implement forgejo-cli actions generate-secret

(cherry picked from commit 6f7905c8ecf17d5f74ac9a71a453d6768c212b6d)
(cherry picked from commit e085d6d2737e6238a4ff00f19f40cf839ac16b34)

[CLI] implement forgejo-cli actions generate-secret (squash) NoInit

(cherry picked from commit 962c944eb20268a394030495c3caab3e3d4bd8b7)

[CLI] implement forgejo-cli actions register

(cherry picked from commit 2f95143000e4ccc94ef14332777b58fe778edbd6)
(cherry picked from commit 42f2f8731e876564b6627a43a248f262f50c04cd)

[CLI] implement forgejo-cli actions register (squash) no private

Do not go through the private API, directly modify the database

(cherry picked from commit 1ba7c0d39d0ecd190b7d9c517bd26af6c84341aa)

[CLI] implement forgejo-cli actions

(cherry picked from commit 6f7905c8ecf17d5f74ac9a71a453d6768c212b6d)
(cherry picked from commit e085d6d2737e6238a4ff00f19f40cf839ac16b34)

[CLI] implement forgejo-cli actions generate-secret (squash) NoInit

(cherry picked from commit 962c944eb20268a394030495c3caab3e3d4bd8b7)
(cherry picked from commit 4c121ef022597e66d902c17e0f46839c26924b18)

Conflicts:
	cmd/forgejo/actions.go
	tests/integration/cmd_forgejo_actions_test.go
(cherry picked from commit 36997a48e38286579850abe4b55e75a235b56537)

[CLI] implement forgejo-cli actions (squash) restore --version

Refs: https://codeberg.org/forgejo/forgejo/issues/1134
(cherry picked from commit 9739eb52d8f94d32f61068d7209958e8d2582818)

[CI] implement forgejo-cli (squash) the actions subcommand needs config

(cherry picked from commit def638475122a26082ab3835842c84cd03839154)

Conflicts:
	cmd/main.go
	https://codeberg.org/forgejo/forgejo/pulls/1209
(cherry picked from commit a1758a391043123903607338cb11490161ac946d)
(cherry picked from commit 935fa650c77b151752a58f621d846b166b97cd79)
(cherry picked from commit cd21026bc94922043dce8e2a5baba68111d1e569)
(cherry picked from commit 1700b8973a58f0fc3469492d8a39b931019d2461)
(cherry picked from commit 1def42a37945cfe88947803f9afe9468fb8798fe)
(cherry picked from commit 839d97521d59a012b06e6c2b9b0655c56b41b6cd)
(cherry picked from commit fd8c13be6b45f9aa939be482c0a4e5a60c89344c)
(cherry picked from commit 588e5d552f044d91218a07fa46e84259d4892c5d)
(cherry picked from commit 151a726620f662ff9af37316dfda38a6bd6744bb)
(cherry picked from commit a93f3689a831e77f79499c8ea8565309a9158f24)
(cherry picked from commit 1e7bd54b2811c1def868c8cece0fc5c348d9011f)
(cherry picked from commit 7f6015382f7b4e59ff2ee027f2708d3f4420018b)
(cherry picked from commit 31afac5daf5dc421427f57bd5c883810457559bf)
(cherry picked from commit 6df8a604a52544f45460ab9d661a519aa14c3386)
(cherry picked from commit 912492f603cfd2cff294344f8d909aa614330653)
This commit is contained in:
Earl Warren 2023-07-09 14:52:21 +02:00
parent f67fd7b2e8
commit 16279d7922
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
10 changed files with 825 additions and 2 deletions

243
cmd/forgejo/actions.go Normal file
View file

@ -0,0 +1,243 @@
// Copyright The Forgejo Authors.
// SPDX-License-Identifier: MIT
package forgejo
import (
"context"
"encoding/hex"
"fmt"
"io"
"os"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
private_routers "code.gitea.io/gitea/routers/private"
"github.com/urfave/cli/v2"
)
func CmdActions(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "actions",
Usage: "Commands for managing Forgejo Actions",
Subcommands: []*cli.Command{
SubcmdActionsGenerateRunnerToken(ctx),
SubcmdActionsGenerateRunnerSecret(ctx),
SubcmdActionsRegister(ctx),
},
}
}
func SubcmdActionsGenerateRunnerToken(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "generate-runner-token",
Usage: "Generate a new token for a runner to use to register with the server",
Action: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) }),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "scope",
Aliases: []string{"s"},
Value: "",
Usage: "{owner}[/{repo}] - leave empty for a global runner",
},
},
}
}
func SubcmdActionsGenerateRunnerSecret(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "generate-secret",
Usage: "Generate a secret suitable for input to the register subcommand",
Action: func(cliCtx *cli.Context) error { return RunGenerateSecret(ctx, cliCtx) },
}
}
func SubcmdActionsRegister(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "register",
Usage: "Idempotent registration of a runner using a shared secret",
Action: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) }),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "secret",
Usage: "the secret the runner will use to connect as a 40 character hexadecimal string",
},
&cli.StringFlag{
Name: "secret-stdin",
Usage: "the secret the runner will use to connect as a 40 character hexadecimal string, read from stdin",
},
&cli.StringFlag{
Name: "secret-file",
Usage: "path to the file containing the secret the runner will use to connect as a 40 character hexadecimal string",
},
&cli.StringFlag{
Name: "scope",
Aliases: []string{"s"},
Value: "",
Usage: "{owner}[/{repo}] - leave empty for a global runner",
},
&cli.StringFlag{
Name: "labels",
Value: "",
Usage: "comma separated list of labels supported by the runner (e.g. docker,ubuntu-latest,self-hosted) (not required since v1.21)",
},
&cli.StringFlag{
Name: "name",
Value: "runner",
Usage: "name of the runner (default runner)",
},
&cli.StringFlag{
Name: "version",
Value: "",
Usage: "version of the runner (not required since v1.21)",
},
},
}
}
func readSecret(ctx context.Context, cliCtx *cli.Context) (string, error) {
if cliCtx.IsSet("secret") {
return cliCtx.String("secret"), nil
}
if cliCtx.IsSet("secret-stdin") {
buf, err := io.ReadAll(ContextGetStdin(ctx))
if err != nil {
return "", err
}
return string(buf), nil
}
if cliCtx.IsSet("secret-file") {
path := cliCtx.String("secret-file")
buf, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(buf), nil
}
return "", fmt.Errorf("at least one of the --secret, --secret-stdin, --secret-file options is required")
}
func validateSecret(secret string) error {
secretLen := len(secret)
if secretLen != 40 {
return fmt.Errorf("the secret must be exactly 40 characters long, not %d: generate-secret can provide a secret matching the requirements", secretLen)
}
if _, err := hex.DecodeString(secret); err != nil {
return fmt.Errorf("the secret must be an hexadecimal string: %w", err)
}
return nil
}
func RunRegister(ctx context.Context, cliCtx *cli.Context) error {
if !ContextGetNoInit(ctx) {
var cancel context.CancelFunc
ctx, cancel = installSignals(ctx)
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
}
setting.MustInstalled()
secret, err := readSecret(ctx, cliCtx)
if err != nil {
return err
}
if err := validateSecret(secret); err != nil {
return err
}
scope := cliCtx.String("scope")
labels := cliCtx.String("labels")
name := cliCtx.String("name")
version := cliCtx.String("version")
//
// There are two kinds of tokens
//
// - "registration token" only used when a runner interacts to
// register
//
// - "token" obtained after a successful registration and stored by
// the runner to authenticate
//
// The register subcommand does not need a "registration token", it
// needs a "token". Using the same name is confusing and secret is
// preferred for this reason in the cli.
//
// The ActionsRunnerRegister argument is token to be consistent with
// the internal naming. It is still confusing to the developer but
// not to the user.
//
owner, repo, err := private_routers.ParseScope(ctx, scope)
if err != nil {
return err
}
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, strings.Split(labels, ","), name, version)
if err != nil {
return fmt.Errorf("error while registering runner: %v", err)
}
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", runner.UUID); err != nil {
panic(err)
}
return nil
}
func RunGenerateSecret(ctx context.Context, cliCtx *cli.Context) error {
runner := actions_model.ActionRunner{}
if err := runner.GenerateToken(); err != nil {
return err
}
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", runner.Token); err != nil {
panic(err)
}
return nil
}
func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) error {
if !ContextGetNoInit(ctx) {
var cancel context.CancelFunc
ctx, cancel = installSignals(ctx)
defer cancel()
}
setting.MustInstalled()
scope := cliCtx.String("scope")
respText, extra := private.GenerateActionsRunnerToken(ctx, scope)
if extra.HasError() {
return handleCliResponseExtra(ctx, extra)
}
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", respText); err != nil {
panic(err)
}
return nil
}
func prepareWorkPathAndCustomConf(ctx context.Context, action cli.ActionFunc) func(cliCtx *cli.Context) error {
return func(cliCtx *cli.Context) error {
if !ContextGetNoInit(ctx) {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
for _, curCtx := range cliCtx.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
}
if curCtx.IsSet("custom-path") && args.CustomPath == "" {
args.CustomPath = curCtx.String("custom-path")
}
if curCtx.IsSet("config") && args.CustomConf == "" {
args.CustomConf = curCtx.String("config")
}
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
}
return action(cliCtx)
}
}

147
cmd/forgejo/forgejo.go Normal file
View file

@ -0,0 +1,147 @@
// Copyright The Forgejo Authors.
// SPDX-License-Identifier: MIT
package forgejo
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"syscall"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2"
)
type key int
const (
noInitKey key = iota + 1
noExitKey
stdoutKey
stderrKey
stdinKey
)
func CmdForgejo(ctx context.Context) *cli.Command {
return &cli.Command{
Name: "forgejo-cli",
Usage: "Forgejo CLI",
Flags: []cli.Flag{},
Subcommands: []*cli.Command{
CmdActions(ctx),
},
}
}
func ContextSetNoInit(ctx context.Context, value bool) context.Context {
return context.WithValue(ctx, noInitKey, value)
}
func ContextGetNoInit(ctx context.Context) bool {
value, ok := ctx.Value(noInitKey).(bool)
return ok && value
}
func ContextSetNoExit(ctx context.Context, value bool) context.Context {
return context.WithValue(ctx, noExitKey, value)
}
func ContextGetNoExit(ctx context.Context) bool {
value, ok := ctx.Value(noExitKey).(bool)
return ok && value
}
func ContextSetStderr(ctx context.Context, value io.Writer) context.Context {
return context.WithValue(ctx, stderrKey, value)
}
func ContextGetStderr(ctx context.Context) io.Writer {
value, ok := ctx.Value(stderrKey).(io.Writer)
if !ok {
return os.Stderr
}
return value
}
func ContextSetStdout(ctx context.Context, value io.Writer) context.Context {
return context.WithValue(ctx, stdoutKey, value)
}
func ContextGetStdout(ctx context.Context) io.Writer {
value, ok := ctx.Value(stderrKey).(io.Writer)
if !ok {
return os.Stdout
}
return value
}
func ContextSetStdin(ctx context.Context, value io.Reader) context.Context {
return context.WithValue(ctx, stdinKey, value)
}
func ContextGetStdin(ctx context.Context) io.Reader {
value, ok := ctx.Value(stdinKey).(io.Reader)
if !ok {
return os.Stdin
}
return value
}
// copied from ../cmd.go
func initDB(ctx context.Context) error {
setting.MustInstalled()
setting.LoadDBSetting()
setting.InitSQLLoggersForCli(log.INFO)
if setting.Database.Type == "" {
log.Fatal(`Database settings are missing from the configuration file: %q.
Ensure you are running in the correct environment or set the correct configuration file with -c.
If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
}
if err := db.InitEngine(ctx); err != nil {
return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err)
}
return nil
}
// copied from ../cmd.go
func installSignals(ctx context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
go func() {
// install notify
signalChannel := make(chan os.Signal, 1)
signal.Notify(
signalChannel,
syscall.SIGINT,
syscall.SIGTERM,
)
select {
case <-signalChannel:
case <-ctx.Done():
}
cancel()
signal.Reset()
}()
return ctx, cancel
}
func handleCliResponseExtra(ctx context.Context, extra private.ResponseExtra) error {
if false && extra.UserMsg != "" {
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", extra.UserMsg); err != nil {
panic(err)
}
}
if ContextGetNoExit(ctx) {
return extra.Error
}
return cli.Exit(extra.Error, 1)
}

View file

@ -4,10 +4,13 @@
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/cmd/forgejo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -119,6 +122,36 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
}
func NewMainApp(version, versionExtra string) *cli.App {
path, err := os.Executable()
if err != nil {
panic(err)
}
executable := filepath.Base(path)
var subCmdsStandalone []*cli.Command = make([]*cli.Command, 0, 10)
var subCmdWithConfig []*cli.Command = make([]*cli.Command, 0, 10)
//
// If the executable is forgejo-cli, provide a Forgejo specific CLI
// that is NOT compatible with Gitea.
//
if executable == "forgejo-cli" {
subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdActions(context.Background()))
} else {
//
// Otherwise provide a Gitea compatible CLI which includes Forgejo
// specific additions under the forgejo-cli subcommand. It allows
// admins to migration from Gitea to Forgejo by replacing the gitea
// binary and rename it to forgejo if they want.
//
subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdForgejo(context.Background()))
subCmdWithConfig = append(subCmdWithConfig, CmdActions)
}
return innerNewMainApp(version, versionExtra, subCmdsStandalone, subCmdWithConfig)
}
func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmdWithConfigArgs []*cli.Command) *cli.App {
app := cli.NewApp()
app.Name = "Gitea"
app.Usage = "A painless self-hosted Git service"
@ -141,13 +174,13 @@ func NewMainApp(version, versionExtra string) *cli.App {
CmdMigrateStorage,
CmdDumpRepository,
CmdRestoreRepository,
CmdActions,
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
}
cmdConvert := util.ToPointer(*cmdDoctorConvert)
cmdConvert.Hidden = true // still support the legacy "./gitea doctor" by the hidden sub-command, remove it in next release
subCmdWithConfig = append(subCmdWithConfig, cmdConvert)
subCmdWithConfig = append(subCmdWithConfig, subCmdWithConfigArgs...)
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{
@ -155,6 +188,7 @@ func NewMainApp(version, versionExtra string) *cli.App {
CmdGenerate,
CmdDocs,
}
subCmdStandalone = append(subCmdStandalone, subCmdsStandaloneArgs...)
app.DefaultCommand = CmdWeb.Name

68
models/actions/forgejo.go Normal file
View file

@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"encoding/hex"
"fmt"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
gouuid "github.com/google/uuid"
)
func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, labels []string, name, version string) (*ActionRunner, error) {
uuid, err := gouuid.FromBytes([]byte(token[:16]))
if err != nil {
return nil, fmt.Errorf("gouuid.FromBytes %v", err)
}
uuidString := uuid.String()
var runner ActionRunner
has, err := db.GetEngine(ctx).Where("uuid=?", uuidString).Get(&runner)
if err != nil {
return nil, fmt.Errorf("GetRunner %v", err)
} else if !has {
//
// The runner does not exist yet, create it
//
saltBytes, err := util.CryptoRandomBytes(16)
if err != nil {
return nil, fmt.Errorf("CryptoRandomBytes %v", err)
}
salt := hex.EncodeToString(saltBytes)
hash := auth_model.HashToken(token, salt)
runner = ActionRunner{
UUID: uuidString,
TokenHash: hash,
TokenSalt: salt,
}
if err := CreateRunner(ctx, &runner); err != nil {
return &runner, fmt.Errorf("can't create new runner %w", err)
}
}
//
// Update the existing runner
//
name, _ = util.SplitStringAtByteN(name, 255)
runner.Name = name
runner.OwnerID = ownerID
runner.RepoID = repoID
runner.Version = version
runner.AgentLabels = labels
if err := UpdateRunner(ctx, &runner, "name", "owner_id", "repo_id", "version", "agent_labels"); err != nil {
return &runner, fmt.Errorf("can't update the runner %+v %w", runner, err)
}
return &runner, nil
}

View file

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
package actions
import (
"crypto/subtle"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestActions_RegisterRunner(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ownerID := int64(0)
repoID := int64(0)
token := "0123456789012345678901234567890123456789"
labels := []string{}
name := "runner"
version := "v1.2.3"
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, labels, name, version)
assert.NoError(t, err)
assert.EqualValues(t, name, runner.Name)
assert.EqualValues(t, 1, subtle.ConstantTimeCompare([]byte(runner.TokenHash), []byte(auth_model.HashToken(token, runner.TokenSalt))), "the token cannot be verified with the same method as routers/api/actions/runner/interceptor.go as of 8228751c55d6a4263f0fec2932ca16181c09c97d")
}

View file

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
package actions_test
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", ".."),
})
}

View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
package private
import (
"context"
"code.gitea.io/gitea/modules/setting"
)
type ActionsRunnerRegisterRequest struct {
Token string
Scope string
Labels []string
Name string
Version string
}
func ActionsRunnerRegister(ctx context.Context, token, scope string, labels []string, name, version string) (string, ResponseExtra) {
reqURL := setting.LocalURL + "api/internal/actions/register"
req := newInternalRequest(ctx, reqURL, "POST", ActionsRunnerRegisterRequest{
Token: token,
Scope: scope,
Labels: labels,
Name: name,
Version: version,
})
resp, extra := requestJSONResp(req, &responseText{})
return resp.Text, extra
}

View file

@ -4,6 +4,7 @@
package private
import (
gocontext "context"
"errors"
"fmt"
"net/http"
@ -64,7 +65,11 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
ctx.PlainText(http.StatusOK, token.Token)
}
func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int64, err error) {
func ParseScope(ctx gocontext.Context, scope string) (ownerID, repoID int64, err error) {
return parseScope(ctx, scope)
}
func parseScope(ctx gocontext.Context, scope string) (ownerID, repoID int64, err error) {
ownerID = 0
repoID = 0
if scope == "" {

View file

@ -0,0 +1,209 @@
// SPDX-License-Identifier: MIT
package integration
import (
gocontext "context"
"net/url"
"os"
"strings"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func Test_CmdForgejo_Actions(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
token, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-runner-token"})
assert.NoError(t, err)
assert.EqualValues(t, 40, len(token))
secret, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-secret"})
assert.NoError(t, err)
assert.EqualValues(t, 40, len(secret))
_, err = cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "register"})
assert.ErrorContains(t, err, "at least one of the --secret")
for _, testCase := range []struct {
testName string
scope string
secret string
errorMessage string
}{
{
testName: "bad user",
scope: "baduser",
secret: "0123456789012345678901234567890123456789",
errorMessage: "user does not exist",
},
{
testName: "bad repo",
scope: "org25/badrepo",
secret: "0123456789012345678901234567890123456789",
errorMessage: "repository does not exist",
},
{
testName: "secret length != 40",
scope: "org25",
secret: "0123456789",
errorMessage: "40 characters long",
},
{
testName: "secret is not a hexadecimal string",
scope: "org25",
secret: "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
errorMessage: "must be an hexadecimal string",
},
} {
t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{"forgejo", "forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope}
output, err := cmdForgejoCaptureOutput(t, cmd)
assert.ErrorContains(t, err, testCase.errorMessage)
assert.EqualValues(t, "", output)
})
}
secret = "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
expecteduuid := "44444444-4444-4444-4444-444444444444"
for _, testCase := range []struct {
testName string
secretOption func() string
stdin []string
}{
{
testName: "secret from argument",
secretOption: func() string {
return "--secret=" + secret
},
},
{
testName: "secret from stdin",
secretOption: func() string {
return "--secret-stdin"
},
stdin: []string{secret},
},
{
testName: "secret from file",
secretOption: func() string {
secretFile := t.TempDir() + "/secret"
assert.NoError(t, os.WriteFile(secretFile, []byte(secret), 0o644))
return "--secret-file=" + secretFile
},
},
} {
t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{"forgejo", "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26"}
uuid, err := cmdForgejoCaptureOutput(t, cmd, testCase.stdin...)
assert.NoError(t, err)
assert.EqualValues(t, expecteduuid, uuid)
})
}
secret = "0123456789012345678901234567890123456789"
expecteduuid = "30313233-3435-3637-3839-303132333435"
for _, testCase := range []struct {
testName string
scope string
secret string
name string
labels string
version string
uuid string
}{
{
testName: "org",
scope: "org25",
secret: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
uuid: "41414141-4141-4141-4141-414141414141",
},
{
testName: "user and repo",
scope: "user2/repo2",
secret: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
uuid: "42424242-4242-4242-4242-424242424242",
},
{
testName: "labels",
scope: "org25",
name: "runnerName",
labels: "label1,label2,label3",
version: "v1.2.3",
secret: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
uuid: "43434343-4343-4343-4343-434343434343",
},
{
testName: "insert a runner",
scope: "user10/repo6",
name: "runnerName",
labels: "label1,label2,label3",
version: "v1.2.3",
secret: secret,
uuid: expecteduuid,
},
{
testName: "update an existing runner",
scope: "user5/repo4",
name: "runnerNameChanged",
labels: "label1,label2,label3,more,label",
version: "v1.2.3-suffix",
secret: secret,
uuid: expecteduuid,
},
} {
t.Run(testCase.testName, func(t *testing.T) {
cmd := []string{
"forgejo", "forgejo-cli", "actions", "register",
"--secret", testCase.secret, "--scope", testCase.scope,
}
if testCase.name != "" {
cmd = append(cmd, "--name", testCase.name)
}
if testCase.labels != "" {
cmd = append(cmd, "--labels", testCase.labels)
}
if testCase.version != "" {
cmd = append(cmd, "--version", testCase.version)
}
//
// Run twice to verify it is idempotent
//
for i := 0; i < 2; i++ {
uuid, err := cmdForgejoCaptureOutput(t, cmd)
assert.NoError(t, err)
if assert.EqualValues(t, testCase.uuid, uuid) {
ownerName, repoName, found := strings.Cut(testCase.scope, "/")
action, err := actions_model.GetRunnerByUUID(gocontext.Background(), uuid)
assert.NoError(t, err)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: action.OwnerID})
assert.Equal(t, ownerName, user.Name, action.OwnerID)
if found {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: action.RepoID})
assert.Equal(t, repoName, repo.Name, action.RepoID)
}
if testCase.name != "" {
assert.EqualValues(t, testCase.name, action.Name)
}
if testCase.labels != "" {
labels := strings.Split(testCase.labels, ",")
assert.EqualValues(t, labels, action.AgentLabels)
}
if testCase.version != "" {
assert.EqualValues(t, testCase.version, action.Version)
}
}
}
})
}
})
}

View file

@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
package integration
import (
"bytes"
"context"
"strings"
"testing"
"code.gitea.io/gitea/cmd/forgejo"
"github.com/urfave/cli/v2"
)
func cmdForgejoCaptureOutput(t *testing.T, args []string, stdin ...string) (string, error) {
buf := new(bytes.Buffer)
app := cli.NewApp()
app.Writer = buf
app.ErrWriter = buf
ctx := context.Background()
ctx = forgejo.ContextSetNoInit(ctx, true)
ctx = forgejo.ContextSetNoExit(ctx, true)
ctx = forgejo.ContextSetStdout(ctx, buf)
ctx = forgejo.ContextSetStderr(ctx, buf)
if len(stdin) > 0 {
ctx = forgejo.ContextSetStdin(ctx, strings.NewReader(strings.Join(stdin, "")))
}
app.Commands = []*cli.Command{
forgejo.CmdForgejo(ctx),
}
err := app.Run(args)
return buf.String(), err
}