···27742774;ENABLED = true
27752775;; Default address to get action plugins, e.g. the default value means downloading from "https://code.forgejo.org/actions/checkout" for "uses: actions/checkout@v3"
27762776;DEFAULT_ACTIONS_URL = https://code.forgejo.org
27772777-;; Default artifact retention time in days, default is 90 days
27772777+;; Logs retention time in days. Old logs will be deleted after this period.
27782778+;LOG_RETENTION_DAYS = 365
27792779+;; Default artifact retention time in days. Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
27782780;ARTIFACT_RETENTION_DAYS = 90
27792781;; Timeout to stop the task which have running status, but haven't been updated for a long time
27802782;ZOMBIE_TASK_TIMEOUT = 10m
+20-5
models/actions/runner.go
···2626)
27272828// ActionRunner represents runner machines
2929+//
3030+// It can be:
3131+// 1. global runner, OwnerID is 0 and RepoID is 0
3232+// 2. org/user level runner, OwnerID is org/user ID and RepoID is 0
3333+// 3. repo level runner, OwnerID is 0 and RepoID is repo ID
3434+//
3535+// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
3636+// or it will be complicated to find runners belonging to a specific owner.
3737+// For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1},
3838+// but it's a repo level runner, not an org/user level runner.
3939+// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners.
2940type ActionRunner struct {
3041 ID int64
3142 UUID string `xorm:"CHAR(36) UNIQUE"`
3243 Name string `xorm:"VARCHAR(255)"`
3344 Version string `xorm:"VARCHAR(64)"`
3434- OwnerID int64 `xorm:"index"` // org level runner, 0 means system
4545+ OwnerID int64 `xorm:"index"`
3546 Owner *user_model.User `xorm:"-"`
3636- RepoID int64 `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global
4747+ RepoID int64 `xorm:"index"`
3748 Repo *repo_model.Repository `xorm:"-"`
3849 Description string `xorm:"TEXT"`
3950 Base int // 0 native 1 docker 2 virtual machine
···176187type FindRunnerOptions struct {
177188 db.ListOptions
178189 RepoID int64
179179- OwnerID int64
190190+ OwnerID int64 // it will be ignored if RepoID is set
180191 Sort string
181192 Filter string
182193 IsOnline optional.Option[bool]
···193204 c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
194205 }
195206 cond = cond.And(c)
196196- }
197197- if opts.OwnerID > 0 {
207207+ } else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set
198208 c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID})
199209 if opts.WithAvailable {
200210 c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
···297307298308// CreateRunner creates new runner.
299309func CreateRunner(ctx context.Context, t *ActionRunner) error {
310310+ if t.OwnerID != 0 && t.RepoID != 0 {
311311+ // It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally.
312312+ // Remove OwnerID to avoid confusion; it's not worth returning an error here.
313313+ t.OwnerID = 0
314314+ }
300315 return db.Insert(ctx, t)
301316}
302317
+26-2
models/actions/runner_token.go
···1515)
16161717// ActionRunnerToken represents runner tokens
1818+//
1919+// It can be:
2020+// 1. global token, OwnerID is 0 and RepoID is 0
2121+// 2. org/user level token, OwnerID is org/user ID and RepoID is 0
2222+// 3. repo level token, OwnerID is 0 and RepoID is repo ID
2323+//
2424+// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
2525+// or it will be complicated to find tokens belonging to a specific owner.
2626+// For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1},
2727+// but it's a repo level token, not an org/user level token.
2828+// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens.
1829type ActionRunnerToken struct {
1930 ID int64
2031 Token string `xorm:"UNIQUE"`
2121- OwnerID int64 `xorm:"index"` // org level runner, 0 means system
3232+ OwnerID int64 `xorm:"index"`
2233 Owner *user_model.User `xorm:"-"`
2323- RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global
3434+ RepoID int64 `xorm:"index"`
2435 Repo *repo_model.Repository `xorm:"-"`
2536 IsActive bool // true means it can be used
2637···5869}
59706071// NewRunnerToken creates a new active runner token and invalidate all old tokens
7272+// ownerID will be ignored and treated as 0 if repoID is non-zero.
6173func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
7474+ if ownerID != 0 && repoID != 0 {
7575+ // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
7676+ // Remove OwnerID to avoid confusion; it's not worth returning an error here.
7777+ ownerID = 0
7878+ }
7979+6280 token, err := util.CryptoRandomString(40)
6381 if err != nil {
6482 return nil, err
···8410285103// GetLatestRunnerToken returns the latest runner token
86104func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
105105+ if ownerID != 0 && repoID != 0 {
106106+ // It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally.
107107+ // Remove OwnerID to avoid confusion; it's not worth returning an error here.
108108+ ownerID = 0
109109+ }
110110+87111 var runnerToken ActionRunnerToken
88112 has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID).
89113 OrderBy("id DESC").Get(&runnerToken)
+9-11
models/actions/schedule.go
···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"
1616-1717- "github.com/robfig/cron/v3"
1816)
19172018// ActionSchedule represents a schedule of a workflow file
···5351 return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
5452}
55535656-var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
5757-5854// CreateScheduleTask creates new schedule task.
5955func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
6056 // Return early if there are no rows to insert
···8076 now := time.Now()
81778278 for _, spec := range row.Specs {
7979+ specRow := &ActionScheduleSpec{
8080+ RepoID: row.RepoID,
8181+ ScheduleID: row.ID,
8282+ Spec: spec,
8383+ }
8384 // Parse the spec and check for errors
8484- schedule, err := cronParser.Parse(spec)
8585+ schedule, err := specRow.Parse()
8586 if err != nil {
8687 continue // skip to the next spec if there's an error
8788 }
88899090+ specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix())
9191+8992 // Insert the new schedule spec row
9090- if err = db.Insert(ctx, &ActionScheduleSpec{
9191- RepoID: row.RepoID,
9292- ScheduleID: row.ID,
9393- Spec: spec,
9494- Next: timeutil.TimeStamp(schedule.Next(now).Unix()),
9595- }); err != nil {
9393+ if err = db.Insert(ctx, specRow); err != nil {
9694 return err
9795 }
9896 }
+24-1
models/actions/schedule_spec.go
···5566import (
77 "context"
88+ "strings"
99+ "time"
810911 "code.gitea.io/gitea/models/db"
1012 repo_model "code.gitea.io/gitea/models/repo"
···3234 Updated timeutil.TimeStamp `xorm:"updated"`
3335}
34363737+// Parse parses the spec and returns a cron.Schedule
3838+// Unlike the default cron parser, Parse uses UTC timezone as the default if none is specified.
3539func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) {
3636- return cronParser.Parse(s.Spec)
4040+ parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
4141+ schedule, err := parser.Parse(s.Spec)
4242+ if err != nil {
4343+ return nil, err
4444+ }
4545+4646+ // If the spec has specified a timezone, use it
4747+ if strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=") {
4848+ return schedule, nil
4949+ }
5050+5151+ specSchedule, ok := schedule.(*cron.SpecSchedule)
5252+ // If it's not a spec schedule, like "@every 5m", timezone is not relevant
5353+ if !ok {
5454+ return schedule, nil
5555+ }
5656+5757+ // Set the timezone to UTC
5858+ specSchedule.Location = time.UTC
5959+ return specSchedule, nil
3760}
38613962func init() {
···3535 RunnerID int64 `xorm:"index"`
3636 Status Status `xorm:"index"`
3737 Started timeutil.TimeStamp `xorm:"index"`
3838- Stopped timeutil.TimeStamp
3838+ Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
39394040 RepoID int64 `xorm:"index"`
4141 OwnerID int64 `xorm:"index"`
···5151 LogInStorage bool // read log from database or from storage
5252 LogLength int64 // lines count
5353 LogSize int64 // blob size
5454- LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset
5555- LogExpired bool // files that are too old will be deleted
5454+ LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset
5555+ LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
56565757 Created timeutil.TimeStamp `xorm:"created"`
5858 Updated timeutil.TimeStamp `xorm:"updated index"`
···468468 }
469469470470 return nil
471471+}
472472+473473+func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, limit int) ([]*ActionTask, error) {
474474+ e := db.GetEngine(ctx)
475475+476476+ tasks := make([]*ActionTask, 0, limit)
477477+ // Check "stopped > 0" to avoid deleting tasks that are still running
478478+ return tasks, e.Where("stopped > 0 AND stopped < ? AND log_expired = ?", olderThan, false).
479479+ Limit(limit).
480480+ Find(&tasks)
471481}
472482473483func isSubset(set, subset []string) bool {
+24-12
models/actions/variable.go
···5566import (
77 "context"
88- "errors"
98 "strings"
1091110 "code.gitea.io/gitea/models/db"
···1514 "xorm.io/builder"
1615)
17161717+// ActionVariable represents a variable that can be used in actions
1818+//
1919+// It can be:
2020+// 1. global variable, OwnerID is 0 and RepoID is 0
2121+// 2. org/user level variable, OwnerID is org/user ID and RepoID is 0
2222+// 3. repo level variable, OwnerID is 0 and RepoID is repo ID
2323+//
2424+// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
2525+// or it will be complicated to find variables belonging to a specific owner.
2626+// For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1},
2727+// but it's a repo level variable, not an org/user level variable.
2828+// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables.
1829type ActionVariable struct {
1930 ID int64 `xorm:"pk autoincr"`
2031 OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
···2940 db.RegisterModel(new(ActionVariable))
3041}
31423232-func (v *ActionVariable) Validate() error {
3333- if v.OwnerID != 0 && v.RepoID != 0 {
3434- return errors.New("a variable should not be bound to an owner and a repository at the same time")
4343+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
4444+ if ownerID != 0 && repoID != 0 {
4545+ // It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
4646+ // Remove OwnerID to avoid confusion; it's not worth returning an error here.
4747+ ownerID = 0
3548 }
3636- return nil
3737-}
38493939-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,
4545- }
4646- if err := variable.Validate(); err != nil {
4747- return variable, err
4855 }
4956 return variable, db.Insert(ctx, variable)
5057}
51585259type FindVariablesOpts struct {
5360 db.ListOptions
5454- OwnerID int64
5561 RepoID int64
6262+ OwnerID int64 // it will be ignored if RepoID is set
5663 Name string
5764}
5865···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`
6363- cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
6470 cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
7171+ if opts.RepoID != 0 { // if RepoID is set
7272+ // ignore OwnerID and treat it as 0
7373+ cond = cond.And(builder.Eq{"owner_id": 0})
7474+ } else {
7575+ cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
7676+ }
65776678 if opts.Name != "" {
6779 cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
+7
models/git/branch.go
···385385 return err
386386 }
387387388388+ // 4.1 Update all not merged pull request head branch name
389389+ if _, err = sess.Table("pull_request").Where("head_repo_id=? AND head_branch=? AND has_merged=?",
390390+ repo.ID, from, false).
391391+ Update(map[string]any{"head_branch": to}); err != nil {
392392+ return err
393393+ }
394394+388395 // 5. insert renamed branch record
389396 renamedBranch := &RenamedBranch{
390397 RepoID: repo.ID,
+47-1
models/git/commit_status.go
···141141 return newIdx, nil
142142}
143143144144-func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
144144+func (status *CommitStatus) loadRepository(ctx context.Context) (err error) {
145145 if status.Repo == nil {
146146 status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID)
147147 if err != nil {
148148 return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err)
149149 }
150150 }
151151+ return nil
152152+}
153153+154154+func (status *CommitStatus) loadCreator(ctx context.Context) (err error) {
151155 if status.Creator == nil && status.CreatorID > 0 {
152156 status.Creator, err = user_model.GetUserByID(ctx, status.CreatorID)
153157 if err != nil {
···157161 return nil
158162}
159163164164+func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
165165+ if err := status.loadRepository(ctx); err != nil {
166166+ return err
167167+ }
168168+ return status.loadCreator(ctx)
169169+}
170170+160171// APIURL returns the absolute APIURL to this commit-status.
161172func (status *CommitStatus) APIURL(ctx context.Context) string {
162173 _ = status.loadAttributes(ctx)
···166177// LocaleString returns the locale string name of the Status
167178func (status *CommitStatus) LocaleString(lang translation.Locale) string {
168179 return lang.TrString("repo.commitstatus." + status.State.String())
180180+}
181181+182182+// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
183183+func (status *CommitStatus) HideActionsURL(ctx context.Context) {
184184+ if status.RepoID == 0 {
185185+ return
186186+ }
187187+188188+ if status.Repo == nil {
189189+ if err := status.loadRepository(ctx); err != nil {
190190+ log.Error("loadRepository: %v", err)
191191+ return
192192+ }
193193+ }
194194+195195+ prefix := fmt.Sprintf("%s/actions", status.Repo.Link())
196196+ if strings.HasPrefix(status.TargetURL, prefix) {
197197+ status.TargetURL = ""
198198+ }
169199}
170200171201// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
···471501 repo,
472502 )
473503}
504504+505505+// CommitStatusesHideActionsURL hide Gitea Actions urls
506506+func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) {
507507+ idToRepos := make(map[int64]*repo_model.Repository)
508508+ for _, status := range statuses {
509509+ if status == nil {
510510+ continue
511511+ }
512512+513513+ if status.Repo == nil {
514514+ status.Repo = idToRepos[status.RepoID]
515515+ }
516516+ status.HideActionsURL(ctx)
517517+ idToRepos[status.RepoID] = status.Repo
518518+ }
519519+}
···766766767767// GetRepositoryByName returns the repository by given name under user if exists.
768768func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
769769- repo := &Repository{
770770- OwnerID: ownerID,
771771- LowerName: strings.ToLower(name),
772772- }
773773- has, err := db.GetEngine(ctx).Get(repo)
769769+ var repo Repository
770770+ has, err := db.GetEngine(ctx).
771771+ Where("`owner_id`=?", ownerID).
772772+ And("`lower_name`=?", strings.ToLower(name)).
773773+ NoAutoCondition().
774774+ Get(&repo)
774775 if err != nil {
775776 return nil, err
776777 } else if !has {
777778 return nil, ErrRepoNotExist{0, ownerID, "", name}
778779 }
779779- return repo, err
780780+ return &repo, err
780781}
781782782783// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
+30-16
models/secret/secret.go
···5566import (
77 "context"
88- "errors"
98 "fmt"
109 "strings"
1110···2221)
23222423// Secret represents a secret
2424+//
2525+// It can be:
2626+// 1. org/user level secret, OwnerID is org/user ID and RepoID is 0
2727+// 2. repo level secret, OwnerID is 0 and RepoID is repo ID
2828+//
2929+// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
3030+// or it will be complicated to find secrets belonging to a specific owner.
3131+// For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1},
3232+// but it's a repo level secret, not an org/user level secret.
3333+// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level secrets.
3434+//
3535+// Please note that it's not acceptable to have both OwnerID and RepoID to zero, global secrets are not supported.
3636+// It's for security reasons, admin may be not aware of that the secrets could be stolen by any user when setting them as global.
2537type Secret struct {
2638 ID int64
2739 OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
···46584759// InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database
4860func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) {
6161+ if ownerID != 0 && repoID != 0 {
6262+ // It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally.
6363+ // Remove OwnerID to avoid confusion; it's not worth returning an error here.
6464+ ownerID = 0
6565+ }
6666+ if ownerID == 0 && repoID == 0 {
6767+ return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
6868+ }
6969+4970 encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
5071 if err != nil {
5172 return nil, err
···5677 Name: strings.ToUpper(name),
5778 Data: encrypted,
5879 }
5959- if err := secret.Validate(); err != nil {
6060- return secret, err
6161- }
6280 return secret, db.Insert(ctx, secret)
6381}
6482···6684 db.RegisterModel(new(Secret))
6785}
68866969-func (s *Secret) Validate() error {
7070- if s.OwnerID == 0 && s.RepoID == 0 {
7171- return errors.New("the secret is not bound to any scope")
7272- }
7373- return nil
7474-}
7575-7687type FindSecretsOptions struct {
7788 db.ListOptions
7878- OwnerID int64
7989 RepoID int64
9090+ OwnerID int64 // it will be ignored if RepoID is set
8091 SecretID int64
8192 Name string
8293}
83948495func (opts FindSecretsOptions) ToConds() builder.Cond {
8596 cond := builder.NewCond()
8686- if opts.OwnerID > 0 {
9797+9898+ cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
9999+ if opts.RepoID != 0 { // if RepoID is set
100100+ // ignore OwnerID and treat it as 0
101101+ cond = cond.And(builder.Eq{"owner_id": 0})
102102+ } else {
87103 cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
88104 }
8989- if opts.RepoID > 0 {
9090- cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
9191- }
105105+92106 if opts.SecretID != 0 {
93107 cond = cond.And(builder.Eq{"id": opts.SecretID})
94108 }
+3-4
modules/lfs/http_client.go
···136136137137 for _, object := range result.Objects {
138138 if object.Error != nil {
139139- objectError := errors.New(object.Error.Message)
140140- log.Trace("Error on object %v: %v", object.Pointer, objectError)
139139+ log.Trace("Error on object %v: %v", object.Pointer, object.Error)
141140 if uc != nil {
142142- if _, err := uc(object.Pointer, objectError); err != nil {
141141+ if _, err := uc(object.Pointer, object.Error); err != nil {
143142 return err
144143 }
145144 } else {
146146- if err := dc(object.Pointer, nil, objectError); err != nil {
145145+ if err := dc(object.Pointer, nil, object.Error); err != nil {
147146 return err
148147 }
149148 }
+37
modules/lfs/shared.go
···44package lfs
5566import (
77+ "errors"
88+ "fmt"
79 "time"
1010+1111+ "code.gitea.io/gitea/modules/util"
812)
9131014const (
···6266type ObjectError struct {
6367 Code int `json:"code"`
6468 Message string `json:"message"`
6969+}
7070+7171+var (
7272+ // See https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md#successful-responses
7373+ // LFS object error codes should match HTTP status codes where possible:
7474+ // 404 - The object does not exist on the server.
7575+ // 409 - The specified hash algorithm disagrees with the server's acceptable options.
7676+ // 410 - The object was removed by the owner.
7777+ // 422 - Validation error.
7878+7979+ ErrObjectNotExist = util.ErrNotExist // the object does not exist on the server
8080+ ErrObjectHashMismatch = errors.New("the specified hash algorithm disagrees with the server's acceptable options")
8181+ ErrObjectRemoved = errors.New("the object was removed by the owner")
8282+ ErrObjectValidation = errors.New("validation error")
8383+)
8484+8585+func (e *ObjectError) Error() string {
8686+ return fmt.Sprintf("[%d] %s", e.Code, e.Message)
8787+}
8888+8989+func (e *ObjectError) Unwrap() error {
9090+ switch e.Code {
9191+ case 404:
9292+ return ErrObjectNotExist
9393+ case 409:
9494+ return ErrObjectHashMismatch
9595+ case 410:
9696+ return ErrObjectRemoved
9797+ case 422:
9898+ return ErrObjectValidation
9999+ default:
100100+ return errors.New(e.Message)
101101+ }
65102}
6610367104// PointerBlob associates a Git blob with a Pointer.
···1212// Actions settings
1313var (
1414 Actions = struct {
1515- LogStorage *Storage // how the created logs should be stored
1616- ArtifactStorage *Storage // how the created artifacts should be stored
1717- ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
1815 Enabled bool
1616+ LogStorage *Storage // how the created logs should be stored
1717+ LogRetentionDays int64 `ini:"LOG_RETENTION_DAYS"`
1818+ ArtifactStorage *Storage // how the created artifacts should be stored
1919+ ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
1920 DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"`
2021 ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
2122 EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
···6162 if err != nil {
6263 return err
6364 }
6565+ // default to 1 year
6666+ if Actions.LogRetentionDays <= 0 {
6767+ Actions.LogRetentionDays = 365
6868+ }
64696570 actionsSec, _ := rootCfg.GetSection("actions.artifacts")
66716772 Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec)
7373+ if err != nil {
7474+ return err
7575+ }
68766977 // default to 90 days in Github Actions
7078 if Actions.ArtifactRetentionDays <= 0 {
···7583 Actions.EndlessTaskTimeout = sec.Key("ENDLESS_TASK_TIMEOUT").MustDuration(3 * time.Hour)
7684 Actions.AbandonedJobTimeout = sec.Key("ABANDONED_JOB_TIMEOUT").MustDuration(24 * time.Hour)
77857878- return err
8686+ return nil
7987}
+1
modules/structs/repo_collaborator.go
···5566// AddCollaboratorOption options when adding a user as a collaborator of a repository
77type AddCollaboratorOption struct {
88+ // enum: read,write,admin
89 Permission *string `json:"permission"`
910}
1011
+6-4
options/locale/locale_en-US.ini
···29972997dashboard.update_checker = Update checker
29982998dashboard.delete_old_system_notices = Delete all old system notices from database
29992999dashboard.gc_lfs = Garbage collect LFS meta objects
30003000-dashboard.stop_zombie_tasks = Stop zombie tasks
30013001-dashboard.stop_endless_tasks = Stop endless tasks
30023002-dashboard.cancel_abandoned_jobs = Cancel abandoned jobs
30033003-dashboard.start_schedule_tasks = Start schedule tasks
30003000+dashboard.stop_zombie_tasks = Stop zombie actions tasks
30013001+dashboard.stop_endless_tasks = Stop endless actions tasks
30023002+dashboard.cancel_abandoned_jobs = Cancel abandoned actions jobs
30033003+dashboard.start_schedule_tasks = Start schedule actions tasks
30043004dashboard.sync_branch.started = Branch sync started
30053005dashboard.sync_tag.started = Tag sync started
30063006dashboard.rebuild_issue_indexer = Rebuild issue indexer
···38033803runs.no_workflows.documentation = For more information on Forgejo Actions, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
38043804runs.no_runs = The workflow has no runs yet.
38053805runs.empty_commit_message = (empty commit message)
38063806+runs.expire_log_message = Logs have been purged because they were too old.
3806380738073808workflow.disable = Disable workflow
38083809workflow.disable_success = Workflow "%s" disabled successfully.
···38363837variables.update.success = The variable has been edited.
3837383838383839[projects]
38403840+deleted.display_name = Deleted Project
38393841type-1.display_name = Individual project
38403842type-2.display_name = Repository project
38413843type-3.display_name = Organization project
+9
release-notes/4801.md
···11+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/0dbc6230286e113accbc6d5e829ce8dae1d1f5d4) Hide the "Details" link of commit status when the user cannot access actions.
22+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6e63afe31f43eaf5ff7c8595ddeaf8515c2dc0c0) The API endpoint to get the actions registration token is GET /repos/{owner}/{repo}/actions/runners/registration-token and not GET /repos/{owner}/{repo}/runners/registration-token.
33+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6e63afe31f43eaf5ff7c8595ddeaf8515c2dc0c0) Runner registration token via API is broken for repo level runners.
44+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/c784a5874066ca1a1fd518408d5767b4eb57bd69) Deleted projects causes bad popover text on issues.
55+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/42bb51af9b8283071e15ac6470ada9824d87cd40) Distinguish LFS object errors to ignore missing objects during migration.
66+feat: [commit](https://codeberg.org/forgejo/forgejo/commit/11b6253e7532ba11dee8bc31d4c262b102674a4d) Use UTC as a timezone when running scheduled actions tasks.
77+feat: [commit](https://codeberg.org/forgejo/forgejo/commit/feb43b2584b7f64ec7f9952af2b50b2210e6e6cf) The actions logs older than `[actions].LOG_RETENTION_DAYS` days are removed (the default is 365).
88+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6328f648decc2754ef10ee5ca6ca9785a156614c) When viewing the revision history of wiki pages, the pagination links are broken: instead of org/repo/wiki/Page?action=_revision&page=2, the link is only org/repo/wiki/Page?page=2, thus bringing the user back to the wiki page.
99+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2310556158d70bf1dbfca96dc928e1be3d3f41be) Also rename the head branch of open pull requests when renaming a branch.
···271271272272 step := steps[cursor.Step]
273273274274+ // if task log is expired, return a consistent log line
275275+ if task.LogExpired {
276276+ if cursor.Cursor == 0 {
277277+ resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
278278+ Step: cursor.Step,
279279+ Cursor: 1,
280280+ Lines: []*ViewStepLogLine{
281281+ {
282282+ Index: 1,
283283+ Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
284284+ // Timestamp doesn't mean anything when the log is expired.
285285+ // Set it to the task's updated time since it's probably the time when the log has expired.
286286+ Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
287287+ },
288288+ },
289289+ Started: int64(step.Started),
290290+ })
291291+ }
292292+ continue
293293+ }
294294+274295 logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead of 'null' in json
275296276297 index := step.LogIndex + cursor.Cursor
+5
routers/web/repo/branch.go
···7070 ctx.ServerError("LoadBranches", err)
7171 return
7272 }
7373+ if !ctx.Repo.CanRead(unit.TypeActions) {
7474+ for key := range commitStatuses {
7575+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
7676+ }
7777+ }
73787479 commitStatus := make(map[string]*git_model.CommitStatus)
7580 for commitID, cs := range commitStatuses {