mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2026-10-23 04:48:57 +03:00
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>
81 lines
2.2 KiB
Go
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, "{", "{{"), "}", "}}")
|
|
}
|