From d013126979e58bdf76ed00814e8526b016464769 Mon Sep 17 00:00:00 2001 From: "Roman K." Date: Fri, 10 Oct 2025 21:08:23 +0200 Subject: [PATCH] fix: prevent premature token revocation in reusable workflows --- act/runner/job_executor.go | 34 +++++++++++++++++------------- act/runner/reusable_workflow.go | 37 +++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/act/runner/job_executor.go b/act/runner/job_executor.go index 7a939930..f1ef21c5 100644 --- a/act/runner/job_executor.go +++ b/act/runner/job_executor.go @@ -180,31 +180,35 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo jobResult = "failure" } + // Set local result on current job (child or parent) info.result(jobResult) + if rc.caller != nil { - // set reusable workflow job result + // Child reusable workflow: + // 1) propagate result to parent job state rc.caller.runContext.result(jobResult) + + // 2) copy workflow_call outputs from child to parent (as in upstream) + jobOutputs := make(map[string]string) + ee := rc.NewExpressionEvaluator(ctx) + if wfcc := rc.Run.Workflow.WorkflowCallConfig(); wfcc != nil { + for k, v := range wfcc.Outputs { + jobOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value)) + } + } + rc.caller.runContext.Run.Job().Outputs = jobOutputs + + // 3) DO NOT print banner in child job (prevents premature token revocation) + logger.Debugf("Reusable job result=%s (parent will finalize, no banner)", jobResult) + return } + // Parent job: print the final banner ONCE (job-level) jobResultMessage := "succeeded" if jobResult != "success" { jobResultMessage = "failed" } - jobOutputs := rc.Run.Job().Outputs - if rc.caller != nil { - // Rewrite the job's outputs into the workflow_call outputs... - jobOutputs = make(map[string]string) - ee := rc.NewExpressionEvaluator(ctx) - for k, v := range rc.Run.Workflow.WorkflowCallConfig().Outputs { - jobOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value)) - } - // When running as a daemon and receiving jobs from Forgejo, the next job (and any of it's `needs` outputs) will - // be provided by Forgejo based upon the data sent to the logger below. However, when running `forgejo-runner - // exec` with a reusable workflow, the next job will only be able to read outputs if those outputs are stored on - // the workflow -- that's what is accomplished here: - rc.caller.runContext.Run.Job().Outputs = jobOutputs - } logger. WithFields(logrus.Fields{ diff --git a/act/runner/reusable_workflow.go b/act/runner/reusable_workflow.go index 5e92ee88..c117c4a7 100644 --- a/act/runner/reusable_workflow.go +++ b/act/runner/reusable_workflow.go @@ -16,6 +16,7 @@ import ( "code.forgejo.org/forgejo/runner/v11/act/common" "code.forgejo.org/forgejo/runner/v11/act/common/git" "code.forgejo.org/forgejo/runner/v11/act/model" + "github.com/sirupsen/logrus" ) func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { @@ -115,7 +116,10 @@ func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, rem return err } - return runner.NewPlanExecutor(plan)(ctx) + planErr := runner.NewPlanExecutor(plan)(ctx) + + // Finalize from parent context: one job-level banner + return finalizeReusableWorkflow(ctx, rc, planErr) } } @@ -171,7 +175,10 @@ func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) com return err } - return runner.NewPlanExecutor(plan)(ctx) + planErr := runner.NewPlanExecutor(plan)(ctx) + + // Finalize from parent context: one job-level banner + return finalizeReusableWorkflow(ctx, rc, planErr) } } @@ -229,3 +236,29 @@ func newRemoteReusableWorkflowWithPlat(url, uses string) *remoteReusableWorkflow URL: url, } } + +// finalizeReusableWorkflow prints the final job banner from the parent job context. +// +// The Forgejo reporter waits for this banner (log entry with "jobResult" +// field and without stage="Main") before marking the job as complete and revoking +// tokens. Printing this banner from the child reusable workflow would cause +// premature token revocation, breaking subsequent steps in the parent workflow. +func finalizeReusableWorkflow(ctx context.Context, rc *RunContext, planErr error) error { + jobResult := "success" + jobResultMessage := "succeeded" + if planErr != nil { + jobResult = "failure" + jobResultMessage = "failed" + } + + // Outputs should already be present in the parent context: + // - copied by child's setJobResult branch (rc.caller != nil) + jobOutputs := rc.Run.Job().Outputs + + common.Logger(ctx).WithFields(logrus.Fields{ + "jobResult": jobResult, + "jobOutputs": jobOutputs, + }).Infof("\U0001F3C1 Job %s", jobResultMessage) + + return planErr +}