mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-11 15:10:34 +01:00
Replace index with id in actions routes (#36842)
This PR migrates the web Actions run/job routes from index-based `runIndex` or `jobIndex` to database IDs. **⚠️ BREAKING ⚠️**: Existing saved links/bookmarks that use the old index-based URLs will no longer resolve after this change. Improvements of this change: - Previously, `jobIndex` depended on list order, making it hard to locate a specific job. Using `jobID` provides stable addressing. - Web routes now align with API, which already use IDs. - Behavior is closer to GitHub, which exposes run/job IDs in URLs. - Provides a cleaner base for future features without relying on list order. - #36388 this PR improves the support for reusable workflows. If a job uses a reusable workflow, it may contain multiple child jobs, which makes relying on job index to locate a job much more complicated --------- Signed-off-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -70,14 +70,14 @@ func (run *ActionRun) HTMLURL() string {
|
||||
if run.Repo == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index)
|
||||
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.ID)
|
||||
}
|
||||
|
||||
func (run *ActionRun) Link() string {
|
||||
if run.Repo == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index)
|
||||
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.ID)
|
||||
}
|
||||
|
||||
func (run *ActionRun) WorkflowLink() string {
|
||||
@@ -299,7 +299,7 @@ func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, err
|
||||
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
|
||||
return cancelledJobs, err
|
||||
}
|
||||
updatedJob, err := GetRunJobByID(ctx, job.ID)
|
||||
updatedJob, err := GetRunJobByRunAndID(ctx, job.RunID, job.ID)
|
||||
if err != nil {
|
||||
return cancelledJobs, fmt.Errorf("get job: %w", err)
|
||||
}
|
||||
|
||||
@@ -118,13 +118,25 @@ func (job *ActionRunJob) ParseJob() (*jobparser.Job, error) {
|
||||
return workflowJob, nil
|
||||
}
|
||||
|
||||
func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
|
||||
func GetRunJobByRepoAndID(ctx context.Context, repoID, jobID int64) (*ActionRunJob, error) {
|
||||
var job ActionRunJob
|
||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", jobID, repoID).Get(&job)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("run job with id %d: %w", id, util.ErrNotExist)
|
||||
return nil, fmt.Errorf("run job with id %d: %w", jobID, util.ErrNotExist)
|
||||
}
|
||||
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
func GetRunJobByRunAndID(ctx context.Context, runID, jobID int64) (*ActionRunJob, error) {
|
||||
var job ActionRunJob
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND run_id=?", jobID, runID).Get(&job)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("run job with id %d: %w", jobID, util.ErrNotExist)
|
||||
}
|
||||
|
||||
return &job, nil
|
||||
@@ -168,7 +180,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
||||
|
||||
if job.RunID == 0 {
|
||||
var err error
|
||||
if job, err = GetRunJobByID(ctx, job.ID); err != nil {
|
||||
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func (task *ActionTask) GetRepoLink() string {
|
||||
|
||||
func (task *ActionTask) LoadJob(ctx context.Context) error {
|
||||
if task.Job == nil {
|
||||
job, err := GetRunJobByID(ctx, task.JobID)
|
||||
job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,6 +388,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
}
|
||||
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
||||
ID: task.JobID,
|
||||
RepoID: task.RepoID,
|
||||
Status: task.Status,
|
||||
Stopped: task.Stopped,
|
||||
}, nil); err != nil {
|
||||
@@ -449,6 +450,7 @@ func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||
task.Stopped = now
|
||||
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
||||
ID: task.JobID,
|
||||
RepoID: task.RepoID,
|
||||
Status: task.Status,
|
||||
Stopped: task.Stopped,
|
||||
}, nil); err != nil {
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestCommitStatusesHideActionsURL(t *testing.T) {
|
||||
statuses := []*git_model.CommitStatus{
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index),
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.ID),
|
||||
},
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# type ActionRun struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# RepoID int64 `xorm:"index"`
|
||||
# Index int64
|
||||
# }
|
||||
-
|
||||
id: 106
|
||||
repo_id: 1
|
||||
index: 7
|
||||
@@ -0,0 +1,10 @@
|
||||
# type ActionRunJob struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# RunID int64 `xorm:"index"`
|
||||
# }
|
||||
-
|
||||
id: 530
|
||||
run_id: 106
|
||||
-
|
||||
id: 531
|
||||
run_id: 106
|
||||
@@ -0,0 +1,29 @@
|
||||
# type CommitStatus struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# RepoID int64 `xorm:"index"`
|
||||
# TargetURL string
|
||||
# }
|
||||
-
|
||||
id: 10
|
||||
repo_id: 1
|
||||
target_url: /testuser/repo1/actions/runs/7/jobs/0
|
||||
-
|
||||
id: 11
|
||||
repo_id: 1
|
||||
target_url: /testuser/repo1/actions/runs/7/jobs/1
|
||||
-
|
||||
id: 12
|
||||
repo_id: 1
|
||||
target_url: /otheruser/badrepo/actions/runs/7/jobs/0
|
||||
-
|
||||
id: 13
|
||||
repo_id: 1
|
||||
target_url: /testuser/repo1/actions/runs/10/jobs/0
|
||||
-
|
||||
id: 14
|
||||
repo_id: 1
|
||||
target_url: /testuser/repo1/actions/runs/7/jobs/3
|
||||
-
|
||||
id: 15
|
||||
repo_id: 1
|
||||
target_url: https://ci.example.com/build/123
|
||||
@@ -0,0 +1,19 @@
|
||||
# type CommitStatusSummary struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# RepoID int64 `xorm:"index"`
|
||||
# SHA string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
# State string `xorm:"VARCHAR(7) NOT NULL"`
|
||||
# TargetURL string
|
||||
# }
|
||||
-
|
||||
id: 20
|
||||
repo_id: 1
|
||||
sha: "012345"
|
||||
state: success
|
||||
target_url: /testuser/repo1/actions/runs/7/jobs/0
|
||||
-
|
||||
id: 21
|
||||
repo_id: 1
|
||||
sha: "678901"
|
||||
state: success
|
||||
target_url: https://ci.example.com/build/123
|
||||
@@ -0,0 +1,9 @@
|
||||
# type Repository struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# OwnerName string
|
||||
# Name string
|
||||
# }
|
||||
-
|
||||
id: 1
|
||||
owner_name: testuser
|
||||
name: repo1
|
||||
@@ -400,6 +400,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
|
||||
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
|
||||
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
|
||||
newMigration(326, "Migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
216
models/migrations/v1_26/v326.go
Normal file
216
models/migrations/v1_26/v326.go
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const actionsRunPath = "/actions/runs/"
|
||||
|
||||
type migrationRepository struct {
|
||||
ID int64
|
||||
OwnerName string
|
||||
Name string
|
||||
}
|
||||
|
||||
type migrationActionRun struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
type migrationActionRunJob struct {
|
||||
ID int64
|
||||
RunID int64
|
||||
}
|
||||
|
||||
type migrationCommitStatus struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
TargetURL string
|
||||
}
|
||||
|
||||
func FixCommitStatusTargetURLToUseRunAndJobID(x *xorm.Engine) error {
|
||||
runByIndexCache := make(map[int64]map[int64]*migrationActionRun)
|
||||
jobsByRunIDCache := make(map[int64][]int64)
|
||||
repoLinkCache := make(map[int64]string)
|
||||
|
||||
if err := migrateCommitStatusTargetURL(x, "commit_status", runByIndexCache, jobsByRunIDCache, repoLinkCache); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateCommitStatusTargetURL(x, "commit_status_summary", runByIndexCache, jobsByRunIDCache, repoLinkCache)
|
||||
}
|
||||
|
||||
func migrateCommitStatusTargetURL(
|
||||
x *xorm.Engine,
|
||||
table string,
|
||||
runByIndexCache map[int64]map[int64]*migrationActionRun,
|
||||
jobsByRunIDCache map[int64][]int64,
|
||||
repoLinkCache map[int64]string,
|
||||
) error {
|
||||
const batchSize = 500
|
||||
var lastID int64
|
||||
|
||||
for {
|
||||
var rows []migrationCommitStatus
|
||||
sess := x.Table(table).
|
||||
Where("target_url LIKE ?", "%"+actionsRunPath+"%").
|
||||
And("id > ?", lastID).
|
||||
Asc("id").
|
||||
Limit(batchSize)
|
||||
if err := sess.Find(&rows); err != nil {
|
||||
return fmt.Errorf("query %s: %w", table, err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
lastID = row.ID
|
||||
if row.TargetURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
repoLink, err := getRepoLinkCached(x, repoLinkCache, row.RepoID)
|
||||
if err != nil || repoLink == "" {
|
||||
if err != nil {
|
||||
log.Warn("convert %s id=%d getRepoLinkCached: %v", table, row.ID, err)
|
||||
} else {
|
||||
log.Warn("convert %s id=%d: repo=%d not found", table, row.ID, row.RepoID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
runNum, jobNum, ok := parseTargetURL(row.TargetURL, repoLink)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
run, err := getRunByIndexCached(x, runByIndexCache, row.RepoID, runNum)
|
||||
if err != nil || run == nil {
|
||||
if err != nil {
|
||||
log.Warn("convert %s id=%d getRunByIndexCached: %v", table, row.ID, err)
|
||||
} else {
|
||||
log.Warn("convert %s id=%d: run not found for repo_id=%d run_index=%d", table, row.ID, row.RepoID, runNum)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
jobID, ok, err := getJobIDByIndexCached(x, jobsByRunIDCache, run.ID, jobNum)
|
||||
if err != nil || !ok {
|
||||
if err != nil {
|
||||
log.Warn("convert %s id=%d getJobIDByIndexCached: %v", table, row.ID, err)
|
||||
} else {
|
||||
log.Warn("convert %s id=%d: job not found for run_id=%d job_index=%d", table, row.ID, run.ID, jobNum)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
oldURL := row.TargetURL
|
||||
newURL := fmt.Sprintf("%s%s%d/jobs/%d", repoLink, actionsRunPath, run.ID, jobID) // expect: {repo_link}/actions/runs/{run_id}/jobs/{job_id}
|
||||
if oldURL == newURL {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := x.Table(table).ID(row.ID).Cols("target_url").Update(&migrationCommitStatus{TargetURL: newURL}); err != nil {
|
||||
return fmt.Errorf("update %s id=%d target_url from %s to %s: %w", table, row.ID, oldURL, newURL, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getRepoLinkCached(x *xorm.Engine, cache map[int64]string, repoID int64) (string, error) {
|
||||
if link, ok := cache[repoID]; ok {
|
||||
return link, nil
|
||||
}
|
||||
repo := &migrationRepository{}
|
||||
has, err := x.Table("repository").Where("id=?", repoID).Get(repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !has {
|
||||
cache[repoID] = ""
|
||||
return "", nil
|
||||
}
|
||||
link := setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
|
||||
cache[repoID] = link
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func getRunByIndexCached(x *xorm.Engine, cache map[int64]map[int64]*migrationActionRun, repoID, runIndex int64) (*migrationActionRun, error) {
|
||||
if repoCache, ok := cache[repoID]; ok {
|
||||
if run, ok := repoCache[runIndex]; ok {
|
||||
if run == nil {
|
||||
return nil, fmt.Errorf("run repo_id=%d run_index=%d not found", repoID, runIndex)
|
||||
}
|
||||
return run, nil
|
||||
}
|
||||
}
|
||||
|
||||
var run migrationActionRun
|
||||
has, err := x.Table("action_run").Where("repo_id=? AND `index`=?", repoID, runIndex).Get(&run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
if cache[repoID] == nil {
|
||||
cache[repoID] = make(map[int64]*migrationActionRun)
|
||||
}
|
||||
cache[repoID][runIndex] = nil
|
||||
return nil, fmt.Errorf("run repo_id=%d run_index=%d not found", repoID, runIndex)
|
||||
}
|
||||
if cache[repoID] == nil {
|
||||
cache[repoID] = make(map[int64]*migrationActionRun)
|
||||
}
|
||||
cache[repoID][runIndex] = &run
|
||||
return &run, nil
|
||||
}
|
||||
|
||||
func getJobIDByIndexCached(x *xorm.Engine, cache map[int64][]int64, runID, jobIndex int64) (int64, bool, error) {
|
||||
jobIDs, ok := cache[runID]
|
||||
if !ok {
|
||||
var jobs []migrationActionRunJob
|
||||
if err := x.Table("action_run_job").Where("run_id=?", runID).Asc("id").Cols("id").Find(&jobs); err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
jobIDs = make([]int64, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
jobIDs = append(jobIDs, job.ID)
|
||||
}
|
||||
cache[runID] = jobIDs
|
||||
}
|
||||
if jobIndex < 0 || jobIndex >= int64(len(jobIDs)) {
|
||||
return 0, false, nil
|
||||
}
|
||||
return jobIDs[jobIndex], true, nil
|
||||
}
|
||||
|
||||
func parseTargetURL(targetURL, repoLink string) (runNum, jobNum int64, ok bool) {
|
||||
prefix := repoLink + actionsRunPath
|
||||
if !strings.HasPrefix(targetURL, prefix) {
|
||||
return 0, 0, false
|
||||
}
|
||||
rest := targetURL[len(prefix):]
|
||||
|
||||
parts := strings.Split(rest, "/") // expect: {run_num}/jobs/{job_num}
|
||||
if len(parts) == 3 && parts[1] == "jobs" {
|
||||
runNum, err1 := strconv.ParseInt(parts[0], 10, 64)
|
||||
jobNum, err2 := strconv.ParseInt(parts[2], 10, 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return runNum, jobNum, true
|
||||
}
|
||||
|
||||
return 0, 0, false
|
||||
}
|
||||
104
models/migrations/v1_26/v326_test.go
Normal file
104
models/migrations/v1_26/v326_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
_ "code.gitea.io/gitea/models/git"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||
|
||||
type Repository struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerName string
|
||||
Name string
|
||||
}
|
||||
|
||||
type ActionRun struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"index"`
|
||||
Index int64
|
||||
}
|
||||
|
||||
type ActionRunJob struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RunID int64 `xorm:"index"`
|
||||
}
|
||||
|
||||
type CommitStatus struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"index"`
|
||||
TargetURL string
|
||||
}
|
||||
|
||||
type CommitStatusSummary struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"index"`
|
||||
SHA string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
State string `xorm:"VARCHAR(7) NOT NULL"`
|
||||
TargetURL string
|
||||
}
|
||||
|
||||
x, deferable := base.PrepareTestEnv(t, 0,
|
||||
new(Repository),
|
||||
new(ActionRun),
|
||||
new(ActionRunJob),
|
||||
new(CommitStatus),
|
||||
new(CommitStatusSummary),
|
||||
)
|
||||
defer deferable()
|
||||
|
||||
newURL1 := "/testuser/repo1/actions/runs/106/jobs/530"
|
||||
newURL2 := "/testuser/repo1/actions/runs/106/jobs/531"
|
||||
|
||||
invalidWrongRepo := "/otheruser/badrepo/actions/runs/7/jobs/0"
|
||||
invalidNonexistentRun := "/testuser/repo1/actions/runs/10/jobs/0"
|
||||
invalidNonexistentJob := "/testuser/repo1/actions/runs/7/jobs/3"
|
||||
externalTargetURL := "https://ci.example.com/build/123"
|
||||
|
||||
require.NoError(t, FixCommitStatusTargetURLToUseRunAndJobID(x))
|
||||
|
||||
cases := []struct {
|
||||
table string
|
||||
id int64
|
||||
want string
|
||||
}{
|
||||
{table: "commit_status", id: 10, want: newURL1},
|
||||
{table: "commit_status", id: 11, want: newURL2},
|
||||
{table: "commit_status", id: 12, want: invalidWrongRepo},
|
||||
{table: "commit_status", id: 13, want: invalidNonexistentRun},
|
||||
{table: "commit_status", id: 14, want: invalidNonexistentJob},
|
||||
{table: "commit_status", id: 15, want: externalTargetURL},
|
||||
{table: "commit_status_summary", id: 20, want: newURL1},
|
||||
{table: "commit_status_summary", id: 21, want: externalTargetURL},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
assertTargetURL(t, x, tc.table, tc.id, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTargetURL(t *testing.T, x *xorm.Engine, table string, id int64, want string) {
|
||||
t.Helper()
|
||||
|
||||
var row struct {
|
||||
TargetURL string
|
||||
}
|
||||
has, err := x.Table(table).Where("id=?", id).Cols("target_url").Get(&row)
|
||||
require.NoError(t, err)
|
||||
require.Truef(t, has, "row not found: table=%s id=%d", table, id)
|
||||
require.Equal(t, want, row.TargetURL)
|
||||
}
|
||||
@@ -1041,15 +1041,9 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
workflowRun, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.RunDetails{
|
||||
WorkflowRunID: runID,
|
||||
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), workflowRun.Index),
|
||||
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), runID),
|
||||
RunURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.APIURL(), runID),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,9 +43,13 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
jobID := ctx.PathParamInt64("job_id")
|
||||
curJob, err := actions_model.GetRunJobByID(ctx, jobID)
|
||||
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = curJob.LoadRepo(ctx); err != nil {
|
||||
|
||||
@@ -14,18 +14,15 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error {
|
||||
runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
|
||||
func DownloadActionsRunJobLogsWithID(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobID int64) error {
|
||||
job, err := actions_model.GetRunJobByRunAndID(ctx, runID, jobID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRunJobsByRunID: %w", err)
|
||||
return err
|
||||
}
|
||||
if err = runJobs.LoadRepos(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRepos: %w", err)
|
||||
if err := job.LoadRepo(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRepo: %w", err)
|
||||
}
|
||||
if jobIndex < 0 || jobIndex >= int64(len(runJobs)) {
|
||||
return util.NewNotExistErrorf("job index is out of range: %d", jobIndex)
|
||||
}
|
||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
|
||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, job)
|
||||
}
|
||||
|
||||
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
||||
|
||||
@@ -59,8 +59,8 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt
|
||||
}
|
||||
|
||||
func MockActionsView(ctx *context.Context) {
|
||||
ctx.Data["RunIndex"] = ctx.PathParam("run")
|
||||
ctx.Data["JobIndex"] = ctx.PathParam("job")
|
||||
ctx.Data["RunID"] = ctx.PathParam("run")
|
||||
ctx.Data["JobID"] = ctx.PathParam("job")
|
||||
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ import (
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func getRunIndex(ctx *context_module.Context) int64 {
|
||||
// if run param is "latest", get the latest run index
|
||||
func getRunID(ctx *context_module.Context) int64 {
|
||||
// if run param is "latest", get the latest run id
|
||||
if ctx.PathParam("run") == "latest" {
|
||||
if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil {
|
||||
return run.Index
|
||||
return run.ID
|
||||
}
|
||||
}
|
||||
return ctx.PathParamInt64("run")
|
||||
@@ -50,24 +50,25 @@ func getRunIndex(ctx *context_module.Context) int64 {
|
||||
|
||||
func View(ctx *context_module.Context) {
|
||||
ctx.Data["PageIsActions"] = true
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt("job")
|
||||
ctx.Data["RunIndex"] = runIndex
|
||||
ctx.Data["JobIndex"] = jobIndex
|
||||
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
|
||||
runID := getRunID(ctx)
|
||||
|
||||
if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
|
||||
_, _, current := getRunJobsAndCurrentJob(ctx, runID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["RunID"] = runID
|
||||
ctx.Data["JobID"] = current.ID
|
||||
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
|
||||
|
||||
ctx.HTML(http.StatusOK, tplViewActions)
|
||||
}
|
||||
|
||||
func ViewWorkflowFile(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
runID := getRunID(ctx)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
|
||||
ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
@@ -187,8 +188,8 @@ type ViewStepLogLine struct {
|
||||
Timestamp float64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -208,14 +209,12 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artif
|
||||
|
||||
func ViewPost(ctx *context_module.Context) {
|
||||
req := web.GetForm(ctx).(*ViewRequest)
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt("job")
|
||||
runID := getRunID(ctx)
|
||||
|
||||
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||
run, jobs, current := getRunJobsAndCurrentJob(ctx, runID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
run := current.Run
|
||||
if err := run.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("run.LoadAttributes", err)
|
||||
return
|
||||
@@ -223,7 +222,7 @@ func ViewPost(ctx *context_module.Context) {
|
||||
|
||||
var err error
|
||||
resp := &ViewResponse{}
|
||||
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("getActionsViewArtifacts", err)
|
||||
@@ -400,15 +399,12 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
|
||||
}
|
||||
|
||||
// Rerun will rerun jobs in the given run
|
||||
// If jobIndexStr is a blank string, it means rerun all jobs
|
||||
// If jobIDStr is a blank string, it means rerun all jobs
|
||||
func Rerun(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndexHas := ctx.PathParam("job") != ""
|
||||
jobIndex := ctx.PathParamInt("job")
|
||||
runID := getRunID(ctx)
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
run, jobs, currentJob := getRunJobsAndCurrentJob(ctx, runID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -426,19 +422,9 @@ func Rerun(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return
|
||||
}
|
||||
|
||||
var targetJob *actions_model.ActionRunJob // nil means rerun all jobs
|
||||
if jobIndexHas {
|
||||
if jobIndex < 0 || jobIndex >= len(jobs) {
|
||||
ctx.JSONError(ctx.Locale.Tr("error.not_found"))
|
||||
return
|
||||
}
|
||||
targetJob = jobs[jobIndex] // only rerun the selected job
|
||||
if ctx.PathParam("job") != "" {
|
||||
targetJob = currentJob
|
||||
}
|
||||
|
||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
|
||||
@@ -450,28 +436,28 @@ func Rerun(ctx *context_module.Context) {
|
||||
}
|
||||
|
||||
func Logs(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt64("job")
|
||||
runID := getRunID(ctx)
|
||||
jobID := ctx.PathParamInt64("job")
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
|
||||
ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
|
||||
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
|
||||
if err = common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
|
||||
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Cancel(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
runID := getRunID(ctx)
|
||||
|
||||
firstJob, jobs := getRunJobs(ctx, runIndex, -1)
|
||||
run, jobs, _ := getRunJobsAndCurrentJob(ctx, runID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -490,7 +476,7 @@ func Cancel(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, firstJob.Run, jobs...)
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...)
|
||||
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
|
||||
|
||||
for _, job := range updatedJobs {
|
||||
@@ -505,9 +491,9 @@ func Cancel(ctx *context_module.Context) {
|
||||
}
|
||||
|
||||
func Approve(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
runID := getRunID(ctx)
|
||||
|
||||
approveRuns(ctx, []int64{runIndex})
|
||||
approveRuns(ctx, []int64{runID})
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -515,17 +501,17 @@ func Approve(ctx *context_module.Context) {
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func approveRuns(ctx *context_module.Context, runIndexes []int64) {
|
||||
func approveRuns(ctx *context_module.Context, runIDs []int64) {
|
||||
doer := ctx.Doer
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
updatedJobs := make([]*actions_model.ActionRunJob, 0)
|
||||
runMap := make(map[int64]*actions_model.ActionRun, len(runIndexes))
|
||||
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIndexes))
|
||||
runMap := make(map[int64]*actions_model.ActionRun, len(runIDs))
|
||||
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIDs))
|
||||
|
||||
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||
for _, runIndex := range runIndexes {
|
||||
run, err := actions_model.GetRunByIndex(ctx, repo.ID, runIndex)
|
||||
for _, runID := range runIDs {
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -560,7 +546,9 @@ func approveRuns(ctx *context_module.Context, runIndexes []int64) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdateRunJob", err)
|
||||
ctx.NotFoundOrServerError("approveRuns", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -580,16 +568,16 @@ func approveRuns(ctx *context_module.Context, runIndexes []int64) {
|
||||
}
|
||||
|
||||
func Delete(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
runID := getRunID(ctx)
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
ctx.ServerError("GetRunByRepoAndID", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -606,47 +594,54 @@ func Delete(ctx *context_module.Context) {
|
||||
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.
|
||||
func getRunJobs(ctx *context_module.Context, runIndex int64, jobIndex int) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
// getRunJobsAndCurrentJob loads the run and its jobs for runID, and returns the selected job based on the optional "job" path param (or the first job by default).
|
||||
// Any error will be written to the ctx, and nils are returned in that case.
|
||||
func getRunJobsAndCurrentJob(ctx *context_module.Context, runID int64) (*actions_model.ActionRun, []*actions_model.ActionRunJob, *actions_model.ActionRunJob) {
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.NotFound(nil)
|
||||
return nil, nil
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return nil, nil
|
||||
ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return nil, nil, nil
|
||||
}
|
||||
run.Repo = ctx.Repo.Repository
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
for _, v := range jobs {
|
||||
v.Run = run
|
||||
for _, job := range jobs {
|
||||
job.Run = run
|
||||
}
|
||||
|
||||
if jobIndex >= 0 && jobIndex < len(jobs) {
|
||||
return jobs[jobIndex], jobs
|
||||
current := jobs[0]
|
||||
if ctx.PathParam("job") != "" {
|
||||
jobID := ctx.PathParamInt64("job")
|
||||
current, err = actions_model.GetRunJobByRunAndID(ctx, run.ID, jobID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetRunJobByRunAndID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return nil, nil, nil
|
||||
}
|
||||
current.Run = run
|
||||
}
|
||||
return jobs[0], jobs
|
||||
|
||||
return run, jobs, current
|
||||
}
|
||||
|
||||
func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
runID := getRunID(ctx)
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
|
||||
ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
@@ -659,16 +654,16 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
}
|
||||
|
||||
func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
runID := getRunID(ctx)
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
ctx.ServerError("GetRunByRepoAndID", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -754,19 +749,19 @@ func ApproveAllChecks(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
runIndexes := make([]int64, 0, len(runs))
|
||||
runIDs := make([]int64, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
if run.NeedApproval {
|
||||
runIndexes = append(runIndexes, run.Index)
|
||||
runIDs = append(runIDs, run.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(runIndexes) == 0 {
|
||||
if len(runIDs) == 0 {
|
||||
ctx.JSONOK()
|
||||
return
|
||||
}
|
||||
|
||||
approveRuns(ctx, runIndexes)
|
||||
approveRuns(ctx, runIDs)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,21 +56,21 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action
|
||||
func GetRunsFromCommitStatuses(ctx context.Context, statuses []*git_model.CommitStatus) ([]*actions_model.ActionRun, error) {
|
||||
runMap := make(map[int64]*actions_model.ActionRun)
|
||||
for _, status := range statuses {
|
||||
runIndex, _, ok := status.ParseGiteaActionsTargetURL(ctx)
|
||||
runID, _, ok := status.ParseGiteaActionsTargetURL(ctx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, ok = runMap[runIndex]
|
||||
_, ok = runMap[runID]
|
||||
if !ok {
|
||||
run, err := actions_model.GetRunByIndex(ctx, status.RepoID, runIndex)
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, status.RepoID, runID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
// the run may be deleted manually, just skip it
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("GetRunByIndex: %w", err)
|
||||
return nil, fmt.Errorf("GetRunByRepoAndID: %w", err)
|
||||
}
|
||||
runMap[runIndex] = run
|
||||
runMap[runID] = run
|
||||
}
|
||||
}
|
||||
runs := make([]*actions_model.ActionRun, 0, len(runMap))
|
||||
@@ -181,15 +181,10 @@ func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event,
|
||||
description = "Unknown status: " + strconv.Itoa(int(job.Status))
|
||||
}
|
||||
|
||||
index, err := getIndexOfJob(ctx, job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getIndexOfJob: %w", err)
|
||||
}
|
||||
|
||||
creator := user_model.NewActionsUser()
|
||||
status := git_model.CommitStatus{
|
||||
SHA: commitID,
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID),
|
||||
Description: description,
|
||||
Context: ctxName,
|
||||
CreatorID: creator.ID,
|
||||
@@ -213,17 +208,3 @@ func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState
|
||||
return commitstatus.CommitStatusError
|
||||
}
|
||||
}
|
||||
|
||||
func getIndexOfJob(ctx context.Context, job *actions_model.ActionRunJob) (int, error) {
|
||||
// TODO: store job index as a field in ActionRunJob to avoid this
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for i, v := range jobs {
|
||||
if v.ID == job.ID {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -148,6 +148,9 @@ func rerunWorkflowJob(ctx context.Context, job *actions_model.ActionRunJob, shou
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := job.Run.LoadAttributes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
|
||||
if err != nil {
|
||||
|
||||
@@ -324,18 +324,6 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jobIndex := 0
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, j := range jobs {
|
||||
if j.ID == job.ID {
|
||||
jobIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
status, conclusion := ToActionsStatus(job.Status)
|
||||
var runnerID int64
|
||||
var runnerName string
|
||||
@@ -379,7 +367,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
||||
ID: job.ID,
|
||||
// missing api endpoint for this location
|
||||
URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID),
|
||||
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
|
||||
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), job.ID),
|
||||
RunID: job.RunID,
|
||||
// Missing api endpoint for this location, artifacts are available under a nested url
|
||||
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<a href="/devtest/repo-action-view/runs/30/jobs/2">Run:CanRerun</a>
|
||||
</div>
|
||||
{{template "repo/actions/view_component" (dict
|
||||
"RunIndex" (or .RunIndex 10)
|
||||
"JobIndex" (or .JobIndex 0)
|
||||
"RunID" (or .RunID 10)
|
||||
"JobID" (or .JobID 0)
|
||||
"ActionsURL" (print AppSubUrl "/devtest/actions-mock")
|
||||
)}}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="page-content repository">
|
||||
{{template "repo/header" .}}
|
||||
{{template "repo/actions/view_component" (dict
|
||||
"RunIndex" .RunIndex
|
||||
"JobIndex" .JobIndex
|
||||
"RunID" .RunID
|
||||
"JobID" .JobID
|
||||
"ActionsURL" .ActionsURL
|
||||
)}}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="repo-action-view"
|
||||
data-run-index="{{.RunIndex}}"
|
||||
data-job-index="{{.JobIndex}}"
|
||||
data-run-id="{{.RunID}}"
|
||||
data-job-id="{{.JobID}}"
|
||||
data-actions-url="{{.ActionsURL}}"
|
||||
|
||||
data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}"
|
||||
|
||||
@@ -453,7 +453,7 @@ jobs:
|
||||
runner.fetchNoTask(t)
|
||||
// user2 approves the run
|
||||
pr2Run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", baseRepo.OwnerName, baseRepo.Name, pr2Run1.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", baseRepo.OwnerName, baseRepo.Name, pr2Run1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
// fetch the task and the previous task has been cancelled
|
||||
pr2Task1 := runner.fetchTask(t)
|
||||
@@ -619,7 +619,7 @@ jobs:
|
||||
assert.Equal(t, actions_model.StatusCancelled, wf2Job2ActionJob.Status)
|
||||
|
||||
// rerun wf2
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, wf2Run.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, wf2Run.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// (rerun1) cannot fetch wf2-job2
|
||||
@@ -643,7 +643,7 @@ jobs:
|
||||
assert.Equal(t, "job-main-v1.24.0", wf2Job2Rerun1Job.ConcurrencyGroup)
|
||||
|
||||
// rerun wf2-job2
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/1/rerun", user2.Name, repo.Name, wf2Run.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, wf2Run.ID, wf2Job2ActionJob.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
// (rerun2) fetch and exec wf2-job2
|
||||
wf2Job2Rerun2Task := runner1.fetchTask(t)
|
||||
@@ -912,6 +912,10 @@ jobs:
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
runner.fetchNoTask(t) // cannot fetch task because task2 is not completed
|
||||
run3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID}, unittest.OrderBy("id DESC"))
|
||||
assert.Equal(t, actions_model.StatusBlocked, run3.Status)
|
||||
job3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RepoID: repo.ID, RunID: run3.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, job3.Status)
|
||||
|
||||
// run the workflow with appVersion=v1.22 and cancel=true
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
@@ -933,10 +937,10 @@ jobs:
|
||||
|
||||
// rerun cancel true scenario
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run4.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run4.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task5 := runner.fetchTask(t)
|
||||
@@ -952,17 +956,19 @@ jobs:
|
||||
|
||||
// rerun cancel false scenario
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
run2_2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, run2_2.Status)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index+1))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run3.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task6 := runner.fetchTask(t)
|
||||
_, _, run3 := getTaskAndJobAndRunByTaskID(t, task6.Id)
|
||||
_, _, run3_2 := getTaskAndJobAndRunByTaskID(t, task6.Id)
|
||||
assert.Equal(t, run3.ID, run3_2.ID)
|
||||
assert.Equal(t, actions_model.StatusRunning, run3_2.Status)
|
||||
assert.Equal(t, "workflow-dispatch-v1.22", run3.ConcurrencyGroup)
|
||||
|
||||
run2_2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2_2.ID})
|
||||
@@ -1033,7 +1039,7 @@ jobs:
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
task2 := runner.fetchTask(t)
|
||||
_, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
assert.Equal(t, "workflow-dispatch-v1.22", run2.ConcurrencyGroup)
|
||||
|
||||
// run the workflow with appVersion=v1.22 and cancel=false again
|
||||
@@ -1044,6 +1050,10 @@ jobs:
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
runner.fetchNoTask(t) // cannot fetch task because task2 is not completed
|
||||
run3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID}, unittest.OrderBy("id DESC"))
|
||||
assert.Equal(t, actions_model.StatusBlocked, run3.Status)
|
||||
job3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RepoID: repo.ID, RunID: run3.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, job3.Status)
|
||||
|
||||
// run the workflow with appVersion=v1.22 and cancel=true
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
@@ -1053,7 +1063,7 @@ jobs:
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
task4 := runner.fetchTask(t)
|
||||
_, _, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id)
|
||||
_, job4, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id)
|
||||
assert.Equal(t, actions_model.StatusRunning, run4.Status)
|
||||
assert.Equal(t, "workflow-dispatch-v1.22", run4.ConcurrencyGroup)
|
||||
_, _, run2 = getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
@@ -1064,10 +1074,10 @@ jobs:
|
||||
})
|
||||
|
||||
// rerun cancel true scenario
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0/rerun", user2.Name, apiRepo.Name, run2.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run2.ID, job2.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0/rerun", user2.Name, apiRepo.Name, run4.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run4.ID, job4.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task5 := runner.fetchTask(t)
|
||||
@@ -1083,17 +1093,17 @@ jobs:
|
||||
|
||||
// rerun cancel false scenario
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0/rerun", user2.Name, apiRepo.Name, run2.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run2.ID, job2.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
run2_2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, run2_2.Status)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0/rerun", user2.Name, apiRepo.Name, run2.Index+1))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run3.ID, job3.ID))
|
||||
_ = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task6 := runner.fetchTask(t)
|
||||
_, _, run3 := getTaskAndJobAndRunByTaskID(t, task6.Id)
|
||||
_, _, run3 = getTaskAndJobAndRunByTaskID(t, task6.Id)
|
||||
assert.Equal(t, "workflow-dispatch-v1.22", run3.ConcurrencyGroup)
|
||||
|
||||
run2_2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2_2.ID})
|
||||
@@ -1453,7 +1463,7 @@ jobs:
|
||||
runner.fetchNoTask(t)
|
||||
|
||||
// cancel the first run
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo.Name, run1.Index))
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo.Name, run1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// the first run has been cancelled
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -121,47 +122,55 @@ jobs:
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||
|
||||
runIndex := ""
|
||||
var runID int64
|
||||
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)
|
||||
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||
parsedRunIndex, err := strconv.ParseInt(runIndex, 10, 64)
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: apiRepo.ID, Index: parsedRunIndex})
|
||||
runID = run.ID
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i))
|
||||
jobID := jobs[i].ID
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
|
||||
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)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex))
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex))
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i))
|
||||
jobID := jobs[i].ID
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
|
||||
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)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
@@ -171,7 +169,7 @@ jobs:
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute tasks
|
||||
for jobIndex, outcome := range tc.outcome {
|
||||
for _, outcome := range tc.outcome {
|
||||
task := runner.fetchTask(t)
|
||||
runner.execTask(t, task, outcome)
|
||||
|
||||
@@ -183,9 +181,10 @@ jobs:
|
||||
_, err := storage.Actions.Stat(logFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
|
||||
|
||||
// download task logs and check content
|
||||
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)).
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
@@ -198,15 +197,8 @@ jobs:
|
||||
)
|
||||
}
|
||||
|
||||
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, jobs, len(tc.outcome))
|
||||
jobID := jobs[jobIndex].ID
|
||||
|
||||
// download task logs from API and check content
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
|
||||
@@ -54,21 +54,22 @@ jobs:
|
||||
|
||||
// fetch and exec job1
|
||||
job1Task := runner.fetchTask(t)
|
||||
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
runner.execTask(t, job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// RERUN-FAILURE: the run is not done
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index))
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
// fetch and exec job2
|
||||
job2Task := runner.fetchTask(t)
|
||||
_, job2, _ := getTaskAndJobAndRunByTaskID(t, job2Task.Id)
|
||||
runner.execTask(t, job2Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// RERUN-1: rerun the run
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// fetch and exec job1
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
@@ -82,7 +83,7 @@ jobs:
|
||||
})
|
||||
|
||||
// RERUN-2: rerun job1
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job1.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// job2 needs job1, so rerunning job1 will also rerun job2
|
||||
// fetch and exec job1
|
||||
@@ -97,7 +98,7 @@ jobs:
|
||||
})
|
||||
|
||||
// RERUN-3: rerun job2
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1))
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// only job2 will rerun
|
||||
// fetch and exec job2
|
||||
|
||||
100
tests/integration/actions_route_test.go
Normal file
100
tests/integration/actions_route_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
actions_web "code.gitea.io/gitea/routers/web/repo/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestActionsRoute(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
repo1 := createActionsTestRepo(t, user2Token, "actions-route-test-1", false)
|
||||
runner1 := newMockRunner()
|
||||
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
repo2 := createActionsTestRepo(t, user2Token, "actions-route-test-2", false)
|
||||
runner2 := newMockRunner()
|
||||
runner2.registerAsRepoRunner(t, user2.Name, repo2.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
workflowTreePath := ".gitea/workflows/test.yml"
|
||||
workflowContent := `name: test
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/test.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
`
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+workflowTreePath, workflowContent)
|
||||
createWorkflowFile(t, user2Token, user2.Name, repo1.Name, workflowTreePath, opts)
|
||||
createWorkflowFile(t, user2Token, user2.Name, repo2.Name, workflowTreePath, opts)
|
||||
|
||||
task1 := runner1.fetchTask(t)
|
||||
_, job1, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id)
|
||||
task2 := runner2.fetchTask(t)
|
||||
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
|
||||
// run1 and job1 belong to repo1, success
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID))
|
||||
resp := user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
var viewResp actions_web.ViewResponse
|
||||
DecodeJSON(t, resp, &viewResp)
|
||||
assert.Len(t, viewResp.State.Run.Jobs, 1)
|
||||
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
|
||||
|
||||
// run2 and job2 do not belong to repo1, failure
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/workflow", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// make the tasks complete, then test rerun
|
||||
runner1.execTask(t, task1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
runner2.execTask(t, task2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run1.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
@@ -209,12 +209,12 @@ func TestAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
|
||||
job198, err := actions_model.GetRunJobByID(t.Context(), 198)
|
||||
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job198.Status)
|
||||
assert.Equal(t, int64(0), job198.TaskID)
|
||||
|
||||
job199, err := actions_model.GetRunJobByID(t.Context(), 199)
|
||||
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
||||
assert.Equal(t, int64(0), job199.TaskID)
|
||||
@@ -269,12 +269,12 @@ func TestAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
|
||||
job198, err := actions_model.GetRunJobByID(t.Context(), 198)
|
||||
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusSuccess, job198.Status)
|
||||
assert.Equal(t, int64(53), job198.TaskID)
|
||||
|
||||
job199, err := actions_model.GetRunJobByID(t.Context(), 199)
|
||||
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
||||
assert.Equal(t, int64(0), job199.TaskID)
|
||||
|
||||
@@ -1066,7 +1066,7 @@ jobs:
|
||||
assert.Equal(t, "repo1", payloads[3].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName)
|
||||
assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0))
|
||||
assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[3].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[3].WorkflowJob.Steps, 1)
|
||||
|
||||
assert.Equal(t, "queued", payloads[4].Action)
|
||||
@@ -1102,7 +1102,7 @@ jobs:
|
||||
assert.Equal(t, "repo1", payloads[6].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName)
|
||||
assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1))
|
||||
assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[6].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[6].WorkflowJob.Steps, 2)
|
||||
})
|
||||
}
|
||||
@@ -1274,7 +1274,7 @@ jobs:
|
||||
|
||||
// Call cancel ui api
|
||||
// Only a web UI API exists for cancelling workflow runs, so use the UI endpoint.
|
||||
cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.RunNumber)
|
||||
cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.ID)
|
||||
req := NewRequest(t, "POST", cancelURL)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -1406,7 +1406,7 @@ jobs:
|
||||
|
||||
// Call cancel ui api
|
||||
// Only a web UI API exists for cancelling workflow runs, so use the UI endpoint.
|
||||
cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.RunNumber)
|
||||
cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.ID)
|
||||
req := NewRequest(t, "POST", cancelURL)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -1424,7 +1424,7 @@ jobs:
|
||||
|
||||
// Call rerun ui api
|
||||
// Only a web UI API exists for rerunning workflow runs, so use the UI endpoint.
|
||||
rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", webhookData.payloads[0].WorkflowRun.RunNumber)
|
||||
rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", webhookData.payloads[0].WorkflowRun.ID)
|
||||
req = NewRequest(t, "POST", rerunURL)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
||||
@@ -110,8 +110,8 @@ export default defineComponent({
|
||||
WorkflowGraph,
|
||||
},
|
||||
props: {
|
||||
runIndex: {type: Number, required: true},
|
||||
jobIndex: {type: Number, required: true},
|
||||
runId: {type: Number, required: true},
|
||||
jobId: {type: Number, required: true},
|
||||
actionsURL: {type: String, required: true},
|
||||
locale: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
@@ -366,7 +366,7 @@ export default defineComponent({
|
||||
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
|
||||
return {step: idx, cursor: it.cursor, expanded: it.expanded};
|
||||
});
|
||||
const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
|
||||
const resp = await POST(`${this.actionsURL}/runs/${this.runId}/jobs/${this.jobId}`, {
|
||||
signal: abortController.signal,
|
||||
data: {logCursors},
|
||||
});
|
||||
@@ -538,13 +538,13 @@ export default defineComponent({
|
||||
<div class="action-view-left">
|
||||
<div class="job-group-section">
|
||||
<div class="job-brief-list">
|
||||
<a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="jobIndex === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id">
|
||||
<a class="job-brief-item" :href="run.link+'/jobs/'+job.id" :class="jobId === job.id ? 'selected' : ''" v-for="job in run.jobs" :key="job.id">
|
||||
<div class="job-brief-item-left">
|
||||
<ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
|
||||
<span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
|
||||
</div>
|
||||
<span class="job-brief-item-right">
|
||||
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
|
||||
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${job.id}/rerun`" v-if="job.canRerun"/>
|
||||
<span class="step-summary-duration">{{ job.duration }}</span>
|
||||
</span>
|
||||
</a>
|
||||
@@ -581,7 +581,7 @@ export default defineComponent({
|
||||
<WorkflowGraph
|
||||
v-if="showWorkflowGraph && run.jobs.length > 1"
|
||||
:jobs="run.jobs"
|
||||
:current-job-index="jobIndex"
|
||||
:current-job-id="jobId"
|
||||
:run-link="run.link"
|
||||
:workflow-id="run.workflowID"
|
||||
class="workflow-graph-container"
|
||||
@@ -626,7 +626,7 @@ export default defineComponent({
|
||||
</a>
|
||||
|
||||
<div class="divider"/>
|
||||
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" download>
|
||||
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobId+'/logs'" download>
|
||||
<i class="icon"><SvgIcon name="octicon-download"/></i>
|
||||
{{ locale.downloadLogs }}
|
||||
</a>
|
||||
|
||||
@@ -40,7 +40,7 @@ interface StoredState {
|
||||
|
||||
const props = defineProps<{
|
||||
jobs: ActionsJob[];
|
||||
currentJobIndex: number;
|
||||
currentJobId: number;
|
||||
runLink: string;
|
||||
workflowId: string;
|
||||
}>()
|
||||
@@ -588,9 +588,9 @@ function computeJobLevels(jobs: ActionsJob[]): Map<string, number> {
|
||||
}
|
||||
|
||||
function onNodeClick(job: JobNode, event: MouseEvent) {
|
||||
if (job.index === props.currentJobIndex) return;
|
||||
if (job.id === props.currentJobId) return;
|
||||
|
||||
const link = `${props.runLink}/jobs/${job.index}`;
|
||||
const link = `${props.runLink}/jobs/${job.id}`;
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(link, '_blank');
|
||||
return;
|
||||
@@ -652,7 +652,7 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
|
||||
<g
|
||||
v-for="job in jobsWithLayout"
|
||||
:key="job.id"
|
||||
:class="{'current-job': job.index === currentJobIndex}"
|
||||
:class="{'current-job': job.id === currentJobId}"
|
||||
class="job-node-group"
|
||||
@click="onNodeClick(job, $event)"
|
||||
@mouseenter="handleNodeMouseEnter(job)"
|
||||
@@ -665,8 +665,8 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
|
||||
:height="nodeHeight"
|
||||
rx="8"
|
||||
:fill="getNodeColor(job.status)"
|
||||
:stroke="job.index === currentJobIndex ? 'var(--color-primary)' : 'var(--color-card-border)'"
|
||||
:stroke-width="job.index === currentJobIndex ? '3' : '2'"
|
||||
:stroke="job.id === currentJobId ? 'var(--color-primary)' : 'var(--color-card-border)'"
|
||||
:stroke-width="job.id === currentJobId ? '3' : '2'"
|
||||
class="job-rect"
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ export function initRepositoryActionView() {
|
||||
if (parentFullHeight) parentFullHeight.classList.add('tw-pb-0');
|
||||
|
||||
const view = createApp(RepoActionView, {
|
||||
runIndex: parseInt(el.getAttribute('data-run-index')!),
|
||||
jobIndex: parseInt(el.getAttribute('data-job-index')!),
|
||||
runId: parseInt(el.getAttribute('data-run-id')!),
|
||||
jobId: parseInt(el.getAttribute('data-job-id')!),
|
||||
actionsURL: el.getAttribute('data-actions-url'),
|
||||
locale: {
|
||||
approve: el.getAttribute('data-locale-approve'),
|
||||
|
||||
Reference in New Issue
Block a user