mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10: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>
1257 lines
41 KiB
Go
1257 lines
41 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
auth_model "forgejo.org/models/auth"
|
|
"forgejo.org/models/db"
|
|
git_model "forgejo.org/models/git"
|
|
issues_model "forgejo.org/models/issues"
|
|
repo_model "forgejo.org/models/repo"
|
|
unit_model "forgejo.org/models/unit"
|
|
"forgejo.org/models/unittest"
|
|
user_model "forgejo.org/models/user"
|
|
actions_module "forgejo.org/modules/actions"
|
|
"forgejo.org/modules/git"
|
|
"forgejo.org/modules/gitrepo"
|
|
"forgejo.org/modules/optional"
|
|
"forgejo.org/modules/setting"
|
|
api "forgejo.org/modules/structs"
|
|
"forgejo.org/modules/test"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
actions_service "forgejo.org/services/actions"
|
|
issue_service "forgejo.org/services/issue"
|
|
pull_service "forgejo.org/services/pull"
|
|
release_service "forgejo.org/services/release"
|
|
repo_service "forgejo.org/services/repository"
|
|
files_service "forgejo.org/services/repository/files"
|
|
"forgejo.org/tests"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestActionsPullRequestCommitStatus(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo
|
|
session := loginUser(t, "user2")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
|
|
// prepare the repository
|
|
files := make([]*files_service.ChangeRepoFile, 0, 10)
|
|
for _, onType := range []string{
|
|
"opened",
|
|
"synchronize",
|
|
"labeled",
|
|
"unlabeled",
|
|
"assigned",
|
|
"unassigned",
|
|
"milestoned",
|
|
"demilestoned",
|
|
"closed",
|
|
"reopened",
|
|
} {
|
|
files = append(files, &files_service.ChangeRepoFile{
|
|
Operation: "create",
|
|
TreePath: fmt.Sprintf(".forgejo/workflows/%s.yml", onType),
|
|
ContentReader: strings.NewReader(fmt.Sprintf(`name: %[1]s
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- %[1]s
|
|
jobs:
|
|
%[1]s:
|
|
runs-on: docker
|
|
steps:
|
|
- run: true
|
|
`, onType)),
|
|
})
|
|
}
|
|
baseRepo, _, f := tests.CreateDeclarativeRepo(t, user2, "repo-pull-request",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil, files)
|
|
defer f()
|
|
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
baseGitRepo.Close()
|
|
}()
|
|
|
|
// prepare the repository labels
|
|
labelStr := "/api/v1/repos/user2/repo-pull-request/labels"
|
|
labelsCount := 2
|
|
labels := make([]*api.Label, labelsCount)
|
|
for i := range labelsCount {
|
|
color := "abcdef"
|
|
req := NewRequestWithJSON(t, "POST", labelStr, &api.CreateLabelOption{
|
|
Name: fmt.Sprintf("label%d", i),
|
|
Color: color,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
labels[i] = new(api.Label)
|
|
DecodeJSON(t, resp, &labels[i])
|
|
assert.Equal(t, color, labels[i].Color)
|
|
}
|
|
|
|
// create the pull request
|
|
testEditFileToNewBranch(t, session, "user2", "repo-pull-request", "main", "wip-something", "README.md", "Hello, world 1")
|
|
testPullCreate(t, session, "user2", "repo-pull-request", true, "main", "wip-something", "Commit status PR")
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: baseRepo.ID})
|
|
require.NoError(t, pr.LoadIssue(db.DefaultContext))
|
|
|
|
// prepare the assignees
|
|
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%s", "user2", "repo-pull-request", fmt.Sprintf("%d", pr.Issue.Index))
|
|
|
|
// prepare the labels
|
|
labelURL := fmt.Sprintf("%s/labels", issueURL)
|
|
|
|
// prepare the milestone
|
|
milestoneStr := "/api/v1/repos/user2/repo-pull-request/milestones"
|
|
req := NewRequestWithJSON(t, "POST", milestoneStr, &api.CreateMilestoneOption{
|
|
Title: "mymilestone",
|
|
State: "open",
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
milestone := new(api.Milestone)
|
|
DecodeJSON(t, resp, &milestone)
|
|
|
|
// check that one of the status associated with the commit sha matches both
|
|
// context & state
|
|
checkCommitStatus := func(sha, context string, state api.CommitStatusState) bool {
|
|
commitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, pr.BaseRepoID, sha, db.ListOptionsAll)
|
|
require.NoError(t, err)
|
|
for _, commitStatus := range commitStatuses {
|
|
if state == commitStatus.State && context == commitStatus.Context {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
assertActionRun := func(t *testing.T, sha, onType string, action api.HookIssueAction, actionRun *actions_model.ActionRun) {
|
|
assert.Equal(t, fmt.Sprintf("%s.yml", onType), actionRun.WorkflowID)
|
|
assert.Equal(t, sha, actionRun.CommitSHA)
|
|
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
|
event, err := actionRun.GetPullRequestEventPayload()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, action, event.Action)
|
|
}
|
|
|
|
type assertType func(t *testing.T, sha, onType string, action api.HookIssueAction, actionRuns []*actions_model.ActionRun)
|
|
assertActionRuns := func(t *testing.T, sha, onType string, action api.HookIssueAction, actionRuns []*actions_model.ActionRun) {
|
|
require.Len(t, actionRuns, 1)
|
|
assertActionRun(t, sha, onType, action, actionRuns[0])
|
|
}
|
|
|
|
for _, testCase := range []struct {
|
|
onType string
|
|
jobID string
|
|
doSomething func()
|
|
actionRunCount int
|
|
action api.HookIssueAction
|
|
assert assertType
|
|
}{
|
|
{
|
|
onType: "opened",
|
|
doSomething: func() {},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueOpened,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "synchronize",
|
|
doSomething: func() {
|
|
testEditFile(t, session, "user2", "repo-pull-request", "wip-something", "README.md", "Hello, world 2")
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueSynchronized,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "labeled",
|
|
doSomething: func() {
|
|
req := NewRequestWithJSON(t, "POST", labelURL, &api.IssueLabelsOption{
|
|
Labels: []any{labels[0].ID, labels[1].ID},
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
},
|
|
actionRunCount: 2,
|
|
action: api.HookIssueLabelUpdated,
|
|
assert: func(t *testing.T, sha, onType string, action api.HookIssueAction, actionRuns []*actions_model.ActionRun) {
|
|
assertActionRun(t, sha, onType, api.HookIssueLabelUpdated, actionRuns[0])
|
|
assertActionRun(t, sha, onType, api.HookIssueLabelUpdated, actionRuns[1])
|
|
},
|
|
},
|
|
{
|
|
onType: "unlabeled",
|
|
doSomething: func() {
|
|
req := NewRequestWithJSON(t, "PUT", labelURL, &api.IssueLabelsOption{
|
|
Labels: []any{labels[0].ID},
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
},
|
|
actionRunCount: 3,
|
|
action: api.HookIssueLabelCleared,
|
|
assert: func(t *testing.T, sha, onType string, action api.HookIssueAction, actionRuns []*actions_model.ActionRun) {
|
|
foundPayloadWithLabels := false
|
|
knownLabels := []string{"label0", "label1"}
|
|
for _, actionRun := range actionRuns {
|
|
assert.Equal(t, sha, actionRun.CommitSHA)
|
|
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
|
event, err := actionRun.GetPullRequestEventPayload()
|
|
require.NoError(t, err)
|
|
switch event.Action {
|
|
case api.HookIssueLabelUpdated:
|
|
assert.Equal(t, "labeled.yml", actionRun.WorkflowID)
|
|
assert.Equal(t, "label0", event.Label.Name)
|
|
require.Len(t, event.PullRequest.Labels, 1)
|
|
assert.Contains(t, "label0", event.PullRequest.Labels[0].Name)
|
|
case api.HookIssueLabelCleared:
|
|
assert.Equal(t, "unlabeled.yml", actionRun.WorkflowID)
|
|
assert.Contains(t, knownLabels, event.Label.Name)
|
|
if len(event.PullRequest.Labels) > 0 {
|
|
foundPayloadWithLabels = true
|
|
assert.Contains(t, knownLabels, event.PullRequest.Labels[0].Name)
|
|
}
|
|
default:
|
|
require.Fail(t, fmt.Sprintf("unexpected action '%s'", event.Action))
|
|
}
|
|
}
|
|
assert.True(t, foundPayloadWithLabels, "expected at least one clear label payload with non empty labels")
|
|
},
|
|
},
|
|
{
|
|
onType: "assigned",
|
|
doSomething: func() {
|
|
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
|
|
Assignees: []string{"user2"},
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueAssigned,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "unassigned",
|
|
doSomething: func() {
|
|
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
|
|
Assignees: []string{},
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueUnassigned,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "milestoned",
|
|
doSomething: func() {
|
|
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
|
|
Milestone: &milestone.ID,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueMilestoned,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "demilestoned",
|
|
doSomething: func() {
|
|
var zero int64
|
|
req := NewRequestWithJSON(t, "PATCH", issueURL, &api.EditIssueOption{
|
|
Milestone: &zero,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueDemilestoned,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "closed",
|
|
doSomething: func() {
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
require.NoError(t, err)
|
|
err = issue_service.ChangeStatus(db.DefaultContext, pr.Issue, user2, sha, true)
|
|
require.NoError(t, err)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueClosed,
|
|
assert: assertActionRuns,
|
|
},
|
|
{
|
|
onType: "reopened",
|
|
doSomething: func() {
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
require.NoError(t, err)
|
|
err = issue_service.ChangeStatus(db.DefaultContext, pr.Issue, user2, sha, false)
|
|
require.NoError(t, err)
|
|
},
|
|
actionRunCount: 1,
|
|
action: api.HookIssueReOpened,
|
|
assert: assertActionRuns,
|
|
},
|
|
} {
|
|
t.Run(testCase.onType, func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer func() {
|
|
// cleanup leftovers, start from scratch
|
|
unittest.AssertSuccessfulDelete(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
|
unittest.AssertSuccessfulDelete(t, &actions_model.ActionRunJob{RepoID: baseRepo.ID})
|
|
}()
|
|
|
|
// trigger the onType event
|
|
testCase.doSomething()
|
|
count := testCase.actionRunCount
|
|
context := fmt.Sprintf("%[1]s / %[1]s (pull_request)", testCase.onType)
|
|
|
|
var actionRuns []*actions_model.ActionRun
|
|
|
|
// wait for ActionRun(s) to be created
|
|
require.Eventually(t, func() bool {
|
|
actionRuns = make([]*actions_model.ActionRun, 0)
|
|
require.NoError(t, db.GetEngine(db.DefaultContext).Where("repo_id=?", baseRepo.ID).Find(&actionRuns))
|
|
return len(actionRuns) == count
|
|
}, 30*time.Second, 1*time.Second)
|
|
|
|
// verify the expected ActionRuns were created
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
require.NoError(t, err)
|
|
// verify the commit status changes to CommitStatusSuccess when the job changes to StatusSuccess
|
|
require.Eventually(t, func() bool {
|
|
return checkCommitStatus(sha, context, api.CommitStatusPending)
|
|
}, 30*time.Second, 1*time.Second)
|
|
for _, actionRun := range actionRuns {
|
|
// verify the expected ActionRunJob was created and is StatusWaiting
|
|
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, CommitSHA: sha})
|
|
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
|
|
|
// change the state of the job to success
|
|
job.Status = actions_model.StatusSuccess
|
|
actions_service.CreateCommitStatus(db.DefaultContext, job)
|
|
}
|
|
// verify the commit status changed to CommitStatusSuccess because the job(s) changed to StatusSuccess
|
|
require.Eventually(t, func() bool {
|
|
return checkCommitStatus(sha, context, api.CommitStatusSuccess)
|
|
}, 30*time.Second, 1*time.Second)
|
|
|
|
testCase.assert(t, sha, testCase.onType, testCase.action, actionRuns)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActionsPullRequestWithInvalidWorkflow(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo
|
|
session := loginUser(t, "user2")
|
|
|
|
// prepare the repository
|
|
baseRepo, _, f := tests.CreateDeclarativeRepo(t, user2, "repo-pull-request",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil, []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".forgejo/workflows/broken.yml",
|
|
ContentReader: strings.NewReader(`name: broken
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- opened
|
|
jobs:
|
|
test:
|
|
runs-on: docker
|
|
- run: true
|
|
`),
|
|
},
|
|
})
|
|
defer f()
|
|
baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
baseGitRepo.Close()
|
|
}()
|
|
|
|
// create the pull request
|
|
testEditFileToNewBranch(t, session, "user2", "repo-pull-request", "main", "wip-something", "README.md", "Hello, world 1")
|
|
testPullCreate(t, session, "user2", "repo-pull-request", true, "main", "wip-something", "Commit status PR")
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: baseRepo.ID})
|
|
require.NoError(t, pr.LoadIssue(t.Context()))
|
|
|
|
// check that one of the status associated with the commit sha matches both
|
|
// context & state
|
|
checkCommitStatus := func(sha, context string, state api.CommitStatusState) bool {
|
|
commitStatuses, _, err := git_model.GetLatestCommitStatus(t.Context(), pr.BaseRepoID, sha, db.ListOptionsAll)
|
|
require.NoError(t, err)
|
|
for _, commitStatus := range commitStatuses {
|
|
if state == commitStatus.State && context == commitStatus.Context {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var actionRuns []*actions_model.ActionRun
|
|
|
|
// wait for ActionRun(s) to be created
|
|
require.Eventually(t, func() bool {
|
|
actionRuns = make([]*actions_model.ActionRun, 0)
|
|
require.NoError(t, db.GetEngine(t.Context()).Where("event=? AND status=? AND repo_id=?", "pull_request", actions_model.StatusFailure, baseRepo.ID).Find(&actionRuns))
|
|
return len(actionRuns) == 1
|
|
}, 30*time.Second, 1*time.Second)
|
|
|
|
// verify the expected ActionRuns were created
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
require.NoError(t, err)
|
|
|
|
// verify the commit status changes to CommitStatusFailure
|
|
require.Eventually(t, func() bool {
|
|
return checkCommitStatus(sha, "broken.yml / Update README.md (pull_request)", api.CommitStatusFailure)
|
|
}, 30*time.Second, 1*time.Second)
|
|
|
|
require.Len(t, actionRuns, 1)
|
|
actionRun := actionRuns[0]
|
|
// verify the expected ActionRunJob was created and is StatusFailure
|
|
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, CommitSHA: sha})
|
|
assert.Equal(t, actions_model.StatusFailure, job.Status)
|
|
assert.Equal(t, "broken.yml", actionRun.WorkflowID)
|
|
assert.Equal(t, sha, actionRun.CommitSHA)
|
|
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
|
event, err := actionRun.GetPullRequestEventPayload()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, api.HookIssueOpened, event.Action)
|
|
})
|
|
}
|
|
|
|
func TestActionsPullRequestTargetEvent(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo
|
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the forked repo
|
|
|
|
// create the base repo
|
|
baseRepo, _, f := tests.CreateDeclarativeRepo(t, user2, "repo-pull-request-target",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil, nil,
|
|
)
|
|
defer f()
|
|
|
|
// create the forked repo
|
|
forkedRepo, err := repo_service.ForkRepositoryAndUpdates(git.DefaultContext, user2, org3, repo_service.ForkRepoOptions{
|
|
BaseRepo: baseRepo,
|
|
Name: "forked-repo-pull-request-target",
|
|
Description: "test pull-request-target event",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, forkedRepo)
|
|
|
|
// add workflow file to the base repo
|
|
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user2, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".gitea/workflows/pr.yml",
|
|
ContentReader: strings.NewReader("name: test\non:\n pull_request_target:\n paths:\n - 'file_*.txt'\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
|
|
},
|
|
},
|
|
Message: "add workflow",
|
|
OldBranch: "main",
|
|
NewBranch: "main",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addWorkflowToBaseResp)
|
|
|
|
// add a new file to the forked repo
|
|
addFileToForkedResp, err := files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, org3, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "file_1.txt",
|
|
ContentReader: strings.NewReader("file1"),
|
|
},
|
|
},
|
|
Message: "add file1",
|
|
OldBranch: "main",
|
|
NewBranch: "fork-branch-1",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: org3.Name,
|
|
Email: org3.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: org3.Name,
|
|
Email: org3.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addFileToForkedResp)
|
|
|
|
// create Pull
|
|
pullIssue := &issues_model.Issue{
|
|
RepoID: baseRepo.ID,
|
|
Title: "Test pull-request-target-event",
|
|
PosterID: org3.ID,
|
|
Poster: org3,
|
|
IsPull: true,
|
|
}
|
|
pullRequest := &issues_model.PullRequest{
|
|
HeadRepoID: forkedRepo.ID,
|
|
BaseRepoID: baseRepo.ID,
|
|
HeadBranch: "fork-branch-1",
|
|
BaseBranch: "main",
|
|
HeadRepo: forkedRepo,
|
|
BaseRepo: baseRepo,
|
|
Type: issues_model.PullRequestGitea,
|
|
}
|
|
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
|
require.NoError(t, err)
|
|
// if a PR "synchronized" event races the "opened" event by having the same SHA, it must be skipped. See https://codeberg.org/forgejo/forgejo/issues/2009.
|
|
assert.True(t, actions_service.SkipPullRequestEvent(git.DefaultContext, webhook_module.HookEventPullRequestSync, baseRepo.ID, addFileToForkedResp.Commit.SHA))
|
|
|
|
// load and compare ActionRun
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
|
|
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
|
assert.Equal(t, addFileToForkedResp.Commit.SHA, actionRun.CommitSHA)
|
|
assert.Equal(t, actions_module.GithubEventPullRequestTarget, actionRun.TriggerEvent)
|
|
|
|
// add another file whose name cannot match the specified path
|
|
addFileToForkedResp, err = files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, org3, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "foo.txt",
|
|
ContentReader: strings.NewReader("foo"),
|
|
},
|
|
},
|
|
Message: "add foo.txt",
|
|
OldBranch: "main",
|
|
NewBranch: "fork-branch-2",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: org3.Name,
|
|
Email: org3.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: org3.Name,
|
|
Email: org3.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addFileToForkedResp)
|
|
|
|
// create Pull
|
|
pullIssue = &issues_model.Issue{
|
|
RepoID: baseRepo.ID,
|
|
Title: "A mismatched path cannot trigger pull-request-target-event",
|
|
PosterID: org3.ID,
|
|
Poster: org3,
|
|
IsPull: true,
|
|
}
|
|
pullRequest = &issues_model.PullRequest{
|
|
HeadRepoID: forkedRepo.ID,
|
|
BaseRepoID: baseRepo.ID,
|
|
HeadBranch: "fork-branch-2",
|
|
BaseBranch: "main",
|
|
HeadRepo: forkedRepo,
|
|
BaseRepo: baseRepo,
|
|
Type: issues_model.PullRequestGitea,
|
|
}
|
|
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
// the new pull request cannot trigger actions, so there is still only 1 record
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
|
|
})
|
|
}
|
|
|
|
func TestActionsSkipCI(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
session := loginUser(t, "user2")
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, _, f := tests.CreateDeclarativeRepo(t, user2, "skip-ci",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".gitea/workflows/pr.yml",
|
|
ContentReader: strings.NewReader("name: test\non:\n push:\n branches: [main]\n pull_request:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
// a run has been created
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
|
|
// add a file with a configured skip-ci string in commit message
|
|
addFileResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "bar.txt",
|
|
ContentReader: strings.NewReader("bar"),
|
|
},
|
|
},
|
|
Message: fmt.Sprintf("%s add bar", setting.Actions.SkipWorkflowStrings[0]),
|
|
OldBranch: "main",
|
|
NewBranch: "main",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addFileResp)
|
|
|
|
// the commit message contains a configured skip-ci string, so there is still only 1 record
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
|
|
// add file to new branch
|
|
addFileToBranchResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: "test-skip-ci",
|
|
ContentReader: strings.NewReader("test-skip-ci"),
|
|
},
|
|
},
|
|
Message: "add test file",
|
|
OldBranch: "main",
|
|
NewBranch: "test-skip-ci",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addFileToBranchResp)
|
|
|
|
resp := testPullCreate(t, session, "user2", "skip-ci", true, "main", "test-skip-ci", "[skip ci] test-skip-ci")
|
|
|
|
// check the redirected URL
|
|
url := test.RedirectURL(resp)
|
|
assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url)
|
|
|
|
// the pr title contains a configured skip-ci string, so there is still only 1 record
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
})
|
|
}
|
|
|
|
func TestActionsCreateDeleteRefEvent(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
|
|
Name: "create-delete-ref-event",
|
|
Description: "test create delete ref ci event",
|
|
AutoInit: true,
|
|
Gitignores: "Go",
|
|
License: "MIT",
|
|
Readme: "Default",
|
|
DefaultBranch: "main",
|
|
IsPrivate: false,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, repo)
|
|
|
|
// enable actions
|
|
err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{
|
|
RepoID: repo.ID,
|
|
Type: unit_model.TypeActions,
|
|
}}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// add workflow file to the repo
|
|
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".gitea/workflows/createdelete.yml",
|
|
ContentReader: strings.NewReader("name: test\non:\n [create,delete]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
|
|
},
|
|
},
|
|
Message: "add workflow",
|
|
OldBranch: "main",
|
|
NewBranch: "main",
|
|
Author: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Committer: &files_service.IdentityOptions{
|
|
Name: user2.Name,
|
|
Email: user2.Email,
|
|
},
|
|
Dates: &files_service.CommitDateOptions{
|
|
Author: time.Now(),
|
|
Committer: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, addWorkflowToBaseResp)
|
|
|
|
// Get the commit ID of the default branch
|
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
|
|
require.NoError(t, err)
|
|
|
|
// create a branch
|
|
err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-create-branch")
|
|
require.NoError(t, err)
|
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
|
Title: "add workflow",
|
|
RepoID: repo.ID,
|
|
Event: "create",
|
|
Ref: "refs/heads/test-create-branch",
|
|
WorkflowID: "createdelete.yml",
|
|
CommitSHA: branch.CommitID,
|
|
})
|
|
assert.NotNil(t, run)
|
|
|
|
// create a tag
|
|
err = release_service.CreateNewTag(db.DefaultContext, user2, repo, branch.CommitID, "test-create-tag", "test create tag event")
|
|
require.NoError(t, err)
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
|
Title: "add workflow",
|
|
RepoID: repo.ID,
|
|
Event: "create",
|
|
Ref: "refs/tags/test-create-tag",
|
|
WorkflowID: "createdelete.yml",
|
|
CommitSHA: branch.CommitID,
|
|
})
|
|
assert.NotNil(t, run)
|
|
|
|
// delete the branch
|
|
err = repo_service.DeleteBranch(db.DefaultContext, user2, repo, gitRepo, "test-create-branch")
|
|
require.NoError(t, err)
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
|
Title: "add workflow",
|
|
RepoID: repo.ID,
|
|
Event: "delete",
|
|
Ref: "refs/heads/main",
|
|
WorkflowID: "createdelete.yml",
|
|
CommitSHA: branch.CommitID,
|
|
})
|
|
assert.NotNil(t, run)
|
|
|
|
// delete the tag
|
|
tag, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "test-create-tag")
|
|
require.NoError(t, err)
|
|
err = release_service.DeleteReleaseByID(db.DefaultContext, repo, tag, user2, true)
|
|
require.NoError(t, err)
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
|
Title: "add workflow",
|
|
RepoID: repo.ID,
|
|
Event: "delete",
|
|
Ref: "refs/heads/main",
|
|
WorkflowID: "createdelete.yml",
|
|
CommitSHA: branch.CommitID,
|
|
})
|
|
assert.NotNil(t, run)
|
|
})
|
|
}
|
|
|
|
func TestActionsWorkflowDispatch(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
workflowID string
|
|
workflowDirectory string
|
|
}{
|
|
{
|
|
name: "GitHub",
|
|
workflowID: "dispatch.yml",
|
|
workflowDirectory: ".github/workflows",
|
|
},
|
|
{
|
|
name: "Gitea",
|
|
workflowID: "test.yml",
|
|
workflowDirectory: ".gitea/workflows",
|
|
},
|
|
{
|
|
name: "Forgejo",
|
|
workflowID: "build.yml",
|
|
workflowDirectory: ".forgejo/workflows",
|
|
},
|
|
}
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: fmt.Sprintf("%s/%s", testCase.workflowDirectory, testCase.workflowID),
|
|
ContentReader: strings.NewReader(
|
|
"name: test\n" +
|
|
"on: [workflow_dispatch]\n" +
|
|
"jobs:\n" +
|
|
" test:\n" +
|
|
" runs-on: ubuntu-latest\n" +
|
|
" steps:\n" +
|
|
" - run: echo helloworld\n",
|
|
),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", testCase.workflowID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "refs/heads/main", workflow.Ref)
|
|
assert.Equal(t, sha, workflow.Commit.ID.String())
|
|
|
|
inputGetter := func(key string) string {
|
|
return ""
|
|
}
|
|
|
|
var r *actions_model.ActionRun
|
|
var j []string
|
|
r, j, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
|
|
assert.Equal(t, "test", r.Title)
|
|
assert.Equal(t, testCase.workflowID, r.WorkflowID)
|
|
assert.Equal(t, testCase.workflowDirectory, r.WorkflowDirectory)
|
|
assert.Equal(t, sha, r.CommitSHA)
|
|
assert.Equal(t, actions_module.GithubEventWorkflowDispatch, r.TriggerEvent)
|
|
assert.Len(t, j, 1)
|
|
assert.Equal(t, "test", j[0])
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActionsWorkflowDispatchRejectsInputsThatExceedLimit(t *testing.T) {
|
|
workflow := `
|
|
name: test
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
boolean:
|
|
description: 'Boolean'
|
|
type: boolean
|
|
number:
|
|
description: 'Number'
|
|
default: '100'
|
|
type: number
|
|
string:
|
|
description: 'String'
|
|
type: string
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo "OK"
|
|
`
|
|
|
|
defer test.MockVariableValue(&setting.Actions.LimitDispatchInputs, 2)()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
inputs map[string]string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "below-limit",
|
|
inputs: map[string]string{"boolean": "true", "number": "10"},
|
|
},
|
|
{
|
|
name: "beyond-limit",
|
|
inputs: map[string]string{"boolean": "true", "number": "10", "string": "my input"},
|
|
expectedError: "too many inputs",
|
|
},
|
|
}
|
|
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".forgejo/workflows/dispatch.yaml",
|
|
ContentReader: strings.NewReader(workflow),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yaml")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "refs/heads/main", workflow.Ref)
|
|
assert.Equal(t, sha, workflow.Commit.ID.String())
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
inputGetter := func(key string) string {
|
|
return testCase.inputs[key]
|
|
}
|
|
|
|
_, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
if testCase.expectedError == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
assert.EqualError(t, err, testCase.expectedError)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActionsWorkflowDispatchDynamicMatrix(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".gitea/workflows/dispatch.yml",
|
|
ContentReader: strings.NewReader(
|
|
"name: test\n" +
|
|
"on: [workflow_dispatch]\n" +
|
|
"jobs:\n" +
|
|
" test:\n" +
|
|
" runs-on: ubuntu-latest\n" +
|
|
" strategy:\n" +
|
|
" matrix: \n" +
|
|
" dim1: \"${{ fromJSON(needs.other-job.outputs.some-output) }}\"\n" +
|
|
" steps:\n" +
|
|
" - run: echo helloworld\n",
|
|
),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "refs/heads/main", workflow.Ref)
|
|
assert.Equal(t, sha, workflow.Commit.ID.String())
|
|
|
|
inputGetter := func(key string) string {
|
|
return ""
|
|
}
|
|
|
|
run, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
require.NoError(t, err)
|
|
|
|
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID})
|
|
assert.Contains(t, string(job.WorkflowPayload), "incomplete_matrix: true")
|
|
})
|
|
}
|
|
|
|
func TestActionsWorkflowDispatchReusableWorkflow(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".forgejo/workflows/dispatch.yml",
|
|
ContentReader: strings.NewReader(
|
|
"name: test\n" +
|
|
"on: [workflow_dispatch]\n" +
|
|
"jobs:\n" +
|
|
" test:\n" +
|
|
" uses: ./.forgejo/workflows/reusable.yml\n",
|
|
),
|
|
},
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".forgejo/workflows/reusable.yml",
|
|
ContentReader: strings.NewReader(
|
|
"name: test\n" +
|
|
"on: [workflow_call]\n" +
|
|
"jobs:\n" +
|
|
" inner:\n" +
|
|
" runs-on: ubuntu-latest\n" +
|
|
" steps:\n" +
|
|
" - run: echo helloworld\n",
|
|
),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "refs/heads/main", workflow.Ref)
|
|
assert.Equal(t, sha, workflow.Commit.ID.String())
|
|
|
|
inputGetter := func(key string) string {
|
|
return ""
|
|
}
|
|
|
|
run, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
require.NoError(t, err)
|
|
|
|
var runJobs []*actions_model.ActionRunJob
|
|
db.GetEngine(t.Context()).Where("run_id=?", run.ID).Find(&runJobs)
|
|
assert.Len(t, runJobs, 2)
|
|
|
|
var parentJob *actions_model.ActionRunJob
|
|
var childJob *actions_model.ActionRunJob
|
|
for _, j := range runJobs {
|
|
switch j.JobID {
|
|
case "test":
|
|
parentJob = j
|
|
case "test.inner":
|
|
childJob = j
|
|
}
|
|
}
|
|
assert.NotNil(t, parentJob, "parentJob")
|
|
assert.NotNil(t, childJob, "childJob")
|
|
})
|
|
}
|
|
|
|
func TestActionsWorkflowDispatchConcurrencyGroup(t *testing.T) {
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: ".gitea/workflows/dispatch.yml",
|
|
ContentReader: strings.NewReader(
|
|
"name: test\n" +
|
|
"on: [workflow_dispatch]\n" +
|
|
"jobs:\n" +
|
|
" test:\n" +
|
|
" runs-on: ubuntu-latest\n" +
|
|
" steps:\n" +
|
|
" - run: echo helloworld\n" +
|
|
"concurrency:\n" +
|
|
" group: workflow-magic-group\n" +
|
|
" cancel-in-progress: true\n",
|
|
),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
|
require.NoError(t, err)
|
|
defer gitRepo.Close()
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "refs/heads/main", workflow.Ref)
|
|
assert.Equal(t, sha, workflow.Commit.ID.String())
|
|
|
|
inputGetter := func(key string) string {
|
|
return ""
|
|
}
|
|
|
|
firstRun, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
assert.Equal(t, "workflow-magic-group", firstRun.ConcurrencyGroup)
|
|
assert.Equal(t, actions_model.CancelInProgress, firstRun.ConcurrencyType)
|
|
|
|
// Dispatch again and verify previous run was cancelled:
|
|
secondRun, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
|
assert.Equal(t, "workflow-magic-group", secondRun.ConcurrencyGroup)
|
|
assert.Equal(t, actions_model.CancelInProgress, secondRun.ConcurrencyType)
|
|
firstRunReload := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: firstRun.ID})
|
|
assert.Equal(t, actions_model.StatusCancelled, firstRunReload.Status)
|
|
})
|
|
}
|
|
|
|
func TestActionsScheduledWorkflow(t *testing.T) {
|
|
type expectedSpec struct {
|
|
cron string
|
|
timeZone optional.Option[string]
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
workflowID string
|
|
workflowDirectory string
|
|
workflowContent string
|
|
expectedWorkflowTitle string
|
|
expectedCronSpecs []expectedSpec
|
|
}{
|
|
{
|
|
name: "GitHub",
|
|
workflowID: "scheduled.yml",
|
|
workflowDirectory: ".github/workflows",
|
|
workflowContent: `
|
|
on:
|
|
schedule:
|
|
- cron: "30 5,17 * * *"
|
|
jobs:
|
|
test:
|
|
steps:
|
|
- run: echo OK
|
|
`,
|
|
expectedWorkflowTitle: ".github/workflows/scheduled.yml",
|
|
expectedCronSpecs: []expectedSpec{{cron: "30 5,17 * * *", timeZone: optional.None[string]()}},
|
|
},
|
|
{
|
|
name: "Gitea",
|
|
workflowID: "test.yml",
|
|
workflowDirectory: ".gitea/workflows",
|
|
workflowContent: `
|
|
name: My scheduled workflow
|
|
on:
|
|
schedule:
|
|
- cron: "* * * * *"
|
|
jobs:
|
|
test:
|
|
steps:
|
|
- run: echo OK
|
|
`,
|
|
expectedWorkflowTitle: "My scheduled workflow",
|
|
expectedCronSpecs: []expectedSpec{{cron: "* * * * *", timeZone: optional.None[string]()}},
|
|
},
|
|
{
|
|
name: "Forgejo with time zone",
|
|
workflowID: "tz.yml",
|
|
workflowDirectory: ".forgejo/workflows",
|
|
workflowContent: `
|
|
on:
|
|
schedule:
|
|
- cron: "44 10 * * *"
|
|
- cron: "25 19 * * *"
|
|
timezone: Europe/Madrid
|
|
jobs:
|
|
test:
|
|
steps:
|
|
- run: echo OK
|
|
`,
|
|
expectedWorkflowTitle: ".forgejo/workflows/tz.yml",
|
|
expectedCronSpecs: []expectedSpec{
|
|
{cron: "44 10 * * *", timeZone: optional.None[string]()},
|
|
{cron: "25 19 * * *", timeZone: optional.Some("Europe/Madrid")},
|
|
},
|
|
},
|
|
}
|
|
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// create the repo
|
|
repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
|
[]unit_model.Type{unit_model.TypeActions}, nil,
|
|
[]*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "create",
|
|
TreePath: fmt.Sprintf("%s/%s", testCase.workflowDirectory, testCase.workflowID),
|
|
ContentReader: strings.NewReader(testCase.workflowContent),
|
|
},
|
|
},
|
|
)
|
|
defer f()
|
|
|
|
schedules, err := db.Find[actions_model.ActionSchedule](t.Context(), actions_model.FindScheduleOptions{RepoID: repo.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, schedules, 1)
|
|
|
|
assert.Equal(t, testCase.expectedWorkflowTitle, schedules[0].Title)
|
|
assert.Equal(t, repo.ID, schedules[0].RepoID)
|
|
assert.Equal(t, repo.OwnerID, schedules[0].OwnerID)
|
|
assert.Equal(t, testCase.workflowID, schedules[0].WorkflowID)
|
|
assert.Equal(t, testCase.workflowDirectory, schedules[0].WorkflowDirectory)
|
|
assert.Equal(t, int64(-2), schedules[0].TriggerUserID)
|
|
assert.Equal(t, sha, schedules[0].CommitSHA)
|
|
assert.Equal(t, webhook_module.HookEventPush, schedules[0].Event)
|
|
assert.Equal(t, []byte(testCase.workflowContent), schedules[0].Content)
|
|
|
|
specs, total, err := actions_model.FindSpecs(t.Context(), actions_model.FindSpecOptions{RepoID: repo.ID})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(len(testCase.expectedCronSpecs)), total)
|
|
|
|
// The query to return cron specs orders by `id DESC`.
|
|
slices.Reverse(testCase.expectedCronSpecs)
|
|
|
|
for i, expected := range testCase.expectedCronSpecs {
|
|
assert.Equal(t, schedules[0].ID, specs[i].ScheduleID)
|
|
assert.Equal(t, expected.cron, specs[i].Spec)
|
|
assert.Equal(t, expected.timeZone, specs[i].TimeZone)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|