fix: a corrupted Forgejo Actions scheduled workflow is disabled (#8942)

The following errors are specific to the scheduled workflow being handled. They do not imply the remaining scheduled workflows cannot be scheduled successfully.

- Failure to create a scheduled task which is most likely caused   by an invalid YAML file.
- Failure to parse the scheduling specs which can be caused by a   number of formating errors.

Instead of returning on error, the corrupted workflow is disabled.

Also display more informative error messages so that the failed workflow can be identified from the logged error.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Bug fixes
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/8942): <!--number 8942 --><!--line 0 --><!--description YSBjb3JydXB0ZWQgRm9yZ2VqbyBBY3Rpb25zIHNjaGVkdWxlZCB3b3JrZmxvdyBpcyBkaXNhYmxlZA==-->a corrupted Forgejo Actions scheduled workflow is disabled<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8942
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Earl Warren <contact@earl-warren.org>
Co-committed-by: Earl Warren <contact@earl-warren.org>
This commit is contained in:
Earl Warren 2025-08-18 22:45:10 +02:00 committed by Earl Warren
commit b0b6bd3658
5 changed files with 136 additions and 9 deletions

View file

@ -336,5 +336,8 @@ func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err
// UpdateRepoUnit updates the provided repo unit // UpdateRepoUnit updates the provided repo unit
func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
return err if err != nil {
return fmt.Errorf("UpdateRepoUnit: %v", err)
}
return nil
} }

View file

@ -0,0 +1,41 @@
# A corrupted cron spec with a valid schedule workflow
-
id: 1
title: schedule_title1
specs:
- '* * * * *'
repo_id: 4
owner_id: 2
workflow_id: 'workflow1.yml'
trigger_user_id: 2
ref: main
commit_sha: shashasha
event: "schedule"
event_payload: "fakepayload"
content: |
jobs:
job2:
runs-on: ubuntu-latest
steps:
- run: true
# A valid cron spec with a corrupted schedule workflow
-
id: 2
title: schedule_title2
specs:
- '* * * * *'
repo_id: 4
owner_id: 2
workflow_id: 'workflow2.yml'
trigger_user_id: 2
ref: main
commit_sha: shashasha
event: "schedule"
event_payload: "fakepayload"
content: |
jobs:
job2: { invalid yaml
runs-on: ubuntu-latest
steps:
- run: true

View file

@ -0,0 +1,15 @@
# A corrupted cron spec with a valid schedule workflow
-
id: 1
repo_id: 4
schedule_id: 1
next: 1
spec: 'corrupted * *'
# A valid cron spec with a corrupted schedule workflow
-
id: 2
repo_id: 4
schedule_id: 2
next: 1
spec: '* * * * *'

View file

@ -20,6 +20,7 @@ import (
"code.forgejo.org/forgejo/runner/v9/act/jobparser" "code.forgejo.org/forgejo/runner/v9/act/jobparser"
act_model "code.forgejo.org/forgejo/runner/v9/act/model" act_model "code.forgejo.org/forgejo/runner/v9/act/model"
"github.com/robfig/cron/v3"
"xorm.io/builder" "xorm.io/builder"
) )
@ -83,21 +84,34 @@ func startTasks(ctx context.Context) error {
} }
return fmt.Errorf("GetUnit: %w", err) return fmt.Errorf("GetUnit: %w", err)
} }
if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) { actionConfig := cfg.ActionsConfig()
if actionConfig.IsWorkflowDisabled(row.Schedule.WorkflowID) {
continue continue
} }
createAndSchedule := func(row *actions_model.ActionScheduleSpec) (cron.Schedule, error) {
if err := CreateScheduleTask(ctx, row.Schedule); err != nil { if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
log.Error("CreateScheduleTask: %v", err) return nil, fmt.Errorf("CreateScheduleTask: %v", err)
return err
} }
// Parse the spec // Parse the spec
schedule, err := row.Parse() schedule, err := row.Parse()
if err != nil { if err != nil {
log.Error("Parse: %v", err) return nil, fmt.Errorf("Parse(Spec=%v): %v", row.Spec, err)
}
return schedule, nil
}
schedule, err := createAndSchedule(row)
if err != nil {
log.Error("RepoID=%v WorkflowID=%v: %v", row.Schedule.RepoID, row.Schedule.WorkflowID, err)
actionConfig.DisableWorkflow(row.Schedule.WorkflowID)
if err := repo_model.UpdateRepoUnit(ctx, cfg); err != nil {
log.Error("RepoID=%v WorkflowID=%v: CreateScheduleTask: %v", row.Schedule.RepoID, row.Schedule.WorkflowID, err)
return err return err
} }
continue
}
// Update the spec's next run time and previous run time // Update the spec's next run time and previous run time
row.Prev = row.Next row.Prev = row.Next

View file

@ -7,7 +7,9 @@ import (
"testing" "testing"
actions_model "forgejo.org/models/actions" actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo" repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit"
"forgejo.org/models/unittest" "forgejo.org/models/unittest"
webhook_module "forgejo.org/modules/webhook" webhook_module "forgejo.org/modules/webhook"
@ -15,6 +17,58 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestServiceActions_startTask(t *testing.T) {
defer unittest.OverrideFixtures("services/actions/TestServiceActions_startTask")()
require.NoError(t, unittest.PrepareTestDatabase())
// Load fixtures that are corrupted and create one valid scheduled workflow
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
workflowID := "some.yml"
schedules := []*actions_model.ActionSchedule{
{
Title: "scheduletitle1",
RepoID: repo.ID,
OwnerID: repo.OwnerID,
WorkflowID: workflowID,
TriggerUserID: repo.OwnerID,
Ref: "branch",
CommitSHA: "fakeSHA",
Event: webhook_module.HookEventSchedule,
EventPayload: "fakepayload",
Specs: []string{"* * * * *"},
Content: []byte(
`
jobs:
job2:
runs-on: ubuntu-latest
steps:
- run: true
`),
},
}
require.Equal(t, 2, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
require.NoError(t, actions_model.CreateScheduleTask(t.Context(), schedules))
require.Equal(t, 3, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
_, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `action_schedule_spec` SET next = 1")
require.NoError(t, err)
// After running startTasks an ActionRun row is created for the valid scheduled workflow
require.Empty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
require.NoError(t, startTasks(t.Context()))
require.NotEmpty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
// The invalid workflows loaded from the fixtures are disabled
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
actionUnit, err := repo.GetUnit(t.Context(), unit.TypeActions)
require.NoError(t, err)
actionConfig := actionUnit.ActionsConfig()
assert.True(t, actionConfig.IsWorkflowDisabled("workflow2.yml"))
assert.True(t, actionConfig.IsWorkflowDisabled("workflow1.yml"))
assert.False(t, actionConfig.IsWorkflowDisabled("some.yml"))
}
func TestCreateScheduleTask(t *testing.T) { func TestCreateScheduleTask(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase()) require.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2})