Skip to content

Commit

Permalink
Support configuration variables on Gitea Actions (#24724)
Browse files Browse the repository at this point in the history
Co-Author: @silverwind @wxiaoguang 
Replace: #24404

See:
- [defining configuration variables for multiple
workflows](https://docs.github.com/en/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows)
- [vars
context](https://docs.github.com/en/actions/learn-github-actions/contexts#vars-context)

Related to:
- [x] protocol: https://gitea.com/gitea/actions-proto-def/pulls/7
- [x] act_runner: https://gitea.com/gitea/act_runner/pulls/157
- [x] act: https://gitea.com/gitea/act/pulls/43

#### Screenshoot
Create Variable:

![image](https://user-images.githubusercontent.com/33891828/236758288-032b7f64-44e7-48ea-b07d-de8b8b0e3729.png)


![image](https://user-images.githubusercontent.com/33891828/236758174-5203f64c-1d0e-4737-a5b0-62061dee86f8.png)

Workflow:
```yaml
  test_vars:
    runs-on: ubuntu-latest
    steps:
      - name: Print Custom Variables
        run: echo "${{ vars.test_key }}"
      - name: Try to print a non-exist var
        run: echo "${{ vars.NON_EXIST_VAR }}"
```

Actions Log:

![image](https://user-images.githubusercontent.com/33891828/236759075-af0c5950-368d-4758-a8ac-47a96e43b6e2.png)

---
This PR just implement the org / user (depends on the owner of the
current repository) and repo level variables, The Environment level
variables have not been implemented.
Because
[Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#about-environments)
is a module separate from `Actions`. Maybe it would be better to create
a new PR to do it.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
  • Loading branch information
4 people committed Jun 20, 2023
1 parent 8220e50 commit 35a653d
Show file tree
Hide file tree
Showing 22 changed files with 680 additions and 135 deletions.
97 changes: 97 additions & 0 deletions models/actions/variable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"context"
"errors"
"fmt"
"strings"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"

"xorm.io/builder"
)

type ActionVariable struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
Data string `xorm:"LONGTEXT NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

func init() {
db.RegisterModel(new(ActionVariable))
}

func (v *ActionVariable) Validate() error {
if v.OwnerID == 0 && v.RepoID == 0 {
return errors.New("the variable is not bound to any scope")
}
return nil
}

func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
variable := &ActionVariable{
OwnerID: ownerID,
RepoID: repoID,
Name: strings.ToUpper(name),
Data: data,
}
if err := variable.Validate(); err != nil {
return variable, err
}
return variable, db.Insert(ctx, variable)
}

type FindVariablesOpts struct {
db.ListOptions
OwnerID int64
RepoID int64
}

func (opts *FindVariablesOpts) toConds() builder.Cond {
cond := builder.NewCond()
if opts.OwnerID > 0 {
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
}
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
return cond
}

func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
var variables []*ActionVariable
sess := db.GetEngine(ctx)
if opts.PageSize != 0 {
sess = db.SetSessionPagination(sess, &opts.ListOptions)
}
return variables, sess.Where(opts.toConds()).Find(&variables)
}

func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
var variable ActionVariable
has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
}
return &variable, nil
}

func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
Update(&ActionVariable{
Name: variable.Name,
Data: variable.Data,
})
return count != 0, err
}
3 changes: 3 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ var migrations = []Migration{

// v260 -> v261
NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner),

// v261 -> v262
NewMigration("Add variable table", v1_21.CreateVariableTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
24 changes: 24 additions & 0 deletions models/migrations/v1_21/v261.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_21 //nolint

import (
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func CreateVariableTable(x *xorm.Engine) error {
type ActionVariable struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
Data string `xorm:"LONGTEXT NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

return x.Sync(new(ActionVariable))
}
42 changes: 4 additions & 38 deletions models/secret/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,17 @@ package secret

import (
"context"
"fmt"
"regexp"
"errors"
"strings"

"code.gitea.io/gitea/models/db"
secret_module "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"

"xorm.io/builder"
)

type ErrSecretInvalidValue struct {
Name *string
Data *string
}

func (err ErrSecretInvalidValue) Error() string {
if err.Name != nil {
return fmt.Sprintf("secret name %q is invalid", *err.Name)
}
if err.Data != nil {
return fmt.Sprintf("secret data %q is invalid", *err.Data)
}
return util.ErrInvalidArgument.Error()
}

func (err ErrSecretInvalidValue) Unwrap() error {
return util.ErrInvalidArgument
}

// Secret represents a secret
type Secret struct {
ID int64
Expand Down Expand Up @@ -74,24 +53,11 @@ func init() {
db.RegisterModel(new(Secret))
}

var (
secretNameReg = regexp.MustCompile("^[A-Z_][A-Z0-9_]*$")
forbiddenSecretPrefixReg = regexp.MustCompile("^GIT(EA|HUB)_")
)

// Validate validates the required fields and formats.
func (s *Secret) Validate() error {
switch {
case len(s.Name) == 0 || len(s.Name) > 50:
return ErrSecretInvalidValue{Name: &s.Name}
case len(s.Data) == 0:
return ErrSecretInvalidValue{Data: &s.Data}
case !secretNameReg.MatchString(s.Name) ||
forbiddenSecretPrefixReg.MatchString(s.Name):
return ErrSecretInvalidValue{Name: &s.Name}
default:
return nil
if s.OwnerID == 0 && s.RepoID == 0 {
return errors.New("the secret is not bound to any scope")
}
return nil
}

type FindSecretsOptions struct {
Expand Down
21 changes: 19 additions & 2 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ show_full_screen = Show full screen

confirm_delete_selected = Confirm to delete all selected items?

name = Name
value = Value

[aria]
navbar = Navigation Bar
footer = Footer
Expand Down Expand Up @@ -3391,8 +3394,6 @@ owner.settings.chef.keypair.description = Generate a key pair used to authentica
secrets = Secrets
description = Secrets will be passed to certain actions and cannot be read otherwise.
none = There are no secrets yet.
value = Value
name = Name
creation = Add Secret
creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_
creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted.
Expand Down Expand Up @@ -3462,6 +3463,22 @@ runs.no_matching_runner_helper = No matching runner: %s
need_approval_desc = Need approval to run workflows for fork pull request.
variables = Variables
variables.management = Variables Management
variables.creation = Add Variable
variables.none = There are no variables yet.
variables.deletion = Remove variable
variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue?
variables.description = Variables will be passed to certain actions and cannot be read otherwise.
variables.id_not_exist = Variable with id %d not exists.
variables.edit = Edit Variable
variables.deletion.failed = Failed to remove variable.
variables.deletion.success = The variable has been removed.
variables.creation.failed = Failed to add variable.
variables.creation.success = The variable "%s" has been added.
variables.update.failed = Failed to edit variable.
variables.update.success = The variable has been edited.
[projects]
type-1.display_name = Individual Project
type-2.display_name = Repository Project
Expand Down
24 changes: 24 additions & 0 deletions routers/api/actions/runner/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
WorkflowPayload: t.Job.WorkflowPayload,
Context: generateTaskContext(t),
Secrets: getSecretsOfTask(ctx, t),
Vars: getVariablesOfTask(ctx, t),
}

if needs, err := findTaskNeeds(ctx, t); err != nil {
Expand Down Expand Up @@ -88,6 +89,29 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s
return secrets
}

func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
variables := map[string]string{}

// Org / User level
ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
if err != nil {
log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
}

// Repo level
repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
if err != nil {
log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
}

// Level precedence: Repo > Org / User
for _, v := range append(ownerVariables, repoVariables...) {
variables[v.Name] = v.Data
}

return variables
}

func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
event := map[string]interface{}{}
_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
Expand Down
6 changes: 6 additions & 0 deletions routers/web/repo/setting/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ func SecretsPost(ctx *context.Context) {
ctx.ServerError("getSecretsCtx", err)
return
}

if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}

shared.PerformSecretsPost(
ctx,
sCtx.OwnerID,
Expand Down
Loading

0 comments on commit 35a653d

Please sign in to comment.