mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-20 19:36:28 +01:00
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>
295 lines
12 KiB
Go
295 lines
12 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"testing"
|
|
|
|
actions_model "code.gitea.io/gitea/models/actions"
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/json"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAPIActionsGetWorkflowRun(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
t.Run("GetRun", func(t *testing.T) {
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
})
|
|
|
|
t.Run("GetJobSteps", func(t *testing.T) {
|
|
// Insert task steps for task_id 53 (job 198) so the API can return them once the backend loads them
|
|
_, err := db.GetEngine(t.Context()).Insert(&actions_model.ActionTaskStep{
|
|
Name: "main",
|
|
TaskID: 53,
|
|
Index: 0,
|
|
RepoID: repo.ID,
|
|
Status: actions_model.StatusSuccess,
|
|
Started: timeutil.TimeStamp(1683636528),
|
|
Stopped: timeutil.TimeStamp(1683636626),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
var jobList api.ActionWorkflowJobsResponse
|
|
err = json.Unmarshal(resp.Body.Bytes(), &jobList)
|
|
require.NoError(t, err)
|
|
|
|
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
|
|
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
|
|
job198 := jobList.Entries[job198Idx]
|
|
require.NotEmpty(t, job198.Steps, "job must return at least one step when task has steps")
|
|
assert.Equal(t, "main", job198.Steps[0].Name, "first step name")
|
|
})
|
|
}
|
|
|
|
func TestAPIActionsGetWorkflowJob(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/198198", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/198", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/196", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPIActionsDeleteRun(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
testAPIActionsDeleteRunListArtifacts(t, repo, token, 2)
|
|
testAPIActionsDeleteRunListTasks(t, repo, token, true)
|
|
testAPIActionsDeleteRun(t, repo, token, http.StatusNoContent)
|
|
|
|
testAPIActionsDeleteRunListArtifacts(t, repo, token, 0)
|
|
testAPIActionsDeleteRunListTasks(t, repo, token, false)
|
|
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPIActionsDeleteRunRunning(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
}
|
|
|
|
func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token string, expected int) {
|
|
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, expected)
|
|
}
|
|
|
|
func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) {
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
var listResp api.ActionArtifactsResponse
|
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, listResp.Entries, artifacts)
|
|
}
|
|
|
|
func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) {
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())).
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
var listResp api.ActionTaskResponse
|
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
|
assert.NoError(t, err)
|
|
findTask1 := false
|
|
findTask2 := false
|
|
for _, entry := range listResp.Entries {
|
|
if entry.ID == 53 {
|
|
findTask1 = true
|
|
continue
|
|
}
|
|
if entry.ID == 54 {
|
|
findTask2 = true
|
|
continue
|
|
}
|
|
}
|
|
assert.Equal(t, expected, findTask1)
|
|
assert.Equal(t, expected, findTask2)
|
|
}
|
|
|
|
func TestAPIActionsRerunWorkflowRun(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
t.Run("NotDone", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
})
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
|
|
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
|
|
var rerunResp api.ActionWorkflowRun
|
|
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(795), rerunResp.ID)
|
|
assert.Equal(t, "queued", rerunResp.Status)
|
|
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
|
|
|
|
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
|
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
|
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
|
|
|
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.GetRunJobByRunAndID(t.Context(), 795, 199)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
|
assert.Equal(t, int64(0), job199.TaskID)
|
|
})
|
|
|
|
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
|
AddTokenAuth(readToken)
|
|
MakeRequest(t, req, http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
t.Run("NotDone", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/jobs/194/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
})
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
session := loginUser(t, user.Name)
|
|
|
|
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
|
|
var rerunResp api.ActionWorkflowJob
|
|
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(199), rerunResp.ID)
|
|
assert.Equal(t, "queued", rerunResp.Status)
|
|
|
|
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
|
|
|
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.GetRunJobByRunAndID(t.Context(), 795, 199)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
|
assert.Equal(t, int64(0), job199.TaskID)
|
|
})
|
|
|
|
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
|
|
AddTokenAuth(readToken)
|
|
MakeRequest(t, req, http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("NotFoundJob", func(t *testing.T) {
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/rerun", repo.FullName())).
|
|
AddTokenAuth(writeToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
})
|
|
}
|