1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-09-15 18:57:01 +00:00

GitHub env file support (#426)

* Upgrade to the official golangci-lint action and fix some issues it found

* Update deps

* Remove a shadow warning

* Initialize the splitPattern only once

* Initial attempt at supporting $GITHUB_ENV

Needs some polishing and tests

* Now it's actually working

* Replace golang.org/x/crypto/ssh/terminal with golang.org/x/term

* Disable the issue-228 test again

* The linter is picky

* Discovered that the workflow/envs.txt had to exist in certain cases

* Fix small linter issue
This commit is contained in:
Torbjørn Vatn 2021-01-12 07:39:43 +01:00 committed by GitHub
parent 0fe34092d1
commit b60851b818
13 changed files with 125 additions and 51 deletions

View file

@ -7,11 +7,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: docker://golangci/golangci-lint:v1.23.8 - uses: golangci/golangci-lint-action@v2
with:
args: golangci-lint run
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
with:
version: v1.32.2
test: test:
name: Test name: Test
@ -35,7 +35,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: GoReleaser - name: GoReleaser
uses: goreleaser/goreleaser-action@v1 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --snapshot --rm-dist args: release --snapshot --rm-dist

View file

@ -8,6 +8,7 @@ env:
KEY_WITH_UNDERSCORES: value_with_underscores KEY_WITH_UNDERSCORES: value_with_underscores
SOMETHING_TRUE: true SOMETHING_TRUE: true
SOMETHING_FALSE: false SOMETHING_FALSE: false
ACT: true
jobs: jobs:

View file

@ -43,7 +43,6 @@ type styleDef struct {
var styleDefs = []styleDef{ var styleDefs = []styleDef{
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"}, {"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
//{"\u250c", "\u2510", "\u2514", "\u2518", "\u2500", "\u2502"},
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"}, {"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"}, {"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
{" ", " ", " ", " ", " ", " "}, {" ", " ", " ", " ", " ", " "},

View file

@ -157,7 +157,6 @@ func findGitDirectory(fromFile string) (string, error) {
return "", err return "", err
} }
//log.Debugf("Searching for git directory in %s", absPath)
fi, err := os.Stat(absPath) fi, err := os.Stat(absPath)
if err != nil { if err != nil {
return "", err return "", err

View file

@ -2,6 +2,7 @@ package container
import ( import (
"archive/tar" "archive/tar"
"bufio"
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
@ -9,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
@ -24,7 +26,7 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
// NewContainerInput the input for the New function // NewContainerInput the input for the New function
@ -58,6 +60,7 @@ type Container interface {
Pull(forcePull bool) common.Executor Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor Start(attach bool) common.Executor
Exec(command []string, env map[string]string) common.Executor Exec(command []string, env map[string]string) common.Executor
UpdateFromGithubEnv(env *map[string]string) common.Executor
Remove() common.Executor Remove() common.Executor
} }
@ -116,6 +119,10 @@ func (cr *containerReference) CopyDir(destPath string, srcPath string) common.Ex
).IfNot(common.Dryrun) ).IfNot(common.Dryrun)
} }
func (cr *containerReference) UpdateFromGithubEnv(env *map[string]string) common.Executor {
return cr.extractGithubEnv(env).IfNot(common.Dryrun)
}
func (cr *containerReference) Exec(command []string, env map[string]string) common.Executor { func (cr *containerReference) Exec(command []string, env map[string]string) common.Executor {
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
@ -196,10 +203,10 @@ func (cr *containerReference) find() common.Executor {
return errors.WithStack(err) return errors.WithStack(err)
} }
for _, container := range containers { for _, c := range containers {
for _, name := range container.Names { for _, name := range c.Names {
if name[1:] == cr.input.Name { if name[1:] == cr.input.Name {
cr.id = container.ID cr.id = c.ID
return nil return nil
} }
} }
@ -237,7 +244,7 @@ func (cr *containerReference) create() common.Executor {
return nil return nil
} }
logger := common.Logger(ctx) logger := common.Logger(ctx)
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
input := cr.input input := cr.input
config := &container.Config{ config := &container.Config{
@ -275,11 +282,59 @@ func (cr *containerReference) create() common.Executor {
} }
} }
var singleLineEnvPattern, mulitiLineEnvPattern *regexp.Regexp
func (cr *containerReference) extractGithubEnv(env *map[string]string) common.Executor {
if singleLineEnvPattern == nil {
singleLineEnvPattern = regexp.MustCompile("^([^=]+)=([^=]+)$")
mulitiLineEnvPattern = regexp.MustCompile(`^([^<]+)<<(\w+)$`)
}
localEnv := *env
return func(ctx context.Context) error {
githubEnvTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, localEnv["GITHUB_ENV"])
if err != nil {
return nil
}
reader := tar.NewReader(githubEnvTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return errors.WithStack(err)
}
s := bufio.NewScanner(reader)
multiLineEnvKey := ""
multiLineEnvDelimiter := ""
multiLineEnvContent := ""
for s.Scan() {
line := s.Text()
if singleLineEnv := singleLineEnvPattern.FindStringSubmatch(line); singleLineEnv != nil {
localEnv[singleLineEnv[1]] = singleLineEnv[2]
}
if line == multiLineEnvDelimiter {
localEnv[multiLineEnvKey] = multiLineEnvContent
multiLineEnvKey, multiLineEnvDelimiter, multiLineEnvContent = "", "", ""
}
if multiLineEnvKey != "" && multiLineEnvDelimiter != "" {
if multiLineEnvContent != "" {
multiLineEnvContent += "\n"
}
multiLineEnvContent += line
}
if mulitiLineEnvStart := mulitiLineEnvPattern.FindStringSubmatch(line); mulitiLineEnvStart != nil {
multiLineEnvKey = mulitiLineEnvStart[1]
multiLineEnvDelimiter = mulitiLineEnvStart[2]
}
}
env = &localEnv
return nil
}
}
func (cr *containerReference) exec(cmd []string, env map[string]string) common.Executor { func (cr *containerReference) exec(cmd []string, env map[string]string) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Debugf("Exec command '%s'", cmd) logger.Debugf("Exec command '%s'", cmd)
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
envList := make([]string, 0) envList := make([]string, 0)
for k, v := range env { for k, v := range env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v)) envList = append(envList, fmt.Sprintf("%s=%s", k, v))
@ -492,7 +547,7 @@ func (cr *containerReference) attach() common.Executor {
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
var outWriter io.Writer var outWriter io.Writer
outWriter = cr.input.Stdout outWriter = cr.input.Stdout

View file

@ -283,13 +283,13 @@ const (
// StepTypeRun is all steps that have a `run` attribute // StepTypeRun is all steps that have a `run` attribute
StepTypeRun StepType = iota StepTypeRun StepType = iota
//StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...` // StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...`
StepTypeUsesDockerURL StepTypeUsesDockerURL
//StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory // StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory
StepTypeUsesActionLocal StepTypeUsesActionLocal
//StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo // StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
StepTypeUsesActionRemote StepTypeUsesActionRemote
) )

View file

@ -13,7 +13,7 @@ var commandPatternADO *regexp.Regexp
func init() { func init() {
commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$") commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?\\]([^\r\n]*)[\r\n]+$") commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$")
} }
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
@ -103,7 +103,7 @@ func unescapeCommandData(arg string) string {
"%0A": "\n", "%0A": "\n",
} }
for k, v := range escapeMap { for k, v := range escapeMap {
arg = strings.Replace(arg, k, v, -1) arg = strings.ReplaceAll(arg, k, v)
} }
return arg return arg
} }
@ -116,7 +116,7 @@ func unescapeCommandProperty(arg string) string {
"%2C": ",", "%2C": ",",
} }
for k, v := range escapeMap { for k, v := range escapeMap {
arg = strings.Replace(arg, k, v, -1) arg = strings.ReplaceAll(arg, k, v)
} }
return arg return arg
} }

View file

@ -8,17 +8,17 @@ import (
) )
func TestSetEnv(t *testing.T) { func TestSetEnv(t *testing.T) {
assert := assert.New(t) a := assert.New(t)
ctx := context.Background() ctx := context.Background()
rc := new(RunContext) rc := new(RunContext)
handler := rc.commandHandler(ctx) handler := rc.commandHandler(ctx)
handler("::set-env name=x::valz\n") handler("::set-env name=x::valz\n")
assert.Equal("valz", rc.Env["x"]) a.Equal("valz", rc.Env["x"])
} }
func TestSetOutput(t *testing.T) { func TestSetOutput(t *testing.T) {
assert := assert.New(t) a := assert.New(t)
ctx := context.Background() ctx := context.Background()
rc := new(RunContext) rc := new(RunContext)
rc.StepResults = make(map[string]*stepResult) rc.StepResults = make(map[string]*stepResult)
@ -29,62 +29,62 @@ func TestSetOutput(t *testing.T) {
Outputs: make(map[string]string), Outputs: make(map[string]string),
} }
handler("::set-output name=x::valz\n") handler("::set-output name=x::valz\n")
assert.Equal("valz", rc.StepResults["my-step"].Outputs["x"]) a.Equal("valz", rc.StepResults["my-step"].Outputs["x"])
handler("::set-output name=x::percent2%25\n") handler("::set-output name=x::percent2%25\n")
assert.Equal("percent2%", rc.StepResults["my-step"].Outputs["x"]) a.Equal("percent2%", rc.StepResults["my-step"].Outputs["x"])
handler("::set-output name=x::percent2%25%0Atest\n") handler("::set-output name=x::percent2%25%0Atest\n")
assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x"]) a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x"])
handler("::set-output name=x::percent2%25%0Atest another3%25test\n") handler("::set-output name=x::percent2%25%0Atest another3%25test\n")
assert.Equal("percent2%\ntest another3%test", rc.StepResults["my-step"].Outputs["x"]) a.Equal("percent2%\ntest another3%test", rc.StepResults["my-step"].Outputs["x"])
handler("::set-output name=x%3A::percent2%25%0Atest\n") handler("::set-output name=x%3A::percent2%25%0Atest\n")
assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:"]) a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:"])
handler("::set-output name=x%3A%2C%0A%25%0D%3A::percent2%25%0Atest\n") handler("::set-output name=x%3A%2C%0A%25%0D%3A::percent2%25%0Atest\n")
assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:,\n%\r:"]) a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:,\n%\r:"])
} }
func TestAddpath(t *testing.T) { func TestAddpath(t *testing.T) {
assert := assert.New(t) a := assert.New(t)
ctx := context.Background() ctx := context.Background()
rc := new(RunContext) rc := new(RunContext)
handler := rc.commandHandler(ctx) handler := rc.commandHandler(ctx)
handler("::add-path::/zoo\n") handler("::add-path::/zoo\n")
assert.Equal("/zoo", rc.ExtraPath[0]) a.Equal("/zoo", rc.ExtraPath[0])
handler("::add-path::/boo\n") handler("::add-path::/boo\n")
assert.Equal("/boo", rc.ExtraPath[1]) a.Equal("/boo", rc.ExtraPath[1])
} }
func TestStopCommands(t *testing.T) { func TestStopCommands(t *testing.T) {
assert := assert.New(t) a := assert.New(t)
ctx := context.Background() ctx := context.Background()
rc := new(RunContext) rc := new(RunContext)
handler := rc.commandHandler(ctx) handler := rc.commandHandler(ctx)
handler("::set-env name=x::valz\n") handler("::set-env name=x::valz\n")
assert.Equal("valz", rc.Env["x"]) a.Equal("valz", rc.Env["x"])
handler("::stop-commands::my-end-token\n") handler("::stop-commands::my-end-token\n")
handler("::set-env name=x::abcd\n") handler("::set-env name=x::abcd\n")
assert.Equal("valz", rc.Env["x"]) a.Equal("valz", rc.Env["x"])
handler("::my-end-token::\n") handler("::my-end-token::\n")
handler("::set-env name=x::abcd\n") handler("::set-env name=x::abcd\n")
assert.Equal("abcd", rc.Env["x"]) a.Equal("abcd", rc.Env["x"])
} }
func TestAddpathADO(t *testing.T) { func TestAddpathADO(t *testing.T) {
assert := assert.New(t) a := assert.New(t)
ctx := context.Background() ctx := context.Background()
rc := new(RunContext) rc := new(RunContext)
handler := rc.commandHandler(ctx) handler := rc.commandHandler(ctx)
handler("##[add-path]/zoo\n") handler("##[add-path]/zoo\n")
assert.Equal("/zoo", rc.ExtraPath[0]) a.Equal("/zoo", rc.ExtraPath[0])
handler("##[add-path]/boo\n") handler("##[add-path]/boo\n")
assert.Equal("/boo", rc.ExtraPath[1]) a.Equal("/boo", rc.ExtraPath[1])
} }

View file

@ -185,7 +185,6 @@ func TestInterpolate(t *testing.T) {
func updateTestExpressionWorkflow(t *testing.T, tables []struct { func updateTestExpressionWorkflow(t *testing.T, tables []struct {
in string in string
out string out string
//wantErr bool
}, rc *RunContext) { }, rc *RunContext) {
var envs string var envs string

View file

@ -12,11 +12,11 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
const ( const (
//nocolor = 0 // nocolor = 0
red = 31 red = 31
green = 32 green = 32
yellow = 33 yellow = 33
@ -126,7 +126,7 @@ func (f *stepLogFormatter) isColored(entry *logrus.Entry) bool {
func checkIfTerminal(w io.Writer) bool { func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) { switch v := w.(type) {
case *os.File: case *os.File:
return terminal.IsTerminal(int(v.Fd())) return term.IsTerminal(int(v.Fd()))
default: default:
return false return false
} }

View file

@ -125,6 +125,10 @@ func (rc *RunContext) startJobContainer() common.Executor {
Name: "workflow/event.json", Name: "workflow/event.json",
Mode: 0644, Mode: 0644,
Body: rc.EventJSON, Body: rc.EventJSON,
}, &container.FileEntry{
Name: "workflow/envs.txt",
Mode: 0644,
Body: "",
}, &container.FileEntry{ }, &container.FileEntry{
Name: "home/.act", Name: "home/.act",
Mode: 0644, Mode: 0644,
@ -199,6 +203,13 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
} }
_ = sc.setupEnv()(ctx) _ = sc.setupEnv()(ctx)
if sc.Env != nil {
err := rc.JobContainer.UpdateFromGithubEnv(&sc.Env)(ctx)
if err != nil {
return err
}
}
rc.ExprEval = sc.NewExpressionEvaluator() rc.ExprEval = sc.NewExpressionEvaluator()
runStep, err := rc.EvalBool(sc.Step.If) runStep, err := rc.EvalBool(sc.Step.If)
@ -267,14 +278,17 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool {
return true return true
} }
var splitPattern *regexp.Regexp
// EvalBool evaluates an expression against current run context // EvalBool evaluates an expression against current run context
func (rc *RunContext) EvalBool(expr string) (bool, error) { func (rc *RunContext) EvalBool(expr string) (bool, error) {
if splitPattern == nil {
splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
}
if strings.HasPrefix(strings.TrimSpace(expr), "!") { if strings.HasPrefix(strings.TrimSpace(expr), "!") {
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}") return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
} }
if expr != "" { if expr != "" {
splitPattern := regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
parts := splitPattern.FindAllString(expr, -1) parts := splitPattern.FindAllString(expr, -1)
var evaluatedParts []string var evaluatedParts []string
for i, part := range parts { for i, part := range parts {
@ -550,6 +564,8 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
github := rc.getGithubContext() github := rc.getGithubContext()
env["CI"] = "true" env["CI"] = "true"
env["HOME"] = "/github/home" env["HOME"] = "/github/home"
env["GITHUB_ENV"] = "/github/workflow/envs.txt"
env["GITHUB_WORKFLOW"] = github.Workflow env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID env["GITHUB_RUN_ID"] = github.RunID
env["GITHUB_RUN_NUMBER"] = github.RunNumber env["GITHUB_RUN_NUMBER"] = github.RunNumber

View file

@ -92,7 +92,7 @@ func TestRunEvent(t *testing.T) {
{"testdata", "matrix-include-exclude", "push", "", platforms}, {"testdata", "matrix-include-exclude", "push", "", platforms},
{"testdata", "commands", "push", "", platforms}, {"testdata", "commands", "push", "", platforms},
{"testdata", "workdir", "push", "", platforms}, {"testdata", "workdir", "push", "", platforms},
//{"testdata", "issue-228", "push", "", platforms}, // TODO [igni]: Remove this once everything passes // {"testdata", "issue-228", "push", "", platforms}, // TODO [igni]: Remove this once everything passes
{"testdata", "defaults-run", "push", "", platforms}, {"testdata", "defaults-run", "push", "", platforms},
} }
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)

View file

@ -11,7 +11,7 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
fswatch "github.com/andreaskoch/go-fswatch" "github.com/andreaskoch/go-fswatch"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/runner" "github.com/nektos/act/pkg/runner"
@ -79,7 +79,12 @@ func readArgsFile(file string) []string {
if err != nil { if err != nil {
return args return args
} }
defer f.Close() defer func() {
err := f.Close()
if err != nil {
log.Errorf("Failed to close args file: %v", err)
}
}()
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
arg := scanner.Text() arg := scanner.Text()
@ -91,7 +96,7 @@ func readArgsFile(file string) []string {
} }
func setupLogging(cmd *cobra.Command, args []string) { func setupLogging(cmd *cobra.Command, _ []string) {
verbose, _ := cmd.Flags().GetBool("verbose") verbose, _ := cmd.Flags().GetBool("verbose")
if verbose { if verbose {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
@ -189,7 +194,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
Platforms: input.newPlatforms(), Platforms: input.newPlatforms(),
Privileged: input.privileged, Privileged: input.privileged,
} }
runner, err := runner.New(config) r, err := runner.New(config)
if err != nil { if err != nil {
return err return err
} }
@ -198,10 +203,10 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
if watch, err := cmd.Flags().GetBool("watch"); err != nil { if watch, err := cmd.Flags().GetBool("watch"); err != nil {
return err return err
} else if watch { } else if watch {
return watchAndRun(ctx, runner.NewPlanExecutor(plan)) return watchAndRun(ctx, r.NewPlanExecutor(plan))
} }
return runner.NewPlanExecutor(plan)(ctx) return r.NewPlanExecutor(plan)(ctx)
} }
} }