Skip to content

Commit 92d14a2

Browse files
fuxiaoheisilverwind
authored andcommitted
Artifact deletion in actions ui (go-gitea#27172)
Add deletion link in runs view page. Fix go-gitea#26315 ![image](https://github.com/go-gitea/gitea/assets/2142787/aa65a4ab-f434-4deb-b953-21e63c212033) When click deletion button. It marks this artifact `need-delete`. This artifact would be deleted when actions cleanup cron task.
1 parent 2792b26 commit 92d14a2

File tree

8 files changed

+120
-11
lines changed

8 files changed

+120
-11
lines changed

models/actions/artifact.go

+22
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const (
2626
ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
2727
ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored
2828
ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired
29+
ArtifactStatusPendingDeletion // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
30+
ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
2931
)
3032

3133
func init() {
@@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
147149
Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
148150
}
149151

152+
// ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
153+
// limit is the max number of artifacts to return.
154+
func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) {
155+
arts := make([]*ActionArtifact, 0, limit)
156+
return arts, db.GetEngine(ctx).
157+
Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
158+
}
159+
150160
// SetArtifactExpired sets an artifact to expired
151161
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
152162
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
153163
return err
154164
}
165+
166+
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
167+
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
168+
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
169+
return err
170+
}
171+
172+
// SetArtifactDeleted sets an artifact to deleted
173+
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
174+
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
175+
return err
176+
}

options/locale/locale_en-US.ini

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ pin = Pin
123123
unpin = Unpin
124124

125125
artifacts = Artifacts
126+
confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ?
126127

127128
archived = Archived
128129

routers/web/repo/actions/view.go

+42-9
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,16 @@ type ViewRequest struct {
5757
type ViewResponse struct {
5858
State struct {
5959
Run struct {
60-
Link string `json:"link"`
61-
Title string `json:"title"`
62-
Status string `json:"status"`
63-
CanCancel bool `json:"canCancel"`
64-
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
65-
CanRerun bool `json:"canRerun"`
66-
Done bool `json:"done"`
67-
Jobs []*ViewJob `json:"jobs"`
68-
Commit ViewCommit `json:"commit"`
60+
Link string `json:"link"`
61+
Title string `json:"title"`
62+
Status string `json:"status"`
63+
CanCancel bool `json:"canCancel"`
64+
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
65+
CanRerun bool `json:"canRerun"`
66+
CanDeleteArtifact bool `json:"canDeleteArtifact"`
67+
Done bool `json:"done"`
68+
Jobs []*ViewJob `json:"jobs"`
69+
Commit ViewCommit `json:"commit"`
6970
} `json:"run"`
7071
CurrentJob struct {
7172
Title string `json:"title"`
@@ -146,6 +147,7 @@ func ViewPost(ctx *context_module.Context) {
146147
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
147148
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
148149
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
150+
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
149151
resp.State.Run.Done = run.Status.IsDone()
150152
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
151153
resp.State.Run.Status = run.Status.String()
@@ -535,6 +537,29 @@ func ArtifactsView(ctx *context_module.Context) {
535537
ctx.JSON(http.StatusOK, artifactsResponse)
536538
}
537539

540+
func ArtifactsDeleteView(ctx *context_module.Context) {
541+
if !ctx.Repo.CanWrite(unit.TypeActions) {
542+
ctx.Error(http.StatusForbidden, "no permission")
543+
return
544+
}
545+
546+
runIndex := ctx.ParamsInt64("run")
547+
artifactName := ctx.Params("artifact_name")
548+
549+
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
550+
if err != nil {
551+
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
552+
return errors.Is(err, util.ErrNotExist)
553+
}, err)
554+
return
555+
}
556+
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
557+
ctx.Error(http.StatusInternalServerError, err.Error())
558+
return
559+
}
560+
ctx.JSON(http.StatusOK, struct{}{})
561+
}
562+
538563
func ArtifactsDownloadView(ctx *context_module.Context) {
539564
runIndex := ctx.ParamsInt64("run")
540565
artifactName := ctx.Params("artifact_name")
@@ -562,6 +587,14 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
562587
return
563588
}
564589

590+
// if artifacts status is not uploaded-confirmed, treat it as not found
591+
for _, art := range artifacts {
592+
if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
593+
ctx.Error(http.StatusNotFound, "artifact not found")
594+
return
595+
}
596+
}
597+
565598
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
566599

567600
writer := zip.NewWriter(ctx.Resp)

routers/web/web.go

+1
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,7 @@ func registerRoutes(m *web.Route) {
13681368
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
13691369
m.Post("/artifacts", actions.ArtifactsView)
13701370
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
1371+
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
13711372
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
13721373
})
13731374
}, reqRepoActionsReader, actions.MustEnableActions)

services/actions/cleanup.go

+37-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,15 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
2020
return CleanupArtifacts(taskCtx)
2121
}
2222

23-
// CleanupArtifacts removes expired artifacts and set records expired status
23+
// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
2424
func CleanupArtifacts(taskCtx context.Context) error {
25+
if err := cleanExpiredArtifacts(taskCtx); err != nil {
26+
return err
27+
}
28+
return cleanNeedDeleteArtifacts(taskCtx)
29+
}
30+
31+
func cleanExpiredArtifacts(taskCtx context.Context) error {
2532
artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx)
2633
if err != nil {
2734
return err
@@ -40,3 +47,32 @@ func CleanupArtifacts(taskCtx context.Context) error {
4047
}
4148
return nil
4249
}
50+
51+
// deleteArtifactBatchSize is the batch size of deleting artifacts
52+
const deleteArtifactBatchSize = 100
53+
54+
func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
55+
for {
56+
artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
57+
if err != nil {
58+
return err
59+
}
60+
log.Info("Found %d artifacts pending deletion", len(artifacts))
61+
for _, artifact := range artifacts {
62+
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
63+
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
64+
continue
65+
}
66+
if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
67+
log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
68+
continue
69+
}
70+
log.Info("Artifact %d set deleted", artifact.ID)
71+
}
72+
if len(artifacts) < deleteArtifactBatchSize {
73+
log.Debug("No more artifacts pending deletion")
74+
break
75+
}
76+
}
77+
return nil
78+
}

templates/repo/actions/view.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}"
2020
data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
2121
data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
22+
data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}"
2223
data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}"
2324
data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
2425
data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}"

web_src/js/components/RepoActionView.vue

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {createApp} from 'vue';
55
import {toggleElem} from '../utils/dom.js';
66
import {getCurrentLocale} from '../utils.js';
77
import {renderAnsi} from '../render/ansi.js';
8-
import {POST} from '../modules/fetch.js';
8+
import {POST, DELETE} from '../modules/fetch.js';
99
1010
const sfc = {
1111
name: 'RepoActionView',
@@ -200,6 +200,12 @@ const sfc = {
200200
return await resp.json();
201201
},
202202
203+
async deleteArtifact(name) {
204+
if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
205+
await DELETE(`${this.run.link}/artifacts/${name}`);
206+
await this.loadJob();
207+
},
208+
203209
async fetchJob() {
204210
const logCursors = this.currentJobStepsStates.map((it, idx) => {
205211
// cursor is used to indicate the last position of the logs
@@ -329,6 +335,8 @@ export function initRepositoryActionView() {
329335
cancel: el.getAttribute('data-locale-cancel'),
330336
rerun: el.getAttribute('data-locale-rerun'),
331337
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
338+
areYouSure: el.getAttribute('data-locale-are-you-sure'),
339+
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
332340
rerun_all: el.getAttribute('data-locale-rerun-all'),
333341
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
334342
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
@@ -404,6 +412,9 @@ export function initRepositoryActionView() {
404412
<a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
405413
<SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
406414
</a>
415+
<a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
416+
<SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
417+
</a>
407418
</li>
408419
</ul>
409420
</div>
@@ -528,6 +539,8 @@ export function initRepositoryActionView() {
528539
.job-artifacts-item {
529540
margin: 5px 0;
530541
padding: 6px;
542+
display: flex;
543+
justify-content: space-between;
531544
}
532545
533546
.job-artifacts-list {

web_src/js/svg.js

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethro
6767
import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
6868
import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
6969
import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
70+
import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
7071
import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
7172
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
7273
import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
@@ -139,6 +140,7 @@ const svgs = {
139140
'octicon-sync': octiconSync,
140141
'octicon-table': octiconTable,
141142
'octicon-tag': octiconTag,
143+
'octicon-trash': octiconTrash,
142144
'octicon-triangle-down': octiconTriangleDown,
143145
'octicon-x': octiconX,
144146
'octicon-x-circle-fill': octiconXCircleFill,

0 commit comments

Comments
 (0)