1
0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2026-10-23 04:48:57 +03:00
Files
runner/act/exprparser/rewrite_sub_expr.go
Mathieu Fenniak ca76b4ce12 feat(jobparser): allow access to the outputs of a reusable workflow (#1216)
Reusable workflow expansion is a capability that will allow Forgejo to schedule every job of a reusable workflow as a separate job, allowing the use of multiple runners and clear independent logging.  This is a multi-PR implementation effort, project work tracking link: https://codeberg.org/forgejo/forgejo/issues/9768#issuecomment-8828016

This PR extends reusable workflow expansion to support accessing the outputs of a reusable workflow.  Four changes are involved:
- Centralizing a duplicated `rewriteSubExpression` function between the jobparser and the runner.
- Creating an expression mutator which allows for transformations of expressions by their abstract syntax tree.
- Transforming the outputs in `on.workflow_call.outputs`, which are in the form of `jobs.some-job.outputs`, into `needs[format("{0}.{1}", parent-job-id, job-id)].outputs....` on the parent job.  This moves evaluation of the outputs into the placeholder job that was previously created in #1215 to support dependencies between jobs.
- Addition of a utility function `EvaluateWorkflowCallOutputs` which Forgejo can use to evaluate the outputs, as the placeholder job is never sent to a runner to be executed.
- Corrected an earlier persistent misuse of "callee" to refer to "caller".

In addition to test automation present here, this change has been manually tested on an [experimental Forgejo branch](https://codeberg.org/mfenniak/forgejo/pulls/3).

<!--start release-notes-assistant-->
<!--URL:https://code.forgejo.org/forgejo/runner-->
- features
  - [PR](https://code.forgejo.org/forgejo/runner/pulls/1216): <!--number 1216 --><!--line 0 --><!--description ZmVhdChqb2JwYXJzZXIpOiBhbGxvdyBhY2Nlc3MgdG8gdGhlIG91dHB1dHMgb2YgYSByZXVzYWJsZSB3b3JrZmxvdw==-->feat(jobparser): allow access to the outputs of a reusable workflow<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1216
Reviewed-by: earl-warren <earl-warren@noreply.code.forgejo.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2025-12-13 14:27:51 +00:00

81 lines
2.2 KiB
Go

package exprparser
import (
"fmt"
"regexp"
"strings"
)
// The expression parsing library that runner uses to evaluate workflow expressions (eg. `${{ ... }}`) doesn't support a
// string which is partially text and partially expression, like `Hello ${{ vars.world }}!`. `RewriteSubExpression`
// converts a string in this format into a single expression evaluation, such as `format('Hello {0}!', vars.world)`.
// The return value may still be `${{ ... }}` delimited.
func RewriteSubExpression(in string, forceFormat bool) string {
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
return in
}
strPattern := regexp.MustCompile("(?:''|[^'])*'")
pos := 0
exprStart := -1
strStart := -1
var results []string
formatOut := ""
for pos < len(in) {
if strStart > -1 {
matches := strPattern.FindStringIndex(in[pos:])
if matches == nil {
panic("unclosed string.")
}
strStart = -1
pos += matches[1]
} else if exprStart > -1 {
exprEnd := strings.Index(in[pos:], "}}")
strStart = strings.Index(in[pos:], "'")
if exprEnd > -1 && strStart > -1 {
if exprEnd < strStart {
strStart = -1
} else {
exprEnd = -1
}
}
if exprEnd > -1 {
formatOut += fmt.Sprintf("{%d}", len(results))
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
pos += exprEnd + 2
exprStart = -1
} else if strStart > -1 {
pos += strStart + 1
} else {
panic("unclosed expression.")
}
} else {
exprStart = strings.Index(in[pos:], "${{")
if exprStart != -1 {
formatOut += escapeFormatString(in[pos : pos+exprStart])
exprStart = pos + exprStart + 3
pos = exprStart
} else {
formatOut += escapeFormatString(in[pos:])
pos = len(in)
}
}
}
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
return in
}
out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
return out
}
// Within a `format('abc')` function call, references to arguments are .NET-style formatted with `{0}`. If a static
// string contains '{' or `}` and is put into this arg, it must be escaped as '{{' or '}}'.
func escapeFormatString(in string) string {
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
}