
Currently references a pre-release version of `code.forgejo.org/forgejo/runner/v11`, pending release of https://code.forgejo.org/forgejo/runner/pulls/1026. Fixes #5914. This PR is quite large, but it can be reviewed commit-by-commit in relatively small, logical chunks. Adds support for workflows with a `concurrency` block, and submembers `group` and `cancel-in-progress`. For example: ``` on: workflow_dispatch: jobs: rust-checks: runs-on: debian-latest steps: - run: sleep 300 concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false ``` The concurrency block effectively ends up with four supported behaviors that users will want to choose from: - Backwards compatibility / default -- if omitted completely, the existing Forgejo behavior will be implemented. That behavior is that push and pull request synchronize events will cancel all previous runs on the same repository, branch, and workflow. - Unlimited concurrency -- if the `cancel-in-progress` value is set to `false` and no `group` is provided, then the previously described Forgejo behavior will be disabled and an unlimited number of workflows can be executed simultaneously (to the maximum supported by the Forgejo Runner capacity). - Queue-behind -- if a `group` is provided and `cancel-in-progress: false` is set, then every new action run with in the same repository with the same group value will be queued behind previous workflow runs, allowing only one workflow to execute at a time in the group, but allowing all workflows to finish naturally. - Cancel-in-progress -- if a `group` is provided and `cancel-in-progress: true` is set, then every new action run with in the same repository with the same group value will cause previously queued or running runs to be cancelled, allowing only one workflow to execute at a time in the group, but preferring execution of the most recent workflow. Both the `group` and `cancel-in-progress` values can access values from the `github`, `inputs` and `vars` context for dynamic behavior. ## 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. - [x] 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 - [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/1513 - [ ] 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--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/9434): <!--number 9434 --><!--line 0 --><!--description aW1wbGVtZW50ICJjb25jdXJyZW5jeSIgYmxvY2sgaW4gRm9yZ2VqbyBBY3Rpb25zIGF0IHRoZSB3b3JrZmxvdyBsZXZlbA==-->implement "concurrency" block in Forgejo Actions at the workflow level<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9434 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
353 lines
10 KiB
Go
353 lines
10 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"testing"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
"forgejo.org/models/repo"
|
|
"forgejo.org/models/unittest"
|
|
"forgejo.org/models/user"
|
|
actions_module "forgejo.org/modules/actions"
|
|
"forgejo.org/modules/json"
|
|
"forgejo.org/modules/setting"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFindTaskNeeds(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
|
|
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
|
|
|
ret, err := FindTaskNeeds(t.Context(), job)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ret, 1)
|
|
assert.Contains(t, ret, "job1")
|
|
assert.Len(t, ret["job1"].Outputs, 2)
|
|
assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
|
|
assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
|
|
}
|
|
|
|
func TestGenerateGiteaContext(t *testing.T) {
|
|
testUser := &user.User{
|
|
ID: 1,
|
|
Name: "testuser",
|
|
}
|
|
|
|
testRepo := &repo.Repository{
|
|
ID: 1,
|
|
OwnerName: "testowner",
|
|
Name: "testrepo",
|
|
}
|
|
|
|
emptyField := func(t *testing.T, context map[string]any, field string) {
|
|
v, ok := context[field]
|
|
assert.True(t, ok, "expected field %q to be present", field)
|
|
assert.Empty(t, v)
|
|
}
|
|
|
|
t.Run("Basic workflow run without job", func(t *testing.T) {
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: "push",
|
|
Ref: "refs/heads/main",
|
|
CommitSHA: "abc123def456",
|
|
WorkflowID: "test-workflow",
|
|
EventPayload: `{"repository": {"name": "testrepo"}}`,
|
|
}
|
|
|
|
context := GenerateGiteaContext(run, nil)
|
|
|
|
assert.Equal(t, "testuser", context["actor"])
|
|
assert.Equal(t, setting.AppURL+"api/v1", context["api_url"])
|
|
assert.Equal(t, "push", context["event_name"])
|
|
assert.Equal(t, "refs/heads/main", context["ref"])
|
|
assert.Equal(t, "main", context["ref_name"])
|
|
assert.Equal(t, "branch", context["ref_type"])
|
|
assert.Equal(t, "testowner/testrepo", context["repository"])
|
|
assert.Equal(t, "testowner", context["repository_owner"])
|
|
assert.Equal(t, "abc123def456", context["sha"])
|
|
assert.Equal(t, "42", context["run_number"])
|
|
assert.Equal(t, "test-workflow", context["workflow"])
|
|
assert.Equal(t, false, context["ref_protected"])
|
|
assert.Equal(t, "Actions", context["secret_source"])
|
|
assert.Equal(t, setting.AppURL, context["server_url"])
|
|
|
|
event, ok := context["event"].(map[string]any)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "testrepo", event["repository"].(map[string]any)["name"])
|
|
|
|
emptyField(t, context, "action_path")
|
|
emptyField(t, context, "action_ref")
|
|
emptyField(t, context, "action_repository")
|
|
emptyField(t, context, "action_status")
|
|
emptyField(t, context, "action")
|
|
emptyField(t, context, "base_ref")
|
|
emptyField(t, context, "env")
|
|
emptyField(t, context, "event_path")
|
|
emptyField(t, context, "graphql_url")
|
|
emptyField(t, context, "head_ref")
|
|
emptyField(t, context, "job")
|
|
emptyField(t, context, "path")
|
|
emptyField(t, context, "retention_days")
|
|
emptyField(t, context, "run_attempt")
|
|
emptyField(t, context, "run_id")
|
|
emptyField(t, context, "triggering_actor")
|
|
emptyField(t, context, "workspace")
|
|
})
|
|
|
|
t.Run("Workflow run with job", func(t *testing.T) {
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: "push",
|
|
Ref: "refs/heads/main",
|
|
CommitSHA: "abc123def456",
|
|
WorkflowID: "test-workflow",
|
|
EventPayload: `{}`,
|
|
}
|
|
|
|
job := &actions_model.ActionRunJob{
|
|
ID: 100,
|
|
RunID: 1,
|
|
JobID: "test-job",
|
|
Attempt: 1,
|
|
}
|
|
|
|
context := GenerateGiteaContext(run, job)
|
|
|
|
assert.Equal(t, "test-job", context["job"])
|
|
assert.Equal(t, "1", context["run_id"])
|
|
assert.Equal(t, "1", context["run_attempt"])
|
|
})
|
|
|
|
t.Run("Pull request event", func(t *testing.T) {
|
|
pullRequestPayload := map[string]any{
|
|
"pull_request": map[string]any{
|
|
"base": map[string]any{
|
|
"ref": "main",
|
|
"label": "main",
|
|
"sha": "base123sha",
|
|
},
|
|
"head": map[string]any{
|
|
"ref": "feature-branch",
|
|
"label": "feature-branch",
|
|
"sha": "head456sha",
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, _ := json.Marshal(pullRequestPayload)
|
|
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: "pull_request",
|
|
Ref: "refs/pull/1/merge",
|
|
CommitSHA: "merge789sha",
|
|
WorkflowID: "test-workflow",
|
|
Event: webhook_module.HookEventPullRequest,
|
|
EventPayload: string(payloadBytes),
|
|
}
|
|
|
|
context := GenerateGiteaContext(run, nil)
|
|
|
|
assert.Equal(t, "main", context["base_ref"])
|
|
assert.Equal(t, "feature-branch", context["head_ref"])
|
|
assert.Equal(t, "refs/pull/1/merge", context["ref"])
|
|
assert.Equal(t, "merge789sha", context["sha"])
|
|
})
|
|
|
|
t.Run("Pull request target event", func(t *testing.T) {
|
|
pullRequestPayload := map[string]any{
|
|
"pull_request": map[string]any{
|
|
"base": map[string]any{
|
|
"ref": "main",
|
|
"label": "main",
|
|
"sha": "base123sha",
|
|
},
|
|
"head": map[string]any{
|
|
"ref": "feature-branch",
|
|
"label": "feature-branch",
|
|
"sha": "head456sha",
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, _ := json.Marshal(pullRequestPayload)
|
|
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: actions_module.GithubEventPullRequestTarget,
|
|
Ref: "refs/pull/1/merge",
|
|
CommitSHA: "merge789sha",
|
|
WorkflowID: "test-workflow",
|
|
Event: webhook_module.HookEventPullRequest,
|
|
EventPayload: string(payloadBytes),
|
|
}
|
|
|
|
context := GenerateGiteaContext(run, nil)
|
|
|
|
assert.Equal(t, "main", context["base_ref"])
|
|
assert.Equal(t, "feature-branch", context["head_ref"])
|
|
// For pull_request_target, ref and sha should be from base
|
|
assert.Equal(t, "refs/heads/main", context["ref"])
|
|
assert.Equal(t, "base123sha", context["sha"])
|
|
assert.Equal(t, "main", context["ref_name"])
|
|
assert.Equal(t, "branch", context["ref_type"])
|
|
})
|
|
}
|
|
|
|
func TestGenerateGiteaContextForRun(t *testing.T) {
|
|
testUser := &user.User{
|
|
ID: 1,
|
|
Name: "testuser",
|
|
}
|
|
|
|
testRepo := &repo.Repository{
|
|
ID: 1,
|
|
OwnerName: "testowner",
|
|
Name: "testrepo",
|
|
}
|
|
|
|
t.Run("Basic workflow run", func(t *testing.T) {
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: "push",
|
|
Ref: "refs/heads/main",
|
|
CommitSHA: "abc123def456",
|
|
WorkflowID: "test-workflow",
|
|
EventPayload: `{"repository": {"name": "testrepo"}}`,
|
|
}
|
|
|
|
gitContextObj := generateGiteaContextForRun(run)
|
|
|
|
assert.Equal(t, "testuser", gitContextObj.Actor)
|
|
assert.Equal(t, setting.AppURL+"api/v1", gitContextObj.APIURL)
|
|
assert.Equal(t, "push", gitContextObj.EventName)
|
|
assert.Equal(t, "refs/heads/main", gitContextObj.Ref)
|
|
assert.Equal(t, "main", gitContextObj.RefName)
|
|
assert.Equal(t, "branch", gitContextObj.RefType)
|
|
assert.Equal(t, "testowner/testrepo", gitContextObj.Repository)
|
|
assert.Equal(t, "testowner", gitContextObj.RepositoryOwner)
|
|
assert.Equal(t, "abc123def456", gitContextObj.Sha)
|
|
assert.Equal(t, "42", gitContextObj.RunNumber)
|
|
assert.Equal(t, "test-workflow", gitContextObj.Workflow)
|
|
|
|
assert.Equal(t, "testrepo", gitContextObj.Event["repository"].(map[string]any)["name"])
|
|
|
|
assert.Empty(t, gitContextObj.ActionPath)
|
|
assert.Empty(t, gitContextObj.ActionRef)
|
|
assert.Empty(t, gitContextObj.ActionRepository)
|
|
assert.Empty(t, gitContextObj.Action)
|
|
assert.Empty(t, gitContextObj.BaseRef)
|
|
assert.Empty(t, gitContextObj.EventPath)
|
|
assert.Empty(t, gitContextObj.GraphQLURL)
|
|
assert.Empty(t, gitContextObj.HeadRef)
|
|
assert.Empty(t, gitContextObj.Job)
|
|
assert.Empty(t, gitContextObj.RetentionDays)
|
|
assert.Empty(t, gitContextObj.RunAttempt)
|
|
assert.Empty(t, gitContextObj.RunID)
|
|
assert.Empty(t, gitContextObj.Workspace)
|
|
})
|
|
|
|
t.Run("Pull request event", func(t *testing.T) {
|
|
pullRequestPayload := map[string]any{
|
|
"pull_request": map[string]any{
|
|
"base": map[string]any{
|
|
"ref": "main",
|
|
"label": "main",
|
|
"sha": "base123sha",
|
|
},
|
|
"head": map[string]any{
|
|
"ref": "feature-branch",
|
|
"label": "feature-branch",
|
|
"sha": "head456sha",
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, _ := json.Marshal(pullRequestPayload)
|
|
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: "pull_request",
|
|
Ref: "refs/pull/1/merge",
|
|
CommitSHA: "merge789sha",
|
|
WorkflowID: "test-workflow",
|
|
Event: webhook_module.HookEventPullRequest,
|
|
EventPayload: string(payloadBytes),
|
|
}
|
|
|
|
gitContextObj := generateGiteaContextForRun(run)
|
|
|
|
assert.Equal(t, "main", gitContextObj.BaseRef)
|
|
assert.Equal(t, "feature-branch", gitContextObj.HeadRef)
|
|
assert.Equal(t, "refs/pull/1/merge", gitContextObj.Ref)
|
|
assert.Equal(t, "merge789sha", gitContextObj.Sha)
|
|
})
|
|
|
|
t.Run("Pull request target event", func(t *testing.T) {
|
|
pullRequestPayload := map[string]any{
|
|
"pull_request": map[string]any{
|
|
"base": map[string]any{
|
|
"ref": "main",
|
|
"label": "main",
|
|
"sha": "base123sha",
|
|
},
|
|
"head": map[string]any{
|
|
"ref": "feature-branch",
|
|
"label": "feature-branch",
|
|
"sha": "head456sha",
|
|
},
|
|
},
|
|
}
|
|
|
|
payloadBytes, _ := json.Marshal(pullRequestPayload)
|
|
|
|
run := &actions_model.ActionRun{
|
|
ID: 1,
|
|
Index: 42,
|
|
TriggerUser: testUser,
|
|
Repo: testRepo,
|
|
TriggerEvent: actions_module.GithubEventPullRequestTarget,
|
|
Ref: "refs/pull/1/merge",
|
|
CommitSHA: "merge789sha",
|
|
WorkflowID: "test-workflow",
|
|
Event: webhook_module.HookEventPullRequest,
|
|
EventPayload: string(payloadBytes),
|
|
}
|
|
|
|
gitContextObj := generateGiteaContextForRun(run)
|
|
|
|
assert.Equal(t, "main", gitContextObj.BaseRef)
|
|
assert.Equal(t, "feature-branch", gitContextObj.HeadRef)
|
|
// For pull_request_target, ref and sha should be from base
|
|
assert.Equal(t, "refs/heads/main", gitContextObj.Ref)
|
|
assert.Equal(t, "base123sha", gitContextObj.Sha)
|
|
assert.Equal(t, "main", gitContextObj.RefName)
|
|
assert.Equal(t, "branch", gitContextObj.RefType)
|
|
})
|
|
}
|