fix: prevent premature token revocation in reusable workflows

This commit is contained in:
Roman K. 2025-10-10 21:08:23 +02:00
parent 66a7e82c43
commit d013126979
2 changed files with 54 additions and 17 deletions

View file

@ -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{

View file

@ -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
}