mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Artifacts retention and auto clean up (#26131)
Currently, Artifact does not have an expiration and automatic cleanup mechanism, and this feature needs to be added. It contains the following key points: - [x] add global artifact retention days option in config file. Default value is 90 days. - [x] add cron task to clean up expired artifacts. It should run once a day. - [x] support custom retention period from `retention-days: 5` in `upload-artifact@v3`. - [x] artifacts link in actions view should be non-clickable text when expired.
This commit is contained in:
		| @@ -2564,6 +2564,8 @@ LEVEL = Info | |||||||
| ;; | ;; | ||||||
| ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. | ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. | ||||||
| ;DEFAULT_ACTIONS_URL = github | ;DEFAULT_ACTIONS_URL = github | ||||||
|  | ;; Default artifact retention time in days, default is 90 days | ||||||
|  | ;ARTIFACT_RETENTION_DAYS = 90 | ||||||
|  |  | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|   | |||||||
| @@ -955,6 +955,12 @@ Default templates for project boards: | |||||||
| - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. | - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. | ||||||
| - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. | - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. | ||||||
|  |  | ||||||
|  | ## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`) | ||||||
|  |  | ||||||
|  | - `ENABLED`: **true**: Enable cleanup expired actions assets job. | ||||||
|  | - `RUN_AT_START`: **true**: Run job at start time (if ENABLED). | ||||||
|  | - `SCHEDULE`: **@midnight** : Cron syntax for the job. | ||||||
|  |  | ||||||
| ### Extended cron tasks (not enabled by default) | ### Extended cron tasks (not enabled by default) | ||||||
|  |  | ||||||
| #### Cron - Garbage collect all repositories (`cron.git_gc_repos`) | #### Cron - Garbage collect all repositories (`cron.git_gc_repos`) | ||||||
| @@ -1381,6 +1387,7 @@ PROXY_HOSTS = *.github.com | |||||||
| - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. | - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. | ||||||
| - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` | - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` | ||||||
| - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` | - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` | ||||||
|  | - `ARTIFACT_RETENTION_DAYS`: **90**: Number of days to keep artifacts. Set to 0 to disable artifact retention. Default is 90 days if not set. | ||||||
|  |  | ||||||
| `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. | `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. | ||||||
| For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. | For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. | ||||||
|   | |||||||
| @@ -9,19 +9,21 @@ package actions | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // ArtifactStatus is the status of an artifact, uploading, expired or need-delete | ||||||
|  | type ArtifactStatus int64 | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	// ArtifactStatusUploadPending is the status of an artifact upload that is pending | 	ArtifactStatusUploadPending   ArtifactStatus = iota + 1 // 1, ArtifactStatusUploadPending is the status of an artifact upload that is pending | ||||||
| 	ArtifactStatusUploadPending = 1 | 	ArtifactStatusUploadConfirmed                           // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed | ||||||
| 	// ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed | 	ArtifactStatusUploadError                               // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored | ||||||
| 	ArtifactStatusUploadConfirmed = 2 | 	ArtifactStatusExpired                                   // 4, ArtifactStatusExpired is the status of an artifact that is expired | ||||||
| 	// ArtifactStatusUploadError is the status of an artifact upload that is errored |  | ||||||
| 	ArtifactStatusUploadError = 3 |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| @@ -45,9 +47,10 @@ type ActionArtifact struct { | |||||||
| 	Status             int64              `xorm:"index"`                         // The status of the artifact, uploading, expired or need-delete | 	Status             int64              `xorm:"index"`                         // The status of the artifact, uploading, expired or need-delete | ||||||
| 	CreatedUnix        timeutil.TimeStamp `xorm:"created"` | 	CreatedUnix        timeutil.TimeStamp `xorm:"created"` | ||||||
| 	UpdatedUnix        timeutil.TimeStamp `xorm:"updated index"` | 	UpdatedUnix        timeutil.TimeStamp `xorm:"updated index"` | ||||||
|  | 	ExpiredUnix        timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired | ||||||
| } | } | ||||||
|  |  | ||||||
| func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string) (*ActionArtifact, error) { | func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { | ||||||
| 	if err := t.LoadJob(ctx); err != nil { | 	if err := t.LoadJob(ctx); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @@ -61,7 +64,8 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa | |||||||
| 			RepoID:       t.RepoID, | 			RepoID:       t.RepoID, | ||||||
| 			OwnerID:      t.OwnerID, | 			OwnerID:      t.OwnerID, | ||||||
| 			CommitSHA:    t.CommitSHA, | 			CommitSHA:    t.CommitSHA, | ||||||
| 			Status:       ArtifactStatusUploadPending, | 			Status:       int64(ArtifactStatusUploadPending), | ||||||
|  | 			ExpiredUnix:  timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays), | ||||||
| 		} | 		} | ||||||
| 		if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { | 		if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| @@ -126,15 +130,16 @@ func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionAr | |||||||
| type ActionArtifactMeta struct { | type ActionArtifactMeta struct { | ||||||
| 	ArtifactName string | 	ArtifactName string | ||||||
| 	FileSize     int64 | 	FileSize     int64 | ||||||
|  | 	Status       int64 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run | // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run | ||||||
| func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { | func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { | ||||||
| 	arts := make([]*ActionArtifactMeta, 0, 10) | 	arts := make([]*ActionArtifactMeta, 0, 10) | ||||||
| 	return arts, db.GetEngine(ctx).Table("action_artifact"). | 	return arts, db.GetEngine(ctx).Table("action_artifact"). | ||||||
| 		Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed). | 		Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). | ||||||
| 		GroupBy("artifact_name"). | 		GroupBy("artifact_name"). | ||||||
| 		Select("artifact_name, sum(file_size) as file_size"). | 		Select("artifact_name, sum(file_size) as file_size, max(status) as status"). | ||||||
| 		Find(&arts) | 		Find(&arts) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -149,3 +154,16 @@ func ListArtifactsByRunIDAndName(ctx context.Context, runID int64, name string) | |||||||
| 	arts := make([]*ActionArtifact, 0, 10) | 	arts := make([]*ActionArtifact, 0, 10) | ||||||
| 	return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) | 	return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ListNeedExpiredArtifacts returns all need expired artifacts but not deleted | ||||||
|  | func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { | ||||||
|  | 	arts := make([]*ActionArtifact, 0, 10) | ||||||
|  | 	return arts, db.GetEngine(ctx). | ||||||
|  | 		Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetArtifactExpired sets an artifact to expired | ||||||
|  | func SetArtifactExpired(ctx context.Context, artifactID int64) error { | ||||||
|  | 	_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|   | |||||||
| @@ -528,6 +528,8 @@ var migrations = []Migration{ | |||||||
| 	NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), | 	NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), | ||||||
| 	// v273 -> v274 | 	// v273 -> v274 | ||||||
| 	NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), | 	NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), | ||||||
|  | 	// v274 -> v275 | ||||||
|  | 	NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetCurrentDBVersion returns the current db version | // GetCurrentDBVersion returns the current db version | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								models/migrations/v1_21/v274.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								models/migrations/v1_21/v274.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package v1_21 //nolint | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  |  | ||||||
|  | 	"xorm.io/xorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func AddExpiredUnixColumnInActionArtifactTable(x *xorm.Engine) error { | ||||||
|  | 	type ActionArtifact struct { | ||||||
|  | 		ExpiredUnix timeutil.TimeStamp `xorm:"index"` // time when the artifact will be expired | ||||||
|  | 	} | ||||||
|  | 	if err := x.Sync(new(ActionArtifact)); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return updateArtifactsExpiredUnixTo90Days(x) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func updateArtifactsExpiredUnixTo90Days(x *xorm.Engine) error { | ||||||
|  | 	sess := x.NewSession() | ||||||
|  | 	defer sess.Close() | ||||||
|  |  | ||||||
|  | 	if err := sess.Begin(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	expiredTime := time.Now().AddDate(0, 0, 90).Unix() | ||||||
|  | 	if _, err := sess.Exec(`UPDATE action_artifact SET expired_unix=? WHERE status='2' AND expired_unix is NULL`, expiredTime); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return sess.Commit() | ||||||
|  | } | ||||||
| @@ -13,10 +13,11 @@ import ( | |||||||
| // Actions settings | // Actions settings | ||||||
| var ( | var ( | ||||||
| 	Actions = struct { | 	Actions = struct { | ||||||
| 		LogStorage        *Storage // how the created logs should be stored | 		LogStorage            *Storage // how the created logs should be stored | ||||||
| 		ArtifactStorage   *Storage // how the created artifacts should be stored | 		ArtifactStorage       *Storage // how the created artifacts should be stored | ||||||
| 		Enabled           bool | 		ArtifactRetentionDays int64    `ini:"ARTIFACT_RETENTION_DAYS"` | ||||||
| 		DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` | 		Enabled               bool | ||||||
|  | 		DefaultActionsURL     defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` | ||||||
| 	}{ | 	}{ | ||||||
| 		Enabled:           false, | 		Enabled:           false, | ||||||
| 		DefaultActionsURL: defaultActionsURLGitHub, | 		DefaultActionsURL: defaultActionsURLGitHub, | ||||||
| @@ -76,5 +77,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { | |||||||
|  |  | ||||||
| 	Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) | 	Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) | ||||||
|  |  | ||||||
|  | 	// default to 90 days in Github Actions | ||||||
|  | 	if Actions.ArtifactRetentionDays <= 0 { | ||||||
|  | 		Actions.ArtifactRetentionDays = 90 | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2731,6 +2731,7 @@ dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for w | |||||||
| dashboard.sync_external_users = Synchronize external user data | dashboard.sync_external_users = Synchronize external user data | ||||||
| dashboard.cleanup_hook_task_table = Cleanup hook_task table | dashboard.cleanup_hook_task_table = Cleanup hook_task table | ||||||
| dashboard.cleanup_packages = Cleanup expired packages | dashboard.cleanup_packages = Cleanup expired packages | ||||||
|  | dashboard.cleanup_actions = Cleanup actions expired logs and artifacts | ||||||
| dashboard.server_uptime = Server Uptime | dashboard.server_uptime = Server Uptime | ||||||
| dashboard.current_goroutine = Current Goroutines | dashboard.current_goroutine = Current Goroutines | ||||||
| dashboard.current_memory_usage = Current Memory Usage | dashboard.current_memory_usage = Current Memory Usage | ||||||
|   | |||||||
| @@ -170,8 +170,9 @@ func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix stri | |||||||
| } | } | ||||||
|  |  | ||||||
| type getUploadArtifactRequest struct { | type getUploadArtifactRequest struct { | ||||||
| 	Type string | 	Type          string | ||||||
| 	Name string | 	Name          string | ||||||
|  | 	RetentionDays int64 | ||||||
| } | } | ||||||
|  |  | ||||||
| type getUploadArtifactResponse struct { | type getUploadArtifactResponse struct { | ||||||
| @@ -192,10 +193,16 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// set retention days | ||||||
|  | 	retentionQuery := "" | ||||||
|  | 	if req.RetentionDays > 0 { | ||||||
|  | 		retentionQuery = fmt.Sprintf("?retentionDays=%d", req.RetentionDays) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// use md5(artifact_name) to create upload url | 	// use md5(artifact_name) to create upload url | ||||||
| 	artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) | 	artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) | ||||||
| 	resp := getUploadArtifactResponse{ | 	resp := getUploadArtifactResponse{ | ||||||
| 		FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"), | 		FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery), | ||||||
| 	} | 	} | ||||||
| 	log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) | 	log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) | ||||||
| 	ctx.JSON(http.StatusOK, resp) | 	ctx.JSON(http.StatusOK, resp) | ||||||
| @@ -219,8 +226,21 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// get artifact retention days | ||||||
|  | 	expiredDays := setting.Actions.ArtifactRetentionDays | ||||||
|  | 	if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { | ||||||
|  | 		expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Error parse retention days: %v", err) | ||||||
|  | 			ctx.Error(http.StatusBadRequest, "Error parse retention days") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	log.Debug("[artifact] upload chunk, name: %s, path: %s, size: %d, retention days: %d", | ||||||
|  | 		artifactName, artifactPath, fileRealTotalSize, expiredDays) | ||||||
|  |  | ||||||
| 	// create or get artifact with name and path | 	// create or get artifact with name and path | ||||||
| 	artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath) | 	artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath, expiredDays) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Error create or get artifact: %v", err) | 		log.Error("Error create or get artifact: %v", err) | ||||||
| 		ctx.Error(http.StatusInternalServerError, "Error create or get artifact") | 		ctx.Error(http.StatusInternalServerError, "Error create or get artifact") | ||||||
|   | |||||||
| @@ -179,7 +179,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st | |||||||
| 	// save storage path to artifact | 	// save storage path to artifact | ||||||
| 	log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) | 	log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) | ||||||
| 	artifact.StoragePath = storagePath | 	artifact.StoragePath = storagePath | ||||||
| 	artifact.Status = actions.ArtifactStatusUploadConfirmed | 	artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) | ||||||
| 	if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { | 	if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { | ||||||
| 		return fmt.Errorf("update artifact error: %v", err) | 		return fmt.Errorf("update artifact error: %v", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -486,8 +486,9 @@ type ArtifactsViewResponse struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type ArtifactsViewItem struct { | type ArtifactsViewItem struct { | ||||||
| 	Name string `json:"name"` | 	Name   string `json:"name"` | ||||||
| 	Size int64  `json:"size"` | 	Size   int64  `json:"size"` | ||||||
|  | 	Status string `json:"status"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func ArtifactsView(ctx *context_module.Context) { | func ArtifactsView(ctx *context_module.Context) { | ||||||
| @@ -510,9 +511,14 @@ func ArtifactsView(ctx *context_module.Context) { | |||||||
| 		Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)), | 		Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)), | ||||||
| 	} | 	} | ||||||
| 	for _, art := range artifacts { | 	for _, art := range artifacts { | ||||||
|  | 		status := "completed" | ||||||
|  | 		if art.Status == int64(actions_model.ArtifactStatusExpired) { | ||||||
|  | 			status = "expired" | ||||||
|  | 		} | ||||||
| 		artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{ | 		artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{ | ||||||
| 			Name: art.ArtifactName, | 			Name:   art.ArtifactName, | ||||||
| 			Size: art.FileSize, | 			Size:   art.FileSize, | ||||||
|  | 			Status: status, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusOK, artifactsResponse) | 	ctx.JSON(http.StatusOK, artifactsResponse) | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								services/actions/cleanup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								services/actions/cleanup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package actions | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/actions" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Cleanup removes expired actions logs, data and artifacts | ||||||
|  | func Cleanup(taskCtx context.Context, olderThan time.Duration) error { | ||||||
|  | 	// TODO: clean up expired actions logs | ||||||
|  |  | ||||||
|  | 	// clean up expired artifacts | ||||||
|  | 	return CleanupArtifacts(taskCtx) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CleanupArtifacts removes expired artifacts and set records expired status | ||||||
|  | func CleanupArtifacts(taskCtx context.Context) error { | ||||||
|  | 	artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	log.Info("Found %d expired artifacts", len(artifacts)) | ||||||
|  | 	for _, artifact := range artifacts { | ||||||
|  | 		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { | ||||||
|  | 			log.Error("Cannot delete artifact %d: %v", artifact.ID, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { | ||||||
|  | 			log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		log.Info("Artifact %d set expired", artifact.ID) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @@ -13,6 +13,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/webhook" | 	"code.gitea.io/gitea/models/webhook" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/services/actions" | ||||||
| 	"code.gitea.io/gitea/services/auth" | 	"code.gitea.io/gitea/services/auth" | ||||||
| 	"code.gitea.io/gitea/services/migrations" | 	"code.gitea.io/gitea/services/migrations" | ||||||
| 	mirror_service "code.gitea.io/gitea/services/mirror" | 	mirror_service "code.gitea.io/gitea/services/mirror" | ||||||
| @@ -156,6 +157,20 @@ func registerCleanupPackages() { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func registerActionsCleanup() { | ||||||
|  | 	RegisterTaskFatal("cleanup_actions", &OlderThanConfig{ | ||||||
|  | 		BaseConfig: BaseConfig{ | ||||||
|  | 			Enabled:    true, | ||||||
|  | 			RunAtStart: true, | ||||||
|  | 			Schedule:   "@midnight", | ||||||
|  | 		}, | ||||||
|  | 		OlderThan: 24 * time.Hour, | ||||||
|  | 	}, func(ctx context.Context, _ *user_model.User, config Config) error { | ||||||
|  | 		realConfig := config.(*OlderThanConfig) | ||||||
|  | 		return actions.Cleanup(ctx, realConfig.OlderThan) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
| func initBasicTasks() { | func initBasicTasks() { | ||||||
| 	if setting.Mirror.Enabled { | 	if setting.Mirror.Enabled { | ||||||
| 		registerUpdateMirrorTask() | 		registerUpdateMirrorTask() | ||||||
| @@ -172,4 +187,7 @@ func initBasicTasks() { | |||||||
| 	if setting.Packages.Enabled { | 	if setting.Packages.Enabled { | ||||||
| 		registerCleanupPackages() | 		registerCleanupPackages() | ||||||
| 	} | 	} | ||||||
|  | 	if setting.Actions.Enabled { | ||||||
|  | 		registerActionsCleanup() | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,8 +18,9 @@ type uploadArtifactResponse struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type getUploadArtifactRequest struct { | type getUploadArtifactRequest struct { | ||||||
| 	Type string | 	Type          string | ||||||
| 	Name string | 	Name          string | ||||||
|  | 	RetentionDays int64 | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestActionsArtifactUploadSingleFile(t *testing.T) { | func TestActionsArtifactUploadSingleFile(t *testing.T) { | ||||||
| @@ -252,3 +253,40 @@ func TestActionsArtifactDownloadMultiFiles(t *testing.T) { | |||||||
| 		assert.Equal(t, resp.Body.String(), body) | 		assert.Equal(t, resp.Body.String(), body) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestActionsArtifactUploadWithRetentionDays(t *testing.T) { | ||||||
|  | 	defer tests.PrepareTestEnv(t)() | ||||||
|  |  | ||||||
|  | 	// acquire artifact upload url | ||||||
|  | 	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{ | ||||||
|  | 		Type:          "actions_storage", | ||||||
|  | 		Name:          "artifact-retention-days", | ||||||
|  | 		RetentionDays: 9, | ||||||
|  | 	}) | ||||||
|  | 	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") | ||||||
|  | 	resp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	var uploadResp uploadArtifactResponse | ||||||
|  | 	DecodeJSON(t, resp, &uploadResp) | ||||||
|  | 	assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") | ||||||
|  | 	assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9") | ||||||
|  |  | ||||||
|  | 	// get upload url | ||||||
|  | 	idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") | ||||||
|  | 	url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt" | ||||||
|  |  | ||||||
|  | 	// upload artifact chunk | ||||||
|  | 	body := strings.Repeat("A", 1024) | ||||||
|  | 	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) | ||||||
|  | 	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") | ||||||
|  | 	req.Header.Add("Content-Range", "bytes 0-1023/1024") | ||||||
|  | 	req.Header.Add("x-tfs-filelength", "1024") | ||||||
|  | 	req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body)) | ||||||
|  | 	MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	t.Logf("Create artifact confirm") | ||||||
|  |  | ||||||
|  | 	// confirm artifact upload | ||||||
|  | 	req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days") | ||||||
|  | 	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") | ||||||
|  | 	MakeRequest(t, req, http.StatusOK) | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user