mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-14 06:50:25 +00:00
GitHub recently added the ability to [specify a time zone for scheduled workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onschedule), thereby making it possible to run scheduled workflows at a certain local time, no matter whether daylight saving time (DST) is currently active or not. Example copied from GitHub's documentation: ```yaml on: schedule: - cron: '30 5 * * 1-5' timezone: "America/New_York" ``` The workflow would run at 05:30 each morning in the America/New_York timezone every Monday through Friday. `timezone` accepts IANA time zone names. If `timezone` is absent, `Etc/UTC` is used. GitHub runs workflows that were scheduled during DST jumps forward, for example, between 2 o'clock and 3 o'clock, directly after the clock jumped forward. In this case, that would be 3 o'clock. Forgejo already supports time zones by prepending cron schedules with `TZ=<zone-id>` or `CRON_TZ=<zone-id>`: ```yaml on: schedule: - cron: 'CRON_TZ=America/New_York 30 5 * * 1-5' ``` However, that capability is not documented. Workflows that are scheduled to run during DST changes are skipped when the clock jumps forward and run twice when it jumps backward. This two-part PR adds support for `timezone` to improve compatibility with GitHub. `TZ` and `CRON_TZ` continue working. When both `timezone` and `TZ` or `CRON_TZ` are present, `timezone` takes precedence. When neither `timezone` nor `TZ` nor `CRON_TZ` are present, `Etc/UTC` is used as before. Because `TZ` and `CRON_TZ` were already supported by Forgejo before GitHub introduced `timezone`, `timezone` behaves during DST changes as previous versions of Forgejo, thereby deviating from GitHub. That means that workflows that are scheduled to run during DST changes are skipped when the clock jumps forward. And they run twice when it jumps backwards. However, it is generally recommended not to schedule workflows during the time of day when DST changes occur. This part of the PR integrates the [workflow validation and parsing of the `timezone` field](https://code.forgejo.org/forgejo/runner/pulls/1454) supplied by Forgejo Runner. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). 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 for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes (can be removed for Go changes) - 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 - [x] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - https://codeberg.org/forgejo/docs/pulls/1853 - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/11851): <!--number 11851 --><!--line 0 --><!--description c3VwcG9ydCBgdGltZXpvbmVgIGluIHNjaGVkdWxlZCB3b3JrZmxvd3M=-->support `timezone` in scheduled workflows<!--description--> <!--end release-notes-assistant--> Co-authored-by: Renovate Bot <bot@kriese.eu> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11851 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch> Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
512 lines
19 KiB
Go
512 lines
19 KiB
Go
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
"forgejo.org/models/db"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unit"
|
|
"forgejo.org/models/unittest"
|
|
"forgejo.org/modules/optional"
|
|
"forgejo.org/modules/test"
|
|
"forgejo.org/modules/timeutil"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
|
|
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
|
|
"code.forgejo.org/forgejo/runner/v12/act/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"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})
|
|
|
|
spec, err := actions_model.NewActionScheduleSpec("* * * * *", optional.None[string](), time.Now())
|
|
require.NoError(t, err)
|
|
|
|
workflowID := "some.yml"
|
|
schedules := []*actions_model.ActionSchedule{
|
|
{
|
|
Title: "scheduletitle1",
|
|
RepoID: repo.ID,
|
|
OwnerID: repo.OwnerID,
|
|
WorkflowID: workflowID,
|
|
WorkflowDirectory: ".forgejo/workflows",
|
|
TriggerUserID: repo.OwnerID,
|
|
Ref: "branch",
|
|
CommitSHA: "fakeSHA",
|
|
Event: webhook_module.HookEventSchedule,
|
|
EventPayload: "fakepayload",
|
|
Specs: []*actions_model.ActionScheduleSpec{spec},
|
|
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) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2})
|
|
|
|
assertConstant := func(t *testing.T, cron *actions_model.ActionSchedule, run *actions_model.ActionRun) {
|
|
t.Helper()
|
|
assert.Equal(t, cron.Title, run.Title)
|
|
assert.Equal(t, cron.RepoID, run.RepoID)
|
|
assert.Equal(t, cron.OwnerID, run.OwnerID)
|
|
assert.Equal(t, cron.WorkflowID, run.WorkflowID)
|
|
assert.Equal(t, cron.WorkflowDirectory, run.WorkflowDirectory)
|
|
assert.Equal(t, cron.TriggerUserID, run.TriggerUserID)
|
|
assert.Equal(t, cron.Ref, run.Ref)
|
|
assert.Equal(t, cron.CommitSHA, run.CommitSHA)
|
|
assert.Equal(t, cron.Event, run.Event)
|
|
assert.Equal(t, cron.EventPayload, run.EventPayload)
|
|
assert.Equal(t, cron.ID, run.ScheduleID)
|
|
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
|
assert.Equal(t, "branch_some.yml_schedule__auto", run.ConcurrencyGroup)
|
|
assert.Equal(t, actions_model.UnlimitedConcurrency, run.ConcurrencyType)
|
|
}
|
|
|
|
assertMutable := func(t *testing.T, expected, run *actions_model.ActionRun) {
|
|
t.Helper()
|
|
assert.Equal(t, expected.NotifyEmail, run.NotifyEmail)
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
cron actions_model.ActionSchedule
|
|
want []actions_model.ActionRun
|
|
}{
|
|
{
|
|
name: "simple",
|
|
cron: actions_model.ActionSchedule{
|
|
Title: "scheduletitle1",
|
|
RepoID: repo.ID,
|
|
OwnerID: repo.OwnerID,
|
|
WorkflowID: "some.yml",
|
|
WorkflowDirectory: ".forgejo/workflows",
|
|
TriggerUserID: repo.OwnerID,
|
|
Ref: "branch",
|
|
CommitSHA: "fakeSHA",
|
|
Event: webhook_module.HookEventSchedule,
|
|
EventPayload: "fakepayload",
|
|
Content: []byte(
|
|
`
|
|
name: test
|
|
on: push
|
|
jobs:
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: true
|
|
`),
|
|
},
|
|
want: []actions_model.ActionRun{
|
|
{
|
|
Title: "scheduletitle1",
|
|
NotifyEmail: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "enable-email-notifications is true",
|
|
cron: actions_model.ActionSchedule{
|
|
Title: "scheduletitle2",
|
|
RepoID: repo.ID,
|
|
OwnerID: repo.OwnerID,
|
|
WorkflowID: "some.yml",
|
|
WorkflowDirectory: ".github/workflows",
|
|
TriggerUserID: repo.OwnerID,
|
|
Ref: "branch",
|
|
CommitSHA: "fakeSHA",
|
|
Event: webhook_module.HookEventSchedule,
|
|
EventPayload: "fakepayload",
|
|
Content: []byte(
|
|
`
|
|
name: test
|
|
enable-email-notifications: true
|
|
on: push
|
|
jobs:
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: true
|
|
`),
|
|
},
|
|
want: []actions_model.ActionRun{
|
|
{
|
|
Title: "scheduletitle2",
|
|
NotifyEmail: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
require.NoError(t, CreateScheduleTask(t.Context(), &testCase.cron))
|
|
require.Equal(t, len(testCase.want), unittest.GetCount(t, actions_model.ActionRun{RepoID: repo.ID}))
|
|
for _, expected := range testCase.want {
|
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{Title: expected.Title})
|
|
assertConstant(t, &testCase.cron, run)
|
|
assertMutable(t, &expected, run)
|
|
}
|
|
unittest.AssertSuccessfulDelete(t, actions_model.ActionRun{RepoID: repo.ID})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCancelPreviousJobs(t *testing.T) {
|
|
defer unittest.OverrideFixtures("services/actions/TestCancelPreviousJobs")()
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 894})
|
|
assert.Equal(t, actions_model.StatusRunning, run.Status)
|
|
assert.EqualValues(t, 1683636626, run.Updated)
|
|
runJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: 894})
|
|
assert.Equal(t, actions_model.StatusRunning, runJob.Status)
|
|
assert.EqualValues(t, 1683636528, runJob.Started)
|
|
|
|
err := CancelPreviousJobs(t.Context(), 63, "refs/heads/main", "running.yaml", webhook_module.HookEventWorkflowDispatch)
|
|
require.NoError(t, err)
|
|
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 894})
|
|
assert.Equal(t, actions_model.StatusCancelled, run.Status)
|
|
assert.Greater(t, run.Updated, timeutil.TimeStamp(1683636626))
|
|
runJob = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: 894})
|
|
assert.Equal(t, actions_model.StatusCancelled, runJob.Status)
|
|
assert.Greater(t, runJob.Stopped, timeutil.TimeStamp(1683636528))
|
|
}
|
|
|
|
func TestCancelPreviousWithConcurrencyGroup(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
updateRun901 map[string]any
|
|
updateRun901Jobs map[string]any
|
|
expected901Status actions_model.Status
|
|
}{
|
|
// run 900 & 901 in the fixture data have almost the same data and so should both be cancelled by
|
|
// TestCancelPreviousWithConcurrencyGroup -- but each test case will vary something different about 601 to
|
|
// ensure that only run 600 is targeted by the cancellation
|
|
{
|
|
name: "only cancels target repo",
|
|
updateRun901: map[string]any{"repo_id": 2},
|
|
},
|
|
{
|
|
name: "only cancels target concurrency group",
|
|
updateRun901: map[string]any{"concurrency_group": "321cba"},
|
|
},
|
|
{
|
|
name: "only cancels running",
|
|
updateRun901: map[string]any{"status": actions_model.StatusSuccess},
|
|
updateRun901Jobs: map[string]any{"status": actions_model.StatusSuccess},
|
|
expected901Status: actions_model.StatusSuccess,
|
|
},
|
|
{
|
|
name: "cancels running job in failed run",
|
|
updateRun901: map[string]any{"status": actions_model.StatusFailure}, // still has a running job in it (601), but run is marked as failed as-if another job had failed in it
|
|
expected901Status: actions_model.StatusCancelled,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer unittest.OverrideFixtures("services/actions/TestCancelPreviousWithConcurrencyGroup")()
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
e := db.GetEngine(t.Context())
|
|
|
|
expected901Status := actions_model.StatusRunning
|
|
if tc.updateRun901 != nil {
|
|
affected, err := e.Table(&actions_model.ActionRun{}).Where("id = ?", 901).Update(tc.updateRun901)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
}
|
|
if tc.updateRun901Jobs != nil {
|
|
affected, err := e.Table(&actions_model.ActionRunJob{}).Where("run_id = ?", 901).Update(tc.updateRun901Jobs)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
}
|
|
if tc.expected901Status != actions_model.StatusUnknown {
|
|
expected901Status = tc.expected901Status
|
|
}
|
|
|
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 900})
|
|
assert.Equal(t, actions_model.StatusRunning, run.Status)
|
|
assert.EqualValues(t, 1683636626, run.Updated)
|
|
assert.Equal(t, "abc123", run.ConcurrencyGroup)
|
|
runJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: 900})
|
|
assert.Equal(t, actions_model.StatusRunning, runJob.Status)
|
|
assert.EqualValues(t, 1683636528, runJob.Started)
|
|
|
|
// Search for concurrency group should be case-insensitive, which we test here by using a different capitalization
|
|
// than the fixture data
|
|
err := CancelPreviousWithConcurrencyGroup(t.Context(), 63, "ABC123")
|
|
require.NoError(t, err)
|
|
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 900})
|
|
assert.Equal(t, actions_model.StatusCancelled, run.Status)
|
|
assert.Greater(t, run.Updated, timeutil.TimeStamp(1683636626))
|
|
runJob = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: 900})
|
|
assert.Equal(t, actions_model.StatusCancelled, runJob.Status)
|
|
assert.Greater(t, runJob.Stopped, timeutil.TimeStamp(1683636528))
|
|
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 901})
|
|
assert.Equal(t, expected901Status, run.Status)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceActions_DynamicMatrix(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})
|
|
|
|
spec, err := actions_model.NewActionScheduleSpec("* * * * *", optional.None[string](), time.Now())
|
|
require.NoError(t, err)
|
|
|
|
workflowID := "some.yml"
|
|
schedules := []*actions_model.ActionSchedule{
|
|
{
|
|
Title: "scheduletitle1",
|
|
RepoID: repo.ID,
|
|
OwnerID: repo.OwnerID,
|
|
WorkflowID: workflowID,
|
|
WorkflowDirectory: ".forgejo/workflows",
|
|
TriggerUserID: repo.OwnerID,
|
|
Ref: "branch",
|
|
CommitSHA: "fakeSHA",
|
|
Event: webhook_module.HookEventSchedule,
|
|
EventPayload: "fakepayload",
|
|
Specs: []*actions_model.ActionScheduleSpec{spec},
|
|
Content: []byte(
|
|
`
|
|
jobs:
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
dim1: "${{ fromJSON(needs.other-job.outputs.some-output) }}"
|
|
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}))
|
|
|
|
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
|
WorkflowID: workflowID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, runs, 1)
|
|
run := runs[0]
|
|
|
|
jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
job := jobs[0]
|
|
|
|
// With a matrix that contains ${{ needs ... }} references, the only requirement to work is that when the job is
|
|
// first inserted it is tagged w/ incomplete_matrix
|
|
assert.Contains(t, string(job.WorkflowPayload), "incomplete_matrix: true")
|
|
}
|
|
|
|
func TestServiceActions_RunsOnNeeds(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})
|
|
|
|
spec, err := actions_model.NewActionScheduleSpec("* * * * *", optional.None[string](), time.Now())
|
|
require.NoError(t, err)
|
|
|
|
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: []*actions_model.ActionScheduleSpec{spec},
|
|
Content: []byte(
|
|
`
|
|
jobs:
|
|
job2:
|
|
runs-on: "${{ fromJSON(needs.other-job.outputs.some-output) }}"
|
|
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}))
|
|
|
|
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
|
WorkflowID: workflowID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, runs, 1)
|
|
run := runs[0]
|
|
|
|
jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
job := jobs[0]
|
|
|
|
// With a runs-on that contains ${{ needs ... }} references, the only requirement to work is that when the job is
|
|
// first inserted it is tagged w/ incomplete_runs_on
|
|
assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true")
|
|
}
|
|
|
|
func TestServiceActions_ExpandReusableWorkflow(t *testing.T) {
|
|
defer unittest.OverrideFixtures("services/actions/TestServiceActions_startTask")()
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
type callArgs struct {
|
|
repoID int64
|
|
commitSHA string
|
|
path string
|
|
}
|
|
var localReusableCalled []*callArgs
|
|
var cleanupCallCount int
|
|
defer test.MockVariableValue(&lazyRepoExpandLocalReusableWorkflow,
|
|
func(ctx context.Context, repoID int64, commitSHA string) (jobparser.LocalWorkflowFetcher, CleanupFunc) {
|
|
fetcher := func(job *jobparser.Job, path string) ([]byte, error) {
|
|
localReusableCalled = append(localReusableCalled, &callArgs{repoID, commitSHA, path})
|
|
return []byte("{ on: pull_request, jobs: { j1: { runs-on: debian-latest } } }"), nil
|
|
}
|
|
cleanup := func() {
|
|
cleanupCallCount++
|
|
}
|
|
return fetcher, cleanup
|
|
})()
|
|
remoteReusableCalled := []*model.NonLocalReusableWorkflowReference{}
|
|
defer test.MockVariableValue(&expandInstanceReusableWorkflows,
|
|
func(ctx context.Context) jobparser.InstanceWorkflowFetcher {
|
|
return func(job *jobparser.Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) {
|
|
remoteReusableCalled = append(remoteReusableCalled, ref)
|
|
return []byte("{ on: pull_request, jobs: { j1: { runs-on: debian-latest } } }"), nil
|
|
}
|
|
})()
|
|
|
|
// Load fixtures that are corrupted and create one valid scheduled workflow
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
|
|
spec, err := actions_model.NewActionScheduleSpec("* * * * *", optional.None[string](), time.Now())
|
|
require.NoError(t, err)
|
|
|
|
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: []*actions_model.ActionScheduleSpec{spec},
|
|
Content: []byte(
|
|
`
|
|
jobs:
|
|
job2:
|
|
uses: ./.forgejo/workflows/reusable.yml
|
|
job3:
|
|
uses: some-org/some-repo/.forgejo/workflows/reusable-path.yml@main
|
|
`),
|
|
},
|
|
}
|
|
|
|
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}))
|
|
|
|
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
|
WorkflowID: workflowID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, runs, 1)
|
|
run := runs[0]
|
|
assert.EqualValues(t, 0, run.PreExecutionErrorCode, "pre execution error details: %#v", run.PreExecutionErrorDetails)
|
|
|
|
require.Len(t, localReusableCalled, 1, "localReusableCalled")
|
|
require.Len(t, remoteReusableCalled, 1, "remoteReusableCalled")
|
|
|
|
assert.Equal(t, &callArgs{4, "fakeSHA", "./.forgejo/workflows/reusable.yml"}, localReusableCalled[0])
|
|
assert.Equal(t, 2, cleanupCallCount, "cleanupCallCount")
|
|
assert.Equal(t, &model.NonLocalReusableWorkflowReference{
|
|
Org: "some-org",
|
|
Repo: "some-repo",
|
|
Filename: "reusable-path.yml",
|
|
Ref: "main",
|
|
GitPlatform: "forgejo",
|
|
}, remoteReusableCalled[0])
|
|
}
|