Files
forge/tests/integration/actions_rerun_test.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

536 lines
22 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
actions_model "github.com/hanzoai/git/models/actions"
auth_model "github.com/hanzoai/git/models/auth"
"github.com/hanzoai/git/models/db"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unittest"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/actions/jobparser"
runner_module "github.com/hanzoai/forge/modules/actions/runner"
"github.com/hanzoai/git/modules/setting"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/test"
"github.com/hanzoai/git/modules/timeutil"
actions_web "github.com/hanzoai/git/routers/web/repo/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsRerun(t *testing.T) {
onGitRun(t, func(t *testing.T, u *url.URL) {
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sessionAdmin := loginUser(t, userAdmin.Name)
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-rerun", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(httpContext)(t)
runner := newMockRunner()
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
wfTreePath := ".hanzo/workflows/actions-rerun-workflow-1.yml"
wfFileContent := `name: actions-rerun-workflow-1
on:
push:
paths:
- '.hanzo/workflows/actions-rerun-workflow-1.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo 'job1'
job2:
runs-on: ubuntu-latest
needs: [job1]
steps:
- run: echo 'job2'
`
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create"+wfTreePath, wfFileContent)
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
// fetch and exec job1
job1Task := runner.fetchTask(t)
assert.Equal(t, "1", job1Task.Context.RunAttempt)
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.ID)
runner.execTask(t, job1Task, &mockTaskOutcome{
result: runner_module.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.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: runner_module.Success,
})
assert.EqualValues(t, 1, getRunLatestAttemptNum(t, run.ID))
// RERUN-1: rerun the run
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
sessionAdmin.MakeRequest(t, req, http.StatusOK) // triggered by admin user
// fetch and exec job1
job1TaskR1 := runner.fetchTask(t)
assert.Equal(t, "2", job1TaskR1.Context.RunAttempt)
_, job1R1, _ := getTaskAndJobAndRunByTaskID(t, job1TaskR1.ID)
assert.Equal(t, job1.AttemptJobID, job1R1.AttemptJobID)
runner.execTask(t, job1TaskR1, &mockTaskOutcome{
result: runner_module.Success,
})
// fetch and exec job2
job2TaskR1 := runner.fetchTask(t)
assert.Equal(t, "2", job2TaskR1.Context.RunAttempt)
_, job2R1, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.ID)
assert.Equal(t, job2.AttemptJobID, job2R1.AttemptJobID)
runner.execTask(t, job2TaskR1, &mockTaskOutcome{
result: runner_module.Success,
})
assert.EqualValues(t, 2, getRunLatestAttemptNum(t, run.ID))
// RERUN-2: rerun job1
job1 = getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
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
job1TaskR2 := runner.fetchTask(t)
assert.Equal(t, "3", job1TaskR2.Context.RunAttempt)
runner.execTask(t, job1TaskR2, &mockTaskOutcome{
result: runner_module.Success,
})
// fetch and exec job2
job2TaskR2 := runner.fetchTask(t)
assert.Equal(t, "3", job2TaskR2.Context.RunAttempt)
runner.execTask(t, job2TaskR2, &mockTaskOutcome{
result: runner_module.Success,
})
assert.EqualValues(t, 3, getRunLatestAttemptNum(t, run.ID))
// RERUN-3: rerun job2
job2 = getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
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
job2TaskR3 := runner.fetchTask(t)
assert.Equal(t, "4", job2TaskR3.Context.RunAttempt)
runner.execTask(t, job2TaskR3, &mockTaskOutcome{
result: runner_module.Success,
})
runner.fetchNoTask(t)
assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))
runLatestAttempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
job2LatestAttempt := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
assert.Equal(t, runLatestAttempt.LatestAttemptID, job2LatestAttempt.RunAttemptID)
t.Run("AttemptAPI", func(t *testing.T) {
req = NewRequest(t, "GET", fmt.Sprintf("/v1/repos/%s/%s/actions/runs/%d/attempts/2", user2.Name, repo.Name, run.ID)).
AddTokenAuth(token)
attemptResp := MakeRequest(t, req, http.StatusOK)
apiAttempt := DecodeJSON(t, attemptResp, &api.ActionWorkflowRun{})
assert.Equal(t, run.ID, apiAttempt.ID)
assert.EqualValues(t, 2, apiAttempt.RunAttempt)
assert.Equal(t, "completed", apiAttempt.Status)
assert.Equal(t, "success", apiAttempt.Conclusion)
assert.NotNil(t, apiAttempt.PreviousAttemptURL)
assert.True(t, strings.HasSuffix(*apiAttempt.PreviousAttemptURL, fmt.Sprintf("/v1/repos/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, run.ID)))
assert.Equal(t, user2.Name, apiAttempt.Actor.UserName)
assert.Equal(t, userAdmin.Name, apiAttempt.TriggerActor.UserName)
req = NewRequest(t, "GET", fmt.Sprintf("/v1/repos/%s/%s/actions/runs/%d/attempts/2/jobs", user2.Name, repo.Name, run.ID)).
AddTokenAuth(token)
attemptJobsResp := MakeRequest(t, req, http.StatusOK)
apiAttemptJobs := DecodeJSON(t, attemptJobsResp, &api.ActionWorkflowJobsResponse{})
assert.Len(t, apiAttemptJobs.Entries, 2)
assert.ElementsMatch(t, []int64{job1R1.ID, job2R1.ID}, []int64{apiAttemptJobs.Entries[0].ID, apiAttemptJobs.Entries[1].ID})
})
t.Run("MaxRerunAttempts", func(t *testing.T) {
// The run has 4 attempts after the previous reruns. Lower the cap to 4 to hit the limit.
defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(4))()
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
resp := session.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "workflow run has reached the maximum")
assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))
// Raising the cap lets rerun proceed again.
defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(5))()
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
job1TaskR4 := runner.fetchTask(t)
assert.Equal(t, "5", job1TaskR4.Context.RunAttempt)
runner.execTask(t, job1TaskR4, &mockTaskOutcome{
result: runner_module.Success,
})
job2TaskR4 := runner.fetchTask(t)
assert.Equal(t, "5", job2TaskR4.Context.RunAttempt)
runner.execTask(t, job2TaskR4, &mockTaskOutcome{
result: runner_module.Success,
})
assert.EqualValues(t, 5, getRunLatestAttemptNum(t, run.ID))
})
})
}
func TestActionsRerunLegacyNoAttemptRun(t *testing.T) {
onGitRun(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-rerun-legacy", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(httpContext)(t)
runner := newMockRunner()
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
wfTreePath := ".hanzo/workflows/actions-rerun-legacy.yml"
wfFileContent := `name: actions-rerun-legacy
on:
workflow_dispatch:
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo 'job1'
job2:
runs-on: ubuntu-latest
needs: [job1]
steps:
- run: echo 'job2'
`
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
fileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
require.NotNil(t, fileResp)
// Start preparing legacy data
payloads := mustParseSingleWorkflowPayloads(t, wfFileContent)
now := timeutil.TimeStamp(time.Now().Unix())
started := now - 20
stopped := now - 10
legacyRun := &actions_model.ActionRun{
Title: "legacy rerun test",
RepoID: repo.ID,
OwnerID: repo.OwnerID,
WorkflowID: "actions-rerun-legacy.yml",
Index: 1,
TriggerUserID: user2.ID,
Ref: "refs/heads/" + repo.DefaultBranch,
CommitSHA: fileResp.Commit.SHA,
Event: "workflow_dispatch",
TriggerEvent: "workflow_dispatch",
EventPayload: "{}",
Status: actions_model.StatusSuccess,
Started: started,
Stopped: stopped,
Created: started - 5,
Updated: stopped,
}
require.NoError(t, db.Insert(t.Context(), legacyRun))
// xorm does not update "created"-tagged fields via ORM methods; use raw SQL to backfill historical timestamps.
_, err := db.GetEngine(t.Context()).Exec("UPDATE action_run SET created=?, updated=? WHERE id=?", int64(started-5), int64(stopped), legacyRun.ID)
require.NoError(t, err)
legacyRun.Created = started - 5
legacyRun.Updated = stopped
legacyJob1 := &actions_model.ActionRunJob{
RunID: legacyRun.ID,
RepoID: repo.ID,
OwnerID: repo.OwnerID,
CommitSHA: legacyRun.CommitSHA,
Name: payloads["job1"].name,
Attempt: 1,
WorkflowPayload: payloads["job1"].payload,
JobID: "job1",
Needs: payloads["job1"].needs,
RunsOn: payloads["job1"].runsOn,
Status: actions_model.StatusSuccess,
RunAttemptID: 0,
AttemptJobID: 0,
Started: started,
Stopped: stopped,
IsForkPullRequest: false,
}
legacyJob2 := &actions_model.ActionRunJob{
RunID: legacyRun.ID,
RepoID: repo.ID,
OwnerID: repo.OwnerID,
CommitSHA: legacyRun.CommitSHA,
Name: payloads["job2"].name,
Attempt: 1,
WorkflowPayload: payloads["job2"].payload,
JobID: "job2",
Needs: payloads["job2"].needs,
RunsOn: payloads["job2"].runsOn,
Status: actions_model.StatusSuccess,
RunAttemptID: 0,
AttemptJobID: 0,
Started: started,
Stopped: stopped,
IsForkPullRequest: false,
}
require.NoError(t, db.Insert(t.Context(), legacyJob1, legacyJob2))
legacyTask1 := &actions_model.ActionTask{
JobID: legacyJob1.ID,
Attempt: 1,
Status: actions_model.StatusSuccess,
Started: started,
Stopped: stopped,
RepoID: repo.ID,
OwnerID: repo.OwnerID,
CommitSHA: legacyRun.CommitSHA,
IsForkPullRequest: false,
}
legacyTask1.GenerateAndFillToken()
legacyTask2 := &actions_model.ActionTask{
JobID: legacyJob2.ID,
Attempt: 1,
Status: actions_model.StatusSuccess,
Started: started,
Stopped: stopped,
RepoID: repo.ID,
OwnerID: repo.OwnerID,
CommitSHA: legacyRun.CommitSHA,
IsForkPullRequest: false,
}
legacyTask2.GenerateAndFillToken()
require.NoError(t, db.Insert(t.Context(), legacyTask1, legacyTask2))
legacyJob1.TaskID = legacyTask1.ID
legacyJob2.TaskID = legacyTask2.ID
_, err = db.GetEngine(t.Context()).ID(legacyJob1.ID).Cols("task_id").Update(legacyJob1)
require.NoError(t, err)
_, err = db.GetEngine(t.Context()).ID(legacyJob2.ID).Cols("task_id").Update(legacyJob2)
require.NoError(t, err)
legacyArtifact := &actions_model.ActionArtifact{
RunID: legacyRun.ID,
RunAttemptID: 0,
RepoID: repo.ID,
OwnerID: repo.OwnerID,
CommitSHA: legacyRun.CommitSHA,
StoragePath: "artifacts/legacy-artifact.zip",
FileSize: 123,
FileCompressedSize: 123,
ContentEncodingOrType: actions_model.ContentTypeZip,
ArtifactPath: "legacy-artifact.zip",
ArtifactName: "legacy-artifact",
Status: actions_model.ArtifactStatusUploadConfirmed,
ExpiredUnix: now + timeutil.Day,
}
require.NoError(t, db.Insert(t.Context(), legacyArtifact))
// Done preparing legacy data
// assert the web view for the legacy run before rerun
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
legacyResp := session.MakeRequest(t, req, http.StatusOK)
legacyView := DecodeJSON(t, legacyResp, &actions_web.ViewResponse{})
// legacy run has no attempt records, so RunAttempt is 0 and Attempts list is empty
assert.EqualValues(t, 0, legacyView.State.Run.RunAttempt)
assert.Empty(t, legacyView.State.Run.Attempts)
assert.Equal(t, "success", legacyView.State.Run.Status)
assert.True(t, legacyView.State.Run.Done)
// isLatestAttempt=true, done=true: can rerun but not cancel
assert.False(t, legacyView.State.Run.CanCancel)
assert.False(t, legacyView.State.Run.CanApprove)
assert.True(t, legacyView.State.Run.CanRerun)
assert.False(t, legacyView.State.Run.CanRerunFailed) // all jobs succeeded
assert.True(t, legacyView.State.Run.CanDeleteArtifact)
if assert.Len(t, legacyView.State.Run.Jobs, 2) {
assert.Equal(t, legacyJob1.ID, legacyView.State.Run.Jobs[0].ID)
assert.Equal(t, legacyJob2.ID, legacyView.State.Run.Jobs[1].ID)
}
if assert.Len(t, legacyView.Artifacts, 1) {
assert.Equal(t, legacyArtifact.ArtifactName, legacyView.Artifacts[0].Name)
assert.Equal(t, "completed", legacyView.Artifacts[0].Status)
}
// rerun the legacy run
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, legacyRun.ID))
session.MakeRequest(t, req, http.StatusOK)
runAfterRerun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
assert.EqualValues(t, 2, getRunLatestAttemptNum(t, legacyRun.ID))
jobsAfterRerun, err := actions_model.GetRunJobsByRunAndAttemptID(t.Context(), legacyRun.ID, runAfterRerun.LatestAttemptID)
require.NoError(t, err)
require.Len(t, jobsAfterRerun, 2)
rerunJobsByJobID := map[string]*actions_model.ActionRunJob{}
for _, job := range jobsAfterRerun {
rerunJobsByJobID[job.JobID] = job
}
require.Contains(t, rerunJobsByJobID, "job1")
require.Contains(t, rerunJobsByJobID, "job2")
assert.Equal(t, actions_model.StatusWaiting, rerunJobsByJobID["job1"].Status)
assert.Equal(t, actions_model.StatusBlocked, rerunJobsByJobID["job2"].Status)
// fetch job1 rerun task
job1TaskR1 := runner.fetchTask(t)
assert.Equal(t, "2", job1TaskR1.Context.RunAttempt)
rerunJob1Task, rerunJob1, rerunRun := getTaskAndJobAndRunByTaskID(t, job1TaskR1.ID)
assert.Equal(t, legacyRun.ID, rerunRun.ID)
assert.Equal(t, rerunJob1.RunAttemptID, rerunRun.LatestAttemptID)
runner.execTask(t, job1TaskR1, &mockTaskOutcome{result: runner_module.Success})
// fetch job2 rerun task
job2TaskR1 := runner.fetchTask(t)
assert.Equal(t, "2", job2TaskR1.Context.RunAttempt)
rerunJob2Task, rerunJob2, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.ID)
runner.execTask(t, job2TaskR1, &mockTaskOutcome{result: runner_module.Success})
runner.fetchNoTask(t)
// query the 2 attempts
runAfterRerun = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
attempt1, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 1)
require.NoError(t, err)
assert.Equal(t, legacyRun.Created, attempt1.Created)
assert.Equal(t, legacyRun.Started, attempt1.Started)
assert.Equal(t, legacyRun.Stopped, attempt1.Stopped)
attempt2, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 2)
require.NoError(t, err)
assert.Equal(t, attempt2.ID, runAfterRerun.LatestAttemptID)
assert.Equal(t, runAfterRerun.Created, attempt1.Created)
assert.Equal(t, runAfterRerun.Started, attempt2.Started)
assert.Equal(t, runAfterRerun.Stopped, attempt2.Stopped)
// assert legacy jobs
legacyJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob1.ID})
legacyJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob2.ID})
assert.Equal(t, attempt1.ID, legacyJob1.RunAttemptID)
assert.Equal(t, attempt1.ID, legacyJob2.RunAttemptID)
assert.EqualValues(t, 1, legacyJob1.Attempt)
assert.EqualValues(t, 1, legacyJob2.Attempt)
assert.EqualValues(t, 1, legacyJob1.AttemptJobID)
assert.EqualValues(t, 2, legacyJob2.AttemptJobID)
legacyTask1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask1.ID})
legacyTask2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask2.ID})
assert.EqualValues(t, 1, legacyTask1.Attempt)
assert.EqualValues(t, 1, legacyTask2.Attempt)
// assert legacy artifacts
legacyArtifact = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: legacyArtifact.ID})
assert.Equal(t, attempt1.ID, legacyArtifact.RunAttemptID)
// assert jobs of the latest rerun
rerunJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob1.ID})
rerunJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob2.ID})
assert.Equal(t, attempt2.ID, rerunJob1.RunAttemptID)
assert.Equal(t, attempt2.ID, rerunJob2.RunAttemptID)
assert.Equal(t, legacyJob1.AttemptJobID, rerunJob1.AttemptJobID)
assert.Equal(t, legacyJob2.AttemptJobID, rerunJob2.AttemptJobID)
assert.EqualValues(t, 2, rerunJob1Task.Attempt)
assert.EqualValues(t, 2, rerunJob2Task.Attempt)
// assert the web view for the original attempt
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, legacyRun.ID))
attempt1Resp := session.MakeRequest(t, req, http.StatusOK)
attempt1View := DecodeJSON(t, attempt1Resp, &actions_web.ViewResponse{})
assert.EqualValues(t, 1, attempt1View.State.Run.RunAttempt)
if assert.Len(t, attempt1View.State.Run.Attempts, 2) {
// attempts ordered by attempt DESC: index 0 = attempt #2 (latest), index 1 = attempt #1 (current)
assert.False(t, attempt1View.State.Run.Attempts[0].Current)
assert.True(t, attempt1View.State.Run.Attempts[0].Latest)
assert.True(t, attempt1View.State.Run.Attempts[1].Current)
assert.False(t, attempt1View.State.Run.Attempts[1].Latest)
}
// isLatestAttempt=false: all write operations disabled
assert.False(t, attempt1View.State.Run.CanCancel)
assert.False(t, attempt1View.State.Run.CanApprove)
assert.False(t, attempt1View.State.Run.CanRerun)
assert.False(t, attempt1View.State.Run.CanRerunFailed)
assert.True(t, attempt1View.State.Run.CanDeleteArtifact)
assert.Equal(t, legacyJob1.ID, attempt1View.State.Run.Jobs[0].ID)
assert.Equal(t, legacyJob2.ID, attempt1View.State.Run.Jobs[1].ID)
if assert.Len(t, attempt1View.Artifacts, 1) {
assert.Equal(t, attempt1View.Artifacts[0].Name, legacyArtifact.ArtifactName)
}
// assert the web view for the latest attempt
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
attempt2Resp := session.MakeRequest(t, req, http.StatusOK)
attempt2View := DecodeJSON(t, attempt2Resp, &actions_web.ViewResponse{})
assert.EqualValues(t, 2, attempt2View.State.Run.RunAttempt)
if assert.Len(t, attempt2View.State.Run.Attempts, 2) {
// attempts ordered by attempt DESC: index 0 = attempt #2 (latest, current), index 1 = attempt #1
assert.True(t, attempt2View.State.Run.Attempts[0].Current)
assert.True(t, attempt2View.State.Run.Attempts[0].Latest)
assert.False(t, attempt2View.State.Run.Attempts[1].Current)
assert.False(t, attempt2View.State.Run.Attempts[1].Latest)
}
// isLatestAttempt=true, done=true: can rerun but not cancel
assert.False(t, attempt2View.State.Run.CanCancel)
assert.False(t, attempt2View.State.Run.CanApprove)
assert.True(t, attempt2View.State.Run.CanRerun)
assert.False(t, attempt2View.State.Run.CanRerunFailed) // all jobs succeeded
assert.True(t, attempt2View.State.Run.CanDeleteArtifact)
assert.Equal(t, rerunJob1.ID, attempt2View.State.Run.Jobs[0].ID)
assert.Equal(t, rerunJob2.ID, attempt2View.State.Run.Jobs[1].ID)
assert.Empty(t, attempt2View.Artifacts)
})
}
type workflowJobPayload struct {
name string
payload []byte
needs []string
runsOn []string
}
func mustParseSingleWorkflowPayloads(t *testing.T, workflowContent string) map[string]workflowJobPayload {
t.Helper()
workflows, err := jobparser.Parse([]byte(workflowContent))
require.NoError(t, err)
payloads := make(map[string]workflowJobPayload, len(workflows))
for _, workflow := range workflows {
id, job := workflow.Job()
needs := job.Needs()
require.NoError(t, workflow.SetJob(id, job.EraseNeeds()))
payload, err := workflow.Marshal()
require.NoError(t, err)
payloads[id] = workflowJobPayload{
name: job.Name,
payload: payload,
needs: needs,
runsOn: job.RunsOn(),
}
}
return payloads
}
func getRunLatestAttemptNum(t *testing.T, runID int64) int64 {
t.Helper()
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: run.LatestAttemptID})
return attempt.Attempt
}