[v13.0/forgejo] fix: failure to parse on block results in unconditional workflow execution (#9536)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9536 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
80fcbf165c
11 changed files with 182 additions and 63 deletions
|
|
@ -56,6 +56,8 @@ type ActionRun struct {
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
NotifyEmail bool
|
NotifyEmail bool
|
||||||
|
|
||||||
|
PreExecutionError string `xorm:"LONGTEXT"` // used to report errors that blocked execution of a workflow
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,9 @@ var migrations = []*Migration{
|
||||||
NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying),
|
NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying),
|
||||||
// v39 -> v40
|
// v39 -> v40
|
||||||
NewMigration("Add index for release sha1", AddIndexForReleaseSha1),
|
NewMigration("Add index for release sha1", AddIndexForReleaseSha1),
|
||||||
|
// NOTE: v42 -> v43 -- effectively backported into Forgejo v13 as part of backporting
|
||||||
|
// https://codeberg.org/forgejo/forgejo/pulls/9530, but the migration was omitted to avoid future upgrade conflicts.
|
||||||
|
// The migration will effectively occur automatically via table `Sync` on DB engine initialization.
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type DetectedWorkflow struct {
|
||||||
EntryName string
|
EntryName string
|
||||||
TriggerEvent *jobparser.Event
|
TriggerEvent *jobparser.Event
|
||||||
Content []byte
|
Content []byte
|
||||||
|
EventDetectionError error
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -128,6 +129,7 @@ func DetectWorkflows(
|
||||||
Name: triggedEvent.Event(),
|
Name: triggedEvent.Event(),
|
||||||
},
|
},
|
||||||
Content: content,
|
Content: content,
|
||||||
|
EventDetectionError: err,
|
||||||
}
|
}
|
||||||
workflows = append(workflows, dwf)
|
workflows = append(workflows, dwf)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -150,5 +150,8 @@
|
||||||
"actions.runs.run_attempt_label": "Run attempt #%[1]s (%[2]s)",
|
"actions.runs.run_attempt_label": "Run attempt #%[1]s (%[2]s)",
|
||||||
"actions.runs.viewing_out_of_date_run": "You are viewing an out-of-date run of this job that was executed %[1]s.",
|
"actions.runs.viewing_out_of_date_run": "You are viewing an out-of-date run of this job that was executed %[1]s.",
|
||||||
"actions.runs.view_most_recent_run": "View most recent run",
|
"actions.runs.view_most_recent_run": "View most recent run",
|
||||||
|
"actions.workflow.job_parsing_error": "Unable to parse jobs in workflow: %v",
|
||||||
|
"actions.workflow.event_detection_error": "Unable to parse supported events in workflow: %v",
|
||||||
|
"actions.workflow.pre_execution_error": "Workflow was not executed due to an error that blocked the execution attempt.",
|
||||||
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
|
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,7 @@ type ViewRunInfo struct {
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
Jobs []*ViewJob `json:"jobs"`
|
Jobs []*ViewJob `json:"jobs"`
|
||||||
Commit ViewCommit `json:"commit"`
|
Commit ViewCommit `json:"commit"`
|
||||||
|
PreExecutionError string `json:"preExecutionError"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewCurrentJob struct {
|
type ViewCurrentJob struct {
|
||||||
|
|
@ -285,6 +286,7 @@ func getViewResponse(ctx *context_module.Context, req *ViewRequest, runIndex, jo
|
||||||
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||||
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead of 'null' in json
|
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead of 'null' in json
|
||||||
resp.State.Run.Status = run.Status.String()
|
resp.State.Run.Status = run.Status.String()
|
||||||
|
resp.State.Run.PreExecutionError = run.PreExecutionError
|
||||||
|
|
||||||
// It's possible for the run to be marked with a finalized status (eg. failure) because of a single job within the
|
// It's possible for the run to be marked with a finalized status (eg. failure) because of a single job within the
|
||||||
// run; eg. one job fails, the run fails. But other jobs can still be running. The frontend RepoActionView uses the
|
// run; eg. one job fails, the run fails. But other jobs can still be running. The frontend RepoActionView uses the
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
api "forgejo.org/modules/structs"
|
api "forgejo.org/modules/structs"
|
||||||
|
"forgejo.org/modules/translation"
|
||||||
"forgejo.org/modules/util"
|
"forgejo.org/modules/util"
|
||||||
webhook_module "forgejo.org/modules/webhook"
|
webhook_module "forgejo.org/modules/webhook"
|
||||||
"forgejo.org/services/convert"
|
"forgejo.org/services/convert"
|
||||||
|
|
@ -378,13 +379,25 @@ func handleWorkflows(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs, err := jobParser(dwf.Content, jobparser.WithVars(vars))
|
var jobs []*jobparser.SingleWorkflow
|
||||||
if err != nil {
|
if dwf.EventDetectionError != nil { // don't even bother trying to parse jobs due to event detection error
|
||||||
|
tr := translation.NewLocale(input.Doer.Language)
|
||||||
|
run.PreExecutionError = tr.TrString("actions.workflow.event_detection_error", dwf.EventDetectionError)
|
||||||
run.Status = actions_model.StatusFailure
|
run.Status = actions_model.StatusFailure
|
||||||
log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
|
|
||||||
jobs = []*jobparser.SingleWorkflow{{
|
jobs = []*jobparser.SingleWorkflow{{
|
||||||
Name: dwf.EntryName,
|
Name: dwf.EntryName,
|
||||||
}}
|
}}
|
||||||
|
} else {
|
||||||
|
jobs, err = jobParser(dwf.Content, jobparser.WithVars(vars))
|
||||||
|
if err != nil {
|
||||||
|
log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
|
||||||
|
tr := translation.NewLocale(input.Doer.Language)
|
||||||
|
run.PreExecutionError = tr.TrString("actions.workflow.job_parsing_error", err)
|
||||||
|
run.Status = actions_model.StatusFailure
|
||||||
|
jobs = []*jobparser.SingleWorkflow{{
|
||||||
|
Name: dwf.EntryName,
|
||||||
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel running jobs if the event is push or pull_request_sync
|
// cancel running jobs if the event is push or pull_request_sync
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "forgejo.org/models/actions"
|
actions_model "forgejo.org/models/actions"
|
||||||
|
|
@ -144,3 +145,88 @@ func Test_OpenForkPullRequestEvent(t *testing.T) {
|
||||||
assert.Equal(t, webhook_module.HookEventPullRequest, runs[0].Event)
|
assert.Equal(t, webhook_module.HookEventPullRequest, runs[0].Event)
|
||||||
assert.True(t, runs[0].IsForkPullRequest)
|
assert.True(t, runs[0].IsForkPullRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionsPreExecutionErrorInvalidJobs(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||||
|
|
||||||
|
commit := &git.Commit{
|
||||||
|
ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
|
||||||
|
CommitMessage: "test",
|
||||||
|
}
|
||||||
|
detectedWorkflows := []*actions_module.DetectedWorkflow{
|
||||||
|
{
|
||||||
|
EntryName: "test.yml",
|
||||||
|
TriggerEvent: &jobparser.Event{
|
||||||
|
Name: "pull_request",
|
||||||
|
},
|
||||||
|
Content: []byte("{ on: pull_request, jobs: 'hello, I am the jobs!' }"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
input := ¬ifyInput{
|
||||||
|
Repo: repo,
|
||||||
|
Doer: doer,
|
||||||
|
Event: webhook_module.HookEventPullRequestSync,
|
||||||
|
PullRequest: pr,
|
||||||
|
Payload: &api.PullRequestPayload{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "refs/head/main")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, runs, 1)
|
||||||
|
createdRun := runs[0]
|
||||||
|
|
||||||
|
assert.Equal(t, actions_model.StatusFailure, createdRun.Status)
|
||||||
|
assert.Contains(t, createdRun.PreExecutionError, "actions.workflow.job_parsing_error%!(EXTRA *fmt.wrapError=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsPreExecutionEventDetectionError(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||||
|
|
||||||
|
commit := &git.Commit{
|
||||||
|
ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
|
||||||
|
CommitMessage: "test",
|
||||||
|
}
|
||||||
|
detectedWorkflows := []*actions_module.DetectedWorkflow{
|
||||||
|
{
|
||||||
|
EntryName: "test.yml",
|
||||||
|
TriggerEvent: &jobparser.Event{
|
||||||
|
Name: "pull_request",
|
||||||
|
},
|
||||||
|
Content: []byte("{ on: nothing, jobs: { j1: {} }}"),
|
||||||
|
EventDetectionError: errors.New("nothing is not a valid event"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
input := ¬ifyInput{
|
||||||
|
Repo: repo,
|
||||||
|
Doer: doer,
|
||||||
|
Event: webhook_module.HookEventPullRequestSync,
|
||||||
|
PullRequest: pr,
|
||||||
|
Payload: &api.PullRequestPayload{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "refs/head/main")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, runs, 1)
|
||||||
|
createdRun := runs[0]
|
||||||
|
|
||||||
|
assert.Equal(t, actions_model.StatusFailure, createdRun.Status)
|
||||||
|
assert.Equal(t, "actions.workflow.event_detection_error%!(EXTRA *errors.errorString=nothing is not a valid event)", createdRun.PreExecutionError)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
data-locale-run-attempt-label="{{ctx.Locale.Tr "actions.runs.run_attempt_label"}}"
|
data-locale-run-attempt-label="{{ctx.Locale.Tr "actions.runs.run_attempt_label"}}"
|
||||||
data-locale-viewing-out-of-date-run="{{ctx.Locale.Tr "actions.runs.viewing_out_of_date_run"}}"
|
data-locale-viewing-out-of-date-run="{{ctx.Locale.Tr "actions.runs.viewing_out_of_date_run"}}"
|
||||||
data-locale-view-most-recent-run="{{ctx.Locale.Tr "actions.runs.view_most_recent_run"}}"
|
data-locale-view-most-recent-run="{{ctx.Locale.Tr "actions.runs.view_most_recent_run"}}"
|
||||||
|
data-locale-pre-execution-error="{{ctx.Locale.Tr "actions.workflow.pre_execution_error"}}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ func TestActionViewsView(t *testing.T) {
|
||||||
re = regexp.MustCompile(pattern)
|
re = regexp.MustCompile(pattern)
|
||||||
actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`)
|
actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`)
|
||||||
|
|
||||||
return assert.JSONEq(t, "{\"state\":{\"run\":{\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Success\",\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean)
|
return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Success\",\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean)
|
||||||
})
|
})
|
||||||
htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[{\"name\":\"multi-file-download\",\"size\":2048,\"status\":\"completed\"}]}\n")
|
htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[{\"name\":\"multi-file-download\",\"size\":2048,\"status\":\"completed\"}]}\n")
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +184,7 @@ func TestActionViewsViewAttemptOutOfRange(t *testing.T) {
|
||||||
re = regexp.MustCompile(pattern)
|
re = regexp.MustCompile(pattern)
|
||||||
actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`)
|
actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`)
|
||||||
|
|
||||||
return assert.JSONEq(t, "{\"state\":{\"run\":{\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Waiting\",\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean)
|
return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Waiting\",\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean)
|
||||||
})
|
})
|
||||||
htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[]}\n")
|
htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[]}\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const testLocale = {
|
||||||
runAttemptLabel: 'Run attempt %[1]s %[2]s',
|
runAttemptLabel: 'Run attempt %[1]s %[2]s',
|
||||||
viewingOutOfDateRun: 'oh no, out of date since %[1]s give or take or so',
|
viewingOutOfDateRun: 'oh no, out of date since %[1]s give or take or so',
|
||||||
viewMostRecentRun: '',
|
viewMostRecentRun: '',
|
||||||
|
preExecutionError: 'pre-execution error',
|
||||||
status: {
|
status: {
|
||||||
unknown: '',
|
unknown: '',
|
||||||
waiting: '',
|
waiting: '',
|
||||||
|
|
@ -53,6 +54,18 @@ const minimalInitialJobData = {
|
||||||
const minimalInitialArtifactData = {
|
const minimalInitialArtifactData = {
|
||||||
artifacts: [],
|
artifacts: [],
|
||||||
};
|
};
|
||||||
|
const defaultTestProps = {
|
||||||
|
actionsURL: 'https://example.com/example-org/example-repo/actions',
|
||||||
|
jobIndex: '1',
|
||||||
|
attemptNumber: '1',
|
||||||
|
runIndex: '10',
|
||||||
|
runID: '1001',
|
||||||
|
initialJobData: minimalInitialJobData,
|
||||||
|
initialArtifactData: minimalInitialArtifactData,
|
||||||
|
locale: testLocale,
|
||||||
|
workflowName: 'workflow name',
|
||||||
|
workflowURL: 'https://example.com/example-org/example-repo/actions?workflow=test.yml',
|
||||||
|
};
|
||||||
|
|
||||||
test('processes ##[group] and ##[endgroup]', async () => {
|
test('processes ##[group] and ##[endgroup]', async () => {
|
||||||
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
||||||
|
|
@ -105,13 +118,7 @@ test('processes ##[group] and ##[endgroup]', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: defaultTestProps,
|
||||||
jobIndex: '1',
|
|
||||||
attemptNumber: '1',
|
|
||||||
initialJobData: minimalInitialJobData,
|
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
await wrapper.get('.job-step-summary').trigger('click');
|
await wrapper.get('.job-step-summary').trigger('click');
|
||||||
|
|
@ -208,15 +215,7 @@ test('load multiple steps on a finished action', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: defaultTestProps,
|
||||||
actionsURL: 'https://example.com/example-org/example-repo/actions',
|
|
||||||
initialJobData: minimalInitialJobData,
|
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
runIndex: '1',
|
|
||||||
jobIndex: '2',
|
|
||||||
attemptNumber: '1',
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.
|
wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
@ -288,13 +287,11 @@ function configureForMultipleAttemptTests({viewHistorical}) {
|
||||||
|
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: {
|
||||||
|
...defaultTestProps,
|
||||||
runIndex: '123',
|
runIndex: '123',
|
||||||
jobIndex: '1',
|
|
||||||
attemptNumber: viewHistorical ? '1' : '2',
|
attemptNumber: viewHistorical ? '1' : '2',
|
||||||
actionsURL: toAbsoluteUrl('/user1/repo2/actions'),
|
actionsURL: toAbsoluteUrl('/user1/repo2/actions'),
|
||||||
initialJobData: {...minimalInitialJobData, state: myJobState},
|
initialJobData: {...minimalInitialJobData, state: myJobState},
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return wrapper;
|
return wrapper;
|
||||||
|
|
@ -465,16 +462,7 @@ test('artifacts download links', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: defaultTestProps,
|
||||||
actionsURL: 'https://example.com/example-org/example-repo/actions',
|
|
||||||
initialJobData: minimalInitialJobData,
|
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
runIndex: '10',
|
|
||||||
runID: '1001',
|
|
||||||
jobIndex: '2',
|
|
||||||
attemptNumber: '1',
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.
|
wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
@ -501,11 +489,8 @@ test('initial load schedules refresh when job is not done', async () => {
|
||||||
doneInitialJobData.state.run.done = true;
|
doneInitialJobData.state.run.done = true;
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: {
|
||||||
jobIndex: '1',
|
...defaultTestProps,
|
||||||
attemptNumber: '1',
|
|
||||||
initialJobData: doneInitialJobData,
|
initialJobData: doneInitialJobData,
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
@ -520,13 +505,7 @@ test('initial load schedules refresh when job is not done', async () => {
|
||||||
const runningInitialJobData = structuredClone(minimalInitialJobData);
|
const runningInitialJobData = structuredClone(minimalInitialJobData);
|
||||||
runningInitialJobData.state.run.done = false;
|
runningInitialJobData.state.run.done = false;
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: defaultTestProps,
|
||||||
jobIndex: '1',
|
|
||||||
attemptNumber: '1',
|
|
||||||
initialJobData: runningInitialJobData,
|
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
const container = wrapper.find('.action-view-container');
|
const container = wrapper.find('.action-view-container');
|
||||||
|
|
@ -548,13 +527,7 @@ test('initial load data is used without calling fetch()', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
mount(RepoActionView, {
|
mount(RepoActionView, {
|
||||||
props: {
|
props: defaultTestProps,
|
||||||
jobIndex: '1',
|
|
||||||
attemptNumber: '1',
|
|
||||||
initialJobData: minimalInitialJobData,
|
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(fetchSpy).not.toHaveBeenCalled();
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
|
@ -564,11 +537,7 @@ test('view non-picked action run job', async () => {
|
||||||
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
||||||
const wrapper = mount(RepoActionView, {
|
const wrapper = mount(RepoActionView, {
|
||||||
props: {
|
props: {
|
||||||
actionsURL: 'https://example.com/example-org/example-repo/actions',
|
...defaultTestProps,
|
||||||
runIndex: '10',
|
|
||||||
runID: '1001',
|
|
||||||
jobIndex: '2',
|
|
||||||
attemptNumber: '1',
|
|
||||||
initialJobData: {
|
initialJobData: {
|
||||||
...minimalInitialJobData,
|
...minimalInitialJobData,
|
||||||
// Definitions here should match the same type of content as the related backend test,
|
// Definitions here should match the same type of content as the related backend test,
|
||||||
|
|
@ -613,8 +582,6 @@ test('view non-picked action run job', async () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
initialArtifactData: minimalInitialArtifactData,
|
|
||||||
locale: testLocale,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
@ -624,3 +591,35 @@ test('view non-picked action run job', async () => {
|
||||||
expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(2) .job-brief-name').text()).toEqual('check-2');
|
expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(2) .job-brief-name').text()).toEqual('check-2');
|
||||||
expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(3) .job-brief-name').text()).toEqual('check-3');
|
expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(3) .job-brief-name').text()).toEqual('check-3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('view without pre-execution error', async () => {
|
||||||
|
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
||||||
|
const wrapper = mount(RepoActionView, {
|
||||||
|
props: defaultTestProps,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.find('.pre-execution-error').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view with pre-execution error', async () => {
|
||||||
|
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
|
||||||
|
const wrapper = mount(RepoActionView, {
|
||||||
|
props: {
|
||||||
|
...defaultTestProps,
|
||||||
|
initialJobData: {
|
||||||
|
...minimalInitialJobData,
|
||||||
|
state: {
|
||||||
|
...minimalInitialJobData.state,
|
||||||
|
run: {
|
||||||
|
...minimalInitialJobData.state.run,
|
||||||
|
preExecutionError: 'Oops, I dropped it.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
const block = wrapper.find('.pre-execution-error');
|
||||||
|
expect(block.exists()).toBe(true);
|
||||||
|
expect(block.text()).toBe('pre-execution error Oops, I dropped it.');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const sfc = {
|
||||||
canApprove: false,
|
canApprove: false,
|
||||||
canRerun: false,
|
canRerun: false,
|
||||||
done: false,
|
done: false,
|
||||||
|
preExecutionError: '',
|
||||||
jobs: [
|
jobs: [
|
||||||
// {
|
// {
|
||||||
// id: 0,
|
// id: 0,
|
||||||
|
|
@ -525,6 +526,7 @@ export function initRepositoryActionView() {
|
||||||
runAttemptLabel: el.getAttribute('data-locale-run-attempt-label'),
|
runAttemptLabel: el.getAttribute('data-locale-run-attempt-label'),
|
||||||
viewingOutOfDateRun: el.getAttribute('data-locale-viewing-out-of-date-run'),
|
viewingOutOfDateRun: el.getAttribute('data-locale-viewing-out-of-date-run'),
|
||||||
viewMostRecentRun: el.getAttribute('data-locale-view-most-recent-run'),
|
viewMostRecentRun: el.getAttribute('data-locale-view-most-recent-run'),
|
||||||
|
preExecutionError: el.getAttribute('data-locale-pre-execution-error'),
|
||||||
status: {
|
status: {
|
||||||
unknown: el.getAttribute('data-locale-status-unknown'),
|
unknown: el.getAttribute('data-locale-status-unknown'),
|
||||||
waiting: el.getAttribute('data-locale-status-waiting'),
|
waiting: el.getAttribute('data-locale-status-waiting'),
|
||||||
|
|
@ -582,6 +584,12 @@ export function initRepositoryActionView() {
|
||||||
{{ run.commit.localeWorkflow }}
|
{{ run.commit.localeWorkflow }}
|
||||||
<a class="muted" :href="workflowURL">{{ workflowName }}</a>
|
<a class="muted" :href="workflowURL">{{ workflowName }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui error message pre-execution-error" v-if="run.preExecutionError">
|
||||||
|
<div class="header">
|
||||||
|
{{ locale.preExecutionError }}
|
||||||
|
</div>
|
||||||
|
{{ run.preExecutionError }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-view-body">
|
<div class="action-view-body">
|
||||||
<div class="action-view-left" v-if="displayOtherJobs">
|
<div class="action-view-left" v-if="displayOtherJobs">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue