mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2026-10-24 16:59:19 +03:00
Global search and replace of `runner/v12` for `runner/v13`. <!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - other - [PR](https://code.forgejo.org/forgejo/runner/pulls/1645): <!--number 1645 --><!--line 0 --><!--description Y2hvcmU6IHByZXBhcmUgZm9yIHJ1bm5lciB2MTMgcmVsZWFzZQ==-->chore: prepare for runner v13 release<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1645 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.code.forgejo.org>
333 lines
8.1 KiB
Go
333 lines
8.1 KiB
Go
package exprparser
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.forgejo.org/forgejo/runner/v13/act/common/gitignore"
|
|
"code.forgejo.org/forgejo/runner/v13/act/model"
|
|
"github.com/rhysd/actionlint"
|
|
)
|
|
|
|
func (impl *interpreterImpl) contains(search, item reflect.Value) (bool, error) {
|
|
switch search.Kind() {
|
|
case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid:
|
|
return strings.Contains(
|
|
strings.ToLower(impl.coerceToString(search).String()),
|
|
strings.ToLower(impl.coerceToString(item).String()),
|
|
), nil
|
|
|
|
case reflect.Slice:
|
|
for i := 0; i < search.Len(); i++ {
|
|
arrayItem := search.Index(i).Elem()
|
|
result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if isEqual, ok := result.(bool); ok && isEqual {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) {
|
|
return strings.HasPrefix(
|
|
strings.ToLower(impl.coerceToString(searchString).String()),
|
|
strings.ToLower(impl.coerceToString(searchValue).String()),
|
|
), nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) {
|
|
return strings.HasSuffix(
|
|
strings.ToLower(impl.coerceToString(searchString).String()),
|
|
strings.ToLower(impl.coerceToString(searchValue).String()),
|
|
), nil
|
|
}
|
|
|
|
const (
|
|
passThrough = iota
|
|
bracketOpen
|
|
bracketClose
|
|
)
|
|
|
|
func (impl *interpreterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) {
|
|
input := impl.coerceToString(str).String()
|
|
output := ""
|
|
replacementIndex := ""
|
|
|
|
state := passThrough
|
|
for _, character := range input {
|
|
switch state {
|
|
case passThrough: // normal buffer output
|
|
switch character {
|
|
case '{':
|
|
state = bracketOpen
|
|
|
|
case '}':
|
|
state = bracketClose
|
|
|
|
default:
|
|
output += string(character)
|
|
}
|
|
|
|
case bracketOpen: // found {
|
|
switch character {
|
|
case '{':
|
|
output += "{"
|
|
replacementIndex = ""
|
|
state = passThrough
|
|
|
|
case '}':
|
|
index, err := strconv.ParseInt(replacementIndex, 10, 32)
|
|
if err != nil {
|
|
return "", fmt.Errorf("The following format string is invalid: '%s'", input)
|
|
}
|
|
|
|
replacementIndex = ""
|
|
|
|
if len(replaceValue) <= int(index) {
|
|
return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input)
|
|
}
|
|
|
|
output += impl.coerceToString(replaceValue[index]).String()
|
|
|
|
state = passThrough
|
|
|
|
default:
|
|
replacementIndex += string(character)
|
|
}
|
|
|
|
case bracketClose: // found }
|
|
switch character {
|
|
case '}':
|
|
output += "}"
|
|
replacementIndex = ""
|
|
state = passThrough
|
|
|
|
default:
|
|
panic("Invalid format parser state")
|
|
}
|
|
}
|
|
}
|
|
|
|
if state != passThrough {
|
|
switch state {
|
|
case bracketOpen:
|
|
return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input)
|
|
|
|
case bracketClose:
|
|
return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input)
|
|
}
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) join(array, sep reflect.Value) (string, error) {
|
|
separator := impl.coerceToString(sep).String()
|
|
switch array.Kind() {
|
|
case reflect.Slice:
|
|
var items []string
|
|
for i := 0; i < array.Len(); i++ {
|
|
items = append(items, impl.coerceToString(array.Index(i).Elem()).String())
|
|
}
|
|
|
|
return strings.Join(items, separator), nil
|
|
default:
|
|
return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil
|
|
}
|
|
}
|
|
|
|
func (impl *interpreterImpl) toJSON(value reflect.Value) (string, error) {
|
|
if value.Kind() == reflect.Invalid {
|
|
return "null", nil
|
|
}
|
|
|
|
iface := value.Interface()
|
|
if envWrapper, ok := value.Interface().(*EnvWrapper); ok {
|
|
iface = envWrapper.Env
|
|
} else if secretsWrapper, ok := value.Interface().(*SecretsWrapper); ok {
|
|
iface = secretsWrapper.Secrets
|
|
} else if matrixWrapper, ok := value.Interface().(*MatrixWrapper); ok {
|
|
iface = matrixWrapper.Matrix
|
|
}
|
|
|
|
json, err := json.MarshalIndent(iface, "", " ")
|
|
if err != nil {
|
|
return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err)
|
|
}
|
|
|
|
return string(json), nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) fromJSON(value reflect.Value) (any, error) {
|
|
if value.Kind() != reflect.String {
|
|
return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind())
|
|
}
|
|
|
|
var data any
|
|
|
|
err := json.Unmarshal([]byte(value.String()), &data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Invalid JSON: %v", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) hashFiles(paths ...reflect.Value) (string, error) {
|
|
var ps []gitignore.Pattern
|
|
|
|
const cwdPrefix = "./"
|
|
const excludeCwdPrefix = "!./"
|
|
for _, path := range paths {
|
|
if path.Kind() == reflect.String {
|
|
cleanPath := filepath.ToSlash(path.String()) // use '/' for cross-platform consistency
|
|
if strings.HasPrefix(cleanPath, cwdPrefix) {
|
|
cleanPath = cleanPath[len(cwdPrefix):]
|
|
} else if strings.HasPrefix(cleanPath, excludeCwdPrefix) {
|
|
cleanPath = "!" + cleanPath[len(excludeCwdPrefix):]
|
|
}
|
|
ps = append(ps, gitignore.ParsePattern(cleanPath, nil))
|
|
} else {
|
|
return "", fmt.Errorf("Non-string path passed to hashFiles")
|
|
}
|
|
}
|
|
|
|
matcher := gitignore.NewMatcher(ps)
|
|
|
|
var files []string
|
|
if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
|
|
parts := strings.Split(sansPrefix, string(filepath.Separator))
|
|
if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
}); err != nil {
|
|
return "", fmt.Errorf("Unable to filepath.Walk: %v", err)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
|
|
for _, file := range files {
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Unable to os.Open: %v", err)
|
|
}
|
|
|
|
if _, err := io.Copy(hasher, f); err != nil {
|
|
return "", fmt.Errorf("Unable to io.Copy: %v", err)
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
return "", fmt.Errorf("Unable to Close file: %v", err)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) getNeedsTransitive(job *model.Job) []string {
|
|
needs := job.Needs()
|
|
|
|
for _, need := range needs {
|
|
parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need))
|
|
needs = append(needs, parentNeeds...)
|
|
}
|
|
|
|
return needs
|
|
}
|
|
|
|
func (impl *interpreterImpl) always() (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) jobSuccess() (bool, error) {
|
|
jobs := impl.config.Run.Workflow.Jobs
|
|
jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
|
|
|
|
for _, needs := range jobNeeds {
|
|
if jobs[needs].Result != "success" {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) stepSuccess() (bool, error) {
|
|
return impl.env.Job.Status == "success", nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) jobFailure() (bool, error) {
|
|
jobs := impl.config.Run.Workflow.Jobs
|
|
jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
|
|
|
|
for _, needs := range jobNeeds {
|
|
if jobs[needs].Result == "failure" {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) stepFailure() (bool, error) {
|
|
return impl.env.Job.Status == "failure", nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) cancelled() (bool, error) {
|
|
return impl.env.Job.Status == "cancelled", nil
|
|
}
|
|
|
|
func (impl *interpreterImpl) caseStatement(args ...reflect.Value) (any, error) {
|
|
lengthOfRealArguments := len(args) - 1
|
|
defaultValue := args[lengthOfRealArguments]
|
|
|
|
if len(args) < 3 {
|
|
return nil, fmt.Errorf("'case' expected at least three arguments")
|
|
}
|
|
|
|
if len(args)%2 != 1 {
|
|
return nil, fmt.Errorf("'case' expected odd number of arguments")
|
|
}
|
|
|
|
for i := 0; i < lengthOfRealArguments; i++ {
|
|
predicate := args[i]
|
|
i++
|
|
value := args[i]
|
|
|
|
if predicate.Kind() != reflect.Bool {
|
|
return nil, fmt.Errorf("'case' expected predicate '%s' to be boolean", impl.coerceToString(predicate).String())
|
|
}
|
|
|
|
if predicate.Bool() {
|
|
return impl.coerceToString(value).String(), nil
|
|
}
|
|
}
|
|
|
|
return defaultValue.String(), nil
|
|
}
|