Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions models/actions/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ type ActionRun struct {
RawConcurrency string // raw concurrency
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`

// ParentJobID == 0: it's a regular ActionRun without a parent job.
// ParentJobID > 0: it's a child ActionRun and the ParentJobID identifies the parent job's ID.
ParentJobID int64 `xorm:"index"`
ParentJob *ActionRunJob `xorm:"-"`

// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Expand Down Expand Up @@ -136,6 +142,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
run.TriggerUser = u
}

if err := run.LoadParentJob(ctx); err != nil {
return err
}

return nil
}

Expand All @@ -152,6 +162,19 @@ func (run *ActionRun) LoadRepo(ctx context.Context) error {
return nil
}

func (run *ActionRun) LoadParentJob(ctx context.Context) error {
if run.ParentJobID == 0 || run.ParentJob != nil {
return nil
}

parentJob, err := GetRunJobByID(ctx, run.ParentJobID)
if err != nil {
return err
}
run.ParentJob = parentJob
return nil
}

func (run *ActionRun) Duration() time.Duration {
return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration
}
Expand Down Expand Up @@ -291,6 +314,15 @@ func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, err
}

cancelledJobs = append(cancelledJobs, job)

if job.ChildRunID > 0 {
cancelledChildRunJobs, err := cancelChildRun(ctx, job)
if err != nil {
return cancelledJobs, fmt.Errorf("cancelChildRun: %w", err)
}
cancelledJobs = append(cancelledJobs, cancelledChildRunJobs...)
}

// Continue with the next job.
continue
}
Expand All @@ -310,6 +342,22 @@ func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, err
return cancelledJobs, nil
}

func cancelChildRun(ctx context.Context, parentJob *ActionRunJob) ([]*ActionRunJob, error) {
if !parentJob.Status.IsCancelled() {
return nil, fmt.Errorf("parent job status should be cancelled, but got %s", parentJob.Status.String())
}
if parentJob.ChildRunID <= 0 {
return nil, errors.New("no child run")
}

childRunJobs, err := GetRunJobsByRunID(ctx, parentJob.ChildRunID)
if err != nil {
return nil, err
}

return CancelJobs(ctx, childRunJobs)
}

func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
var run ActionRun
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
Expand Down
40 changes: 40 additions & 0 deletions models/actions/run_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ type ActionRunJob struct {
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress

// ChildRunID == 0: the job is a regular job without a child run.
// ChildRunID == -1, the job uses a reusable workflow but the child run has not yet been created. (For example, the job is currently blocked and will only create the child run after transitioning to waiting)
// ChildRunID > 0, the child run has been created and the value represents that child run's ID.
ChildRunID int64 `xorm:"index"`
ChildRun *ActionRun `xorm:"-"`

Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
Expand All @@ -76,6 +82,19 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error {
return nil
}

func (job *ActionRunJob) LoadChildRun(ctx context.Context) error {
if job.ChildRunID <= 0 || job.ChildRun != nil {
return nil
}

childRun, err := GetRunByRepoAndID(ctx, job.RepoID, job.ChildRunID)
if err != nil {
return err
}
job.ChildRun = childRun
return job.ChildRun.LoadAttributes(ctx)
}

func (job *ActionRunJob) LoadRepo(ctx context.Context) error {
if job.Repo == nil {
repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID)
Expand All @@ -97,6 +116,10 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error {
return err
}

if err := job.LoadChildRun(ctx); err != nil {
return err
}

return job.Run.LoadAttributes(ctx)
}

Expand Down Expand Up @@ -194,6 +217,23 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
return 0, fmt.Errorf("update run %d: %w", run.ID, err)
}

if run.ParentJobID > 0 { //If this run belongs to a parent job, its parent job's status needs to be updated.
if err := run.LoadParentJob(ctx); err != nil {
return 0, err
}
parentJob := run.ParentJob
parentJob.Status = run.Status
if parentJob.Started.IsZero() && parentJob.Status.IsRunning() {
parentJob.Started = timeutil.TimeStampNow()
}
if parentJob.Stopped.IsZero() && parentJob.Status.IsDone() {
parentJob.Stopped = timeutil.TimeStampNow()
}
if _, err := UpdateRunJob(ctx, parentJob, nil, "status", "started", "stopped"); err != nil {
return 0, fmt.Errorf("UpdateRunJob: %w", err)
}
}
}

return affected, nil
Expand Down
2 changes: 1 addition & 1 deletion models/actions/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
}

var jobs []*ActionRunJob
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
if err := e.Where("task_id=? AND status=? AND child_run_id=?", 0, StatusWaiting, 0).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
return nil, false, err
}

Expand Down
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ func prepareMigrationTasks() []*migration {
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
newMigration(326, "Add parent_job_id to ActionRun and child_run_id to ActionRunJob", v1_26.AddParentRunAndJobToActionRun),
}
return preparedMigrations
}
Expand Down
30 changes: 30 additions & 0 deletions models/migrations/v1_26/v326.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_26

import (
"xorm.io/xorm"
)

func AddParentRunAndJobToActionRun(x *xorm.Engine) error {
type ActionRun struct {
ParentJobID int64 `xorm:"index"`
}
type ActionRunJob struct {
ChildRunID int64 `xorm:"index"`
}

if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &ActionRun{}); err != nil {
return err
}

_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &ActionRunJob{})
return err
}
20 changes: 15 additions & 5 deletions models/perm/access/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,25 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
if err != nil {
return perm, err
}
if err := task.LoadAttributes(ctx); err != nil {
return perm, err
}

return GetActionsUserRepoPermissionByActionRun(ctx, repo, actionsUser, task.Job.Run)
}

func GetActionsUserRepoPermissionByActionRun(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, run *actions_model.ActionRun) (perm Permission, err error) {
if actionsUser.ID != user_model.ActionsUserID {
return perm, errors.New("api GetActionsUserRepoPermissionByActionRun can only be called by the actions user")
}

var accessMode perm_model.AccessMode
if task.RepoID != repo.ID {
taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID)
if err != nil || !exist {
if run.RepoID != repo.ID {
if err := run.LoadRepo(ctx); err != nil {
return perm, err
}
actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate {
if !actionsCfg.IsCollaborativeOwner(run.Repo.OwnerID) || !run.Repo.IsPrivate {
// The task repo can access the current repo only if the task repo is private and
// the owner of the task repo is a collaborative owner of the current repo.
// FIXME should owner's visibility also be considered here?
Expand All @@ -289,7 +299,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
return perm, nil
}
accessMode = perm_model.AccessModeRead
} else if task.IsForkPullRequest {
} else if run.IsForkPullRequest {
accessMode = perm_model.AccessModeRead
} else {
accessMode = perm_model.AccessModeWrite
Expand Down
2 changes: 1 addition & 1 deletion modules/actions/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/nektos/act/pkg/jobparser"
"github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/workflowpattern"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
)

type DetectedWorkflow struct {
Expand Down
19 changes: 19 additions & 0 deletions modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,22 @@ type WorkflowJobPayload struct {
func (p *WorkflowJobPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}

// WorkflowCallPayload represents a workflow_call payload
type WorkflowCallPayload struct {
// The name or path of the workflow file
Workflow string `json:"workflow"`
// The git reference (branch, tag, or commit SHA) to run the workflow on
Ref string `json:"ref"`
// Input parameters for the workflow_call event
Inputs map[string]any `json:"inputs"`
// The repository containing the workflow
Repository *Repository `json:"repository"`
// The user who triggered the workflow
Sender *User `json:"sender"`
}

// JSONPayload implements Payload
func (p *WorkflowCallPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
2 changes: 1 addition & 1 deletion routers/web/repo/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"code.gitea.io/gitea/services/convert"

act_model "github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
)

const (
Expand Down
Loading