mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-04-21 14:19:26 -04:00
This change allows the `with:` field of a reusable workflow to reference a previous job, such as `with: { some-input: "${{ needs.other-job.outputs.other-output }}" }`. `strategy.matrix` can also reference `${{ needs... }}`.
When a job is parsed and encounters this situation, the outer job of the workflow is marked with a field `incomplete_with` (or `incomplete_matrix`), indicating to Forgejo that it can't be executed as-is and the other jobs in its `needs` list need to be completed first. And then in `job_emitter.go` when one job is completed, it checks if other jobs had a `needs` reference to it and unblocks those jobs -- but if they're marked with `incomplete_with` then they can be sent back through the job parser, with the now-available job outputs, to be expanded into the correct definition of the job.
The core functionality for this already exists to allow `runs-on` and `strategy.matrix` to reference the outputs of other jobs, but it is expanded upon here to include `with` for reusable workflows.
There is one known defect in this implementation, but it has a limited scope -- if this code path is used to expand a nested reusable workflow, then the `${{ input.... }}` context will be incorrect. This will require an update to the jobparser in runner version 12.4.0, and so it is out-of-scope of this PR.
## Checklist
The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).
### Tests
- I added test coverage for Go changes...
- [x] in their respective `*_test.go` for unit tests.
- [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
- [ ] in `web_src/js/*.test.js` if it can be unit tested.
- [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).
- **end-to-end test:** will require the noted "known defect" to be resolved, but tests are authored at https://code.forgejo.org/forgejo/end-to-end/compare/main...mfenniak:expand-reusable-workflows-needs
### Documentation
- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.
### Release notes
- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10647
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
"forgejo.org/models/db"
|
|
git_model "forgejo.org/models/git"
|
|
user_model "forgejo.org/models/user"
|
|
actions_module "forgejo.org/modules/actions"
|
|
"forgejo.org/modules/log"
|
|
api "forgejo.org/modules/structs"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
commitstatus_service "forgejo.org/services/repository/commitstatus"
|
|
|
|
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
|
|
)
|
|
|
|
// CreateCommitStatus creates a commit status for the given job.
|
|
// It won't return an error failed, but will log it, because it's not critical.
|
|
func CreateCommitStatus(ctx context.Context, jobs ...*actions_model.ActionRunJob) {
|
|
for _, job := range jobs {
|
|
if err := createCommitStatus(ctx, job); err != nil {
|
|
log.Error("Failed to create commit status for job %d: %v", job.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) error {
|
|
if incompleteMatrix, _, err := job.HasIncompleteMatrix(); err != nil {
|
|
return fmt.Errorf("job HasIncompleteMatrix: %w", err)
|
|
} else if incompleteMatrix {
|
|
// Don't create commit statuses for incomplete matrix jobs because they are never completed.
|
|
return nil
|
|
}
|
|
|
|
if err := job.LoadAttributes(ctx); err != nil {
|
|
return fmt.Errorf("load run: %w", err)
|
|
}
|
|
|
|
run := job.Run
|
|
|
|
var (
|
|
sha string
|
|
event string
|
|
)
|
|
switch run.Event {
|
|
case webhook_module.HookEventPush:
|
|
event = "push"
|
|
payload, err := run.GetPushEventPayload()
|
|
if err != nil {
|
|
return fmt.Errorf("GetPushEventPayload: %w", err)
|
|
}
|
|
if payload.HeadCommit == nil {
|
|
return errors.New("head commit is missing in event payload")
|
|
}
|
|
sha = payload.HeadCommit.ID
|
|
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestMilestone:
|
|
if run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
|
|
event = "pull_request_target"
|
|
} else {
|
|
event = "pull_request"
|
|
}
|
|
payload, err := run.GetPullRequestEventPayload()
|
|
if err != nil {
|
|
return fmt.Errorf("GetPullRequestEventPayload: %w", err)
|
|
}
|
|
if payload.PullRequest == nil {
|
|
return errors.New("pull request is missing in event payload")
|
|
} else if payload.PullRequest.Head == nil {
|
|
return errors.New("head of pull request is missing in event payload")
|
|
}
|
|
sha = payload.PullRequest.Head.Sha
|
|
case webhook_module.HookEventRelease:
|
|
event = string(run.Event)
|
|
sha = run.CommitSHA
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
repo := run.Repo
|
|
// TODO: store workflow name as a field in ActionRun to avoid parsing
|
|
runName := path.Base(run.WorkflowID)
|
|
if wfs, err := jobparser.Parse(job.WorkflowPayload, false); err == nil && len(wfs) > 0 {
|
|
runName = wfs[0].Name
|
|
}
|
|
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
|
|
state := toCommitStatus(job.Status)
|
|
if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
|
|
for _, v := range statuses {
|
|
if v.Context == ctxname {
|
|
if v.State == state {
|
|
// no need to update
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
return fmt.Errorf("GetLatestCommitStatus: %w", err)
|
|
}
|
|
|
|
description := ""
|
|
switch job.Status {
|
|
// TODO: if we want support description in different languages, we need to support i18n placeholders in it
|
|
case actions_model.StatusSuccess:
|
|
description = fmt.Sprintf("Successful in %s", job.Duration())
|
|
case actions_model.StatusFailure:
|
|
description = fmt.Sprintf("Failing after %s", job.Duration())
|
|
case actions_model.StatusCancelled:
|
|
description = "Has been cancelled"
|
|
case actions_model.StatusSkipped:
|
|
description = "Has been skipped"
|
|
case actions_model.StatusRunning:
|
|
description = "Has started running"
|
|
case actions_model.StatusWaiting:
|
|
description = "Waiting to run"
|
|
case actions_model.StatusBlocked:
|
|
description = "Blocked by required conditions"
|
|
}
|
|
|
|
index, err := getIndexOfJob(ctx, job)
|
|
if err != nil {
|
|
return fmt.Errorf("getIndexOfJob: %w", err)
|
|
}
|
|
|
|
creator := user_model.NewActionsUser()
|
|
if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator,
|
|
sha,
|
|
&git_model.CommitStatus{
|
|
SHA: sha,
|
|
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
|
|
Description: description,
|
|
Context: ctxname,
|
|
CreatorID: creator.ID,
|
|
State: state,
|
|
}); err != nil {
|
|
return fmt.Errorf("NewCommitStatus: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toCommitStatus(status actions_model.Status) api.CommitStatusState {
|
|
switch status {
|
|
case actions_model.StatusSuccess, actions_model.StatusSkipped:
|
|
return api.CommitStatusSuccess
|
|
case actions_model.StatusFailure, actions_model.StatusCancelled:
|
|
return api.CommitStatusFailure
|
|
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning:
|
|
return api.CommitStatusPending
|
|
default:
|
|
return api.CommitStatusError
|
|
}
|
|
}
|
|
|
|
func getIndexOfJob(ctx context.Context, job *actions_model.ActionRunJob) (int, error) {
|
|
// TODO: store job index as a field in ActionRunJob to avoid this
|
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for i, v := range jobs {
|
|
if v.ID == job.ID {
|
|
return i, nil
|
|
}
|
|
}
|
|
return 0, nil
|
|
}
|