Files
upstream-gitea/services/actions/approve.go
upstream-mirror 52ae0c5802
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:41:59 +05:00

150 lines
4.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"errors"
"fmt"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
// ApproveRuns returns the approved runs in the same order as runIDs.
func ApproveRuns(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, runIDs []int64) ([]*actions_model.ActionRun, error) {
updatedJobs := make([]*actions_model.ActionRunJob, 0)
cancelledConcurrencyJobs := make([]*actions_model.ActionRunJob, 0)
// Track runs whose reusable callers were just expanded so we can re-emit after the tx commits.
expandedCallerRunIDs := make(container.Set[int64])
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
for _, runID := range runIDs {
run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID)
if err != nil {
return err
}
if !run.NeedApproval {
continue
}
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
jobs, err := actions_model.GetLatestAttemptJobsByRun(ctx, run)
if err != nil {
return err
}
// approval unblocks every job at once, so max-parallel has to cap them here too
slots := maxParallelSlots{}
for _, job := range jobs {
slots.hold(job, job.Status)
}
for _, job := range jobs {
// Skip jobs with `needs`: they stay blocked until their dependencies finish,
// at which point job_emitter will evaluate and start them.
if len(job.Needs) > 0 {
continue
}
// Only a job this approval unblocks competes for a slot, one that is already
// active was counted by the seeding loop above and must not take a second.
isUnblocking := job.Status == actions_model.StatusBlocked
// A slot-starved job cannot start, skip the following checks.
if isUnblocking && !slots.available(job) {
continue
}
var jobsToCancel []*actions_model.ActionRunJob
job.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
return err
}
cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...)
if isUnblocking {
applyMaxParallel(job, slots)
}
if job.Status != actions_model.StatusWaiting {
continue
}
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
if n == 0 {
continue
}
updatedJobs = append(updatedJobs, job)
// A top-level reusable caller was just unblocked by approval, expand it
if job.IsReusableCaller && !job.IsExpanded {
attempt, has, err := run.GetLatestAttempt(ctx)
if err != nil {
return fmt.Errorf("get latest attempt of run %d: %w", run.ID, err)
}
if !has {
return errors.New("run has no attempt")
}
vars, err := actions_model.GetVariablesOfRun(ctx, run)
if err != nil {
return err
}
if err := expandReusableWorkflowCaller(ctx, run, attempt, job, vars); err != nil {
return fmt.Errorf("expand caller %d on approval: %w", job.ID, err)
}
if err := actions_model.RefreshReusableCallerStatus(ctx, job); err != nil {
return fmt.Errorf("refresh caller %d status after approval-time expansion: %w", job.ID, err)
}
expandedCallerRunIDs.Add(run.ID)
}
}
}
return nil
})
if err != nil {
return nil, err
}
// Re-emit AFTER the tx commits so the newly inserted callee rows transition Blocked -> Waiting.
for runID := range expandedCallerRunIDs {
if err := EmitJobsIfReadyByRun(runID); err != nil {
log.Error("emit run %d after approval-time caller expansion: %v", runID, err)
}
}
NotifyWorkflowJobsAndRunsStatusUpdate(ctx, updatedJobs)
NotifyWorkflowJobsAndRunsStatusUpdate(ctx, cancelledConcurrencyJobs)
EmitJobsIfReadyByJobs(cancelledConcurrencyJobs)
// The batches above already notified every run whose jobs changed, which is the only way
// approving alters a run's status, so reload purely to answer the caller.
reloaded, err := actions_model.GetRunsByRepoAndID(ctx, repo.ID, runIDs)
if err != nil {
return nil, fmt.Errorf("GetRunsByRepoAndID: %w", err)
}
runsByID := make(map[int64]*actions_model.ActionRun, len(reloaded))
for _, run := range reloaded {
run.Repo = repo // the caller resolved runIDs against this repo, so spare every consumer a reload
runsByID[run.ID] = run
}
approvedRuns := make([]*actions_model.ActionRun, 0, len(runIDs))
for _, runID := range runIDs {
run := runsByID[runID]
if run == nil {
return nil, util.NewNotExistErrorf("run %d no longer exists after approval", runID)
}
approvedRuns = append(approvedRuns, run)
}
return approvedRuns, nil
}