mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Merge 5576b10604c39c2323521edf36160e4885c84bd8 into 21af8150b7ba315a9f75264ab77813b0b7c697a8
This commit is contained in:
commit
f8da60f218
@ -435,3 +435,56 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActionRunIndex db.ResourceIndex
|
type ActionRunIndex db.ResourceIndex
|
||||||
|
|
||||||
|
// DeleteActionRunAndChild delete action_task_step, action_task_output, action_task, action_run and action_run_job.
|
||||||
|
func DeleteActionRunAndChild(ctx context.Context, runIDs, jobIDs, taskIDs []int64) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
|
||||||
|
Delete(ActionTaskStep{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
|
||||||
|
Delete(ActionTaskOutput{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).In("id", taskIDs).Delete(ActionTask{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).In("id", jobIDs).Delete(ActionRunJob{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).In("id", runIDs).Delete(ActionRun{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunsByIDsAndTriggerUserID -- get all action run by trigger user with selected ids
|
||||||
|
func GetRunsByIDsAndTriggerUserID(ctx context.Context, ids []int64, triggerUserID int64) ([]*ActionRun, error) {
|
||||||
|
var runs []*ActionRun
|
||||||
|
err := db.GetEngine(ctx).Where("trigger_user_id=?", triggerUserID).
|
||||||
|
In("id", ids).Find(&runs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(runs) < 1 {
|
||||||
|
return nil, fmt.Errorf("run with ids %d: %w", ids, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs, nil
|
||||||
|
}
|
||||||
|
@ -184,3 +184,19 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
|||||||
return StatusUnknown // it shouldn't happen
|
return StatusUnknown // it shouldn't happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRunJobsByRunIDs(ctx context.Context, runIDs []int64) ([]*ActionRunJob, error) {
|
||||||
|
var jobs []*ActionRunJob
|
||||||
|
if err := db.GetEngine(ctx).In("run_id", runIDs).Find(&jobs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRunTasksByJobIDs(ctx context.Context, jobIDs []int64) ([]*ActionTask, error) {
|
||||||
|
var tasks []*ActionTask
|
||||||
|
if err := db.GetEngine(ctx).In("job_id", jobIDs).Find(&tasks); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
stdCtx "context"
|
stdCtx "context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -28,6 +31,7 @@ import (
|
|||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -312,6 +316,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
|||||||
pager.AddParamFromRequest(ctx.Req)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||||
|
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
||||||
@ -424,3 +429,94 @@ func decodeNode(node yaml.Node, out any) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteRuns(ctx *context.Context) {
|
||||||
|
rd := ctx.Req.Body
|
||||||
|
defer rd.Close()
|
||||||
|
|
||||||
|
req := DeleteRunsRequest{}
|
||||||
|
if err := json.NewDecoder(rd).Decode(&req); err != nil {
|
||||||
|
ctx.ServerError("failed to decode request body into delte runs request", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.ActionIDs) < 1 {
|
||||||
|
ctx.ServerError("missing action_run.id for delete action run", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
eg = new(errgroup.Group)
|
||||||
|
actionRuns []*actions_model.ActionRun
|
||||||
|
jobIDs, taskIDs []int64
|
||||||
|
taskLogFileNames []string
|
||||||
|
)
|
||||||
|
eg.Go(func() error {
|
||||||
|
var err error
|
||||||
|
actionRuns, err = actions_model.GetRunsByIDsAndTriggerUserID(ctx, req.ActionIDs, ctx.Doer.ID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
actionRunJobs, err := actions_model.GetRunJobsByRunIDs(ctx, req.ActionIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, actionRunJob := range actionRunJobs {
|
||||||
|
jobIDs = append(jobIDs, actionRunJob.ID)
|
||||||
|
}
|
||||||
|
actionTasks, err := actions_model.GetRunTasksByJobIDs(ctx, jobIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, actionTask := range actionTasks {
|
||||||
|
taskIDs = append(taskIDs, actionTask.ID)
|
||||||
|
taskLogFileNames = append(taskLogFileNames, actionTask.LogFilename)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("failed to get action runs and action run jobs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actionRuns) != len(req.ActionIDs) {
|
||||||
|
ctx.ServerError("action ids not match with request", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = actions_model.DeleteActionRunAndChild(ctx, req.ActionIDs, jobIDs, taskIDs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("failed to delete action_run", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removeActionTaskLogFilenames(taskLogFileNames)
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteRunsRequest struct {
|
||||||
|
ActionIDs []int64 `json:"actionIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeActionTaskLogFilenames(taskLogFileNames []string) {
|
||||||
|
dirNameActionLog := "actions_log"
|
||||||
|
go func() {
|
||||||
|
for _, taskLogFileName := range taskLogFileNames {
|
||||||
|
var fileName string
|
||||||
|
if filepath.IsAbs(setting.AppDataPath) {
|
||||||
|
fileName = filepath.Join(setting.AppDataPath, dirNameActionLog, taskLogFileName)
|
||||||
|
} else {
|
||||||
|
fileName = filepath.Join(setting.AppWorkPath, setting.AppDataPath, dirNameActionLog, taskLogFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(fileName); err != nil {
|
||||||
|
log.Error("failed to remove actions_log file %s: %v", fileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
@ -1447,6 +1447,8 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Group("/workflows/{workflow_name}", func() {
|
m.Group("/workflows/{workflow_name}", func() {
|
||||||
m.Get("/badge.svg", actions.GetWorkflowBadge)
|
m.Get("/badge.svg", actions.GetWorkflowBadge)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Post("/runs/delete", reqRepoActionsWriter, actions.DeleteRuns)
|
||||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
||||||
// end "/{username}/{reponame}/actions"
|
// end "/{username}/{reponame}/actions"
|
||||||
|
|
||||||
|
@ -25,7 +25,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="twelve wide column content">
|
<div class="twelve wide column content">
|
||||||
<div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center">
|
<div id="action-filter" class="ui secondary filter menu tw-flex tw-items-center tw-justify-between">
|
||||||
|
<!-- Checkbox -->
|
||||||
|
<div class="action-list-toolbar-left">
|
||||||
|
<input type="checkbox" autocomplete="off" class="action-checkbox-all tw-mr-4 tw-ml-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex tw-items-center">
|
||||||
|
{{if $.IsRepoAdmin}}
|
||||||
|
<div id="action-delete" class="ui jump item tw-hidden">
|
||||||
|
<button class="ui red button action-action" data-action="delete" data-url="{{$.RepoLink}}/actions/runs/delete" data-action-delete-confirm="{{ctx.Locale.Tr "confirm_delete_selected"}}">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.delete"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<!-- Actor -->
|
<!-- Actor -->
|
||||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||||
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
||||||
@ -63,6 +75,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .AllowDisableOrEnableWorkflow}}
|
{{if .AllowDisableOrEnableWorkflow}}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="flex-list run-list">
|
<div id="action-actions" class="flex-list run-list">
|
||||||
{{if not .Runs}}
|
{{if not .Runs}}
|
||||||
<div class="empty-placeholder">
|
<div class="empty-placeholder">
|
||||||
{{svg "octicon-no-entry" 48}}
|
{{svg "octicon-no-entry" 48}}
|
||||||
@ -7,6 +7,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{range .Runs}}
|
{{range .Runs}}
|
||||||
<div class="flex-item tw-items-center">
|
<div class="flex-item tw-items-center">
|
||||||
|
<input type="checkbox" autocomplete="off" class="action-checkbox tw-mr-4 tw-ml-4" data-action-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""{{if or (eq .Status 6) (eq .Status 5)}}disabled{{end}}>
|
||||||
<div class="flex-item-leading">
|
<div class="flex-item-leading">
|
||||||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
||||||
</div>
|
</div>
|
||||||
|
235
tests/integration/api_repo_action_delete_test.go
Normal file
235
tests/integration/api_repo_action_delete_test.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
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/util"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepoActionDelete(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
treePath string
|
||||||
|
fileContent string
|
||||||
|
outcomes map[string]*mockTaskOutcome
|
||||||
|
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
|
||||||
|
fileContent: `name: jobs-outputs-with-matrix
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||||
|
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||||
|
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: [1, 2, 3]
|
||||||
|
steps:
|
||||||
|
- name: Generate output
|
||||||
|
id: gen_output
|
||||||
|
run: |
|
||||||
|
version="${{ matrix.version }}"
|
||||||
|
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1 (1)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (2)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "2",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (3)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||||
|
"job1": {
|
||||||
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "2",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
|
||||||
|
fileContent: `name: jobs-outputs-with-matrix-failure
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||||
|
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||||
|
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: [1, 2, 3]
|
||||||
|
steps:
|
||||||
|
- name: Generate output
|
||||||
|
id: gen_output
|
||||||
|
run: |
|
||||||
|
version="${{ matrix.version }}"
|
||||||
|
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1 (1)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (2)": {
|
||||||
|
result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (3)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||||
|
"job1": {
|
||||||
|
Result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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-jobs-outputs-with-matrix", false)
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||||
|
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||||
|
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||||
|
|
||||||
|
for i := 0; i < len(tc.outcomes); i++ {
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||||
|
outcome := tc.outcomes[jobName]
|
||||||
|
assert.NotNil(t, outcome)
|
||||||
|
runner.execTask(t, task, outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
actualTaskNeeds := task.Needs
|
||||||
|
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
|
||||||
|
for jobID, tn := range tc.expectedTaskNeeds {
|
||||||
|
actualNeed := actualTaskNeeds[jobID]
|
||||||
|
assert.Equal(t, tn.Result, actualNeed.Result)
|
||||||
|
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
|
||||||
|
for outputKey, outputValue := range tn.Outputs {
|
||||||
|
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
// check result
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions", httpContext.Username, httpContext.Reponame))
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
var runIDs []int64
|
||||||
|
list := htmlDoc.doc.Find("#action-actions input.action-checkbox:not(:disabled)")
|
||||||
|
list.Each(func(i int, s *goquery.Selection) {
|
||||||
|
idStr, exists := s.Attr("data-action-id")
|
||||||
|
if exists {
|
||||||
|
runID, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
runIDs = append(runIDs, runID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NotEmpty(t, runIDs)
|
||||||
|
csrf := GetUserCSRFToken(t, session)
|
||||||
|
reqDelete := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/%s/%s/actions/runs/delete", httpContext.Username, httpContext.Reponame), map[string]any{
|
||||||
|
"actionIds": runIDs,
|
||||||
|
"_csrf": csrf,
|
||||||
|
}).AddTokenAuth(token).
|
||||||
|
SetHeader("X-Csrf-Token", csrf)
|
||||||
|
session.MakeRequest(t, reqDelete, http.StatusNoContent)
|
||||||
|
|
||||||
|
// should not found
|
||||||
|
_, err := actions_model.GetRunsByIDsAndTriggerUserID(context.Background(), runIDs, user2.ID)
|
||||||
|
assert.EqualError(t, err, fmt.Errorf("run with ids %d: %w", runIDs, util.ErrNotExist).Error())
|
||||||
|
doAPIDeleteRepository(httpContext)
|
||||||
|
})
|
||||||
|
}
|
97
web_src/js/features/repo-action-list.ts
Normal file
97
web_src/js/features/repo-action-list.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||||
|
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||||
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
|
import {POST} from '../modules/fetch.ts';
|
||||||
|
|
||||||
|
function initRepoActionListCheckboxes() {
|
||||||
|
const actionListSelectAll = document.querySelector<HTMLInputElement>('.action-checkbox-all');
|
||||||
|
if (!actionListSelectAll) return; // logged out state
|
||||||
|
const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.action-checkbox:not([disabled])');
|
||||||
|
const actionDelete = document.querySelector('#action-delete');
|
||||||
|
const syncIssueSelectionState = () => {
|
||||||
|
const enabledCheckboxes = Array.from(issueCheckboxes).filter((el) => !el.disabled);
|
||||||
|
const checkedCheckboxes = enabledCheckboxes.filter((el) => el.checked);
|
||||||
|
const anyChecked = Boolean(checkedCheckboxes.length);
|
||||||
|
const allChecked = anyChecked && checkedCheckboxes.length === enabledCheckboxes.length;
|
||||||
|
|
||||||
|
if (allChecked) {
|
||||||
|
actionListSelectAll.checked = true;
|
||||||
|
actionListSelectAll.indeterminate = false;
|
||||||
|
} else if (anyChecked) {
|
||||||
|
actionListSelectAll.checked = false;
|
||||||
|
actionListSelectAll.indeterminate = true;
|
||||||
|
} else {
|
||||||
|
actionListSelectAll.checked = false;
|
||||||
|
actionListSelectAll.indeterminate = false;
|
||||||
|
}
|
||||||
|
if (actionDelete) {
|
||||||
|
toggleElem('#action-delete', anyChecked);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const el of issueCheckboxes) {
|
||||||
|
el.addEventListener('change', syncIssueSelectionState);
|
||||||
|
}
|
||||||
|
|
||||||
|
actionListSelectAll.addEventListener('change', () => {
|
||||||
|
for (const el of issueCheckboxes) {
|
||||||
|
if (!el.disabled) {
|
||||||
|
el.checked = actionListSelectAll.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncIssueSelectionState();
|
||||||
|
});
|
||||||
|
|
||||||
|
queryElems(document, '.action-action', (el) => el.addEventListener('click',
|
||||||
|
async (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const action = el.getAttribute('data-action');
|
||||||
|
const url = el.getAttribute('data-url');
|
||||||
|
const actionIDList: number[] = [];
|
||||||
|
const radix = 10;
|
||||||
|
for (const el of document.querySelectorAll<HTMLInputElement>('.action-checkbox:checked:not([disabled])')) {
|
||||||
|
const id = el.getAttribute('data-action-id');
|
||||||
|
if (id) {
|
||||||
|
actionIDList.push(parseInt(id, radix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionIDList.length < 1) return;
|
||||||
|
|
||||||
|
// for delete
|
||||||
|
if (action === 'delete') {
|
||||||
|
const confirmText = el.getAttribute('data-action-delete-confirm');
|
||||||
|
if (!await confirmModal({content: confirmText, confirmButtonColor: 'red'})) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteActions(url, actionIDList);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
showErrorToast(err.responseJSON?.error ?? err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteActions(url: string, actionIds: number[]) {
|
||||||
|
try {
|
||||||
|
const response = await POST(url, {
|
||||||
|
data: {
|
||||||
|
actionIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('failed to delete actions');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function initRepoActionList() {
|
||||||
|
if (document.querySelector('.page-content.repository.actions')) {
|
||||||
|
initRepoActionListCheckboxes();
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,7 @@ import {
|
|||||||
initGlobalEnterQuickSubmit,
|
initGlobalEnterQuickSubmit,
|
||||||
initGlobalFormDirtyLeaveConfirm,
|
initGlobalFormDirtyLeaveConfirm,
|
||||||
} from './features/common-form.ts';
|
} from './features/common-form.ts';
|
||||||
|
import {initRepoActionList} from './features/repo-action-list.ts';
|
||||||
|
|
||||||
initGiteaFomantic();
|
initGiteaFomantic();
|
||||||
initDirAuto();
|
initDirAuto();
|
||||||
@ -213,5 +214,6 @@ onDomReady(() => {
|
|||||||
initColorPickers,
|
initColorPickers,
|
||||||
|
|
||||||
initOAuth2SettingsDisableCheckbox,
|
initOAuth2SettingsDisableCheckbox,
|
||||||
|
initRepoActionList,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user