forgejo/services/notify/notify.go
Mathieu Fenniak c434b963b4 feat: implement "concurrency" block in Forgejo Actions at the workflow level (#9434)
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>
2025-10-03 18:43:02 +02:00

397 lines
15 KiB
Go

// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notify
import (
"context"
"slices"
actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/log"
"forgejo.org/modules/repository"
)
var notifiers []Notifier
// RegisterNotifier providers method to receive notify messages
func RegisterNotifier(notifier Notifier) {
go notifier.Run()
notifiers = append(notifiers, notifier)
}
// Intended for undoing RegisterNotifier in tests only, not for production usage
func UnregisterNotifier(notifier Notifier) {
notifiers = slices.DeleteFunc(notifiers, func(maybeNotifier Notifier) bool {
return notifier == maybeNotifier
})
}
// NewWikiPage notifies creating new wiki pages to notifiers
func NewWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) {
for _, notifier := range notifiers {
notifier.NewWikiPage(ctx, doer, repo, page, comment)
}
}
// EditWikiPage notifies editing or renaming wiki pages to notifiers
func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) {
for _, notifier := range notifiers {
notifier.EditWikiPage(ctx, doer, repo, page, comment)
}
}
// DeleteWikiPage notifies deleting wiki pages to notifiers
func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) {
for _, notifier := range notifiers {
notifier.DeleteWikiPage(ctx, doer, repo, page)
}
}
// CreateIssueComment notifies issue comment related message to notifiers
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
) {
for _, notifier := range notifiers {
notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
}
}
// NewIssue notifies new issue to notifiers
func NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
for _, notifier := range notifiers {
notifier.NewIssue(ctx, issue, mentions)
}
}
// IssueChangeStatus notifies close or reopen issue to notifiers
func IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
for _, notifier := range notifiers {
notifier.IssueChangeStatus(ctx, doer, commitID, issue, actionComment, closeOrReopen)
}
}
// DeleteIssue notify when some issue deleted
func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
for _, notifier := range notifiers {
notifier.DeleteIssue(ctx, doer, issue)
}
}
// MergePullRequest notifies merge pull request to notifiers
func MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
for _, notifier := range notifiers {
notifier.MergePullRequest(ctx, doer, pr)
}
}
// AutoMergePullRequest notifies merge pull request to notifiers
func AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
for _, notifier := range notifiers {
notifier.AutoMergePullRequest(ctx, doer, pr)
}
}
// NewPullRequest notifies new pull request to notifiers
func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
if err := pr.LoadIssue(ctx); err != nil {
log.Error("LoadIssue failed: %v", err)
return
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
return
}
for _, notifier := range notifiers {
notifier.NewPullRequest(ctx, pr, mentions)
}
}
// PullRequestSynchronized notifies Synchronized pull request
func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
for _, notifier := range notifiers {
notifier.PullRequestSynchronized(ctx, doer, pr)
}
}
// PullRequestReview notifies new pull request review
func PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
if err := review.LoadReviewer(ctx); err != nil {
log.Error("LoadReviewer failed: %v", err)
return
}
for _, notifier := range notifiers {
notifier.PullRequestReview(ctx, pr, review, comment, mentions)
}
}
// PullRequestCodeComment notifies new pull request code comment
func PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) {
if err := comment.LoadPoster(ctx); err != nil {
log.Error("LoadPoster: %v", err)
return
}
for _, notifier := range notifiers {
notifier.PullRequestCodeComment(ctx, pr, comment, mentions)
}
}
// PullRequestChangeTargetBranch notifies when a pull request's target branch was changed
func PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) {
for _, notifier := range notifiers {
notifier.PullRequestChangeTargetBranch(ctx, doer, pr, oldBranch)
}
}
// PullRequestPushCommits notifies when push commits to pull request's head branch
func PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.PullRequestPushCommits(ctx, doer, pr, comment)
}
}
// PullReviewDismiss notifies when a review was dismissed by repo admin
func PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.PullReviewDismiss(ctx, doer, review, comment)
}
}
// UpdateComment notifies update comment to notifiers
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
for _, notifier := range notifiers {
notifier.UpdateComment(ctx, doer, c, oldContent)
}
}
// DeleteComment notifies delete comment to notifiers
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.DeleteComment(ctx, doer, c)
}
}
// NewRelease notifies new release to notifiers
func NewRelease(ctx context.Context, rel *repo_model.Release) {
if err := rel.LoadAttributes(ctx); err != nil {
log.Error("LoadPublisher: %v", err)
return
}
for _, notifier := range notifiers {
notifier.NewRelease(ctx, rel)
}
}
// UpdateRelease notifies update release to notifiers
func UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
for _, notifier := range notifiers {
notifier.UpdateRelease(ctx, doer, rel)
}
}
// DeleteRelease notifies delete release to notifiers
func DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
for _, notifier := range notifiers {
notifier.DeleteRelease(ctx, doer, rel)
}
}
// IssueChangeMilestone notifies change milestone to notifiers
func IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
for _, notifier := range notifiers {
notifier.IssueChangeMilestone(ctx, doer, issue, oldMilestoneID)
}
}
// IssueChangeContent notifies change content to notifiers
func IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
for _, notifier := range notifiers {
notifier.IssueChangeContent(ctx, doer, issue, oldContent)
}
}
// IssueChangeAssignee notifies change content to notifiers
func IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment)
}
}
// PullRequestReviewRequest notifies Request Review change
func PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.PullRequestReviewRequest(ctx, doer, issue, reviewer, isRequest, comment)
}
}
// IssueClearLabels notifies clear labels to notifiers
func IssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
for _, notifier := range notifiers {
notifier.IssueClearLabels(ctx, doer, issue)
}
}
// IssueChangeTitle notifies change title to notifiers
func IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
for _, notifier := range notifiers {
notifier.IssueChangeTitle(ctx, doer, issue, oldTitle)
}
}
// IssueChangeRef notifies change reference to notifiers
func IssueChangeRef(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldRef string) {
for _, notifier := range notifiers {
notifier.IssueChangeRef(ctx, doer, issue, oldRef)
}
}
// IssueChangeLabels notifies change labels to notifiers
func IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
addedLabels, removedLabels []*issues_model.Label,
) {
for _, notifier := range notifiers {
notifier.IssueChangeLabels(ctx, doer, issue, addedLabels, removedLabels)
}
}
// CreateRepository notifies create repository to notifiers
func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.CreateRepository(ctx, doer, u, repo)
}
}
// AdoptRepository notifies the adoption of a repository to notifiers
func AdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.AdoptRepository(ctx, doer, u, repo)
}
}
// MigrateRepository notifies create repository to notifiers
func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.MigrateRepository(ctx, doer, u, repo)
}
}
// TransferRepository notifies create repository to notifiers
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newOwnerName string) {
for _, notifier := range notifiers {
notifier.TransferRepository(ctx, doer, repo, newOwnerName)
}
}
// DeleteRepository notifies delete repository to notifiers
func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.DeleteRepository(ctx, doer, repo)
}
}
// ForkRepository notifies fork repository to notifiers
func ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.ForkRepository(ctx, doer, oldRepo, repo)
}
}
// RenameRepository notifies repository renamed
func RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldName string) {
for _, notifier := range notifiers {
notifier.RenameRepository(ctx, doer, repo, oldName)
}
}
// PushCommits notifies commits pushed to notifiers
func PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
for _, notifier := range notifiers {
notifier.PushCommits(ctx, pusher, repo, opts, commits)
}
}
// CreateRef notifies branch or tag creation to notifiers
func CreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
for _, notifier := range notifiers {
notifier.CreateRef(ctx, pusher, repo, refFullName, refID)
}
}
// DeleteRef notifies branch or tag deletion to notifiers
func DeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
for _, notifier := range notifiers {
notifier.DeleteRef(ctx, pusher, repo, refFullName)
}
}
// SyncPushCommits notifies commits pushed to notifiers
func SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
for _, notifier := range notifiers {
notifier.SyncPushCommits(ctx, pusher, repo, opts, commits)
}
}
// SyncCreateRef notifies branch or tag creation to notifiers
func SyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
for _, notifier := range notifiers {
notifier.SyncCreateRef(ctx, pusher, repo, refFullName, refID)
}
}
// SyncDeleteRef notifies branch or tag deletion to notifiers
func SyncDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
for _, notifier := range notifiers {
notifier.SyncDeleteRef(ctx, pusher, repo, refFullName)
}
}
// RepoPendingTransfer notifies creation of pending transfer to notifiers
func RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.RepoPendingTransfer(ctx, doer, newOwner, repo)
}
}
// NewUserSignUp notifies about a newly signed up user to notifiers
func NewUserSignUp(ctx context.Context, newUser *user_model.User) {
for _, notifier := range notifiers {
notifier.NewUserSignUp(ctx, newUser)
}
}
// PackageCreate notifies creation of a package to notifiers
func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
for _, notifier := range notifiers {
notifier.PackageCreate(ctx, doer, pd)
}
}
// PackageDelete notifies deletion of a package to notifiers
func PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
for _, notifier := range notifiers {
notifier.PackageDelete(ctx, doer, pd)
}
}
// ChangeDefaultBranch notifies change default branch to notifiers
func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
for _, notifier := range notifiers {
notifier.ChangeDefaultBranch(ctx, repo)
}
}
// ActionRunNowDone notifies that the old status priorStatus with (priorStatus.isDone() == false) of an ActionRun changed to run.Status with (run.Status.isDone() == true)
// run represents the new state of the ActionRun.
// lastRun represents the ActionRun of the same workflow that finished before run.
// lastRun might be nil (e.g. when the run is the first for this workflow). It is the last run of the same workflow for the same repo.
// It can be used to figure out if a successful run follows a failed one.
// Both run and lastRun need their attributes loaded.
func ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
for _, notifier := range notifiers {
notifier.ActionRunNowDone(ctx, run, priorStatus, lastRun)
}
}