Skip to content

Commit c400a49

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Clear up old Actions logs (go-gitea#31735) Fix createElementFromAttrs bug (go-gitea#31751) bump vue-bar-graph (go-gitea#31705) Use UTC as default timezone when schedule Actions cron tasks (go-gitea#31742) Add permission description for API to add repo collaborator (go-gitea#31744) Clarify Actions resources ownership (go-gitea#31724) Exclude protected branches from recently pushed (go-gitea#31748) [skip ci] Updated translations via Crowdin Distinguish LFS object errors to ignore missing objects during migration (go-gitea#31702)
2 parents f387831 + 687c118 commit c400a49

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+531
-184
lines changed

custom/conf/app.example.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2684,6 +2684,8 @@ LEVEL = Info
26842684
;;
26852685
;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance.
26862686
;DEFAULT_ACTIONS_URL = github
2687+
;; Logs retention time in days. Old logs will be deleted after this period.
2688+
;LOG_RETENTION_DAYS = 365
26872689
;; Default artifact retention time in days. Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
26882690
;ARTIFACT_RETENTION_DAYS = 90
26892691
;; Timeout to stop the task which have running status, but haven't been updated for a long time

models/actions/runner.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,25 @@ import (
2323
)
2424

2525
// ActionRunner represents runner machines
26+
//
27+
// It can be:
28+
// 1. global runner, OwnerID is 0 and RepoID is 0
29+
// 2. org/user level runner, OwnerID is org/user ID and RepoID is 0
30+
// 3. repo level runner, OwnerID is 0 and RepoID is repo ID
31+
//
32+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
33+
// or it will be complicated to find runners belonging to a specific owner.
34+
// For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1},
35+
// but it's a repo level runner, not an org/user level runner.
36+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners.
2637
type ActionRunner struct {
2738
ID int64
2839
UUID string `xorm:"CHAR(36) UNIQUE"`
2940
Name string `xorm:"VARCHAR(255)"`
3041
Version string `xorm:"VARCHAR(64)"`
31-
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
42+
OwnerID int64 `xorm:"index"`
3243
Owner *user_model.User `xorm:"-"`
33-
RepoID int64 `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global
44+
RepoID int64 `xorm:"index"`
3445
Repo *repo_model.Repository `xorm:"-"`
3546
Description string `xorm:"TEXT"`
3647
Base int // 0 native 1 docker 2 virtual machine
@@ -157,7 +168,7 @@ func init() {
157168
type FindRunnerOptions struct {
158169
db.ListOptions
159170
RepoID int64
160-
OwnerID int64
171+
OwnerID int64 // it will be ignored if RepoID is set
161172
Sort string
162173
Filter string
163174
IsOnline optional.Option[bool]
@@ -174,8 +185,7 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
174185
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
175186
}
176187
cond = cond.And(c)
177-
}
178-
if opts.OwnerID > 0 {
188+
} else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set
179189
c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID})
180190
if opts.WithAvailable {
181191
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
@@ -263,6 +273,11 @@ func DeleteRunner(ctx context.Context, id int64) error {
263273

264274
// CreateRunner creates new runner.
265275
func CreateRunner(ctx context.Context, t *ActionRunner) error {
276+
if t.OwnerID != 0 && t.RepoID != 0 {
277+
// It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally.
278+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
279+
t.OwnerID = 0
280+
}
266281
return db.Insert(ctx, t)
267282
}
268283

models/actions/runner_token.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@ import (
1515
)
1616

1717
// ActionRunnerToken represents runner tokens
18+
//
19+
// It can be:
20+
// 1. global token, OwnerID is 0 and RepoID is 0
21+
// 2. org/user level token, OwnerID is org/user ID and RepoID is 0
22+
// 3. repo level token, OwnerID is 0 and RepoID is repo ID
23+
//
24+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
25+
// or it will be complicated to find tokens belonging to a specific owner.
26+
// For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1},
27+
// but it's a repo level token, not an org/user level token.
28+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens.
1829
type ActionRunnerToken struct {
1930
ID int64
2031
Token string `xorm:"UNIQUE"`
21-
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
32+
OwnerID int64 `xorm:"index"`
2233
Owner *user_model.User `xorm:"-"`
23-
RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global
34+
RepoID int64 `xorm:"index"`
2435
Repo *repo_model.Repository `xorm:"-"`
2536
IsActive bool // true means it can be used
2637

@@ -58,7 +69,14 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
5869
}
5970

6071
// NewRunnerToken creates a new active runner token and invalidate all old tokens
72+
// ownerID will be ignored and treated as 0 if repoID is non-zero.
6173
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
74+
if ownerID != 0 && repoID != 0 {
75+
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
76+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
77+
ownerID = 0
78+
}
79+
6280
token, err := util.CryptoRandomString(40)
6381
if err != nil {
6482
return nil, err
@@ -84,6 +102,12 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
84102

85103
// GetLatestRunnerToken returns the latest runner token
86104
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
105+
if ownerID != 0 && repoID != 0 {
106+
// It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally.
107+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
108+
ownerID = 0
109+
}
110+
87111
var runnerToken ActionRunnerToken
88112
has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID).
89113
OrderBy("id DESC").Get(&runnerToken)

models/actions/schedule.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import (
1313
user_model "code.gitea.io/gitea/models/user"
1414
"code.gitea.io/gitea/modules/timeutil"
1515
webhook_module "code.gitea.io/gitea/modules/webhook"
16-
17-
"github.com/robfig/cron/v3"
1816
)
1917

2018
// ActionSchedule represents a schedule of a workflow file
@@ -53,8 +51,6 @@ func GetReposMapByIDs(ctx context.Context, ids []int64) (map[int64]*repo_model.R
5351
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
5452
}
5553

56-
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
57-
5854
// CreateScheduleTask creates new schedule task.
5955
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
6056
// Return early if there are no rows to insert
@@ -80,19 +76,21 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
8076
now := time.Now()
8177

8278
for _, spec := range row.Specs {
79+
specRow := &ActionScheduleSpec{
80+
RepoID: row.RepoID,
81+
ScheduleID: row.ID,
82+
Spec: spec,
83+
}
8384
// Parse the spec and check for errors
84-
schedule, err := cronParser.Parse(spec)
85+
schedule, err := specRow.Parse()
8586
if err != nil {
8687
continue // skip to the next spec if there's an error
8788
}
8889

90+
specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix())
91+
8992
// Insert the new schedule spec row
90-
if err = db.Insert(ctx, &ActionScheduleSpec{
91-
RepoID: row.RepoID,
92-
ScheduleID: row.ID,
93-
Spec: spec,
94-
Next: timeutil.TimeStamp(schedule.Next(now).Unix()),
95-
}); err != nil {
93+
if err = db.Insert(ctx, specRow); err != nil {
9694
return err
9795
}
9896
}

models/actions/schedule_spec.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package actions
55

66
import (
77
"context"
8+
"strings"
9+
"time"
810

911
"code.gitea.io/gitea/models/db"
1012
repo_model "code.gitea.io/gitea/models/repo"
@@ -32,8 +34,29 @@ type ActionScheduleSpec struct {
3234
Updated timeutil.TimeStamp `xorm:"updated"`
3335
}
3436

37+
// Parse parses the spec and returns a cron.Schedule
38+
// Unlike the default cron parser, Parse uses UTC timezone as the default if none is specified.
3539
func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) {
36-
return cronParser.Parse(s.Spec)
40+
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
41+
schedule, err := parser.Parse(s.Spec)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
// If the spec has specified a timezone, use it
47+
if strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=") {
48+
return schedule, nil
49+
}
50+
51+
specSchedule, ok := schedule.(*cron.SpecSchedule)
52+
// If it's not a spec schedule, like "@every 5m", timezone is not relevant
53+
if !ok {
54+
return schedule, nil
55+
}
56+
57+
// Set the timezone to UTC
58+
specSchedule.Location = time.UTC
59+
return specSchedule, nil
3760
}
3861

3962
func init() {

models/actions/schedule_spec_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestActionScheduleSpec_Parse(t *testing.T) {
15+
// Mock the local timezone is not UTC
16+
local := time.Local
17+
tz, err := time.LoadLocation("Asia/Shanghai")
18+
require.NoError(t, err)
19+
defer func() {
20+
time.Local = local
21+
}()
22+
time.Local = tz
23+
24+
now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00")
25+
require.NoError(t, err)
26+
27+
tests := []struct {
28+
name string
29+
spec string
30+
want string
31+
wantErr assert.ErrorAssertionFunc
32+
}{
33+
{
34+
name: "regular",
35+
spec: "0 10 * * *",
36+
want: "2024-07-31T10:00:00Z",
37+
wantErr: assert.NoError,
38+
},
39+
{
40+
name: "invalid",
41+
spec: "0 10 * *",
42+
want: "",
43+
wantErr: assert.Error,
44+
},
45+
{
46+
name: "with timezone",
47+
spec: "TZ=America/New_York 0 10 * * *",
48+
want: "2024-07-31T14:00:00Z",
49+
wantErr: assert.NoError,
50+
},
51+
{
52+
name: "timezone irrelevant",
53+
spec: "@every 5m",
54+
want: "2024-07-31T07:52:55Z",
55+
wantErr: assert.NoError,
56+
},
57+
}
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
s := &ActionScheduleSpec{
61+
Spec: tt.spec,
62+
}
63+
got, err := s.Parse()
64+
tt.wantErr(t, err)
65+
66+
if err == nil {
67+
assert.Equal(t, tt.want, got.Next(now).UTC().Format(time.RFC3339))
68+
}
69+
})
70+
}
71+
}

models/actions/task.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type ActionTask struct {
3535
RunnerID int64 `xorm:"index"`
3636
Status Status `xorm:"index"`
3737
Started timeutil.TimeStamp `xorm:"index"`
38-
Stopped timeutil.TimeStamp
38+
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
3939

4040
RepoID int64 `xorm:"index"`
4141
OwnerID int64 `xorm:"index"`
@@ -51,8 +51,8 @@ type ActionTask struct {
5151
LogInStorage bool // read log from database or from storage
5252
LogLength int64 // lines count
5353
LogSize int64 // blob size
54-
LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset
55-
LogExpired bool // files that are too old will be deleted
54+
LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset
55+
LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
5656

5757
Created timeutil.TimeStamp `xorm:"created"`
5858
Updated timeutil.TimeStamp `xorm:"updated index"`
@@ -470,6 +470,16 @@ func StopTask(ctx context.Context, taskID int64, status Status) error {
470470
return nil
471471
}
472472

473+
func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, limit int) ([]*ActionTask, error) {
474+
e := db.GetEngine(ctx)
475+
476+
tasks := make([]*ActionTask, 0, limit)
477+
// Check "stopped > 0" to avoid deleting tasks that are still running
478+
return tasks, e.Where("stopped > 0 AND stopped < ? AND log_expired = ?", olderThan, false).
479+
Limit(limit).
480+
Find(&tasks)
481+
}
482+
473483
func isSubset(set, subset []string) bool {
474484
m := make(container.Set[string], len(set))
475485
for _, v := range set {

models/actions/variable.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package actions
55

66
import (
77
"context"
8-
"errors"
98
"strings"
109

1110
"code.gitea.io/gitea/models/db"
@@ -15,6 +14,18 @@ import (
1514
"xorm.io/builder"
1615
)
1716

17+
// ActionVariable represents a variable that can be used in actions
18+
//
19+
// It can be:
20+
// 1. global variable, OwnerID is 0 and RepoID is 0
21+
// 2. org/user level variable, OwnerID is org/user ID and RepoID is 0
22+
// 3. repo level variable, OwnerID is 0 and RepoID is repo ID
23+
//
24+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
25+
// or it will be complicated to find variables belonging to a specific owner.
26+
// For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1},
27+
// but it's a repo level variable, not an org/user level variable.
28+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables.
1829
type ActionVariable struct {
1930
ID int64 `xorm:"pk autoincr"`
2031
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
@@ -29,39 +40,40 @@ func init() {
2940
db.RegisterModel(new(ActionVariable))
3041
}
3142

32-
func (v *ActionVariable) Validate() error {
33-
if v.OwnerID != 0 && v.RepoID != 0 {
34-
return errors.New("a variable should not be bound to an owner and a repository at the same time")
43+
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
44+
if ownerID != 0 && repoID != 0 {
45+
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
46+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
47+
ownerID = 0
3548
}
36-
return nil
37-
}
3849

39-
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
4050
variable := &ActionVariable{
4151
OwnerID: ownerID,
4252
RepoID: repoID,
4353
Name: strings.ToUpper(name),
4454
Data: data,
4555
}
46-
if err := variable.Validate(); err != nil {
47-
return variable, err
48-
}
4956
return variable, db.Insert(ctx, variable)
5057
}
5158

5259
type FindVariablesOpts struct {
5360
db.ListOptions
54-
OwnerID int64
5561
RepoID int64
62+
OwnerID int64 // it will be ignored if RepoID is set
5663
Name string
5764
}
5865

5966
func (opts FindVariablesOpts) ToConds() builder.Cond {
6067
cond := builder.NewCond()
6168
// Since we now support instance-level variables,
6269
// there is no need to check for null values for `owner_id` and `repo_id`
63-
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
6470
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
71+
if opts.RepoID != 0 { // if RepoID is set
72+
// ignore OwnerID and treat it as 0
73+
cond = cond.And(builder.Eq{"owner_id": 0})
74+
} else {
75+
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
76+
}
6577

6678
if opts.Name != "" {
6779
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})

0 commit comments

Comments
 (0)