mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-19 19:18:43 +02:00
Merge branch 'go-gitea:main' into main
This commit is contained in:
commit
a3c29538f1
@ -16,6 +16,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -343,13 +344,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
|
||||
func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
|
||||
var run ActionRun
|
||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist)
|
||||
return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist)
|
||||
}
|
||||
|
||||
return &run, nil
|
||||
@ -420,18 +421,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
||||
|
||||
if run.Status != 0 || slices.Contains(cols, "status") {
|
||||
if run.RepoID == 0 {
|
||||
run, err = GetRunByID(ctx, run.ID)
|
||||
if err != nil {
|
||||
setting.PanicInDevOrTesting("RepoID should not be 0")
|
||||
}
|
||||
if err = run.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if run.Repo == nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
run.Repo = repo
|
||||
}
|
||||
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration {
|
||||
|
||||
func (job *ActionRunJob) LoadRun(ctx context.Context) error {
|
||||
if job.Run == nil {
|
||||
run, err := GetRunByID(ctx, job.RunID)
|
||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -142,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
||||
{
|
||||
// Other goroutines may aggregate the status of the run and update it too.
|
||||
// So we need load the run and its jobs before updating the run.
|
||||
run, err := GetRunByID(ctx, job.RunID)
|
||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error {
|
||||
type FindTaskOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
JobID int64
|
||||
OwnerID int64
|
||||
CommitSHA string
|
||||
Status Status
|
||||
@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.JobID > 0 {
|
||||
cond = cond.And(builder.Eq{"job_id": opts.JobID})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
|
@ -530,7 +530,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
||||
|
||||
if opts.RequestedTeam != nil {
|
||||
env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
|
||||
teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos)
|
||||
teamRepoIDs, err := env.RepoIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTeamRepositories: %w", err)
|
||||
}
|
||||
|
@ -105,3 +105,39 @@
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 24
|
||||
run_id: 795
|
||||
runner_id: 1
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/zip"
|
||||
artifact_path: "artifact-795-1.zip"
|
||||
artifact_name: "artifact-795-1"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 25
|
||||
run_id: 795
|
||||
runner_id: 1
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/zip"
|
||||
artifact_path: "artifact-795-2.zip"
|
||||
artifact_name: "artifact-795-2"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
@ -48,7 +48,7 @@
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
@ -74,3 +74,23 @@
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
||||
-
|
||||
id: 795
|
||||
title: "to be deleted (test)"
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
workflow_id: "test.yaml"
|
||||
index: 191
|
||||
trigger_user_id: 1
|
||||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
@ -69,3 +69,33 @@
|
||||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
-
|
||||
id: 198
|
||||
run_id: 795
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 1
|
||||
job_id: job_1
|
||||
task_id: 53
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
-
|
||||
id: 199
|
||||
run_id: 795
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_2
|
||||
attempt: 1
|
||||
job_id: job_2
|
||||
task_id: 54
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
@ -117,3 +117,43 @@
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 53
|
||||
job_id: 198
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784223
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
-
|
||||
id: 54
|
||||
job_id: 199
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784224
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
|
@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100)
|
||||
repoIDs, err := env.RepoIDs(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRepoIDs, repoIDs)
|
||||
}
|
||||
@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
||||
testSuccess(4, []int64{3, 32})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_Repos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repos, err := env.Repos(db.DefaultContext, 1, 100)
|
||||
assert.NoError(t, err)
|
||||
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
|
||||
for i, repoID := range expectedRepoIDs {
|
||||
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
|
||||
&repo_model.Repository{ID: repoID})
|
||||
}
|
||||
assert.Equal(t, expectedRepos, repos)
|
||||
}
|
||||
testSuccess(2, []int64{3, 5, 32})
|
||||
testSuccess(4, []int64{3, 32})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
|
@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
|
||||
// accessible to a particular user
|
||||
type AccessibleReposEnvironment interface {
|
||||
CountRepos(ctx context.Context) (int64, error)
|
||||
RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error)
|
||||
Repos(ctx context.Context, page, pageSize int) (RepositoryList, error)
|
||||
RepoIDs(ctx context.Context) ([]int64, error)
|
||||
MirrorRepos(ctx context.Context) (RepositoryList, error)
|
||||
AddKeyword(keyword string)
|
||||
SetSort(db.SearchOrderBy)
|
||||
@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
|
||||
return repoCount, nil
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
repoIDs := make([]int64, 0, pageSize)
|
||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
|
||||
var repoIDs []int64
|
||||
return repoIDs, db.GetEngine(ctx).
|
||||
Table("repository").
|
||||
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
|
||||
Where(env.cond()).
|
||||
GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]).
|
||||
OrderBy(string(env.orderBy)).
|
||||
Limit(pageSize, (page-1)*pageSize).
|
||||
Cols("`repository`.id").
|
||||
Find(&repoIDs)
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) {
|
||||
repoIDs, err := env.RepoIDs(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
|
||||
}
|
||||
|
||||
repos := make([]*Repository, 0, len(repoIDs))
|
||||
if len(repoIDs) == 0 {
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
return repos, db.GetEngine(ctx).
|
||||
In("`repository`.id", repoIDs).
|
||||
OrderBy(string(env.orderBy)).
|
||||
Find(&repos)
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
|
||||
repoIDs := make([]int64, 0, 10)
|
||||
return repoIDs, db.GetEngine(ctx).
|
||||
|
@ -3811,6 +3811,9 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see <a
|
||||
runs.no_runs = The workflow has no runs yet.
|
||||
runs.empty_commit_message = (empty commit message)
|
||||
runs.expire_log_message = Logs have been purged because they were too old.
|
||||
runs.delete = Delete workflow run
|
||||
runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone.
|
||||
runs.not_done = This workflow run is not done.
|
||||
|
||||
workflow.disable = Disable Workflow
|
||||
workflow.disable_success = Workflow '%s' disabled successfully.
|
||||
|
@ -1279,7 +1279,10 @@ func Routes() *web.Router {
|
||||
}, reqToken(), reqAdmin())
|
||||
m.Group("/actions", func() {
|
||||
m.Get("/tasks", repo.ListActionTasks)
|
||||
m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Group("/runs/{run}", func() {
|
||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||
})
|
||||
m.Get("/artifacts", repo.GetArtifacts)
|
||||
m.Group("/artifacts/{artifact_id}", func() {
|
||||
m.Get("", repo.GetArtifact)
|
||||
|
@ -1061,6 +1061,58 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
// DeleteActionRun Delete a workflow run
|
||||
func DeleteActionRun(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun
|
||||
// ---
|
||||
// summary: Delete a workflow run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: runid of the workflow run
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// description: "No Content"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
runID := ctx.PathParamInt64("run")
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !run.Status.IsDone() {
|
||||
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteRun(ctx, run); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetArtifacts Lists all artifacts for a repository.
|
||||
func GetArtifacts(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts
|
||||
|
@ -317,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||
|
||||
ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions)
|
||||
}
|
||||
|
||||
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
||||
|
@ -577,6 +577,33 @@ func Approve(ctx *context_module.Context) {
|
||||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
}
|
||||
|
||||
func Delete(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !run.Status.IsDone() {
|
||||
ctx.JSONError(ctx.Tr("actions.runs.not_done"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteRun(ctx, run); err != nil {
|
||||
ctx.ServerError("DeleteRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
|
||||
// Any error will be written to the ctx.
|
||||
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
|
||||
|
@ -264,7 +264,7 @@ func MergeUpstream(ctx *context.Context) {
|
||||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONError(ctx.Tr("error.not_found"))
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
@ -261,6 +262,10 @@ func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status re
|
||||
|
||||
func handleRepoEmptyOrBroken(ctx *context.Context) {
|
||||
showEmpty := true
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
// in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
|
||||
ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
}
|
||||
if ctx.Repo.GitRepo != nil {
|
||||
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
|
||||
if err != nil {
|
||||
@ -396,10 +401,8 @@ func Home(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
|
||||
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
||||
if len(ctx.Repo.Repository.Description) > 0 {
|
||||
if ctx.Repo.Repository.Description != "" {
|
||||
title += ": " + ctx.Repo.Repository.Description
|
||||
}
|
||||
ctx.Data["Title"] = title
|
||||
@ -412,6 +415,8 @@ func Home(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
|
||||
// get the current git entry which doer user is currently looking at.
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
|
@ -1447,6 +1447,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
})
|
||||
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
||||
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
||||
m.Post("/delete", reqRepoActionsWriter, actions.Delete)
|
||||
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
||||
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||
|
@ -5,12 +5,14 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
@ -27,7 +29,7 @@ func Cleanup(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// clean up old logs
|
||||
if err := CleanupLogs(ctx); err != nil {
|
||||
if err := CleanupExpiredLogs(ctx); err != nil {
|
||||
return fmt.Errorf("cleanup logs: %w", err)
|
||||
}
|
||||
|
||||
@ -98,8 +100,15 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
|
||||
|
||||
const deleteLogBatchSize = 100
|
||||
|
||||
// CleanupLogs removes logs which are older than the configured retention time
|
||||
func CleanupLogs(ctx context.Context) error {
|
||||
func removeTaskLog(ctx context.Context, task *actions_model.ActionTask) {
|
||||
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
|
||||
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
|
||||
// do not return error here, go on
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupExpiredLogs removes logs which are older than the configured retention time
|
||||
func CleanupExpiredLogs(ctx context.Context) error {
|
||||
olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour)
|
||||
|
||||
count := 0
|
||||
@ -109,10 +118,7 @@ func CleanupLogs(ctx context.Context) error {
|
||||
return fmt.Errorf("find old tasks: %w", err)
|
||||
}
|
||||
for _, task := range tasks {
|
||||
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
|
||||
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
|
||||
// do not return error here, go on
|
||||
}
|
||||
removeTaskLog(ctx, task)
|
||||
task.LogIndexes = nil // clear log indexes since it's a heavy field
|
||||
task.LogExpired = true
|
||||
if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil {
|
||||
@ -148,3 +154,91 @@ func CleanupEphemeralRunners(ctx context.Context) error {
|
||||
log.Info("Removed %d runners", affected)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRun deletes workflow run, including all logs and artifacts.
|
||||
func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||
if !run.Status.IsDone() {
|
||||
return errors.New("run is not done")
|
||||
}
|
||||
|
||||
repoID := run.RepoID
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobIDs := container.FilterSlice(jobs, func(j *actions_model.ActionRunJob) (int64, bool) {
|
||||
return j.ID, true
|
||||
})
|
||||
tasks := make(actions_model.TaskList, 0)
|
||||
if len(jobIDs) > 0 {
|
||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).In("job_id", jobIDs).Find(&tasks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var recordsToDelete []any
|
||||
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionRun{
|
||||
RepoID: repoID,
|
||||
ID: run.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJob{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
for _, tas := range tasks {
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTask{
|
||||
RepoID: repoID,
|
||||
ID: tas.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskStep{
|
||||
RepoID: repoID,
|
||||
TaskID: tas.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskOutput{
|
||||
TaskID: tas.ID,
|
||||
})
|
||||
}
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionArtifact{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// TODO: Deleting task records could break current ephemeral runner implementation. This is a temporary workaround suggested by ChristopherHX.
|
||||
// Since you delete potentially the only task an ephemeral act_runner has ever run, please delete the affected runners first.
|
||||
// one of
|
||||
// call cleanup ephemeral runners first
|
||||
// delete affected ephemeral act_runners
|
||||
// I would make ephemeral runners fully delete directly before formally finishing the task
|
||||
//
|
||||
// See also: https://github.com/go-gitea/gitea/pull/34337#issuecomment-2862222788
|
||||
if err := CleanupEphemeralRunners(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.DeleteBeans(ctx, recordsToDelete...)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete files on storage
|
||||
for _, tas := range tasks {
|
||||
removeTaskLog(ctx, tas)
|
||||
}
|
||||
for _, art := range artifacts {
|
||||
if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil {
|
||||
log.Error("remove artifact file %q: %v", art.StoragePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
// APIErrorNotFound handles 404s for APIContext
|
||||
// String will replace message, errors will be added to a slice
|
||||
func (ctx *APIContext) APIErrorNotFound(objs ...any) {
|
||||
message := ctx.Locale.TrString("error.not_found")
|
||||
var message string
|
||||
var errs []string
|
||||
for _, obj := range objs {
|
||||
// Ignore nil
|
||||
@ -259,9 +259,8 @@ func (ctx *APIContext) APIErrorNotFound(objs ...any) {
|
||||
message = obj.(string)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{
|
||||
"message": message,
|
||||
"message": util.IfZero(message, "not found"), // do not use locale in API
|
||||
"url": setting.API.SwaggerURL,
|
||||
"errors": errs,
|
||||
})
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
@ -261,3 +262,11 @@ func (ctx *Context) JSONError(msg any) {
|
||||
panic(fmt.Sprintf("unsupported type: %T", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) JSONErrorNotFound(optMsg ...string) {
|
||||
msg := util.OptionalArg(optMsg)
|
||||
if msg == "" {
|
||||
msg = ctx.Locale.TrString("error.not_found")
|
||||
}
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{"errorMessage": msg, "renderFormat": "text"})
|
||||
}
|
||||
|
@ -795,8 +795,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
|
||||
return func(ctx *Context) {
|
||||
var err error
|
||||
refType := detectRefType
|
||||
if ctx.Repo.Repository.IsBeingCreated() {
|
||||
return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl"
|
||||
if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
|
||||
return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl", or empty repo guide
|
||||
}
|
||||
// Empty repository does not have reference information.
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
|
@ -64,10 +64,11 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us
|
||||
if err != nil {
|
||||
return fmt.Errorf("AccessibleReposEnv: %w", err)
|
||||
}
|
||||
repoIDs, err := env.RepoIDs(ctx, 1, org.NumRepos)
|
||||
repoIDs, err := env.RepoIDs(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
|
||||
}
|
||||
|
||||
for _, repoID := range repoIDs {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
|
@ -5,37 +5,46 @@
|
||||
<h2>{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .Runs}}
|
||||
{{range $run := .Runs}}
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-leading">
|
||||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
||||
{{template "repo/actions/status" (dict "status" $run.Status.String)}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<a class="flex-item-title" title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
|
||||
<a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}">
|
||||
{{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
|
||||
</a>
|
||||
<div class="flex-item-body">
|
||||
<span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span>
|
||||
{{- if .ScheduleID -}}
|
||||
<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
|
||||
{{- if $run.ScheduleID -}}
|
||||
{{ctx.Locale.Tr "actions.runs.scheduled"}}
|
||||
{{- else -}}
|
||||
{{ctx.Locale.Tr "actions.runs.commit"}}
|
||||
<a href="{{$.RepoLink}}/commit/{{.CommitSHA}}">{{ShortSha .CommitSHA}}</a>
|
||||
<a href="{{$.RepoLink}}/commit/{{$run.CommitSHA}}">{{ShortSha $run.CommitSHA}}</a>
|
||||
{{ctx.Locale.Tr "actions.runs.pushed_by"}}
|
||||
<a href="{{.TriggerUser.HomeLink}}">{{.TriggerUser.GetDisplayName}}</a>
|
||||
<a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
{{if .IsRefDeleted}}
|
||||
<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</span>
|
||||
{{if $run.IsRefDeleted}}
|
||||
<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</span>
|
||||
{{else}}
|
||||
<a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</a>
|
||||
<a class="ui label run-list-ref gt-ellipsis" href="{{$run.RefLink}}" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</a>
|
||||
{{end}}
|
||||
<div class="run-list-item-right">
|
||||
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{$run.Duration}}</div>
|
||||
</div>
|
||||
{{if and ($.AllowDeleteWorkflowRuns) ($run.Status.IsDone)}}
|
||||
<button class="btn interact-bg link-action tw-p-2"
|
||||
data-url="{{$run.Link}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "actions.runs.delete"}}"
|
||||
>
|
||||
{{svg "octicon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="item muted" href="{{.Link}}/releases">
|
||||
<a class="item muted" href="{{.RepoLink}}/releases">
|
||||
{{ctx.Locale.Tr "repo.releases"}}
|
||||
<span class="ui small label">{{.NumReleases}}</span>
|
||||
</a>
|
||||
|
@ -37,7 +37,7 @@
|
||||
<span class="color-text-light-2">
|
||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
||||
</span>
|
||||
<button class="ui btn interact-bg show-modal tw-p-2"
|
||||
<button class="btn interact-bg show-modal tw-p-2"
|
||||
data-modal="#add-secret-modal"
|
||||
data-modal-form.action="{{$.Link}}"
|
||||
data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}"
|
||||
@ -49,7 +49,7 @@
|
||||
>
|
||||
{{svg "octicon-pencil"}}
|
||||
</button>
|
||||
<button class="ui btn interact-bg link-action tw-p-2"
|
||||
<button class="btn interact-bg link-action tw-p-2"
|
||||
data-url="{{$.Link}}/delete?id={{.ID}}"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
|
||||
|
46
templates/swagger/v1_json.tmpl
generated
46
templates/swagger/v1_json.tmpl
generated
@ -4758,6 +4758,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}": {
|
||||
"delete": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Delete a workflow run",
|
||||
"operationId": "deleteActionRun",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the owner",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "runid of the workflow run",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
181
tests/integration/actions_delete_run_test.go
Normal file
181
tests/integration/actions_delete_run_test.go
Normal file
@ -0,0 +1,181 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestActionsDeleteRun(t *testing.T) {
|
||||
now := time.Now()
|
||||
testCase := struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedStatuses map[string]string
|
||||
}{
|
||||
treePath: ".gitea/workflows/test1.yml",
|
||||
fileContent: `name: test1
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .gitea/workflows/test1.yml
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job2
|
||||
job3:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job3
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job1",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job2",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job3": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job3",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusSuccess.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
"job3": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-delete-run-test", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||
|
||||
runIndex := ""
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := testCase.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
runIndex = task.Context.GetFields()["run_number"].GetStringValue()
|
||||
assert.Equal(t, "1", runIndex)
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var listResp actions.ViewResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, listResp.State.Run.Jobs, 3)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
@ -72,12 +73,37 @@ func TestAdminDeleteUser(t *testing.T) {
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
usersToDelete := []struct {
|
||||
userID int64
|
||||
purge bool
|
||||
}{
|
||||
{
|
||||
userID: 2,
|
||||
purge: true,
|
||||
},
|
||||
{
|
||||
userID: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, entry := range usersToDelete {
|
||||
t.Run(fmt.Sprintf("DeleteUser%d", entry.userID), func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: entry.userID})
|
||||
assert.NotNil(t, user)
|
||||
|
||||
var query string
|
||||
if entry.purge {
|
||||
query = "?purge=true"
|
||||
}
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", "/-/admin/users/8/delete", map[string]string{
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
|
||||
"_csrf": csrf,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
assertUserDeleted(t, 8)
|
||||
assertUserDeleted(t, entry.userID)
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
98
tests/integration/api_actions_delete_run_test.go
Normal file
98
tests/integration/api_actions_delete_run_test.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIActionsDeleteRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 2)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, true)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNoContent)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 0)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, false)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIActionsDeleteRunRunning(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token string, expected int) {
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp api.ActionArtifactsResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, listResp.Entries, artifacts)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp api.ActionTaskResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
findTask1 := false
|
||||
findTask2 := false
|
||||
for _, entry := range listResp.Entries {
|
||||
if entry.ID == 53 {
|
||||
findTask1 = true
|
||||
continue
|
||||
}
|
||||
if entry.ID == 54 {
|
||||
findTask2 = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
assert.Equal(t, expected, findTask1)
|
||||
assert.Equal(t, expected, findTask2)
|
||||
}
|
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -100,13 +101,16 @@ func TestEmptyRepoAddFile(t *testing.T) {
|
||||
assert.Contains(t, resp.Body.String(), "test-file.md")
|
||||
|
||||
// if the repo is in incorrect state, it should be able to self-heal (recover to correct state)
|
||||
testEmptyOrBrokenRecover := func(t *testing.T, isEmpty, isBroken bool) {
|
||||
user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
user30EmptyRepo.IsEmpty = true
|
||||
user30EmptyRepo.IsEmpty = isEmpty
|
||||
user30EmptyRepo.Status = util.Iif(isBroken, repo_model.RepositoryBroken, repo_model.RepositoryReady)
|
||||
user30EmptyRepo.DefaultBranch = "no-such"
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "default_branch").Update(user30EmptyRepo)
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "status", "default_branch").Update(user30EmptyRepo)
|
||||
require.NoError(t, err)
|
||||
user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
assert.True(t, user30EmptyRepo.IsEmpty)
|
||||
assert.Equal(t, isEmpty, user30EmptyRepo.IsEmpty)
|
||||
assert.Equal(t, isBroken, user30EmptyRepo.Status == repo_model.RepositoryBroken)
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/empty")
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
@ -117,6 +121,10 @@ func TestEmptyRepoAddFile(t *testing.T) {
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "test-file.md")
|
||||
}
|
||||
testEmptyOrBrokenRecover(t, true, false)
|
||||
testEmptyOrBrokenRecover(t, false, true)
|
||||
testEmptyOrBrokenRecover(t, true, true)
|
||||
}
|
||||
|
||||
func TestEmptyRepoUploadFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
@ -161,6 +161,7 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
||||
function resizeToFit() {
|
||||
if (isUserResized) return;
|
||||
if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
|
||||
const previousMargin = textarea.style.marginBottom;
|
||||
|
||||
try {
|
||||
const {top, bottom} = overflowOffset();
|
||||
@ -176,6 +177,9 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
||||
const curHeight = parseFloat(computedStyle.height);
|
||||
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
|
||||
|
||||
// In Firefox, setting auto height momentarily may cause the page to scroll up
|
||||
// unexpectedly, prevent this by setting a temporary margin.
|
||||
textarea.style.marginBottom = `${textarea.clientHeight}px`;
|
||||
textarea.style.height = 'auto';
|
||||
let newHeight = textarea.scrollHeight + borderAddOn;
|
||||
|
||||
@ -196,6 +200,12 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
lastStyleHeight = textarea.style.height;
|
||||
} finally {
|
||||
// restore previous margin
|
||||
if (previousMargin) {
|
||||
textarea.style.marginBottom = previousMargin;
|
||||
} else {
|
||||
textarea.style.removeProperty('margin-bottom');
|
||||
}
|
||||
// ensure that the textarea is fully scrolled to the end, when the cursor
|
||||
// is at the end during an input event
|
||||
if (textarea.selectionStart === textarea.selectionEnd &&
|
||||
|
Loading…
x
Reference in New Issue
Block a user