Merge tag 'nektos/v0.2.43'

Conflicts:
	pkg/container/docker_run.go
	pkg/runner/action.go
	pkg/runner/logger.go
	pkg/runner/run_context.go
	pkg/runner/runner.go
	pkg/runner/step_action_remote_test.go
This commit is contained in:
Jason Song 2023-03-16 11:45:29 +08:00
commit 1dda0aec69
No known key found for this signature in database
GPG key ID: 8402EEEE4511A8B5
97 changed files with 3928 additions and 2256 deletions

View file

@ -1,4 +1,4 @@
FROM alpine:3.16 FROM alpine:3.17
ARG CHOCOVERSION=1.1.0 ARG CHOCOVERSION=1.1.0

77
.github/actions/run-tests/action.yml vendored Normal file
View file

@ -0,0 +1,77 @@
name: 'run-tests'
description: 'Runs go test and upload a step summary'
inputs:
filter:
description: 'The go test pattern for the tests to run'
required: false
default: ''
upload-logs-name:
description: 'Choose the name of the log artifact'
required: false
default: logs-${{ github.job }}-${{ strategy.job-index }}
upload-logs:
description: 'If true uploads logs of each tests as an artifact'
required: false
default: 'true'
runs:
using: composite
steps:
- uses: actions/github-script@v6
with:
github-token: none # No reason to grant access to the GITHUB_TOKEN
script: |
let myOutput = '';
var fs = require('fs');
var uploadLogs = process.env.UPLOAD_LOGS === 'true';
if(uploadLogs) {
await io.mkdirP('logs');
}
var filename = null;
const options = {};
options.ignoreReturnCode = true;
options.env = Object.assign({}, process.env);
delete options.env.ACTIONS_RUNTIME_URL;
delete options.env.ACTIONS_RUNTIME_TOKEN;
delete options.env.ACTIONS_CACHE_URL;
options.listeners = {
stdout: (data) => {
for(line of data.toString().split('\n')) {
if(/^\s*(===\s[^\s]+\s|---\s[^\s]+:\s)/.test(line)) {
if(uploadLogs) {
var runprefix = "=== RUN ";
if(line.startsWith(runprefix)) {
filename = "logs/" + line.substring(runprefix.length).replace(/[^A-Za-z0-9]/g, '-') + ".txt";
fs.writeFileSync(filename, line + "\n");
} else if(filename) {
fs.appendFileSync(filename, line + "\n");
filename = null;
}
}
myOutput += line + "\n";
} else if(filename) {
fs.appendFileSync(filename, line + "\n");
}
}
}
};
var args = ['test', '-v', '-cover', '-coverprofile=coverage.txt', '-covermode=atomic', '-timeout', '15m'];
var filter = process.env.FILTER;
if(filter) {
args.push('-run');
args.push(filter);
}
args.push('./...');
var exitcode = await exec.exec('go', args, options);
if(process.env.GITHUB_STEP_SUMMARY) {
core.summary.addCodeBlock(myOutput);
await core.summary.write();
}
process.exit(exitcode);
env:
FILTER: ${{ inputs.filter }}
UPLOAD_LOGS: ${{ inputs.upload-logs }}
- uses: actions/upload-artifact@v3
if: always() && inputs.upload-logs == 'true' && !env.ACT
with:
name: ${{ inputs.upload-logs-name }}
path: logs

View file

@ -19,10 +19,10 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
- uses: golangci/golangci-lint-action@v3.3.1 - uses: golangci/golangci-lint-action@v3.4.0
with: with:
version: v1.47.2 version: v1.47.2
- uses: megalinter/megalinter/flavors/go@v6.15.0 - uses: megalinter/megalinter/flavors/go@v6.20.0
env: env:
DEFAULT_BRANCH: master DEFAULT_BRANCH: master
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -50,7 +50,10 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- run: go test -v -cover -coverprofile=coverage.txt -covermode=atomic -timeout 15m ./... - name: Run Tests
uses: ./.github/actions/run-tests
with:
upload-logs-name: logs-linux
- name: Upload Codecov report - name: Upload Codecov report
uses: codecov/codecov-action@v3.1.1 uses: codecov/codecov-action@v3.1.1
with: with:
@ -73,8 +76,11 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
- run: go test -v -run ^TestRunEventHostEnvironment$ ./... - name: Run Tests
# TODO merge coverage with test-linux uses: ./.github/actions/run-tests
with:
filter: '^TestRunEventHostEnvironment$'
upload-logs-name: logs-${{ matrix.os }}
snapshot: snapshot:
name: snapshot name: snapshot
@ -93,7 +99,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: GoReleaser - name: GoReleaser
uses: goreleaser/goreleaser-action@v3 uses: goreleaser/goreleaser-action@v4
with: with:
version: latest version: latest
args: release --snapshot --rm-dist args: release --snapshot --rm-dist

View file

@ -27,7 +27,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: GoReleaser - name: GoReleaser
uses: goreleaser/goreleaser-action@v3 uses: goreleaser/goreleaser-action@v4
with: with:
version: latest version: latest
args: release --rm-dist args: release --rm-dist
@ -39,3 +39,29 @@ jobs:
version: ${{ github.ref }} version: ${{ github.ref }}
apiKey: ${{ secrets.CHOCO_APIKEY }} apiKey: ${{ secrets.CHOCO_APIKEY }}
push: true push: true
- name: GitHub CLI extension
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
script: |
const mainRef = (await github.rest.git.getRef({
owner: 'nektos',
repo: 'gh-act',
ref: 'heads/main',
})).data;
console.log(mainRef);
github.rest.git.createRef({
owner: 'nektos',
repo: 'gh-act',
ref: context.ref,
sha: mainRef.object.sha,
});
winget:
needs: release
runs-on: windows-latest # Action can only run on Windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: nektos.act
installers-regex: '_Windows_\w+\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

View file

@ -8,7 +8,7 @@ jobs:
name: Stale name: Stale
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v6 - uses: actions/stale@v7
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Issue is stale and will be closed in 14 days unless there is new activity' stale-issue-message: 'Issue is stale and will be closed in 14 days unless there is new activity'
@ -19,5 +19,5 @@ jobs:
exempt-pr-labels: 'stale-exempt' exempt-pr-labels: 'stale-exempt'
remove-stale-when-updated: 'True' remove-stale-when-updated: 'True'
operations-per-run: 500 operations-per-run: 500
days-before-stale: 30 days-before-stale: 180
days-before-close: 14 days-before-close: 14

View file

@ -14,7 +14,7 @@ DISABLE_LINTERS:
- MARKDOWN_MARKDOWN_LINK_CHECK - MARKDOWN_MARKDOWN_LINK_CHECK
- REPOSITORY_CHECKOV - REPOSITORY_CHECKOV
- REPOSITORY_TRIVY - REPOSITORY_TRIVY
FILTER_REGEX_EXCLUDE: (.*testdata/*|install.sh|pkg/container/docker_cli.go|pkg/container/DOCKER_LICENSE) FILTER_REGEX_EXCLUDE: (.*testdata/*|install.sh|pkg/container/docker_cli.go|pkg/container/DOCKER_LICENSE|VERSION)
MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.yml MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.yml
PARALLEL: false PARALLEL: false
PRINT_ALPACA: false PRINT_ALPACA: false

View file

@ -1,6 +1,7 @@
{ {
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",
"go.lintFlags": ["--fix"], "go.lintFlags": ["--fix"],
"go.testTimeout": "300s",
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },

View file

@ -96,7 +96,11 @@ ifneq ($(shell git status -s),)
@echo "Unable to promote a dirty workspace" @echo "Unable to promote a dirty workspace"
@exit 1 @exit 1
endif endif
echo -n $(NEW_VERSION) > VERSION
git add VERSION
git commit -m "chore: bump VERSION to $(NEW_VERSION)"
git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION) git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION)
git push origin master
git push origin v$(NEW_VERSION) git push origin v$(NEW_VERSION)
.PHONY: snapshot .PHONY: snapshot
@ -105,3 +109,5 @@ snapshot:
--rm-dist \ --rm-dist \
--single-target \ --single-target \
--snapshot --snapshot
.PHONY: clean all

102
README.md
View file

@ -96,6 +96,14 @@ choco install act-cli
scoop install act scoop install act
``` ```
### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/) (Windows)
[![Winget package](https://repology.org/badge/version-for-repo/winget/act-run-github-actions.svg)](https://repology.org/project/act-run-github-actions/versions)
```shell
winget install nektos.act
```
### [AUR](https://aur.archlinux.org/packages/act/) (Linux) ### [AUR](https://aur.archlinux.org/packages/act/) (Linux)
[![aur-shield](https://img.shields.io/aur/version/act)](https://aur.archlinux.org/packages/act/) [![aur-shield](https://img.shields.io/aur/version/act)](https://aur.archlinux.org/packages/act/)
@ -133,6 +141,14 @@ Using the latest [Nix command](https://nixos.wiki/wiki/Nix_command), you can run
nix run nixpkgs#act nix run nixpkgs#act
``` ```
## Installation as GitHub CLI extension
Act can be installed as a [GitHub CLI](https://cli.github.com/) extension:
```sh
gh extension install nektos/gh-act
```
## Other install options ## Other install options
### Bash script ### Bash script
@ -140,7 +156,7 @@ nix run nixpkgs#act
Run this command in your terminal: Run this command in your terminal:
```shell ```shell
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
``` ```
### Manual download ### Manual download
@ -188,49 +204,6 @@ act -v
When running `act` for the first time, it will ask you to choose image to be used as default. When running `act` for the first time, it will ask you to choose image to be used as default.
It will save that information to `~/.actrc`, please refer to [Configuration](#configuration) for more information about `.actrc` and to [Runners](#runners) for information about used/available Docker images. It will save that information to `~/.actrc`, please refer to [Configuration](#configuration) for more information about `.actrc` and to [Runners](#runners) for information about used/available Docker images.
# Flags
```none
-a, --actor string user that triggered the event (default "nektos/act")
--replace-ghe-action-with-github-com If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com=github/super-linter)
--replace-ghe-action-token-with-github-com If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token
--artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.
--artifact-server-port string Defines the port where the artifact server listens (will only bind to localhost). (default "34567")
-b, --bind bind working directory to container, rather than copy
--container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.
--container-cap-add stringArray kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)
--container-cap-drop stringArray kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)
--container-daemon-socket string Path to Docker daemon socket which will be mounted to containers (default "/var/run/docker.sock")
--defaultbranch string the name of the main branch
--detect-event Use first event type from workflow as event that triggered the workflow
-C, --directory string working directory (default ".")
-n, --dryrun dryrun mode
--env stringArray env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)
--env-file string environment file to read and use as env in the containers (default ".env")
-e, --eventpath string path to event JSON file
--github-instance string GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server. (default "github.com")
-g, --graph draw workflows
-h, --help help for act
--insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs.
-j, --job string run job
-l, --list list workflows
--no-recurse Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag
-P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)
--privileged use privileged mode
-p, --pull pull docker image(s) even if already present
-q, --quiet disable logging of output from steps
--rebuild rebuild local action docker image(s) even if already present
-r, --reuse don't remove container(s) on successfully completed workflow(s) to maintain state between runs
--rm automatically remove container(s)/volume(s) after a workflow(s) failure
-s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)
--secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets")
--use-gitignore Controls whether paths specified in .gitignore should be copied into container (default true)
--userns string user namespace to use
-v, --verbose verbose output
-w, --watch watch the contents of the local repo and run when files change
-W, --workflows string path to workflow file(s) (default "./.github/workflows/")
```
## `GITHUB_TOKEN` ## `GITHUB_TOKEN`
GitHub [automatically provides](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) a `GITHUB_TOKEN` secret when running workflows inside GitHub. GitHub [automatically provides](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) a `GITHUB_TOKEN` secret when running workflows inside GitHub.
@ -367,10 +340,41 @@ MY_ENV_VAR=MY_ENV_VAR_VALUE
MY_2ND_ENV_VAR="my 2nd env var value" MY_2ND_ENV_VAR="my 2nd env var value"
``` ```
# Skipping jobs
You cannot use the `env` context in job level if conditions, but you can add a custom event property to the `github` context. You can use this method also on step level if conditions.
```yml
on: push
jobs:
deploy:
if: ${{ !github.event.act }} # skip during local actions testing
runs-on: ubuntu-latest
steps:
- run: exit 0
```
And use this `event.json` file with act otherwise the Job will run:
```json
{
"act": true
}
```
Run act like
```sh
act -e event.json
```
_Hint: you can add / append `-e event.json` as a line into `./.actrc`_
# Skipping steps # Skipping steps
Act adds a special environment variable `ACT` that can be used to skip a step that you Act adds a special environment variable `ACT` that can be used to skip a step that you
don't want to run locally. E.g. a step that posts a Slack message or bumps a version number. don't want to run locally. E.g. a step that posts a Slack message or bumps a version number.
**You cannot use this method in job level if conditions, see [Skipping jobs](#skipping-jobs)**
```yml ```yml
- name: Some step - name: Some step
@ -402,7 +406,7 @@ act pull_request -e pull-request.json
Act will properly provide `github.head_ref` and `github.base_ref` to the action as expected. Act will properly provide `github.head_ref` and `github.base_ref` to the action as expected.
## Pass Inputs to Manually Triggered Workflows # Pass Inputs to Manually Triggered Workflows
Example workflow file Example workflow file
@ -428,6 +432,14 @@ jobs:
echo "Hello ${{ github.event.inputs.NAME }} and ${{ github.event.inputs.SOME_VALUE }}!" echo "Hello ${{ github.event.inputs.NAME }} and ${{ github.event.inputs.SOME_VALUE }}!"
``` ```
## via input or input-file flag
- `act --input NAME=somevalue` - use `somevalue` as the value for `NAME` input.
- `act --input-file my.input` - load input values from `my.input` file.
- input file format is the same as `.env` format
## via JSON
Example JSON payload file conveniently named `payload.json` Example JSON payload file conveniently named `payload.json`
```json ```json

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.2.43

View file

@ -17,12 +17,14 @@ type Input struct {
bindWorkdir bool bindWorkdir bool
secrets []string secrets []string
envs []string envs []string
inputs []string
platforms []string platforms []string
dryrun bool dryrun bool
forcePull bool forcePull bool
forceRebuild bool forceRebuild bool
noOutput bool noOutput bool
envfile string envfile string
inputfile string
secretfile string secretfile string
insecureSecrets bool insecureSecrets bool
defaultBranch string defaultBranch string
@ -30,6 +32,7 @@ type Input struct {
usernsMode string usernsMode string
containerArchitecture string containerArchitecture string
containerDaemonSocket string containerDaemonSocket string
containerOptions string
noWorkflowRecurse bool noWorkflowRecurse bool
useGitIgnore bool useGitIgnore bool
githubInstance string githubInstance string
@ -37,6 +40,7 @@ type Input struct {
containerCapDrop []string containerCapDrop []string
autoRemove bool autoRemove bool
artifactServerPath string artifactServerPath string
artifactServerAddr string
artifactServerPort string artifactServerPort string
jsonLogger bool jsonLogger bool
noSkipCheckout bool noSkipCheckout bool
@ -83,3 +87,8 @@ func (i *Input) WorkflowsPath() string {
func (i *Input) EventPath() string { func (i *Input) EventPath() string {
return i.resolve(i.eventPath) return i.resolve(i.eventPath)
} }
// Inputfile returns the path to the input file
func (i *Input) Inputfile() string {
return i.resolve(i.inputfile)
}

150
cmd/notices.go Normal file
View file

@ -0,0 +1,150 @@
package cmd
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
)
type Notice struct {
Level string `json:"level"`
Message string `json:"message"`
}
func displayNotices(input *Input) {
select {
case notices := <-noticesLoaded:
if len(notices) > 0 {
noticeLogger := log.New()
if input.jsonLogger {
noticeLogger.SetFormatter(&log.JSONFormatter{})
} else {
noticeLogger.SetFormatter(&log.TextFormatter{
DisableQuote: true,
DisableTimestamp: true,
PadLevelText: true,
})
}
fmt.Printf("\n")
for _, notice := range notices {
level, err := log.ParseLevel(notice.Level)
if err != nil {
level = log.InfoLevel
}
noticeLogger.Log(level, notice.Message)
}
}
case <-time.After(time.Second * 1):
log.Debugf("Timeout waiting for notices")
}
}
var noticesLoaded = make(chan []Notice)
func loadVersionNotices(version string) {
go func() {
noticesLoaded <- getVersionNotices(version)
}()
}
const NoticeURL = "https://api.nektosact.com/notices"
func getVersionNotices(version string) []Notice {
if os.Getenv("ACT_DISABLE_VERSION_CHECK") == "1" {
return nil
}
noticeURL, err := url.Parse(NoticeURL)
if err != nil {
log.Error(err)
return nil
}
query := noticeURL.Query()
query.Add("os", runtime.GOOS)
query.Add("arch", runtime.GOARCH)
query.Add("version", version)
noticeURL.RawQuery = query.Encode()
client := &http.Client{}
req, err := http.NewRequest("GET", noticeURL.String(), nil)
if err != nil {
log.Debug(err)
return nil
}
etag := loadNoticesEtag()
if etag != "" {
log.Debugf("Conditional GET for notices etag=%s", etag)
req.Header.Set("If-None-Match", etag)
}
resp, err := client.Do(req)
if err != nil {
log.Debug(err)
return nil
}
newEtag := resp.Header.Get("Etag")
if newEtag != "" {
log.Debugf("Saving notices etag=%s", newEtag)
saveNoticesEtag(newEtag)
}
defer resp.Body.Close()
notices := []Notice{}
if resp.StatusCode == 304 {
log.Debug("No new notices")
return nil
}
if err := json.NewDecoder(resp.Body).Decode(&notices); err != nil {
log.Debug(err)
return nil
}
return notices
}
func loadNoticesEtag() string {
p := etagPath()
content, err := os.ReadFile(p)
if err != nil {
log.Debugf("Unable to load etag from %s: %e", p, err)
}
return strings.TrimSuffix(string(content), "\n")
}
func saveNoticesEtag(etag string) {
p := etagPath()
err := os.WriteFile(p, []byte(strings.TrimSuffix(etag, "\n")), 0o600)
if err != nil {
log.Debugf("Unable to save etag to %s: %e", p, err)
}
}
func etagPath() string {
var xdgCache string
var ok bool
if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok || xdgCache == "" {
if home, err := homedir.Dir(); err == nil {
xdgCache = filepath.Join(home, ".cache")
} else if xdgCache, err = filepath.Abs("."); err != nil {
log.Fatal(err)
}
}
dir := filepath.Join(xdgCache, "act")
if err := os.MkdirAll(dir, 0o777); err != nil {
log.Fatal(err)
}
return filepath.Join(dir, ".notices.etag")
}

View file

@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/adrg/xdg"
"github.com/andreaskoch/go-fswatch" "github.com/andreaskoch/go-fswatch"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
@ -30,13 +31,14 @@ import (
func Execute(ctx context.Context, version string) { func Execute(ctx context.Context, version string) {
input := new(Input) input := new(Input)
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"", Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.", Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: newRunCommand(ctx, input), RunE: newRunCommand(ctx, input),
PersistentPreRun: setupLogging, PersistentPreRun: setup(input),
Version: version, PersistentPostRun: cleanup(input),
SilenceUsage: true, Version: version,
SilenceUsage: true,
} }
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change") rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
rootCmd.Flags().BoolP("list", "l", false, "list workflows") rootCmd.Flags().BoolP("list", "l", false, "list workflows")
@ -47,11 +49,12 @@ func Execute(ctx context.Context, version string) {
rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo") rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)") rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)") rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present")
rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present")
rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow") rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file") rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch") rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch")
@ -74,11 +77,14 @@ func Execute(ctx context.Context, version string) {
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)") rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.") rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers") rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input")
rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.") rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers") rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.") rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).") rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.")
rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout") rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
rootCmd.SetArgs(args()) rootCmd.SetArgs(args())
@ -93,18 +99,21 @@ func configLocations() []string {
log.Fatal(err) log.Fatal(err)
} }
configFileName := ".actrc"
// reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html // reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
var actrcXdg string var actrcXdg string
if xdg, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok && xdg != "" { for _, fileName := range []string{"act/actrc", configFileName} {
actrcXdg = filepath.Join(xdg, ".actrc") if foundConfig, err := xdg.SearchConfigFile(fileName); foundConfig != "" && err == nil {
} else { actrcXdg = foundConfig
actrcXdg = filepath.Join(home, ".config", ".actrc") break
}
} }
return []string{ return []string{
filepath.Join(home, ".actrc"), filepath.Join(home, configFileName),
actrcXdg, actrcXdg,
filepath.Join(".", ".actrc"), filepath.Join(".", configFileName),
} }
} }
@ -241,13 +250,37 @@ func readArgsFile(file string, split bool) []string {
return args return args
} }
func setupLogging(cmd *cobra.Command, _ []string) { func setup(inputs *Input) func(*cobra.Command, []string) {
verbose, _ := cmd.Flags().GetBool("verbose") return func(cmd *cobra.Command, _ []string) {
if verbose { verbose, _ := cmd.Flags().GetBool("verbose")
log.SetLevel(log.DebugLevel) if verbose {
log.SetLevel(log.DebugLevel)
}
loadVersionNotices(cmd.Version)
} }
} }
func cleanup(inputs *Input) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, _ []string) {
displayNotices(inputs)
}
}
func parseEnvs(env []string, envs map[string]string) bool {
if env != nil {
for _, envVar := range env {
e := strings.SplitN(envVar, `=`, 2)
if len(e) == 2 {
envs[e[0]] = e[1]
} else {
envs[e[0]] = ""
}
}
return true
}
return false
}
func readEnvs(path string, envs map[string]string) bool { func readEnvs(path string, envs map[string]string) bool {
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
env, err := godotenv.Read(path) env, err := godotenv.Read(path)
@ -284,18 +317,14 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
log.Debugf("Loading environment from %s", input.Envfile()) log.Debugf("Loading environment from %s", input.Envfile())
envs := make(map[string]string) envs := make(map[string]string)
if input.envs != nil { _ = parseEnvs(input.envs, envs)
for _, envVar := range input.envs {
e := strings.SplitN(envVar, `=`, 2)
if len(e) == 2 {
envs[e[0]] = e[1]
} else {
envs[e[0]] = ""
}
}
}
_ = readEnvs(input.Envfile(), envs) _ = readEnvs(input.Envfile(), envs)
log.Debugf("Loading action inputs from %s", input.Inputfile())
inputs := make(map[string]string)
_ = parseEnvs(input.inputs, inputs)
_ = readEnvs(input.Inputfile(), inputs)
log.Debugf("Loading secrets from %s", input.Secretfile()) log.Debugf("Loading secrets from %s", input.Secretfile())
secrets := newSecrets(input.secrets) secrets := newSecrets(input.secrets)
_ = readEnvs(input.Secretfile(), secrets) _ = readEnvs(input.Secretfile(), secrets)
@ -329,7 +358,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
var filterPlan *model.Plan var filterPlan *model.Plan
// Determine the event name to be filtered // Determine the event name to be filtered
var filterEventName string = "" var filterEventName string
if len(args) > 0 { if len(args) > 0 {
log.Debugf("Using first passed in arguments event for filtering: %s", args[0]) log.Debugf("Using first passed in arguments event for filtering: %s", args[0])
@ -341,23 +370,35 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
filterEventName = events[0] filterEventName = events[0]
} }
var plannerErr error
if jobID != "" { if jobID != "" {
log.Debugf("Preparing plan with a job: %s", jobID) log.Debugf("Preparing plan with a job: %s", jobID)
filterPlan = planner.PlanJob(jobID) filterPlan, plannerErr = planner.PlanJob(jobID)
} else if filterEventName != "" { } else if filterEventName != "" {
log.Debugf("Preparing plan for a event: %s", filterEventName) log.Debugf("Preparing plan for a event: %s", filterEventName)
filterPlan = planner.PlanEvent(filterEventName) filterPlan, plannerErr = planner.PlanEvent(filterEventName)
} else { } else {
log.Debugf("Preparing plan with all jobs") log.Debugf("Preparing plan with all jobs")
filterPlan = planner.PlanAll() filterPlan, plannerErr = planner.PlanAll()
}
if filterPlan == nil && plannerErr != nil {
return plannerErr
} }
if list { if list {
return printList(filterPlan) err = printList(filterPlan)
if err != nil {
return err
}
return plannerErr
} }
if graph { if graph {
return drawGraph(filterPlan) err = drawGraph(filterPlan)
if err != nil {
return err
}
return plannerErr
} }
// plan with triggered jobs // plan with triggered jobs
@ -385,10 +426,13 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
// build the plan for this run // build the plan for this run
if jobID != "" { if jobID != "" {
log.Debugf("Planning job: %s", jobID) log.Debugf("Planning job: %s", jobID)
plan = planner.PlanJob(jobID) plan, plannerErr = planner.PlanJob(jobID)
} else { } else {
log.Debugf("Planning jobs for event: %s", eventName) log.Debugf("Planning jobs for event: %s", eventName)
plan = planner.PlanEvent(eventName) plan, plannerErr = planner.PlanEvent(eventName)
}
if plan == nil && plannerErr != nil {
return plannerErr
} }
// check to see if the main branch was defined // check to see if the main branch was defined
@ -414,6 +458,19 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
input.platforms = readArgsFile(cfgLocations[0], true) input.platforms = readArgsFile(cfgLocations[0], true)
} }
} }
deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`."
if input.privileged {
log.Warnf(deprecationWarning, "privileged", "--privileged")
}
if len(input.usernsMode) > 0 {
log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode))
}
if len(input.containerCapAdd) > 0 {
log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd))
}
if len(input.containerCapDrop) > 0 {
log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop))
}
// run the plan // run the plan
config := &runner.Config{ config := &runner.Config{
@ -430,6 +487,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
JSONLogger: input.jsonLogger, JSONLogger: input.jsonLogger,
Env: envs, Env: envs,
Secrets: secrets, Secrets: secrets,
Inputs: inputs,
Token: secrets["GITHUB_TOKEN"], Token: secrets["GITHUB_TOKEN"],
InsecureSecrets: input.insecureSecrets, InsecureSecrets: input.insecureSecrets,
Platforms: input.newPlatforms(), Platforms: input.newPlatforms(),
@ -437,12 +495,14 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
UsernsMode: input.usernsMode, UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture, ContainerArchitecture: input.containerArchitecture,
ContainerDaemonSocket: input.containerDaemonSocket, ContainerDaemonSocket: input.containerDaemonSocket,
ContainerOptions: input.containerOptions,
UseGitIgnore: input.useGitIgnore, UseGitIgnore: input.useGitIgnore,
GitHubInstance: input.githubInstance, GitHubInstance: input.githubInstance,
ContainerCapAdd: input.containerCapAdd, ContainerCapAdd: input.containerCapAdd,
ContainerCapDrop: input.containerCapDrop, ContainerCapDrop: input.containerCapDrop,
AutoRemove: input.autoRemove, AutoRemove: input.autoRemove,
ArtifactServerPath: input.artifactServerPath, ArtifactServerPath: input.artifactServerPath,
ArtifactServerAddr: input.artifactServerAddr,
ArtifactServerPort: input.artifactServerPort, ArtifactServerPort: input.artifactServerPort,
NoSkipCheckout: input.noSkipCheckout, NoSkipCheckout: input.noSkipCheckout,
RemoteName: input.remoteName, RemoteName: input.remoteName,
@ -454,20 +514,28 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
return err return err
} }
cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort) cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort)
ctx = common.WithDryrun(ctx, input.dryrun) ctx = common.WithDryrun(ctx, input.dryrun)
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, r.NewPlanExecutor(plan)) err = watchAndRun(ctx, r.NewPlanExecutor(plan))
if err != nil {
return err
}
return plannerErr
} }
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
cancel() cancel()
return nil return nil
}) })
return executor(ctx) err = executor(ctx)
if err != nil {
return err
}
return plannerErr
} }
} }
@ -492,7 +560,7 @@ func defaultImageSurvey(actrc string) error {
case "Medium": case "Medium":
option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n" option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n"
case "Micro": case "Micro":
option = "-P ubuntu-latest=node:16-buster-slim\n-P -P ubuntu-22.04=node:16-bullseye-slim\n ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n" option = "-P ubuntu-latest=node:16-buster-slim\n-P ubuntu-22.04=node:16-bullseye-slim\n-P ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n"
} }
f, err := os.Create(actrc) f, err := os.Create(actrc)

60
go.mod
View file

@ -5,79 +5,77 @@ go 1.18
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.6 github.com/AlecAivazis/survey/v2 v2.3.6
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/adrg/xdg v0.4.0
github.com/andreaskoch/go-fswatch v1.0.0 github.com/andreaskoch/go-fswatch v1.0.0
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/docker/cli v20.10.21+incompatible github.com/docker/cli v23.0.1+incompatible
github.com/docker/distribution v2.8.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.21+incompatible github.com/docker/docker v23.0.1+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/go-git/go-billy/v5 v5.3.1 github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/go-ini/ini v1.67.0
github.com/imdario/mergo v0.3.13 github.com/imdario/mergo v0.3.13
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.16 github.com/mattn/go-isatty v0.0.17
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/moby/buildkit v0.10.6 github.com/moby/buildkit v0.11.4
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 github.com/moby/patternmatcher v0.5.0
github.com/opencontainers/selinux v1.10.2 github.com/opencontainers/image-spec v1.1.0-rc2
github.com/opencontainers/selinux v1.11.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rhysd/actionlint v1.6.22 github.com/rhysd/actionlint v1.6.23
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.6.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.4.0 gotest.tools/v3 v3.4.0
) )
require ( require (
github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.3 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect github.com/ProtonMail/go-crypto v0.0.0-20220404123522-616f957b79ad // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/containerd/cgroups v1.0.3 // indirect github.com/containerd/containerd v1.6.18 // indirect
github.com/containerd/containerd v1.6.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.13.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/sys/mount v0.3.1 // indirect github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/mountinfo v0.6.0 // indirect github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.2 // indirect github.com/opencontainers/runc v1.1.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.3.4 // indirect github.com/rivo/uniseg v0.4.3 // indirect
github.com/robfig/cron v1.2.0 // indirect github.com/robfig/cron v1.2.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.2.0 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sys v0.6.0 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

1015
go.sum

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
_ "embed"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -9,7 +10,8 @@ import (
"github.com/nektos/act/cmd" "github.com/nektos/act/cmd"
) )
var version = "v0.2.27-dev" // Manually bump after tagging next release //go:embed VERSION
var version string
func main() { func main() {
ctx := context.Background() ctx := context.Background()

View file

@ -9,12 +9,12 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
) )
@ -46,28 +46,34 @@ type ResponseMessage struct {
Message string `json:"message"` Message string `json:"message"`
} }
type MkdirFS interface { type WritableFile interface {
fs.FS io.WriteCloser
MkdirAll(path string, perm fs.FileMode) error
Open(name string) (fs.File, error)
OpenAtEnd(name string) (fs.File, error)
} }
type MkdirFsImpl struct { type WriteFS interface {
dir string OpenWritable(name string) (WritableFile, error)
fs.FS OpenAppendable(name string) (WritableFile, error)
} }
func (fsys MkdirFsImpl) MkdirAll(path string, perm fs.FileMode) error { type readWriteFSImpl struct {
return os.MkdirAll(fsys.dir+"/"+path, perm)
} }
func (fsys MkdirFsImpl) Open(name string) (fs.File, error) { func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
return os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) return os.Open(name)
} }
func (fsys MkdirFsImpl) OpenAtEnd(name string) (fs.File, error) { func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
file, err := os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR, 0644) if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return nil, err
}
return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
}
func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return nil, err
}
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil { if err != nil {
return nil, err return nil, err
@ -77,13 +83,16 @@ func (fsys MkdirFsImpl) OpenAtEnd(name string) (fs.File, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return file, nil return file, nil
} }
var gzipExtension = ".gz__" var gzipExtension = ".gz__"
func uploads(router *httprouter.Router, fsys MkdirFS) { func safeResolve(baseDir string, relPath string) string {
return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
}
func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
runID := params.ByName("runId") runID := params.ByName("runId")
@ -108,19 +117,15 @@ func uploads(router *httprouter.Router, fsys MkdirFS) {
itemPath += gzipExtension itemPath += gzipExtension
} }
filePath := fmt.Sprintf("%s/%s", runID, itemPath) safeRunPath := safeResolve(baseDir, runID)
safePath := safeResolve(safeRunPath, itemPath)
err := fsys.MkdirAll(path.Dir(filePath), os.ModePerm) file, err := func() (WritableFile, error) {
if err != nil {
panic(err)
}
file, err := func() (fs.File, error) {
contentRange := req.Header.Get("Content-Range") contentRange := req.Header.Get("Content-Range")
if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") { if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") {
return fsys.OpenAtEnd(filePath) return fsys.OpenAppendable(safePath)
} }
return fsys.Open(filePath) return fsys.OpenWritable(safePath)
}() }()
if err != nil { if err != nil {
@ -170,11 +175,13 @@ func uploads(router *httprouter.Router, fsys MkdirFS) {
}) })
} }
func downloads(router *httprouter.Router, fsys fs.FS) { func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
runID := params.ByName("runId") runID := params.ByName("runId")
entries, err := fs.ReadDir(fsys, runID) safePath := safeResolve(baseDir, runID)
entries, err := fs.ReadDir(fsys, safePath)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -204,12 +211,12 @@ func downloads(router *httprouter.Router, fsys fs.FS) {
router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
container := params.ByName("container") container := params.ByName("container")
itemPath := req.URL.Query().Get("itemPath") itemPath := req.URL.Query().Get("itemPath")
dirPath := fmt.Sprintf("%s/%s", container, itemPath) safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
var files []ContainerItem var files []ContainerItem
err := fs.WalkDir(fsys, dirPath, func(path string, entry fs.DirEntry, err error) error { err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error {
if !entry.IsDir() { if !entry.IsDir() {
rel, err := filepath.Rel(dirPath, path) rel, err := filepath.Rel(safePath, path)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -218,7 +225,7 @@ func downloads(router *httprouter.Router, fsys fs.FS) {
rel = strings.TrimSuffix(rel, gzipExtension) rel = strings.TrimSuffix(rel, gzipExtension)
files = append(files, ContainerItem{ files = append(files, ContainerItem{
Path: fmt.Sprintf("%s/%s", itemPath, rel), Path: filepath.Join(itemPath, rel),
ItemType: "file", ItemType: "file",
ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel), ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel),
}) })
@ -245,10 +252,12 @@ func downloads(router *httprouter.Router, fsys fs.FS) {
router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
path := params.ByName("path")[1:] path := params.ByName("path")[1:]
file, err := fsys.Open(path) safePath := safeResolve(baseDir, path)
file, err := fsys.Open(safePath)
if err != nil { if err != nil {
// try gzip file // try gzip file
file, err = fsys.Open(path + gzipExtension) file, err = fsys.Open(safePath + gzipExtension)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -262,7 +271,7 @@ func downloads(router *httprouter.Router, fsys fs.FS) {
}) })
} }
func Serve(ctx context.Context, artifactPath string, port string) context.CancelFunc { func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc {
serverContext, cancel := context.WithCancel(ctx) serverContext, cancel := context.WithCancel(ctx)
logger := common.Logger(serverContext) logger := common.Logger(serverContext)
@ -273,20 +282,19 @@ func Serve(ctx context.Context, artifactPath string, port string) context.Cancel
router := httprouter.New() router := httprouter.New()
logger.Debugf("Artifacts base path '%s'", artifactPath) logger.Debugf("Artifacts base path '%s'", artifactPath)
fs := os.DirFS(artifactPath) fsys := readWriteFSImpl{}
uploads(router, MkdirFsImpl{artifactPath, fs}) uploads(router, artifactPath, fsys)
downloads(router, fs) downloads(router, artifactPath, fsys)
ip := common.GetOutboundIP().String()
server := &http.Server{ server := &http.Server{
Addr: fmt.Sprintf("%s:%s", ip, port), Addr: fmt.Sprintf("%s:%s", addr, port),
ReadHeaderTimeout: 2 * time.Second, ReadHeaderTimeout: 2 * time.Second,
Handler: router, Handler: router,
} }
// run server // run server
go func() { go func() {
logger.Infof("Start server on http://%s:%s", ip, port) logger.Infof("Start server on http://%s:%s", addr, port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal(err) logger.Fatal(err)
} }

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -21,44 +20,43 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type MapFsImpl struct { type writableMapFile struct {
fstest.MapFS fstest.MapFile
} }
func (fsys MapFsImpl) MkdirAll(path string, perm fs.FileMode) error { func (f *writableMapFile) Write(data []byte) (int, error) {
// mocked no-op f.Data = data
return nil
}
type WritableFile struct {
fs.File
fsys fstest.MapFS
path string
}
func (file WritableFile) Write(data []byte) (int, error) {
file.fsys[file.path].Data = data
return len(data), nil return len(data), nil
} }
func (fsys MapFsImpl) Open(path string) (fs.File, error) { func (f *writableMapFile) Close() error {
var file = fstest.MapFile{ return nil
Data: []byte("content2"),
}
fsys.MapFS[path] = &file
result, err := fsys.MapFS.Open(path)
return WritableFile{result, fsys.MapFS, path}, err
} }
func (fsys MapFsImpl) OpenAtEnd(path string) (fs.File, error) { type writeMapFS struct {
var file = fstest.MapFile{ fstest.MapFS
Data: []byte("content2"), }
}
fsys.MapFS[path] = &file
result, err := fsys.MapFS.Open(path) func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
return WritableFile{result, fsys.MapFS, path}, err var file = &writableMapFile{
MapFile: fstest.MapFile{
Data: []byte("content2"),
},
}
fsys.MapFS[name] = &file.MapFile
return file, nil
}
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
var file = &writableMapFile{
MapFile: fstest.MapFile{
Data: []byte("content2"),
},
}
fsys.MapFS[name] = &file.MapFile
return file, nil
} }
func TestNewArtifactUploadPrepare(t *testing.T) { func TestNewArtifactUploadPrepare(t *testing.T) {
@ -67,7 +65,7 @@ func TestNewArtifactUploadPrepare(t *testing.T) {
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, MapFsImpl{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("POST", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest("POST", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -93,7 +91,7 @@ func TestArtifactUploadBlob(t *testing.T) {
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, MapFsImpl{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content")) req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content"))
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -111,7 +109,7 @@ func TestArtifactUploadBlob(t *testing.T) {
} }
assert.Equal("success", response.Message) assert.Equal("success", response.Message)
assert.Equal("content", string(memfs["1/some/file"].Data)) assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
} }
func TestFinalizeArtifactUpload(t *testing.T) { func TestFinalizeArtifactUpload(t *testing.T) {
@ -120,7 +118,7 @@ func TestFinalizeArtifactUpload(t *testing.T) {
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, MapFsImpl{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PATCH", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest("PATCH", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -144,13 +142,13 @@ func TestListArtifacts(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ var memfs = fstest.MapFS(map[string]*fstest.MapFile{
"1/file.txt": { "artifact/server/path/1/file.txt": {
Data: []byte(""), Data: []byte(""),
}, },
}) })
router := httprouter.New() router := httprouter.New()
downloads(router, memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -176,13 +174,13 @@ func TestListArtifactContainer(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ var memfs = fstest.MapFS(map[string]*fstest.MapFile{
"1/some/file": { "artifact/server/path/1/some/file": {
Data: []byte(""), Data: []byte(""),
}, },
}) })
router := httprouter.New() router := httprouter.New()
downloads(router, memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil) req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -200,7 +198,7 @@ func TestListArtifactContainer(t *testing.T) {
} }
assert.Equal(1, len(response.Value)) assert.Equal(1, len(response.Value))
assert.Equal("some/file/.", response.Value[0].Path) assert.Equal("some/file", response.Value[0].Path)
assert.Equal("file", response.Value[0].ItemType) assert.Equal("file", response.Value[0].ItemType)
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation) assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
} }
@ -209,13 +207,13 @@ func TestDownloadArtifactFile(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ var memfs = fstest.MapFS(map[string]*fstest.MapFile{
"1/some/file": { "artifact/server/path/1/some/file": {
Data: []byte("content"), Data: []byte("content"),
}, },
}) })
router := httprouter.New() router := httprouter.New()
downloads(router, memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil) req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -240,7 +238,8 @@ type TestJobFileInfo struct {
containerArchitecture string containerArchitecture string
} }
var aritfactsPath = path.Join(os.TempDir(), "test-artifacts") var artifactsPath = path.Join(os.TempDir(), "test-artifacts")
var artifactsAddr = "127.0.0.1"
var artifactsPort = "12345" var artifactsPort = "12345"
func TestArtifactFlow(t *testing.T) { func TestArtifactFlow(t *testing.T) {
@ -250,7 +249,7 @@ func TestArtifactFlow(t *testing.T) {
ctx := context.Background() ctx := context.Background()
cancel := Serve(ctx, aritfactsPath, artifactsPort) cancel := Serve(ctx, artifactsPath, artifactsAddr, artifactsPort)
defer cancel() defer cancel()
platforms := map[string]string{ platforms := map[string]string{
@ -259,6 +258,7 @@ func TestArtifactFlow(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{"testdata", "upload-and-download", "push", "", platforms, ""}, {"testdata", "upload-and-download", "push", "", platforms, ""},
{"testdata", "GHSL-2023-004", "push", "", platforms, ""},
} }
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
@ -271,7 +271,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
t.Run(tjfi.workflowPath, func(t *testing.T) { t.Run(tjfi.workflowPath, func(t *testing.T) {
fmt.Printf("::group::%s\n", tjfi.workflowPath) fmt.Printf("::group::%s\n", tjfi.workflowPath)
if err := os.RemoveAll(aritfactsPath); err != nil { if err := os.RemoveAll(artifactsPath); err != nil {
panic(err) panic(err)
} }
@ -286,7 +286,8 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
ReuseContainers: false, ReuseContainers: false,
ContainerArchitecture: tjfi.containerArchitecture, ContainerArchitecture: tjfi.containerArchitecture,
GitHubInstance: "github.com", GitHubInstance: "github.com",
ArtifactServerPath: aritfactsPath, ArtifactServerPath: artifactsPath,
ArtifactServerAddr: artifactsAddr,
ArtifactServerPort: artifactsPort, ArtifactServerPort: artifactsPort,
} }
@ -296,15 +297,96 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true) planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
assert.Nil(t, err, fullWorkflowPath) assert.Nil(t, err, fullWorkflowPath)
plan := planner.PlanEvent(tjfi.eventName) plan, err := planner.PlanEvent(tjfi.eventName)
if err == nil {
err = runner.NewPlanExecutor(plan)(ctx) err = runner.NewPlanExecutor(plan)(ctx)
if tjfi.errorMessage == "" { if tjfi.errorMessage == "" {
assert.Nil(t, err, fullWorkflowPath) assert.Nil(t, err, fullWorkflowPath)
} else {
assert.Error(t, err, tjfi.errorMessage)
}
} else { } else {
assert.Error(t, err, tjfi.errorMessage) assert.Nil(t, plan)
} }
fmt.Println("::endgroup::") fmt.Println("::endgroup::")
}) })
} }
func TestMkdirFsImplSafeResolve(t *testing.T) {
assert := assert.New(t)
baseDir := "/foo/bar"
tests := map[string]struct {
input string
want string
}{
"simple": {input: "baz", want: "/foo/bar/baz"},
"nested": {input: "baz/blue", want: "/foo/bar/baz/blue"},
"dots in middle": {input: "baz/../../blue", want: "/foo/bar/blue"},
"leading dots": {input: "../../parent", want: "/foo/bar/parent"},
"root path": {input: "/root", want: "/foo/bar/root"},
"root": {input: "/", want: "/foo/bar"},
"empty": {input: "", want: "/foo/bar"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(tc.want, safeResolve(baseDir, tc.input))
})
}
}
func TestDownloadArtifactFileUnsafePath(t *testing.T) {
assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/some/file": {
Data: []byte("content"),
},
})
router := httprouter.New()
downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/artifact/2/../../some/file", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
}
data := rr.Body.Bytes()
assert.Equal("content", string(data))
}
func TestArtifactUploadBlobUnsafePath(t *testing.T) {
assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=../../some/file", strings.NewReader("content"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.Fail("Wrong status")
}
response := ResponseMessage{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal("success", response.Message)
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
}

View file

@ -0,0 +1,43 @@
name: "GHSL-2023-0004"
on: push
jobs:
test-artifacts:
runs-on: ubuntu-latest
steps:
- run: echo "hello world" > test.txt
- name: curl upload
uses: wei/curl@v1
with:
args: -s --fail ${ACTIONS_RUNTIME_URL}upload/1?itemPath=../../my-artifact/secret.txt --upload-file test.txt
- uses: actions/download-artifact@v2
with:
name: my-artifact
path: test-artifacts
- name: 'Verify Artifact #1'
run: |
file="test-artifacts/secret.txt"
if [ ! -f $file ] ; then
echo "Expected file does not exist"
exit 1
fi
if [ "$(cat $file)" != "hello world" ] ; then
echo "File contents of downloaded artifact are incorrect"
exit 1
fi
- name: Verify download should work by clean extra dots
uses: wei/curl@v1
with:
args: --path-as-is -s -o out.txt --fail ${ACTIONS_RUNTIME_URL}artifact/1/../../../1/my-artifact/secret.txt
- name: 'Verify download content'
run: |
file="out.txt"
if [ ! -f $file ] ; then
echo "Expected file does not exist"
exit 1
fi
if [ "$(cat $file)" != "hello world" ] ; then
echo "File contents of downloaded artifact are incorrect"
exit 1
fi

View file

@ -7,20 +7,19 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/nektos/act/pkg/common"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-ini/ini"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
) )
var ( var (
@ -55,41 +54,40 @@ func (e *Error) Commit() string {
// FindGitRevision get the current git revision // FindGitRevision get the current git revision
func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) { func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
gitDir, err := findGitDirectory(file)
gitDir, err := git.PlainOpenWithOptions(
file,
&git.PlainOpenOptions{
DetectDotGit: true,
EnableDotGitCommonDir: true,
},
)
if err != nil {
logger.WithError(err).Error("path", file, "not located inside a git repository")
return "", "", err
}
head, err := gitDir.Reference(plumbing.HEAD, true)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
bts, err := os.ReadFile(filepath.Join(gitDir, "HEAD")) if head.Hash().IsZero() {
if err != nil { return "", "", fmt.Errorf("HEAD sha1 could not be resolved")
return "", "", err
} }
var ref = strings.TrimSpace(strings.TrimPrefix(string(bts), "ref:")) hash := head.Hash().String()
var refBuf []byte
if strings.HasPrefix(ref, "refs/") {
// load commitid ref
refBuf, err = os.ReadFile(filepath.Join(gitDir, ref))
if err != nil {
return "", "", err
}
} else {
refBuf = []byte(ref)
}
logger.Debugf("Found revision: %s", refBuf) logger.Debugf("Found revision: %s", hash)
return string(refBuf[:7]), strings.TrimSpace(string(refBuf)), nil return hash[:7], strings.TrimSpace(hash), nil
} }
// FindGitRef get the current git ref // FindGitRef get the current git ref
func FindGitRef(ctx context.Context, file string) (string, error) { func FindGitRef(ctx context.Context, file string) (string, error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
gitDir, err := findGitDirectory(file)
if err != nil {
return "", err
}
logger.Debugf("Loading revision from git directory '%s'", gitDir)
logger.Debugf("Loading revision from git directory")
_, ref, err := FindGitRevision(ctx, file) _, ref, err := FindGitRevision(ctx, file)
if err != nil { if err != nil {
return "", err return "", err
@ -100,28 +98,58 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
// Prefer the git library to iterate over the references and find a matching tag or branch. // Prefer the git library to iterate over the references and find a matching tag or branch.
var refTag = "" var refTag = ""
var refBranch = "" var refBranch = ""
r, err := git.PlainOpen(filepath.Join(gitDir, "..")) repo, err := git.PlainOpenWithOptions(
if err == nil { file,
iter, err := r.References() &git.PlainOpenOptions{
if err == nil { DetectDotGit: true,
for { EnableDotGitCommonDir: true,
r, err := iter.Next() },
if r == nil || err != nil { )
break
} if err != nil {
// logger.Debugf("Reference: name=%s sha=%s", r.Name().String(), r.Hash().String()) return "", err
if r.Hash().String() == ref {
if r.Name().IsTag() {
refTag = r.Name().String()
}
if r.Name().IsBranch() {
refBranch = r.Name().String()
}
}
}
iter.Close()
}
} }
iter, err := repo.References()
if err != nil {
return "", err
}
// find the reference that matches the revision's has
err = iter.ForEach(func(r *plumbing.Reference) error {
/* tags and branches will have the same hash
* when a user checks out a tag, it is not mentioned explicitly
* in the go-git package, we must identify the revision
* then check if any tag matches that revision,
* if so then we checked out a tag
* else we look for branches and if matches,
* it means we checked out a branch
*
* If a branches matches first we must continue and check all tags (all references)
* in case we match with a tag later in the interation
*/
if r.Hash().String() == ref {
if r.Name().IsTag() {
refTag = r.Name().String()
}
if r.Name().IsBranch() {
refBranch = r.Name().String()
}
}
// we found what we where looking for
if refTag != "" && refBranch != "" {
return storer.ErrStop
}
return nil
})
if err != nil {
return "", err
}
// order matters here see above comment.
if refTag != "" { if refTag != "" {
return refTag, nil return refTag, nil
} }
@ -129,39 +157,7 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
return refBranch, nil return refBranch, nil
} }
// If the above doesn't work, fall back to the old way return "", fmt.Errorf("failed to identify reference (tag/branch) for the checked-out revision '%s'", ref)
// try tags first
tag, err := findGitPrettyRef(ctx, ref, gitDir, "refs/tags")
if err != nil || tag != "" {
return tag, err
}
// and then branches
return findGitPrettyRef(ctx, ref, gitDir, "refs/heads")
}
func findGitPrettyRef(ctx context.Context, head, root, sub string) (string, error) {
var name string
var err = filepath.Walk(filepath.Join(root, sub), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name != "" || info.IsDir() {
return nil
}
var bts []byte
if bts, err = os.ReadFile(path); err != nil {
return err
}
var pointsTo = strings.TrimSpace(string(bts))
if head == pointsTo {
// On Windows paths are separated with backslash character so they should be replaced to provide proper git refs format
name = strings.TrimPrefix(strings.ReplaceAll(strings.Replace(path, root, "", 1), `\`, `/`), "/")
common.Logger(ctx).Debugf("HEAD matches %s", name)
}
return nil
})
return name, err
} }
// FindGithubRepo get the repo // FindGithubRepo get the repo
@ -179,26 +175,27 @@ func FindGithubRepo(ctx context.Context, file, githubInstance, remoteName string
} }
func findGitRemoteURL(ctx context.Context, file, remoteName string) (string, error) { func findGitRemoteURL(ctx context.Context, file, remoteName string) (string, error) {
gitDir, err := findGitDirectory(file) repo, err := git.PlainOpenWithOptions(
file,
&git.PlainOpenOptions{
DetectDotGit: true,
EnableDotGitCommonDir: true,
},
)
if err != nil { if err != nil {
return "", err return "", err
} }
common.Logger(ctx).Debugf("Loading slug from git directory '%s'", gitDir)
gitconfig, err := ini.InsensitiveLoad(fmt.Sprintf("%s/config", gitDir)) remote, err := repo.Remote(remoteName)
if err != nil { if err != nil {
return "", err return "", err
} }
remote, err := gitconfig.GetSection(fmt.Sprintf(`remote "%s"`, remoteName))
if err != nil { if len(remote.Config().URLs) < 1 {
return "", err return "", fmt.Errorf("remote '%s' exists but has no URL", remoteName)
} }
urlKey, err := remote.GetKey("url")
if err != nil { return remote.Config().URLs[0], nil
return "", err
}
url := urlKey.String()
return url, nil
} }
func findGitSlug(url string, githubInstance string) (string, string, error) { func findGitSlug(url string, githubInstance string) (string, string, error) {
@ -222,35 +219,6 @@ func findGitSlug(url string, githubInstance string) (string, string, error) {
return "", url, nil return "", url, nil
} }
func findGitDirectory(fromFile string) (string, error) {
absPath, err := filepath.Abs(fromFile)
if err != nil {
return "", err
}
fi, err := os.Stat(absPath)
if err != nil {
return "", err
}
var dir string
if fi.Mode().IsDir() {
dir = absPath
} else {
dir = filepath.Dir(absPath)
}
gitPath := filepath.Join(dir, ".git")
fi, err = os.Stat(gitPath)
if err == nil && fi.Mode().IsDir() {
return gitPath, nil
} else if dir == "/" || dir == "C:\\" || dir == "c:\\" {
return "", &Error{err: ErrNoRepo}
}
return findGitDirectory(filepath.Dir(dir))
}
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor // NewGitCloneExecutorInput the input for the NewGitCloneExecutor
type NewGitCloneExecutorInput struct { type NewGitCloneExecutorInput struct {
URL string URL string
@ -292,7 +260,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
return nil, err return nil, err
} }
if err = os.Chmod(input.Dir, 0755); err != nil { if err = os.Chmod(input.Dir, 0o755); err != nil {
return nil, err return nil, err
} }
} }

View file

@ -82,12 +82,19 @@ func TestFindGitRemoteURL(t *testing.T) {
assert.NoError(err) assert.NoError(err)
remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name" remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name"
err = gitCmd("config", "-f", fmt.Sprintf("%s/.git/config", basedir), "--add", "remote.origin.url", remoteURL) err = gitCmd("-C", basedir, "remote", "add", "origin", remoteURL)
assert.NoError(err) assert.NoError(err)
u, err := findGitRemoteURL(context.Background(), basedir, "origin") u, err := findGitRemoteURL(context.Background(), basedir, "origin")
assert.NoError(err) assert.NoError(err)
assert.Equal(remoteURL, u) assert.Equal(remoteURL, u)
remoteURL = "git@github.com/AwesomeOwner/MyAwesomeRepo.git"
err = gitCmd("-C", basedir, "remote", "add", "upstream", remoteURL)
assert.NoError(err)
u, err = findGitRemoteURL(context.Background(), basedir, "upstream")
assert.NoError(err)
assert.Equal(remoteURL, u)
} }
func TestGitFindRef(t *testing.T) { func TestGitFindRef(t *testing.T) {
@ -160,7 +167,7 @@ func TestGitFindRef(t *testing.T) {
name := name name := name
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
dir := filepath.Join(basedir, name) dir := filepath.Join(basedir, name)
require.NoError(t, os.MkdirAll(dir, 0755)) require.NoError(t, os.MkdirAll(dir, 0o755))
require.NoError(t, gitCmd("-C", dir, "init", "--initial-branch=master")) require.NoError(t, gitCmd("-C", dir, "init", "--initial-branch=master"))
require.NoError(t, cleanGitHooks(dir)) require.NoError(t, cleanGitHooks(dir))
tt.Prepare(t, dir) tt.Prepare(t, dir)

View file

@ -0,0 +1,73 @@
package container
import (
"context"
"io"
"github.com/nektos/act/pkg/common"
)
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string
Username string
Password string
Entrypoint []string
Cmd []string
WorkingDir string
Env []string
Binds []string
Mounts map[string]string
Name string
Stdout io.Writer
Stderr io.Writer
NetworkMode string
Privileged bool
UsernsMode string
Platform string
Options string
// Gitea specific
AutoRemove bool
}
// FileEntry is a file to copy to a container
type FileEntry struct {
Name string
Mode int64
Body string
}
// Container for managing docker run containers
type Container interface {
Create(capAdd []string, capDrop []string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor
CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor
Exec(command []string, env map[string]string, user, workdir string) common.Executor
UpdateFromEnv(srcPath string, env *map[string]string) common.Executor
UpdateFromImageEnv(env *map[string]string) common.Executor
Remove() common.Executor
Close() common.Executor
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
}
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
type NewDockerBuildExecutorInput struct {
ContextDir string
Dockerfile string
Container Container
ImageTag string
Platform string
}
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
type NewDockerPullExecutorInput struct {
Image string
ForcePull bool
Platform string
Username string
Password string
}

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
@ -36,3 +38,24 @@ func LoadDockerAuthConfig(ctx context.Context, image string) (types.AuthConfig,
return types.AuthConfig(authConfig), nil return types.AuthConfig(authConfig), nil
} }
func LoadDockerAuthConfigs(ctx context.Context) map[string]types.AuthConfig {
logger := common.Logger(ctx)
config, err := config.Load(config.Dir())
if err != nil {
logger.Warnf("Could not load docker config: %v", err)
return nil
}
if !config.ContainsAuth() {
config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
}
creds, _ := config.GetAllCredentials()
authConfigs := make(map[string]types.AuthConfig, len(creds))
for k, v := range creds {
authConfigs[k] = types.AuthConfig(v)
}
return authConfigs
}

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
@ -8,22 +10,14 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
// github.com/docker/docker/builder/dockerignore is deprecated // github.com/docker/docker/builder/dockerignore is deprecated
"github.com/moby/buildkit/frontend/dockerfile/dockerignore" "github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/patternmatcher"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
) )
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
type NewDockerBuildExecutorInput struct {
ContextDir string
Container Container
ImageTag string
Platform string
}
// NewDockerBuildExecutor function to create a run executor for the container // NewDockerBuildExecutor function to create a run executor for the container
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor { func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
@ -47,15 +41,17 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
tags := []string{input.ImageTag} tags := []string{input.ImageTag}
options := types.ImageBuildOptions{ options := types.ImageBuildOptions{
Tags: tags, Tags: tags,
Remove: true, Remove: true,
Platform: input.Platform, Platform: input.Platform,
AuthConfigs: LoadDockerAuthConfigs(ctx),
Dockerfile: input.Dockerfile,
} }
var buildContext io.ReadCloser var buildContext io.ReadCloser
if input.Container != nil { if input.Container != nil {
buildContext, err = input.Container.GetContainerArchive(ctx, input.ContextDir+"/.") buildContext, err = input.Container.GetContainerArchive(ctx, input.ContextDir+"/.")
} else { } else {
buildContext, err = createBuildContext(ctx, input.ContextDir, "Dockerfile") buildContext, err = createBuildContext(ctx, input.ContextDir, input.Dockerfile)
} }
if err != nil { if err != nil {
return err return err
@ -101,8 +97,8 @@ func createBuildContext(ctx context.Context, contextDir string, relDockerfile st
// parses the Dockerfile. Ignore errors here, as they will have been // parses the Dockerfile. Ignore errors here, as they will have been
// caught by validateContextDirectory above. // caught by validateContextDirectory above.
var includes = []string{"."} var includes = []string{"."}
keepThem1, _ := fileutils.Matches(".dockerignore", excludes) keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
keepThem2, _ := fileutils.Matches(relDockerfile, excludes) keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
if keepThem1 || keepThem2 { if keepThem1 || keepThem2 {
includes = append(includes, ".dockerignore", relDockerfile) includes = append(includes, ".dockerignore", relDockerfile)
} }

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
// This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts.go // This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts.go
// appended with license information. // appended with license information.
// //

View file

@ -663,8 +663,8 @@ func TestRunFlagsParseShmSize(t *testing.T) {
func TestParseRestartPolicy(t *testing.T) { func TestParseRestartPolicy(t *testing.T) {
invalids := map[string]string{ invalids := map[string]string{
"always:2:3": "invalid restart policy format", "always:2:3": "invalid restart policy format: maximum retry count must be an integer",
"on-failure:invalid": "maximum retry count must be an integer", "on-failure:invalid": "invalid restart policy format: maximum retry count must be an integer",
} }
valids := map[string]container.RestartPolicy{ valids := map[string]container.RestartPolicy{
"": {}, "": {},

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
@ -5,7 +7,7 @@ import (
"fmt" "fmt"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/client"
) )
// ImageExistsLocally returns a boolean indicating if an image with the // ImageExistsLocally returns a boolean indicating if an image with the
@ -17,33 +19,15 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
} }
defer cli.Close() defer cli.Close()
filters := filters.NewArgs() inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
filters.Add("reference", imageName) if client.IsErrNotFound(err) {
return false, nil
imageListOptions := types.ImageListOptions{ } else if err != nil {
Filters: filters,
}
images, err := cli.ImageList(ctx, imageListOptions)
if err != nil {
return false, err return false, err
} }
if len(images) > 0 { if platform == "" || platform == "any" || fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
if platform == "any" || platform == "" { return true, nil
return true, nil
}
for _, v := range images {
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, v.ID)
if err != nil {
return false, err
}
if fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
return true, nil
}
}
return false, nil
} }
return false, nil return false, nil
@ -52,38 +36,25 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
// RemoveImage removes image from local store, the function is used to run different // RemoveImage removes image from local store, the function is used to run different
// container image architectures // container image architectures
func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) { func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) {
if exists, err := ImageExistsLocally(ctx, imageName, "any"); !exists {
return false, err
}
cli, err := GetDockerClient(ctx) cli, err := GetDockerClient(ctx)
if err != nil { if err != nil {
return false, err return false, err
} }
defer cli.Close()
filters := filters.NewArgs() inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
filters.Add("reference", imageName) if client.IsErrNotFound(err) {
return false, nil
imageListOptions := types.ImageListOptions{ } else if err != nil {
Filters: filters,
}
images, err := cli.ImageList(ctx, imageListOptions)
if err != nil {
return false, err return false, err
} }
if len(images) > 0 { if _, err = cli.ImageRemove(ctx, inspectImage.ID, types.ImageRemoveOptions{
for _, v := range images { Force: force,
if _, err = cli.ImageRemove(ctx, v.ID, types.ImageRemoveOptions{ PruneChildren: pruneChildren,
Force: force, }); err != nil {
PruneChildren: pruneChildren, return false, err
}); err != nil {
return false, err
}
}
return true, nil
} }
return false, nil return true, nil
} }

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
@ -12,15 +14,6 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
) )
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
type NewDockerPullExecutorInput struct {
Image string
ForcePull bool
Platform string
Username string
Password string
}
// NewDockerPullExecutor function to create a run executor for the container // NewDockerPullExecutor function to create a run executor for the container
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor { func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {

View file

@ -1,8 +1,9 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
"archive/tar" "archive/tar"
"bufio"
"bytes" "bytes"
"context" "context"
"errors" "errors"
@ -38,53 +39,6 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
) )
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string
Username string
Password string
Entrypoint []string
Cmd []string
WorkingDir string
Env []string
Binds []string
Mounts map[string]string
Name string
Stdout io.Writer
Stderr io.Writer
NetworkMode string
Privileged bool
UsernsMode string
Platform string
Options string
AutoRemove bool
}
// FileEntry is a file to copy to a container
type FileEntry struct {
Name string
Mode int64
Body string
}
// Container for managing docker run containers
type Container interface {
Create(capAdd []string, capDrop []string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor
CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor
Exec(command []string, env map[string]string, user, workdir string) common.Executor
UpdateFromEnv(srcPath string, env *map[string]string) common.Executor
UpdateFromImageEnv(env *map[string]string) common.Executor
UpdateFromPath(env *map[string]string) common.Executor
Remove() common.Executor
Close() common.Executor
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
}
// NewContainer creates a reference to a container // NewContainer creates a reference to a container
func NewContainer(input *NewContainerInput) ExecutionsEnvironment { func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
cr := new(containerReference) cr := new(containerReference)
@ -190,17 +144,13 @@ func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath s
} }
func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
return cr.extractEnv(srcPath, env).IfNot(common.Dryrun) return parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun)
} }
func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor { func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor {
return cr.extractFromImageEnv(env).IfNot(common.Dryrun) return cr.extractFromImageEnv(env).IfNot(common.Dryrun)
} }
func (cr *containerReference) UpdateFromPath(env *map[string]string) common.Executor {
return cr.extractPath(env).IfNot(common.Dryrun)
}
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor { func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir), common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
@ -413,10 +363,16 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig) logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
hostConfig.Binds = append(hostConfig.Binds, containerConfig.HostConfig.Binds...)
hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...)
binds := hostConfig.Binds
mounts := hostConfig.Mounts
err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride) err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err)
} }
hostConfig.Binds = binds
hostConfig.Mounts = mounts
logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig) logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
return config, hostConfig, nil return config, hostConfig, nil
@ -500,59 +456,6 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
} }
} }
var singleLineEnvPattern, multiLineEnvPattern *regexp.Regexp
func (cr *containerReference) extractEnv(srcPath string, env *map[string]string) common.Executor {
if singleLineEnvPattern == nil {
// Single line pattern matches:
// SOME_VAR=data=moredata
// SOME_VAR=datamoredata
singleLineEnvPattern = regexp.MustCompile(`^([^=]*)\=(.*)$`)
multiLineEnvPattern = regexp.MustCompile(`^([^<]+)<<([\w-]+)$`)
}
localEnv := *env
return func(ctx context.Context) error {
envTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath)
if err != nil {
return nil
}
defer envTar.Close()
reader := tar.NewReader(envTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return fmt.Errorf("failed to read tar archive: %w", 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 multiLineEnvStart := multiLineEnvPattern.FindStringSubmatch(line); multiLineEnvStart != nil {
multiLineEnvKey = multiLineEnvStart[1]
multiLineEnvDelimiter = multiLineEnvStart[2]
}
}
env = &localEnv
return nil
}
}
func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor { func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor {
envMap := *env envMap := *env
return func(ctx context.Context) error { return func(ctx context.Context) error {
@ -585,31 +488,6 @@ func (cr *containerReference) extractFromImageEnv(env *map[string]string) common
} }
} }
func (cr *containerReference) extractPath(env *map[string]string) common.Executor {
localEnv := *env
return func(ctx context.Context) error {
pathTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, localEnv["GITHUB_PATH"])
if err != nil {
return fmt.Errorf("failed to copy from container: %w", err)
}
defer pathTar.Close()
reader := tar.NewReader(pathTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return fmt.Errorf("failed to read tar archive: %w", err)
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
localEnv["PATH"] = fmt.Sprintf("%s:%s", line, localEnv["PATH"])
}
env = &localEnv
return nil
}
}
func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor { func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@ -706,7 +584,7 @@ func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Exe
} }
exp := regexp.MustCompile(`\d+\n`) exp := regexp.MustCompile(`\d+\n`)
found := exp.FindString(sid) found := exp.FindString(sid)
id, err := strconv.ParseInt(found[:len(found)-1], 10, 32) id, err := strconv.ParseInt(strings.TrimSpace(found), 10, 32)
if err != nil { if err != nil {
return nil return nil
} }

View file

@ -0,0 +1,57 @@
//go:build WITHOUT_DOCKER || !(linux || darwin || windows)
package container
import (
"context"
"runtime"
"github.com/docker/docker/api/types"
"github.com/nektos/act/pkg/common"
"github.com/pkg/errors"
)
// ImageExistsLocally returns a boolean indicating if an image with the
// requested name, tag and architecture exists in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
return false, errors.New("Unsupported Operation")
}
// RemoveImage removes image from local store, the function is used to run different
// container image architectures
func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) {
return false, errors.New("Unsupported Operation")
}
// NewDockerBuildExecutor function to create a run executor for the container
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
return func(ctx context.Context) error {
return errors.New("Unsupported Operation")
}
}
// NewDockerPullExecutor function to create a run executor for the container
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
return func(ctx context.Context) error {
return errors.New("Unsupported Operation")
}
}
// NewContainer creates a reference to a container
func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
return nil
}
func RunnerArch(ctx context.Context) string {
return runtime.GOOS
}
func GetHostInfo(ctx context.Context) (info types.Info, err error) {
return types.Info{}, nil
}
func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {
return func(ctx context.Context) error {
return nil
}
}

View file

@ -1,3 +1,5 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (

View file

@ -65,7 +65,7 @@ type copyCollector struct {
func (cc *copyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error { func (cc *copyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
fdestpath := filepath.Join(cc.DstDir, fpath) fdestpath := filepath.Join(cc.DstDir, fpath)
if err := os.MkdirAll(filepath.Dir(fdestpath), 0777); err != nil { if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
return err return err
} }
if f == nil { if f == nil {

View file

@ -76,7 +76,7 @@ func (mfs *memoryFs) Readlink(path string) (string, error) {
func TestIgnoredTrackedfile(t *testing.T) { func TestIgnoredTrackedfile(t *testing.T) {
fs := memfs.New() fs := memfs.New()
_ = fs.MkdirAll("mygitrepo/.git", 0777) _ = fs.MkdirAll("mygitrepo/.git", 0o777)
dotgit, _ := fs.Chroot("mygitrepo/.git") dotgit, _ := fs.Chroot("mygitrepo/.git")
worktree, _ := fs.Chroot("mygitrepo") worktree, _ := fs.Chroot("mygitrepo")
repo, _ := git.Init(filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault()), worktree) repo, _ := git.Init(filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault()), worktree)

View file

@ -2,9 +2,9 @@ package container
import ( import (
"archive/tar" "archive/tar"
"bufio"
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@ -15,14 +15,13 @@ import (
"strings" "strings"
"time" "time"
"errors"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"golang.org/x/term"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/lookpath" "github.com/nektos/act/pkg/lookpath"
"golang.org/x/term"
) )
type HostEnvironment struct { type HostEnvironment struct {
@ -50,7 +49,7 @@ func (e *HostEnvironment) Close() common.Executor {
func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor { func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
for _, f := range files { for _, f := range files {
if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0777); err != nil { if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil {
return err return err
} }
if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil { if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil {
@ -341,77 +340,7 @@ func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[st
} }
func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
localEnv := *env return parseEnvFile(e, srcPath, env)
return func(ctx context.Context) error {
envTar, err := e.GetContainerArchive(ctx, srcPath)
if err != nil {
return nil
}
defer envTar.Close()
reader := tar.NewReader(envTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return err
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
singleLineEnv := strings.Index(line, "=")
multiLineEnv := strings.Index(line, "<<")
if singleLineEnv != -1 && (multiLineEnv == -1 || singleLineEnv < multiLineEnv) {
localEnv[line[:singleLineEnv]] = line[singleLineEnv+1:]
} else if multiLineEnv != -1 {
multiLineEnvContent := ""
multiLineEnvDelimiter := line[multiLineEnv+2:]
delimiterFound := false
for s.Scan() {
content := s.Text()
if content == multiLineEnvDelimiter {
delimiterFound = true
break
}
if multiLineEnvContent != "" {
multiLineEnvContent += "\n"
}
multiLineEnvContent += content
}
if !delimiterFound {
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
}
localEnv[line[:multiLineEnv]] = multiLineEnvContent
} else {
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
}
}
env = &localEnv
return nil
}
}
func (e *HostEnvironment) UpdateFromPath(env *map[string]string) common.Executor {
localEnv := *env
return func(ctx context.Context) error {
pathTar, err := e.GetContainerArchive(ctx, localEnv["GITHUB_PATH"])
if err != nil {
return err
}
defer pathTar.Close()
reader := tar.NewReader(pathTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return err
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
pathSep := string(filepath.ListSeparator)
localEnv[e.GetPathVariableName()] = fmt.Sprintf("%s%s%s", line, pathSep, localEnv[e.GetPathVariableName()])
}
env = &localEnv
return nil
}
} }
func (e *HostEnvironment) Remove() common.Executor { func (e *HostEnvironment) Remove() common.Executor {
@ -454,10 +383,32 @@ func (*HostEnvironment) JoinPathVariable(paths ...string) string {
return strings.Join(paths, string(filepath.ListSeparator)) return strings.Join(paths, string(filepath.ListSeparator))
} }
func goArchToActionArch(arch string) string {
archMapper := map[string]string{
"x86_64": "X64",
"386": "x86",
"aarch64": "arm64",
}
if arch, ok := archMapper[arch]; ok {
return arch
}
return arch
}
func goOsToActionOs(os string) string {
osMapper := map[string]string{
"darwin": "macOS",
}
if os, ok := osMapper[os]; ok {
return os
}
return os
}
func (e *HostEnvironment) GetRunnerContext(ctx context.Context) map[string]interface{} { func (e *HostEnvironment) GetRunnerContext(ctx context.Context) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"os": runtime.GOOS, "os": goOsToActionOs(runtime.GOOS),
"arch": runtime.GOARCH, "arch": goArchToActionArch(runtime.GOARCH),
"temp": e.TmpDir, "temp": e.TmpDir,
"tool_cache": e.ToolCache, "tool_cache": e.ToolCache,
} }

View file

@ -0,0 +1,60 @@
package container
import (
"archive/tar"
"bufio"
"context"
"fmt"
"io"
"strings"
"github.com/nektos/act/pkg/common"
)
func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
localEnv := *env
return func(ctx context.Context) error {
envTar, err := e.GetContainerArchive(ctx, srcPath)
if err != nil {
return nil
}
defer envTar.Close()
reader := tar.NewReader(envTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return err
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
singleLineEnv := strings.Index(line, "=")
multiLineEnv := strings.Index(line, "<<")
if singleLineEnv != -1 && (multiLineEnv == -1 || singleLineEnv < multiLineEnv) {
localEnv[line[:singleLineEnv]] = line[singleLineEnv+1:]
} else if multiLineEnv != -1 {
multiLineEnvContent := ""
multiLineEnvDelimiter := line[multiLineEnv+2:]
delimiterFound := false
for s.Scan() {
content := s.Text()
if content == multiLineEnvDelimiter {
delimiterFound = true
break
}
if multiLineEnvContent != "" {
multiLineEnvContent += "\n"
}
multiLineEnvContent += content
}
if !delimiterFound {
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
}
localEnv[line[:multiLineEnv]] = multiLineEnvContent
} else {
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
}
}
env = &localEnv
return nil
}
}

View file

@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"github.com/rhysd/actionlint" "github.com/rhysd/actionlint"
) )
@ -202,6 +203,9 @@ func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
var files []string var files []string
if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error { 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)) sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
parts := strings.Split(sansPrefix, string(filepath.Separator)) parts := strings.Split(sansPrefix, string(filepath.Separator))
if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) { if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {

View file

@ -15,15 +15,21 @@ type EvaluationEnvironment struct {
Github *model.GithubContext Github *model.GithubContext
Env map[string]string Env map[string]string
Job *model.JobContext Job *model.JobContext
Jobs *map[string]*model.WorkflowCallResult
Steps map[string]*model.StepResult Steps map[string]*model.StepResult
Runner map[string]interface{} Runner map[string]interface{}
Secrets map[string]string Secrets map[string]string
Strategy map[string]interface{} Strategy map[string]interface{}
Matrix map[string]interface{} Matrix map[string]interface{}
Needs map[string]map[string]map[string]string Needs map[string]Needs
Inputs map[string]interface{} Inputs map[string]interface{}
} }
type Needs struct {
Outputs map[string]string `json:"outputs"`
Result string `json:"result"`
}
type Config struct { type Config struct {
Run *model.Run Run *model.Run
WorkingDir string WorkingDir string
@ -150,6 +156,11 @@ func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableN
return impl.env.Env, nil return impl.env.Env, nil
case "job": case "job":
return impl.env.Job, nil return impl.env.Job, nil
case "jobs":
if impl.env.Jobs == nil {
return nil, fmt.Errorf("Unavailable context: jobs")
}
return impl.env.Jobs, nil
case "steps": case "steps":
return impl.env.Steps, nil return impl.env.Steps, nil
case "runner": case "runner":
@ -361,8 +372,16 @@ func (impl *interperterImpl) compareValues(leftValue reflect.Value, rightValue r
return impl.compareNumber(leftValue.Float(), rightValue.Float(), kind) return impl.compareNumber(leftValue.Float(), rightValue.Float(), kind)
case reflect.Invalid:
if rightValue.Kind() == reflect.Invalid {
return true, nil
}
// not possible situation - params are converted to the same type in code above
return nil, fmt.Errorf("Compare params of Invalid type: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
default: default:
return nil, fmt.Errorf("TODO: evaluateCompare not implemented! left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind()) return nil, fmt.Errorf("Compare not implemented for types: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
} }
} }

View file

@ -69,6 +69,11 @@ func TestOperators(t *testing.T) {
{`true || false`, true, "or", ""}, {`true || false`, true, "or", ""},
{`fromJSON('{}') && true`, true, "and-boolean-object", ""}, {`fromJSON('{}') && true`, true, "and-boolean-object", ""},
{`fromJSON('{}') || false`, make(map[string]interface{}), "or-boolean-object", ""}, {`fromJSON('{}') || false`, make(map[string]interface{}), "or-boolean-object", ""},
{"github.event.commits[0].author.username != github.event.commits[1].author.username", true, "property-comparison1", ""},
{"github.event.commits[0].author.username1 != github.event.commits[1].author.username", true, "property-comparison2", ""},
{"github.event.commits[0].author.username != github.event.commits[1].author.username1", true, "property-comparison3", ""},
{"github.event.commits[0].author.username1 != github.event.commits[1].author.username2", true, "property-comparison4", ""},
{"secrets != env", nil, "property-comparison5", "Compare not implemented for types: left: map, right: map"},
} }
env := &EvaluationEnvironment{ env := &EvaluationEnvironment{
@ -555,6 +560,7 @@ func TestContexts(t *testing.T) {
{"strategy.fail-fast", true, "strategy-context"}, {"strategy.fail-fast", true, "strategy-context"},
{"matrix.os", "Linux", "matrix-context"}, {"matrix.os", "Linux", "matrix-context"},
{"needs.job-id.outputs.output-name", "value", "needs-context"}, {"needs.job-id.outputs.output-name", "value", "needs-context"},
{"needs.job-id.result", "success", "needs-context"},
{"inputs.name", "value", "inputs-context"}, {"inputs.name", "value", "inputs-context"},
} }
@ -593,11 +599,12 @@ func TestContexts(t *testing.T) {
Matrix: map[string]interface{}{ Matrix: map[string]interface{}{
"os": "Linux", "os": "Linux",
}, },
Needs: map[string]map[string]map[string]string{ Needs: map[string]Needs{
"job-id": { "job-id": {
"outputs": { Outputs: map[string]string{
"output-name": "value", "output-name": "value",
}, },
Result: "success",
}, },
}, },
Inputs: map[string]interface{}{ Inputs: map[string]interface{}{

View file

@ -3,6 +3,7 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git" "github.com/nektos/act/pkg/common/git"
@ -89,26 +90,22 @@ func withDefaultBranch(ctx context.Context, b string, event map[string]interface
var findGitRef = git.FindGitRef var findGitRef = git.FindGitRef
var findGitRevision = git.FindGitRevision var findGitRevision = git.FindGitRevision
func (ghc *GithubContext) SetRefAndSha(ctx context.Context, defaultBranch string, repoPath string) { func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repoPath string) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
switch ghc.EventName { switch ghc.EventName {
case "pull_request_target": case "pull_request_target":
ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef) ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef)
ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
case "pull_request", "pull_request_review", "pull_request_review_comment": case "pull_request", "pull_request_review", "pull_request_review_comment":
ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"]) ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"])
case "deployment", "deployment_status": case "deployment", "deployment_status":
ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref")) ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref"))
ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
case "release": case "release":
ghc.Ref = asString(nestedMapLookup(ghc.Event, "release", "tag_name")) ghc.Ref = fmt.Sprintf("refs/tags/%s", asString(nestedMapLookup(ghc.Event, "release", "tag_name")))
case "push", "create", "workflow_dispatch": case "push", "create", "workflow_dispatch":
ghc.Ref = asString(ghc.Event["ref"]) ghc.Ref = asString(ghc.Event["ref"])
if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
ghc.Sha = asString(ghc.Event["after"])
}
default: default:
defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch")) defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))
if defaultBranch != "" { if defaultBranch != "" {
@ -136,6 +133,23 @@ func (ghc *GithubContext) SetRefAndSha(ctx context.Context, defaultBranch string
ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))) ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch")))
} }
} }
}
func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) {
logger := common.Logger(ctx)
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
switch ghc.EventName {
case "pull_request_target":
ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
case "deployment", "deployment_status":
ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
case "push", "create", "workflow_dispatch":
if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
ghc.Sha = asString(ghc.Event["after"])
}
}
if ghc.Sha == "" { if ghc.Sha == "" {
_, sha, err := findGitRevision(ctx, repoPath) _, sha, err := findGitRevision(ctx, repoPath)
@ -146,3 +160,51 @@ func (ghc *GithubContext) SetRefAndSha(ctx context.Context, defaultBranch string
} }
} }
} }
func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance string, remoteName string, repoPath string) {
if ghc.Repository == "" {
repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName)
if err != nil {
common.Logger(ctx).Warningf("unable to get git repo: %v", err)
return
}
ghc.Repository = repo
}
ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0]
}
func (ghc *GithubContext) SetRefTypeAndName() {
var refType, refName string
// https://docs.github.com/en/actions/learn-github-actions/environment-variables
if strings.HasPrefix(ghc.Ref, "refs/tags/") {
refType = "tag"
refName = ghc.Ref[len("refs/tags/"):]
} else if strings.HasPrefix(ghc.Ref, "refs/heads/") {
refType = "branch"
refName = ghc.Ref[len("refs/heads/"):]
} else if strings.HasPrefix(ghc.Ref, "refs/pull/") {
refType = ""
refName = ghc.Ref[len("refs/pull/"):]
}
if ghc.RefType == "" {
ghc.RefType = refType
}
if ghc.RefName == "" {
ghc.RefName = refName
}
}
func (ghc *GithubContext) SetBaseAndHeadRef() {
if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" {
if ghc.BaseRef == "" {
ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
}
if ghc.HeadRef == "" {
ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
}
}
}

View file

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestSetRefAndSha(t *testing.T) { func TestSetRef(t *testing.T) {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
oldFindGitRef := findGitRef oldFindGitRef := findGitRef
@ -29,38 +29,31 @@ func TestSetRefAndSha(t *testing.T) {
eventName string eventName string
event map[string]interface{} event map[string]interface{}
ref string ref string
sha string refName string
}{ }{
{ {
eventName: "pull_request_target", eventName: "pull_request_target",
event: map[string]interface{}{ event: map[string]interface{}{},
"pull_request": map[string]interface{}{ ref: "refs/heads/master",
"base": map[string]interface{}{ refName: "master",
"sha": "pr-base-sha",
},
},
},
ref: "refs/heads/master",
sha: "pr-base-sha",
}, },
{ {
eventName: "pull_request", eventName: "pull_request",
event: map[string]interface{}{ event: map[string]interface{}{
"number": 1234., "number": 1234.,
}, },
ref: "refs/pull/1234/merge", ref: "refs/pull/1234/merge",
sha: "1234fakesha", refName: "1234/merge",
}, },
{ {
eventName: "deployment", eventName: "deployment",
event: map[string]interface{}{ event: map[string]interface{}{
"deployment": map[string]interface{}{ "deployment": map[string]interface{}{
"ref": "refs/heads/somebranch", "ref": "refs/heads/somebranch",
"sha": "deployment-sha",
}, },
}, },
ref: "refs/heads/somebranch", ref: "refs/heads/somebranch",
sha: "deployment-sha", refName: "somebranch",
}, },
{ {
eventName: "release", eventName: "release",
@ -69,18 +62,16 @@ func TestSetRefAndSha(t *testing.T) {
"tag_name": "v1.0.0", "tag_name": "v1.0.0",
}, },
}, },
ref: "v1.0.0", ref: "refs/tags/v1.0.0",
sha: "1234fakesha", refName: "v1.0.0",
}, },
{ {
eventName: "push", eventName: "push",
event: map[string]interface{}{ event: map[string]interface{}{
"ref": "refs/heads/somebranch", "ref": "refs/heads/somebranch",
"after": "push-sha",
"deleted": false,
}, },
ref: "refs/heads/somebranch", ref: "refs/heads/somebranch",
sha: "push-sha", refName: "somebranch",
}, },
{ {
eventName: "unknown", eventName: "unknown",
@ -89,14 +80,14 @@ func TestSetRefAndSha(t *testing.T) {
"default_branch": "main", "default_branch": "main",
}, },
}, },
ref: "refs/heads/main", ref: "refs/heads/main",
sha: "1234fakesha", refName: "main",
}, },
{ {
eventName: "no-event", eventName: "no-event",
event: map[string]interface{}{}, event: map[string]interface{}{},
ref: "refs/heads/master", ref: "refs/heads/master",
sha: "1234fakesha", refName: "master",
}, },
} }
@ -108,10 +99,11 @@ func TestSetRefAndSha(t *testing.T) {
Event: table.event, Event: table.event,
} }
ghc.SetRefAndSha(context.Background(), "main", "/some/dir") ghc.SetRef(context.Background(), "main", "/some/dir")
ghc.SetRefTypeAndName()
assert.Equal(t, table.ref, ghc.Ref) assert.Equal(t, table.ref, ghc.Ref)
assert.Equal(t, table.sha, ghc.Sha) assert.Equal(t, table.refName, ghc.RefName)
}) })
} }
@ -125,9 +117,96 @@ func TestSetRefAndSha(t *testing.T) {
Event: map[string]interface{}{}, Event: map[string]interface{}{},
} }
ghc.SetRefAndSha(context.Background(), "", "/some/dir") ghc.SetRef(context.Background(), "", "/some/dir")
assert.Equal(t, "refs/heads/master", ghc.Ref) assert.Equal(t, "refs/heads/master", ghc.Ref)
assert.Equal(t, "1234fakesha", ghc.Sha)
}) })
} }
func TestSetSha(t *testing.T) {
log.SetLevel(log.DebugLevel)
oldFindGitRef := findGitRef
oldFindGitRevision := findGitRevision
defer func() { findGitRef = oldFindGitRef }()
defer func() { findGitRevision = oldFindGitRevision }()
findGitRef = func(ctx context.Context, file string) (string, error) {
return "refs/heads/master", nil
}
findGitRevision = func(ctx context.Context, file string) (string, string, error) {
return "", "1234fakesha", nil
}
tables := []struct {
eventName string
event map[string]interface{}
sha string
}{
{
eventName: "pull_request_target",
event: map[string]interface{}{
"pull_request": map[string]interface{}{
"base": map[string]interface{}{
"sha": "pr-base-sha",
},
},
},
sha: "pr-base-sha",
},
{
eventName: "pull_request",
event: map[string]interface{}{
"number": 1234.,
},
sha: "1234fakesha",
},
{
eventName: "deployment",
event: map[string]interface{}{
"deployment": map[string]interface{}{
"sha": "deployment-sha",
},
},
sha: "deployment-sha",
},
{
eventName: "release",
event: map[string]interface{}{},
sha: "1234fakesha",
},
{
eventName: "push",
event: map[string]interface{}{
"after": "push-sha",
"deleted": false,
},
sha: "push-sha",
},
{
eventName: "unknown",
event: map[string]interface{}{},
sha: "1234fakesha",
},
{
eventName: "no-event",
event: map[string]interface{}{},
sha: "1234fakesha",
},
}
for _, table := range tables {
t.Run(table.eventName, func(t *testing.T) {
ghc := &GithubContext{
EventName: table.eventName,
BaseRef: "master",
Event: table.event,
}
ghc.SetSha(context.Background(), "/some/dir")
assert.Equal(t, table.sha, ghc.Sha)
})
}
}

View file

@ -15,9 +15,9 @@ import (
// WorkflowPlanner contains methods for creating plans // WorkflowPlanner contains methods for creating plans
type WorkflowPlanner interface { type WorkflowPlanner interface {
PlanEvent(eventName string) *Plan PlanEvent(eventName string) (*Plan, error)
PlanJob(jobName string) *Plan PlanJob(jobName string) (*Plan, error)
PlanAll() *Plan PlanAll() (*Plan, error)
GetEvents() []string GetEvents() []string
} }
@ -176,47 +176,76 @@ type workflowPlanner struct {
} }
// PlanEvent builds a new list of runs to execute in parallel for an event name // PlanEvent builds a new list of runs to execute in parallel for an event name
func (wp *workflowPlanner) PlanEvent(eventName string) *Plan { func (wp *workflowPlanner) PlanEvent(eventName string) (*Plan, error) {
plan := new(Plan) plan := new(Plan)
if len(wp.workflows) == 0 { if len(wp.workflows) == 0 {
log.Debugf("no events found for workflow: %s", eventName) log.Debug("no workflows found by planner")
return plan, nil
} }
var lastErr error
for _, w := range wp.workflows { for _, w := range wp.workflows {
for _, e := range w.On() { events := w.On()
if len(events) == 0 {
log.Debugf("no events found for workflow: %s", w.File)
continue
}
for _, e := range events {
if e == eventName { if e == eventName {
plan.mergeStages(createStages(w, w.GetJobIDs()...)) stages, err := createStages(w, w.GetJobIDs()...)
if err != nil {
log.Warn(err)
lastErr = err
} else {
plan.mergeStages(stages)
}
} }
} }
} }
return plan return plan, lastErr
} }
// PlanJob builds a new run to execute in parallel for a job name // PlanJob builds a new run to execute in parallel for a job name
func (wp *workflowPlanner) PlanJob(jobName string) *Plan { func (wp *workflowPlanner) PlanJob(jobName string) (*Plan, error) {
plan := new(Plan) plan := new(Plan)
if len(wp.workflows) == 0 { if len(wp.workflows) == 0 {
log.Debugf("no jobs found for workflow: %s", jobName) log.Debugf("no jobs found for workflow: %s", jobName)
} }
var lastErr error
for _, w := range wp.workflows { for _, w := range wp.workflows {
plan.mergeStages(createStages(w, jobName)) stages, err := createStages(w, jobName)
if err != nil {
log.Warn(err)
lastErr = err
} else {
plan.mergeStages(stages)
}
} }
return plan return plan, lastErr
} }
// PlanAll builds a new run to execute in parallel all // PlanAll builds a new run to execute in parallel all
func (wp *workflowPlanner) PlanAll() *Plan { func (wp *workflowPlanner) PlanAll() (*Plan, error) {
plan := new(Plan) plan := new(Plan)
if len(wp.workflows) == 0 { if len(wp.workflows) == 0 {
log.Debugf("no jobs found for loaded workflows") log.Debug("no workflows found by planner")
return plan, nil
} }
var lastErr error
for _, w := range wp.workflows { for _, w := range wp.workflows {
plan.mergeStages(createStages(w, w.GetJobIDs()...)) stages, err := createStages(w, w.GetJobIDs()...)
if err != nil {
log.Warn(err)
lastErr = err
} else {
plan.mergeStages(stages)
}
} }
return plan return plan, lastErr
} }
// GetEvents gets all the events in the workflows file // GetEvents gets all the events in the workflows file
@ -289,7 +318,7 @@ func (p *Plan) mergeStages(stages []*Stage) {
p.Stages = newStages p.Stages = newStages
} }
func createStages(w *Workflow, jobIDs ...string) []*Stage { func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
// first, build a list of all the necessary jobs to run, and their dependencies // first, build a list of all the necessary jobs to run, and their dependencies
jobDependencies := make(map[string][]string) jobDependencies := make(map[string][]string)
for len(jobIDs) > 0 { for len(jobIDs) > 0 {
@ -306,6 +335,8 @@ func createStages(w *Workflow, jobIDs ...string) []*Stage {
jobIDs = newJobIDs jobIDs = newJobIDs
} }
var err error
// next, build an execution graph // next, build an execution graph
stages := make([]*Stage, 0) stages := make([]*Stage, 0)
for len(jobDependencies) > 0 { for len(jobDependencies) > 0 {
@ -321,12 +352,16 @@ func createStages(w *Workflow, jobIDs ...string) []*Stage {
} }
} }
if len(stage.Runs) == 0 { if len(stage.Runs) == 0 {
log.Fatalf("Unable to build dependency graph!") return nil, fmt.Errorf("unable to build dependency graph for %s (%s)", w.Name, w.File)
} }
stages = append(stages, stage) stages = append(stages, stage)
} }
return stages if len(stages) == 0 && err != nil {
return nil, err
}
return stages, nil
} }
// return true iff all strings in srcList exist in at least one of the stages // return true iff all strings in srcList exist in at least one of the stages

View file

@ -42,5 +42,4 @@ type StepResult struct {
Outputs map[string]string `json:"outputs"` Outputs map[string]string `json:"outputs"`
Conclusion stepStatus `json:"conclusion"` Conclusion stepStatus `json:"conclusion"`
Outcome stepStatus `json:"outcome"` Outcome stepStatus `json:"outcome"`
State map[string]string
} }

View file

@ -124,6 +124,48 @@ func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
return &config return &config
} }
type WorkflowCallInput struct {
Description string `yaml:"description"`
Required bool `yaml:"required"`
Default string `yaml:"default"`
Type string `yaml:"type"`
}
type WorkflowCallOutput struct {
Description string `yaml:"description"`
Value string `yaml:"value"`
}
type WorkflowCall struct {
Inputs map[string]WorkflowCallInput `yaml:"inputs"`
Outputs map[string]WorkflowCallOutput `yaml:"outputs"`
}
type WorkflowCallResult struct {
Outputs map[string]string
}
func (w *Workflow) WorkflowCallConfig() *WorkflowCall {
if w.RawOn.Kind != yaml.MappingNode {
return nil
}
var val map[string]yaml.Node
err := w.RawOn.Decode(&val)
if err != nil {
log.Fatal(err)
}
var config WorkflowCall
node := val["workflow_call"]
err = node.Decode(&config)
if err != nil {
log.Fatal(err)
}
return &config
}
// Job is the structure of one job in a workflow // Job is the structure of one job in a workflow
type Job struct { type Job struct {
Name string `yaml:"name"` Name string `yaml:"name"`
@ -139,6 +181,8 @@ type Job struct {
Defaults Defaults `yaml:"defaults"` Defaults Defaults `yaml:"defaults"`
Outputs map[string]string `yaml:"outputs"` Outputs map[string]string `yaml:"outputs"`
Uses string `yaml:"uses"` Uses string `yaml:"uses"`
With map[string]interface{} `yaml:"with"`
RawSecrets yaml.Node `yaml:"secrets"`
Result string Result string
} }
@ -193,6 +237,34 @@ func (s Strategy) GetFailFast() bool {
return failFast return failFast
} }
func (j *Job) InheritSecrets() bool {
if j.RawSecrets.Kind != yaml.ScalarNode {
return false
}
var val string
err := j.RawSecrets.Decode(&val)
if err != nil {
log.Fatal(err)
}
return val == "inherit"
}
func (j *Job) Secrets() map[string]string {
if j.RawSecrets.Kind != yaml.MappingNode {
return nil
}
var val map[string]string
err := j.RawSecrets.Decode(&val)
if err != nil {
log.Fatal(err)
}
return val
}
// Container details for the job // Container details for the job
func (j *Job) Container() *ContainerSpec { func (j *Job) Container() *ContainerSpec {
var val *ContainerSpec var val *ContainerSpec
@ -483,16 +555,8 @@ func (s *Step) String() string {
} }
// Environments returns string-based key=value map for a step // Environments returns string-based key=value map for a step
// Note: all keys are uppercase
func (s *Step) Environment() map[string]string { func (s *Step) Environment() map[string]string {
env := environment(s.Env) return environment(s.Env)
for k, v := range env {
delete(env, k)
env[strings.ToUpper(k)] = v
}
return env
} }
// GetEnv gets the env for a step // GetEnv gets the env for a step

View file

@ -323,7 +323,8 @@ func TestReadWorkflow_Strategy(t *testing.T) {
w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true) w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true)
assert.NoError(t, err) assert.NoError(t, err)
p := w.PlanJob("strategy-only-max-parallel") p, err := w.PlanJob("strategy-only-max-parallel")
assert.NoError(t, err)
assert.Equal(t, len(p.Stages), 1) assert.Equal(t, len(p.Stages), 1)
assert.Equal(t, len(p.Stages[0].Runs), 1) assert.Equal(t, len(p.Stages[0].Runs), 1)

View file

@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
@ -29,10 +30,9 @@ type actionStep interface {
type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
type ( type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
actionYamlReader func(filename string) (io.Reader, io.Closer, error)
fileWriter func(filename string, data []byte, perm fs.FileMode) error type fileWriter func(filename string, data []byte, perm fs.FileMode) error
)
type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor
@ -156,6 +156,8 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingDocker: case model.ActionRunsUsingDocker:
location := actionLocation location := actionLocation
@ -235,14 +237,17 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
var prepImage common.Executor var prepImage common.Executor
var image string var image string
forcePull := false
if strings.HasPrefix(action.Runs.Image, "docker://") { if strings.HasPrefix(action.Runs.Image, "docker://") {
image = strings.TrimPrefix(action.Runs.Image, "docker://") image = strings.TrimPrefix(action.Runs.Image, "docker://")
// Apply forcePull only for prebuild docker images
forcePull = rc.Config.ForcePull
} else { } else {
// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names // "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest") image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-")) image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
image = strings.ToLower(image) image = strings.ToLower(image)
contextDir := filepath.Join(basedir, action.Runs.Main) contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any") anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
if err != nil { if err != nil {
@ -272,6 +277,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
} }
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{ prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir, ContextDir: contextDir,
Dockerfile: fileName,
ImageTag: image, ImageTag: image,
Container: actionContainer, Container: actionContainer,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
@ -303,7 +309,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint) stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
prepImage, prepImage,
stepContainer.Pull(rc.Config.ForcePull), stepContainer.Pull(forcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true), stepContainer.Start(true),
@ -364,7 +370,10 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
binds, mounts := rc.GetBindsAndMounts() binds, mounts := rc.GetBindsAndMounts()
networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
if rc.IsHostEnv(ctx) {
networkMode = "default"
}
stepContainer := container.NewContainer(&container.NewContainerInput{ stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd, Cmd: cmd,
Entrypoint: entrypoint, Entrypoint: entrypoint,
@ -375,22 +384,23 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID), Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
Env: envList, Env: envList,
Mounts: mounts, Mounts: mounts,
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()), NetworkMode: networkMode,
Binds: binds, Binds: binds,
Stdout: logWriter, Stdout: logWriter,
Stderr: logWriter, Stderr: logWriter,
Privileged: rc.Config.Privileged, Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
Options: rc.Config.ContainerOptions,
AutoRemove: rc.Config.AutoRemove, AutoRemove: rc.Config.AutoRemove,
}) })
return stepContainer return stepContainer
} }
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) { func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
stepResult := rc.StepResults[step.getStepModel().ID] state, ok := rc.IntraActionState[step.getStepModel().ID]
if stepResult != nil { if ok {
for name, value := range stepResult.State { for name, value := range state {
envName := fmt.Sprintf("STATE_%s", name) envName := fmt.Sprintf("STATE_%s", name)
(*env)[envName] = value (*env)[envName] = value
} }
@ -503,6 +513,8 @@ func runPreStep(step actionStep) common.Executor {
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingComposite: case model.ActionRunsUsingComposite:
@ -510,7 +522,10 @@ func runPreStep(step actionStep) common.Executor {
step.getCompositeRunContext(ctx) step.getCompositeRunContext(ctx)
} }
return step.getCompositeSteps().pre(ctx) if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
return steps.pre(ctx)
}
return fmt.Errorf("missing steps in composite action")
case model.ActionRunsUsingGo: case model.ActionRunsUsingGo:
// defaults in pre steps were missing, however provided inputs are available // defaults in pre steps were missing, however provided inputs are available
@ -626,6 +641,8 @@ func runPostStep(step actionStep) common.Executor {
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingComposite: case model.ActionRunsUsingComposite:
@ -633,7 +650,10 @@ func runPostStep(step actionStep) common.Executor {
return err return err
} }
return step.getCompositeSteps().post(ctx) if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
return steps.post(ctx)
}
return fmt.Errorf("missing steps in composite action")
case model.ActionRunsUsingGo: case model.ActionRunsUsingGo:
populateEnvsFromSavedState(step.getEnv(), step, rc) populateEnvsFromSavedState(step.getEnv(), step, rc)

View file

@ -66,6 +66,7 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
JobContainer: parent.JobContainer, JobContainer: parent.JobContainer,
ActionPath: actionPath, ActionPath: actionPath,
Env: env, Env: env,
GlobalEnv: parent.GlobalEnv,
Masks: parent.Masks, Masks: parent.Masks,
ExtraPath: parent.ExtraPath, ExtraPath: parent.ExtraPath,
Parent: parent, Parent: parent,
@ -85,6 +86,10 @@ func execAsComposite(step actionStep) common.Executor {
steps := step.getCompositeSteps() steps := step.getCompositeSteps()
if steps == nil || steps.main == nil {
return fmt.Errorf("missing steps in composite action")
}
ctx = WithCompositeLogger(ctx, &compositeRC.Masks) ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
err := steps.main(ctx) err := steps.main(ctx)
@ -99,6 +104,14 @@ func execAsComposite(step actionStep) common.Executor {
rc.Masks = append(rc.Masks, compositeRC.Masks...) rc.Masks = append(rc.Masks, compositeRC.Masks...)
rc.ExtraPath = compositeRC.ExtraPath rc.ExtraPath = compositeRC.ExtraPath
// compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
for k, v := range compositeRC.GlobalEnv {
rc.Env[k] = v
if rc.GlobalEnv == nil {
rc.GlobalEnv = map[string]string{}
}
rc.GlobalEnv[k] = v
}
return err return err
} }

View file

@ -201,10 +201,11 @@ func TestActionRunner(t *testing.T) {
}, },
CurrentStep: "post-step", CurrentStep: "post-step",
StepResults: map[string]*model.StepResult{ StepResults: map[string]*model.StepResult{
"step": {},
},
IntraActionState: map[string]map[string]string{
"step": { "step": {
State: map[string]string{ "name": "state value",
"name": "state value",
},
}, },
}, },
}, },

66
pkg/runner/command.go Executable file → Normal file
View file

@ -16,22 +16,27 @@ func init() {
commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$") commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$")
} }
func tryParseRawActionCommand(line string) (command string, kvPairs map[string]string, arg string, ok bool) {
if m := commandPatternGA.FindStringSubmatch(line); m != nil {
command = m[1]
kvPairs = parseKeyValuePairs(m[3], ",")
arg = m[4]
ok = true
} else if m := commandPatternADO.FindStringSubmatch(line); m != nil {
command = m[1]
kvPairs = parseKeyValuePairs(m[3], ";")
arg = m[4]
ok = true
}
return
}
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
logger := common.Logger(ctx) logger := common.Logger(ctx)
resumeCommand := "" resumeCommand := ""
return func(line string) bool { return func(line string) bool {
var command string command, kvPairs, arg, ok := tryParseRawActionCommand(line)
var kvPairs map[string]string if !ok {
var arg string
if m := commandPatternGA.FindStringSubmatch(line); m != nil {
command = m[1]
kvPairs = parseKeyValuePairs(m[3], ",")
arg = m[4]
} else if m := commandPatternADO.FindStringSubmatch(line); m != nil {
command = m[1]
kvPairs = parseKeyValuePairs(m[3], ";")
arg = m[4]
} else {
return true return true
} }
@ -66,6 +71,8 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
case "save-state": case "save-state":
logger.Infof(" \U0001f4be %s", line) logger.Infof(" \U0001f4be %s", line)
rc.saveState(ctx, kvPairs, arg) rc.saveState(ctx, kvPairs, arg)
case "add-matcher":
logger.Infof(" \U00002753 add-matcher %s", arg)
default: default:
logger.Infof(" \U00002753 %s", line) logger.Infof(" \U00002753 %s", line)
} }
@ -75,11 +82,17 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
} }
func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string) { func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string) {
common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", kvPairs["name"], arg) name := kvPairs["name"]
common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", name, arg)
if rc.Env == nil { if rc.Env == nil {
rc.Env = make(map[string]string) rc.Env = make(map[string]string)
} }
rc.Env[kvPairs["name"]] = arg rc.Env[name] = arg
// for composite action GITHUB_ENV and set-env passing
if rc.GlobalEnv == nil {
rc.GlobalEnv = map[string]string{}
}
rc.GlobalEnv[name] = arg
} }
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) { func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@ -101,7 +114,13 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
} }
func (rc *RunContext) addPath(ctx context.Context, arg string) { func (rc *RunContext) addPath(ctx context.Context, arg string) {
common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg) common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg)
rc.ExtraPath = append(rc.ExtraPath, arg) extraPath := []string{arg}
for _, v := range rc.ExtraPath {
if v != arg {
extraPath = append(extraPath, v)
}
}
rc.ExtraPath = extraPath
} }
func parseKeyValuePairs(kvPairs string, separator string) map[string]string { func parseKeyValuePairs(kvPairs string, separator string) map[string]string {
@ -147,13 +166,16 @@ func unescapeKvPairs(kvPairs map[string]string) map[string]string {
} }
func (rc *RunContext) saveState(ctx context.Context, kvPairs map[string]string, arg string) { func (rc *RunContext) saveState(ctx context.Context, kvPairs map[string]string, arg string) {
if rc.CurrentStep != "" { stepID := rc.CurrentStep
stepResult := rc.StepResults[rc.CurrentStep] if stepID != "" {
if stepResult != nil { if rc.IntraActionState == nil {
if stepResult.State == nil { rc.IntraActionState = map[string]map[string]string{}
stepResult.State = map[string]string{}
}
stepResult.State[kvPairs["name"]] = arg
} }
state, ok := rc.IntraActionState[stepID]
if !ok {
state = map[string]string{}
rc.IntraActionState[stepID] = state
}
state[kvPairs["name"]] = arg
} }
} }

View file

@ -64,7 +64,7 @@ func TestAddpath(t *testing.T) {
a.Equal("/zoo", rc.ExtraPath[0]) a.Equal("/zoo", rc.ExtraPath[0])
handler("::add-path::/boo\n") handler("::add-path::/boo\n")
a.Equal("/boo", rc.ExtraPath[1]) a.Equal("/boo", rc.ExtraPath[0])
} }
func TestStopCommands(t *testing.T) { func TestStopCommands(t *testing.T) {
@ -102,7 +102,7 @@ func TestAddpathADO(t *testing.T) {
a.Equal("/zoo", rc.ExtraPath[0]) a.Equal("/zoo", rc.ExtraPath[0])
handler("##[add-path]/boo\n") handler("##[add-path]/boo\n")
a.Equal("/boo", rc.ExtraPath[1]) a.Equal("/boo", rc.ExtraPath[0])
} }
func TestAddmask(t *testing.T) { func TestAddmask(t *testing.T) {
@ -177,11 +177,7 @@ func TestAddmaskUsemask(t *testing.T) {
func TestSaveState(t *testing.T) { func TestSaveState(t *testing.T) {
rc := &RunContext{ rc := &RunContext{
CurrentStep: "step", CurrentStep: "step",
StepResults: map[string]*model.StepResult{ StepResults: map[string]*model.StepResult{},
"step": {
State: map[string]string{},
},
},
} }
ctx := context.Background() ctx := context.Background()
@ -189,5 +185,5 @@ func TestSaveState(t *testing.T) {
handler := rc.commandHandler(ctx) handler := rc.commandHandler(ctx)
handler("::save-state name=state-name::state-value\n") handler("::save-state name=state-name::state-value\n")
assert.Equal(t, "state-value", rc.StepResults["step"].State["state-name"]) assert.Equal(t, "state-value", rc.IntraActionState["step"]["state-name"])
} }

View file

@ -2,6 +2,7 @@ package runner
import ( import (
"context" "context"
"io"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
@ -49,11 +50,6 @@ func (cm *containerMock) UpdateFromImageEnv(env *map[string]string) common.Execu
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
} }
func (cm *containerMock) UpdateFromPath(env *map[string]string) common.Executor {
args := cm.Called(env)
return args.Get(0).(func(context.Context) error)
}
func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor { func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor {
args := cm.Called(destPath, files) args := cm.Called(destPath, files)
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
@ -63,7 +59,17 @@ func (cm *containerMock) CopyDir(destPath string, srcPath string, useGitIgnore b
args := cm.Called(destPath, srcPath, useGitIgnore) args := cm.Called(destPath, srcPath, useGitIgnore)
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
} }
func (cm *containerMock) Exec(command []string, env map[string]string, user, workdir string) common.Executor { func (cm *containerMock) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
args := cm.Called(command, env, user, workdir) args := cm.Called(command, env, user, workdir)
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
} }
func (cm *containerMock) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
args := cm.Called(ctx, srcPath)
err, hasErr := args.Get(1).(error)
if !hasErr {
err = nil
}
return args.Get(0).(io.ReadCloser), err
}

View file

@ -21,8 +21,14 @@ type ExpressionEvaluator interface {
// NewExpressionEvaluator creates a new evaluator // NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator { func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator {
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
}
func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) ExpressionEvaluator {
var workflowCallResult map[string]*model.WorkflowCallResult
// todo: cleanup EvaluationEnvironment creation // todo: cleanup EvaluationEnvironment creation
using := make(map[string]map[string]map[string]string) using := make(map[string]exprparser.Needs)
strategy := make(map[string]interface{}) strategy := make(map[string]interface{})
if rc.Run != nil { if rc.Run != nil {
job := rc.Run.Job() job := rc.Run.Job()
@ -35,8 +41,26 @@ func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEval
jobNeeds := rc.Run.Job().Needs() jobNeeds := rc.Run.Job().Needs()
for _, needs := range jobNeeds { for _, needs := range jobNeeds {
using[needs] = map[string]map[string]string{ using[needs] = exprparser.Needs{
"outputs": jobs[needs].Outputs, Outputs: jobs[needs].Outputs,
Result: jobs[needs].Result,
}
}
// only setup jobs context in case of workflow_call
// and existing expression evaluator (this means, jobs are at
// least ready to run)
if rc.caller != nil && rc.ExprEval != nil {
workflowCallResult = map[string]*model.WorkflowCallResult{}
for jobName, job := range jobs {
result := model.WorkflowCallResult{
Outputs: map[string]string{},
}
for k, v := range job.Outputs {
result.Outputs[k] = v
}
workflowCallResult[jobName] = &result
} }
} }
} }
@ -46,12 +70,13 @@ func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEval
ee := &exprparser.EvaluationEnvironment{ ee := &exprparser.EvaluationEnvironment{
Github: ghc, Github: ghc,
Env: rc.GetEnv(), Env: env,
Job: rc.getJobContext(), Job: rc.getJobContext(),
Jobs: &workflowCallResult,
// todo: should be unavailable // todo: should be unavailable
// but required to interpolate/evaluate the step outputs on the job // but required to interpolate/evaluate the step outputs on the job
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets, Secrets: getWorkflowSecrets(ctx, rc),
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
@ -82,10 +107,11 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
jobs := rc.Run.Workflow.Jobs jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs() jobNeeds := rc.Run.Job().Needs()
using := make(map[string]map[string]map[string]string) using := make(map[string]exprparser.Needs)
for _, needs := range jobNeeds { for _, needs := range jobNeeds {
using[needs] = map[string]map[string]string{ using[needs] = exprparser.Needs{
"outputs": jobs[needs].Outputs, Outputs: jobs[needs].Outputs,
Result: jobs[needs].Result,
} }
} }
@ -97,7 +123,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
Env: *step.getEnv(), Env: *step.getEnv(),
Job: rc.getJobContext(), Job: rc.getJobContext(),
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets, Secrets: getWorkflowSecrets(ctx, rc),
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
@ -311,6 +337,8 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} { func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} {
inputs := map[string]interface{}{} inputs := map[string]interface{}{}
setupWorkflowInputs(ctx, &inputs, rc)
var env map[string]string var env map[string]string
if step != nil { if step != nil {
env = *step.getEnv() env = *step.getEnv()
@ -343,3 +371,54 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
return inputs return inputs
} }
func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc *RunContext) {
if rc.caller != nil {
config := rc.Run.Workflow.WorkflowCallConfig()
for name, input := range config.Inputs {
value := rc.caller.runContext.Run.Job().With[name]
if value != nil {
if str, ok := value.(string); ok {
// evaluate using the calling RunContext (outside)
value = rc.caller.runContext.ExprEval.Interpolate(ctx, str)
}
}
if value == nil && config != nil && config.Inputs != nil {
value = input.Default
if rc.ExprEval != nil {
if str, ok := value.(string); ok {
// evaluate using the called RunContext (inside)
value = rc.ExprEval.Interpolate(ctx, str)
}
}
}
(*inputs)[name] = value
}
}
}
func getWorkflowSecrets(ctx context.Context, rc *RunContext) map[string]string {
if rc.caller != nil {
job := rc.caller.runContext.Run.Job()
secrets := job.Secrets()
if secrets == nil && job.InheritSecrets() {
secrets = rc.caller.runContext.Config.Secrets
}
if secrets == nil {
secrets = map[string]string{}
}
for k, v := range secrets {
secrets[k] = rc.caller.runContext.ExprEval.Interpolate(ctx, v)
}
return secrets
}
return rc.Config.Secrets
}

View file

@ -96,21 +96,18 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
} }
postExecutor = postExecutor.Finally(func(ctx context.Context) error { postExecutor = postExecutor.Finally(func(ctx context.Context) error {
logger := common.Logger(ctx)
jobError := common.JobError(ctx) jobError := common.JobError(ctx)
if jobError != nil { var err error
info.result("failure") if rc.Config.AutoRemove || jobError == nil {
logger.WithField("jobResult", "failure").Infof("\U0001F3C1 Job failed") // always allow 1 min for stopping and removing the runner, even if we were cancelled
} else { ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
err := info.stopContainer()(ctx) defer cancel()
if err != nil { err = info.stopContainer()(ctx)
return err
}
info.result("success")
logger.WithField("jobResult", "success").Infof("\U0001F3C1 Job succeeded")
} }
setJobResult(ctx, info, rc, jobError == nil)
setJobOutputs(ctx, rc)
return nil return err
}) })
pipeline := make([]common.Executor, 0) pipeline := make([]common.Executor, 0)
@ -123,7 +120,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
if ctx.Err() == context.Canceled { if ctx.Err() == context.Canceled {
// in case of an aborted run, we still should execute the // in case of an aborted run, we still should execute the
// post steps to allow cleanup. // post steps to allow cleanup.
ctx, cancel = context.WithTimeout(WithJobLogger(context.Background(), rc.Run.JobID, rc.String(), rc.Config, &rc.Masks, rc.Matrix), 5*time.Minute) ctx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), 5*time.Minute)
defer cancel() defer cancel()
} }
return postExecutor(ctx) return postExecutor(ctx)
@ -132,6 +129,49 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
Finally(info.closeContainer())) Finally(info.closeContainer()))
} }
func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) {
logger := common.Logger(ctx)
jobResult := "success"
// we have only one result for a whole matrix build, so we need
// to keep an existing result state if we run a matrix
if len(info.matrix()) > 0 && rc.Run.Job().Result != "" {
jobResult = rc.Run.Job().Result
}
if !success {
jobResult = "failure"
}
info.result(jobResult)
if rc.caller != nil {
// set reusable workflow job result
rc.caller.runContext.result(jobResult)
}
jobResultMessage := "succeeded"
if jobResult != "success" {
jobResultMessage = "failed"
}
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
}
func setJobOutputs(ctx context.Context, rc *RunContext) {
if rc.caller != nil {
// map outputs for reusable workflows
callerOutputs := make(map[string]string)
ee := rc.NewExpressionEvaluator(ctx)
for k, v := range rc.Run.Workflow.WorkflowCallConfig().Outputs {
callerOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value))
}
rc.caller.runContext.Run.Job().Outputs = callerOutputs
}
}
func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor { func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())

View file

@ -15,15 +15,15 @@ import (
func TestJobExecutor(t *testing.T) { func TestJobExecutor(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms}, {workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, secrets},
{workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, {workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
{workdir, "uses-github-root", "push", "", platforms}, {workdir, "uses-github-root", "push", "", platforms, secrets},
{workdir, "uses-github-path", "push", "", platforms}, {workdir, "uses-github-path", "push", "", platforms, secrets},
{workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms, secrets},
{workdir, "uses-github-full-sha", "push", "", platforms}, {workdir, "uses-github-full-sha", "push", "", platforms, secrets},
{workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms}, {workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, secrets},
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms}, {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets},
} }
// These tests are sufficient to only check syntax. // These tests are sufficient to only check syntax.
ctx := common.WithDryrun(context.Background(), true) ctx := common.WithDryrun(context.Background(), true)

View file

@ -57,38 +57,59 @@ func WithMasks(ctx context.Context, masks *[]string) context.Context {
return context.WithValue(ctx, masksContextKeyVal, masks) return context.WithValue(ctx, masksContextKeyVal, masks)
} }
type JobLoggerFactory interface {
WithJobLogger() *logrus.Logger
}
type jobLoggerFactoryContextKey string
var jobLoggerFactoryContextKeyVal = (jobLoggerFactoryContextKey)("jobloggerkey")
func WithJobLoggerFactory(ctx context.Context, factory JobLoggerFactory) context.Context {
return context.WithValue(ctx, jobLoggerFactoryContextKeyVal, factory)
}
// WithJobLogger attaches a new logger to context that is aware of steps // WithJobLogger attaches a new logger to context that is aware of steps
func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context { func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context {
mux.Lock()
defer mux.Unlock()
var formatter logrus.Formatter
if config.JSONLogger {
formatter = &jobLogJSONFormatter{
formatter: &logrus.JSONFormatter{},
masker: valueMasker(config.InsecureSecrets, config.Secrets),
}
} else {
formatter = &jobLogFormatter{
color: colors[nextColor%len(colors)],
masker: valueMasker(config.InsecureSecrets, config.Secrets),
}
}
nextColor++
ctx = WithMasks(ctx, masks) ctx = WithMasks(ctx, masks)
logger := logrus.New() var logger *logrus.Logger
if hook := common.LoggerHook(ctx); hook != nil { if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
logger.AddHook(hook) logger = jobLoggerFactory.WithJobLogger()
}
logger.SetFormatter(formatter)
logger.SetOutput(os.Stdout)
if config.JobLoggerLevel != nil {
logger.SetLevel(*config.JobLoggerLevel)
} else { } else {
logger.SetLevel(logrus.TraceLevel) var formatter logrus.Formatter
if config.JSONLogger {
formatter = &logrus.JSONFormatter{}
} else {
mux.Lock()
defer mux.Unlock()
nextColor++
formatter = &jobLogFormatter{
color: colors[nextColor%len(colors)],
}
}
logger = logrus.New()
logger.SetOutput(os.Stdout)
logger.SetLevel(logrus.GetLevel())
logger.SetFormatter(formatter)
} }
{ // Adapt to Gitea
if hook := common.LoggerHook(ctx); hook != nil {
logger.AddHook(hook)
}
if config.JobLoggerLevel != nil {
logger.SetLevel(*config.JobLoggerLevel)
} else {
logger.SetLevel(logrus.TraceLevel)
}
}
logger.SetFormatter(&maskedFormatter{
Formatter: logger.Formatter,
masker: valueMasker(config.InsecureSecrets, config.Secrets),
})
rtn := logger.WithFields(logrus.Fields{ rtn := logger.WithFields(logrus.Fields{
"job": jobName, "job": jobName,
"jobID": jobID, "jobID": jobID,
@ -157,16 +178,22 @@ func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor
} }
} }
type jobLogFormatter struct { type maskedFormatter struct {
color int logrus.Formatter
masker entryProcessor masker entryProcessor
} }
func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
return f.Formatter.Format(f.masker(entry))
}
type jobLogFormatter struct {
color int
}
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
entry = f.masker(entry)
if f.isColored(entry) { if f.isColored(entry) {
f.printColored(b, entry) f.printColored(b, entry)
} else { } else {
@ -233,12 +260,3 @@ func checkIfTerminal(w io.Writer) bool {
return false return false
} }
} }
type jobLogJSONFormatter struct {
masker entryProcessor
formatter *logrus.JSONFormatter
}
func (f *jobLogJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
return f.formatter.Format(f.masker(entry))
}

View file

@ -0,0 +1,129 @@
package runner
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path"
"regexp"
"sync"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/model"
)
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, rc.Run.Job().Uses)
}
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
uses := rc.Run.Job().Uses
remoteReusableWorkflow := newRemoteReusableWorkflow(uses)
if remoteReusableWorkflow == nil {
return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.github/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
}
remoteReusableWorkflow.URL = rc.Config.GitHubInstance
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(uses))
return common.NewPipelineExecutor(
newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir)),
newReusableWorkflowExecutor(rc, workflowDir, fmt.Sprintf("./.github/workflows/%s", remoteReusableWorkflow.Filename)),
)
}
var (
executorLock sync.Mutex
)
func newMutexExecutor(executor common.Executor) common.Executor {
return func(ctx context.Context) error {
executorLock.Lock()
defer executorLock.Unlock()
return executor(ctx)
}
}
func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkflow, targetDirectory string) common.Executor {
return common.NewConditionalExecutor(
func(ctx context.Context) bool {
_, err := os.Stat(targetDirectory)
notExists := errors.Is(err, fs.ErrNotExist)
return notExists
},
git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
URL: remoteReusableWorkflow.CloneURL(),
Ref: remoteReusableWorkflow.Ref,
Dir: targetDirectory,
Token: rc.Config.Token,
}),
nil,
)
}
func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
return func(ctx context.Context) error {
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
if err != nil {
return err
}
plan, err := planner.PlanEvent("workflow_call")
if err != nil {
return err
}
runner, err := NewReusableWorkflowRunner(rc)
if err != nil {
return err
}
return runner.NewPlanExecutor(plan)(ctx)
}
}
func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
runner := &runnerImpl{
config: rc.Config,
eventJSON: rc.EventJSON,
caller: &caller{
runContext: rc,
},
}
return runner.configure()
}
type remoteReusableWorkflow struct {
URL string
Org string
Repo string
Filename string
Ref string
}
func (r *remoteReusableWorkflow) CloneURL() string {
return fmt.Sprintf("https://%s/%s/%s", r.URL, r.Org, r.Repo)
}
func newRemoteReusableWorkflow(uses string) *remoteReusableWorkflow {
// GitHub docs:
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
r := regexp.MustCompile(`^([^/]+)/([^/]+)/.github/workflows/([^@]+)@(.*)$`)
matches := r.FindStringSubmatch(uses)
if len(matches) != 5 {
return nil
}
return &remoteReusableWorkflow{
Org: matches[1],
Repo: matches[2],
Filename: matches[3],
Ref: matches[4],
URL: "github.com",
}
}

View file

@ -1,12 +1,16 @@
package runner package runner
import ( import (
"archive/tar"
"bufio"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -19,7 +23,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser" "github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
@ -33,9 +36,11 @@ type RunContext struct {
Run *model.Run Run *model.Run
EventJSON string EventJSON string
Env map[string]string Env map[string]string
GlobalEnv map[string]string // to pass env changes of GITHUB_ENV and set-env correctly, due to dirty Env field
ExtraPath []string ExtraPath []string
CurrentStep string CurrentStep string
StepResults map[string]*model.StepResult StepResults map[string]*model.StepResult
IntraActionState map[string]map[string]string
ExprEval ExpressionEvaluator ExprEval ExpressionEvaluator
JobContainer container.ExecutionsEnvironment JobContainer container.ExecutionsEnvironment
OutputMappings map[MappableOutput]MappableOutput OutputMappings map[MappableOutput]MappableOutput
@ -44,6 +49,7 @@ type RunContext struct {
Parent *RunContext Parent *RunContext
Masks []string Masks []string
cleanUpJobContainer common.Executor cleanUpJobContainer common.Executor
caller *caller // job calling this RunContext (reusable workflows)
} }
func (rc *RunContext) AddMask(mask string) { func (rc *RunContext) AddMask(mask string) {
@ -56,7 +62,13 @@ type MappableOutput struct {
} }
func (rc *RunContext) String() string { func (rc *RunContext) String() string {
return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
if rc.caller != nil {
// prefix the reusable workflow with the caller job
// this is required to create unique container names
name = fmt.Sprintf("%s/%s", rc.caller.runContext.Run.JobID, name)
}
return name
} }
// GetEnv returns the env for the context // GetEnv returns the env for the context
@ -145,15 +157,15 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
_, _ = rand.Read(randBytes) _, _ = rand.Read(randBytes)
miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes)) miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes))
actPath := filepath.Join(miscpath, "act") actPath := filepath.Join(miscpath, "act")
if err := os.MkdirAll(actPath, 0777); err != nil { if err := os.MkdirAll(actPath, 0o777); err != nil {
return err return err
} }
path := filepath.Join(miscpath, "hostexecutor") path := filepath.Join(miscpath, "hostexecutor")
if err := os.MkdirAll(path, 0777); err != nil { if err := os.MkdirAll(path, 0o777); err != nil {
return err return err
} }
runnerTmp := filepath.Join(miscpath, "tmp") runnerTmp := filepath.Join(miscpath, "tmp")
if err := os.MkdirAll(runnerTmp, 0777); err != nil { if err := os.MkdirAll(runnerTmp, 0o777); err != nil {
return err return err
} }
toolCache := filepath.Join(cacheDir, "tool_cache") toolCache := filepath.Join(cacheDir, "tool_cache")
@ -169,29 +181,28 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
StdOut: logWriter, StdOut: logWriter,
} }
rc.cleanUpJobContainer = rc.JobContainer.Remove() rc.cleanUpJobContainer = rc.JobContainer.Remove()
rc.Env["RUNNER_TOOL_CACHE"] = toolCache for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
rc.Env["RUNNER_OS"] = runtime.GOOS if v, ok := v.(string); ok {
rc.Env["RUNNER_ARCH"] = runtime.GOARCH rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
rc.Env["RUNNER_TEMP"] = runnerTmp }
}
for _, env := range os.Environ() { for _, env := range os.Environ() {
i := strings.Index(env, "=") if k, v, ok := strings.Cut(env, "="); ok {
if i > 0 { // don't override
rc.Env[env[0:i]] = env[i+1:] if _, ok := rc.Env[k]; !ok {
rc.Env[k] = v
}
} }
} }
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
Name: "workflow/event.json", Name: "workflow/event.json",
Mode: 0644, Mode: 0o644,
Body: rc.EventJSON, Body: rc.EventJSON,
}, &container.FileEntry{ }, &container.FileEntry{
Name: "workflow/envs.txt", Name: "workflow/envs.txt",
Mode: 0666, Mode: 0o666,
Body: "",
}, &container.FileEntry{
Name: "workflow/paths.txt",
Mode: 0666,
Body: "", Body: "",
}), }),
)(ctx) )(ctx)
@ -226,6 +237,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux")) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx))) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
envList = append(envList, fmt.Sprintf("%s=%s", "LANG", "C.UTF-8")) // Use same locale as GitHub Actions
ext := container.LinuxContainerEnvironmentExtensions{} ext := container.LinuxContainerEnvironmentExtensions{}
binds, mounts := rc.GetBindsAndMounts() binds, mounts := rc.GetBindsAndMounts()
@ -268,19 +280,13 @@ func (rc *RunContext) startJobContainer() common.Executor {
rc.stopJobContainer(), rc.stopJobContainer(),
rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
rc.JobContainer.Start(false), rc.JobContainer.Start(false),
rc.JobContainer.UpdateFromImageEnv(&rc.Env),
rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env),
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
Name: "workflow/event.json", Name: "workflow/event.json",
Mode: 0644, Mode: 0o644,
Body: rc.EventJSON, Body: rc.EventJSON,
}, &container.FileEntry{ }, &container.FileEntry{
Name: "workflow/envs.txt", Name: "workflow/envs.txt",
Mode: 0666, Mode: 0o666,
Body: "",
}, &container.FileEntry{
Name: "workflow/paths.txt",
Mode: 0666,
Body: "", Body: "",
}), }),
)(ctx) )(ctx)
@ -293,6 +299,51 @@ func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user
} }
} }
func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) {
if rc.ExtraPath != nil && len(rc.ExtraPath) > 0 {
path := rc.JobContainer.GetPathVariableName()
if (*env)[path] == "" {
cenv := map[string]string{}
var cpath string
if err := rc.JobContainer.UpdateFromImageEnv(&cenv)(ctx); err == nil {
if p, ok := cenv[path]; ok {
cpath = p
}
}
if len(cpath) == 0 {
cpath = rc.JobContainer.DefaultPathVariable()
}
(*env)[path] = cpath
}
(*env)[path] = rc.JobContainer.JoinPathVariable(append(rc.ExtraPath, (*env)[path])...)
}
}
func (rc *RunContext) UpdateExtraPath(ctx context.Context, githubEnvPath string) error {
if common.Dryrun(ctx) {
return nil
}
pathTar, err := rc.JobContainer.GetContainerArchive(ctx, githubEnvPath)
if err != nil {
return err
}
defer pathTar.Close()
reader := tar.NewReader(pathTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return err
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
if len(line) > 0 {
rc.addPath(ctx, line)
}
}
return nil
}
// stopJobContainer removes the job container (if it exists) and its volume (if it exists) if !rc.Config.ReuseContainers // stopJobContainer removes the job container (if it exists) and its volume (if it exists) if !rc.Config.ReuseContainers
func (rc *RunContext) stopJobContainer() common.Executor { func (rc *RunContext) stopJobContainer() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
@ -335,14 +386,18 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
func (rc *RunContext) startContainer() common.Executor { func (rc *RunContext) startContainer() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
image := rc.platformImage(ctx) if rc.IsHostEnv(ctx) {
if strings.EqualFold(image, "-self-hosted") {
return rc.startHostEnvironment()(ctx) return rc.startHostEnvironment()(ctx)
} }
return rc.startJobContainer()(ctx) return rc.startJobContainer()(ctx)
} }
} }
func (rc *RunContext) IsHostEnv(ctx context.Context) bool {
image := rc.platformImage(ctx)
return strings.EqualFold(image, "-self-hosted")
}
func (rc *RunContext) stopContainer() common.Executor { func (rc *RunContext) stopContainer() common.Executor {
return rc.stopJobContainer() return rc.stopJobContainer()
} }
@ -370,16 +425,25 @@ func (rc *RunContext) steps() []*model.Step {
// Executor returns a pipeline executor for all the steps in the job // Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) Executor() common.Executor { func (rc *RunContext) Executor() common.Executor {
var executor common.Executor
switch rc.Run.Job().Type() {
case model.JobTypeDefault:
executor = newJobExecutor(rc, &stepFactoryImpl{}, rc)
case model.JobTypeReusableWorkflowLocal:
executor = newLocalReusableWorkflowExecutor(rc)
case model.JobTypeReusableWorkflowRemote:
executor = newRemoteReusableWorkflowExecutor(rc)
}
return func(ctx context.Context) error { return func(ctx context.Context) error {
isEnabled, err := rc.isEnabled(ctx) res, err := rc.isEnabled(ctx)
if err != nil { if err != nil {
return err return err
} }
if res {
if isEnabled { return executor(ctx)
return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx)
} }
return nil return nil
} }
} }
@ -421,7 +485,7 @@ func (rc *RunContext) options(ctx context.Context) string {
job := rc.Run.Job() job := rc.Run.Job()
c := job.Container() c := job.Container()
if c == nil { if c == nil {
return "" return rc.Config.ContainerOptions
} }
return c.Options return c.Options
@ -439,6 +503,10 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
return false, nil return false, nil
} }
if job.Type() != model.JobTypeDefault {
return true, nil
}
img := rc.platformImage(ctx) img := rc.platformImage(ctx)
if img == "" { if img == "" {
if job.RunsOn() == nil { if job.RunsOn() == nil {
@ -466,26 +534,16 @@ func mergeMaps(maps ...map[string]string) map[string]string {
// deprecated: use createSimpleContainerName // deprecated: use createSimpleContainerName
func createContainerName(parts ...string) string { func createContainerName(parts ...string) string {
name := make([]string, 0) name := strings.Join(parts, "-")
pattern := regexp.MustCompile("[^a-zA-Z0-9]") pattern := regexp.MustCompile("[^a-zA-Z0-9]")
partLen := (30 / len(parts)) - 1 name = pattern.ReplaceAllString(name, "-")
for i, part := range parts { name = strings.ReplaceAll(name, "--", "-")
if i == len(parts)-1 { hash := sha256.Sum256([]byte(name))
name = append(name, pattern.ReplaceAllString(part, "-"))
} else { // SHA256 is 64 hex characters. So trim name to 63 characters to make room for the hash and separator
// If any part has a '-<number>' on the end it is likely part of a matrix job. trimmedName := strings.Trim(trimToLen(name, 63), "-")
// Let's preserve the number to prevent clashes in container names.
re := regexp.MustCompile("-[0-9]+$") return fmt.Sprintf("%s-%x", trimmedName, hash)
num := re.FindStringSubmatch(part)
if len(num) > 0 {
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen-len(num[0])))
name = append(name, num[0])
} else {
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen))
}
}
}
return strings.ReplaceAll(strings.Trim(strings.Join(name, "-"), "-"), "--", "-")
} }
func createSimpleContainerName(parts ...string) string { func createSimpleContainerName(parts ...string) string {
@ -542,11 +600,20 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
EventName: rc.Config.EventName, EventName: rc.Config.EventName,
Action: rc.CurrentStep, Action: rc.CurrentStep,
Token: rc.Config.Token, Token: rc.Config.Token,
Job: rc.Run.JobID,
ActionPath: rc.ActionPath, ActionPath: rc.ActionPath,
RepositoryOwner: rc.Config.Env["GITHUB_REPOSITORY_OWNER"], RepositoryOwner: rc.Config.Env["GITHUB_REPOSITORY_OWNER"],
RetentionDays: rc.Config.Env["GITHUB_RETENTION_DAYS"], RetentionDays: rc.Config.Env["GITHUB_RETENTION_DAYS"],
RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"], RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"],
RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"], RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"],
Repository: rc.Config.Env["GITHUB_REPOSITORY"],
Ref: rc.Config.Env["GITHUB_REF"],
Sha: rc.Config.Env["SHA_REF"],
RefName: rc.Config.Env["GITHUB_REF_NAME"],
RefType: rc.Config.Env["GITHUB_REF_TYPE"],
BaseRef: rc.Config.Env["GITHUB_BASE_REF"],
HeadRef: rc.Config.Env["GITHUB_HEAD_REF"],
Workspace: rc.Config.Env["GITHUB_WORKSPACE"],
} }
if rc.JobContainer != nil { if rc.JobContainer != nil {
ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json" ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json"
@ -575,58 +642,45 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
ghc.Actor = "nektos/act" ghc.Actor = "nektos/act"
} }
if preset := rc.Config.PresetGitHubContext; preset != nil { { // Adapt to Gitea
ghc.Event = preset.Event if preset := rc.Config.PresetGitHubContext; preset != nil {
ghc.RunID = preset.RunID ghc.Event = preset.Event
ghc.RunNumber = preset.RunNumber ghc.RunID = preset.RunID
ghc.Actor = preset.Actor ghc.RunNumber = preset.RunNumber
ghc.Repository = preset.Repository ghc.Actor = preset.Actor
ghc.EventName = preset.EventName ghc.Repository = preset.Repository
ghc.Sha = preset.Sha ghc.EventName = preset.EventName
ghc.Ref = preset.Ref ghc.Sha = preset.Sha
ghc.RefName = preset.RefName ghc.Ref = preset.Ref
ghc.RefType = preset.RefType ghc.RefName = preset.RefName
ghc.HeadRef = preset.HeadRef ghc.RefType = preset.RefType
ghc.BaseRef = preset.BaseRef ghc.HeadRef = preset.HeadRef
ghc.Token = preset.Token ghc.BaseRef = preset.BaseRef
ghc.RepositoryOwner = preset.RepositoryOwner ghc.Token = preset.Token
ghc.RetentionDays = preset.RetentionDays ghc.RepositoryOwner = preset.RepositoryOwner
return ghc ghc.RetentionDays = preset.RetentionDays
} return ghc
repoPath := rc.Config.Workdir
repo, err := git.FindGithubRepo(ctx, repoPath, rc.Config.GitHubInstance, rc.Config.RemoteName)
if err != nil {
logger.Warningf("unable to get git repo: %v", err)
} else {
ghc.Repository = repo
if ghc.RepositoryOwner == "" {
ghc.RepositoryOwner = strings.Split(repo, "/")[0]
} }
} }
if rc.EventJSON != "" { if rc.EventJSON != "" {
err = json.Unmarshal([]byte(rc.EventJSON), &ghc.Event) err := json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
if err != nil { if err != nil {
logger.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err) logger.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err)
} }
} }
if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" { ghc.SetBaseAndHeadRef()
ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref")) repoPath := rc.Config.Workdir
ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref")) ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath)
if ghc.Ref == "" {
ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath)
}
if ghc.Sha == "" {
ghc.SetSha(ctx, repoPath)
} }
ghc.SetRefAndSha(ctx, rc.Config.DefaultBranch, repoPath) ghc.SetRefTypeAndName()
// https://docs.github.com/en/actions/learn-github-actions/environment-variables
if strings.HasPrefix(ghc.Ref, "refs/tags/") {
ghc.RefType = "tag"
ghc.RefName = ghc.Ref[len("refs/tags/"):]
} else if strings.HasPrefix(ghc.Ref, "refs/heads/") {
ghc.RefType = "branch"
ghc.RefName = ghc.Ref[len("refs/heads/"):]
}
return ghc return ghc
} }
@ -657,15 +711,6 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
return true return true
} }
func asString(v interface{}) string {
if v == nil {
return ""
} else if s, ok := v.(string); ok {
return s
}
return ""
}
func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) { func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) {
var ok bool var ok bool
@ -685,8 +730,6 @@ func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{})
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string { func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string {
env["CI"] = "true" env["CI"] = "true"
env["GITHUB_ENV"] = rc.JobContainer.GetActPath() + "/workflow/envs.txt"
env["GITHUB_PATH"] = rc.JobContainer.GetActPath() + "/workflow/paths.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
@ -705,27 +748,45 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
env["GITHUB_REF_NAME"] = github.RefName env["GITHUB_REF_NAME"] = github.RefName
env["GITHUB_REF_TYPE"] = github.RefType env["GITHUB_REF_TYPE"] = github.RefType
env["GITHUB_TOKEN"] = github.Token env["GITHUB_TOKEN"] = github.Token
env["GITHUB_SERVER_URL"] = "https://github.com" env["GITHUB_JOB"] = github.Job
env["GITHUB_API_URL"] = "https://api.github.com"
env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql"
env["GITHUB_BASE_REF"] = github.BaseRef
env["GITHUB_HEAD_REF"] = github.HeadRef
env["GITHUB_JOB"] = rc.JobName
env["GITHUB_REPOSITORY_OWNER"] = github.RepositoryOwner env["GITHUB_REPOSITORY_OWNER"] = github.RepositoryOwner
env["GITHUB_RETENTION_DAYS"] = github.RetentionDays env["GITHUB_RETENTION_DAYS"] = github.RetentionDays
env["RUNNER_PERFLOG"] = github.RunnerPerflog env["RUNNER_PERFLOG"] = github.RunnerPerflog
env["RUNNER_TRACKING_ID"] = github.RunnerTrackingID env["RUNNER_TRACKING_ID"] = github.RunnerTrackingID
env["GITHUB_BASE_REF"] = github.BaseRef
env["GITHUB_HEAD_REF"] = github.HeadRef
defaultServerURL := "https://github.com"
defaultAPIURL := "https://api.github.com"
defaultGraphqlURL := "https://api.github.com/graphql"
if rc.Config.GitHubInstance != "github.com" { if rc.Config.GitHubInstance != "github.com" {
hasProtocol := strings.HasPrefix(rc.Config.GitHubInstance, "http://") || strings.HasPrefix(rc.Config.GitHubInstance, "https://") defaultServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
if hasProtocol { defaultAPIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
env["GITHUB_SERVER_URL"] = rc.Config.GitHubInstance defaultGraphqlURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
env["GITHUB_API_URL"] = fmt.Sprintf("%s/api/v1", rc.Config.GitHubInstance) }
env["GITHUB_GRAPHQL_URL"] = "" // disable graphql url because Gitea doesn't support that
} else { { // Adapt to Gitea
env["GITHUB_SERVER_URL"] = fmt.Sprintf("https://%s", rc.Config.GitHubInstance) instance := rc.Config.GitHubInstance
env["GITHUB_API_URL"] = fmt.Sprintf("https://%s/api/v1", rc.Config.GitHubInstance) if !strings.HasPrefix(instance, "http://") &&
env["GITHUB_GRAPHQL_URL"] = "" // disable graphql url because Gitea doesn't support that !strings.HasPrefix(instance, "https://") {
instance = "https://" + instance
} }
defaultServerURL = instance
defaultAPIURL = instance + "/api/v1" // the version of Gitea is v1
defaultGraphqlURL = "" // Gitea doesn't support graphql
}
if env["GITHUB_SERVER_URL"] == "" {
env["GITHUB_SERVER_URL"] = defaultServerURL
}
if env["GITHUB_API_URL"] == "" {
env["GITHUB_API_URL"] = defaultAPIURL
}
if env["GITHUB_GRAPHQL_URL"] == "" {
env["GITHUB_GRAPHQL_URL"] = defaultGraphqlURL
} }
if rc.Config.ArtifactServerPath != "" { if rc.Config.ArtifactServerPath != "" {
@ -754,7 +815,7 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
func setActionRuntimeVars(rc *RunContext, env map[string]string) { func setActionRuntimeVars(rc *RunContext, env map[string]string) {
actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL") actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL")
if actionsRuntimeURL == "" { if actionsRuntimeURL == "" {
actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", common.GetOutboundIP().String(), rc.Config.ArtifactServerPort) actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", rc.Config.ArtifactServerAddr, rc.Config.ArtifactServerPort)
} }
env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL

View file

@ -144,6 +144,7 @@ func TestRunContext_EvalBool(t *testing.T) {
// Check github context // Check github context
{in: "github.actor == 'nektos/act'", out: true}, {in: "github.actor == 'nektos/act'", out: true},
{in: "github.actor == 'unknown'", out: false}, {in: "github.actor == 'unknown'", out: false},
{in: "github.job == 'job1'", out: true},
// The special ACT flag // The special ACT flag
{in: "${{ env.ACT }}", out: true}, {in: "${{ env.ACT }}", out: true},
{in: "${{ !env.ACT }}", out: false}, {in: "${{ !env.ACT }}", out: false},
@ -364,6 +365,7 @@ func TestGetGitHubContext(t *testing.T) {
StepResults: map[string]*model.StepResult{}, StepResults: map[string]*model.StepResult{},
OutputMappings: map[MappableOutput]MappableOutput{}, OutputMappings: map[MappableOutput]MappableOutput{},
} }
rc.Run.JobID = "job1"
ghc := rc.getGithubContext(context.Background()) ghc := rc.getGithubContext(context.Background())
@ -392,6 +394,7 @@ func TestGetGitHubContext(t *testing.T) {
assert.Equal(t, ghc.RepositoryOwner, owner) assert.Equal(t, ghc.RepositoryOwner, owner)
assert.Equal(t, ghc.RunnerPerflog, "/dev/null") assert.Equal(t, ghc.RunnerPerflog, "/dev/null")
assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"]) assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"])
assert.Equal(t, ghc.Job, "job1")
} }
func TestGetGithubContextRef(t *testing.T) { func TestGetGithubContextRef(t *testing.T) {
@ -410,7 +413,7 @@ func TestGetGithubContextRef(t *testing.T) {
{event: "pull_request_target", json: `{"pull_request":{"base":{"ref": "main"}}}`, ref: "refs/heads/main"}, {event: "pull_request_target", json: `{"pull_request":{"base":{"ref": "main"}}}`, ref: "refs/heads/main"},
{event: "deployment", json: `{"deployment": {"ref": "tag-name"}}`, ref: "tag-name"}, {event: "deployment", json: `{"deployment": {"ref": "tag-name"}}`, ref: "tag-name"},
{event: "deployment_status", json: `{"deployment": {"ref": "tag-name"}}`, ref: "tag-name"}, {event: "deployment_status", json: `{"deployment": {"ref": "tag-name"}}`, ref: "tag-name"},
{event: "release", json: `{"release": {"tag_name": "tag-name"}}`, ref: "tag-name"}, {event: "release", json: `{"release": {"tag_name": "tag-name"}}`, ref: "refs/tags/tag-name"},
} }
for _, data := range table { for _, data := range table {

View file

@ -2,6 +2,7 @@ package runner
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -32,6 +33,7 @@ type Config struct {
LogOutput bool // log the output from docker run LogOutput bool // log the output from docker run
JSONLogger bool // use json or text logger JSONLogger bool // use json or text logger
Env map[string]string // env for containers Env map[string]string // env for containers
Inputs map[string]string // manually passed action inputs
Secrets map[string]string // list of secrets Secrets map[string]string // list of secrets
Token string // GitHub token Token string // GitHub token
InsecureSecrets bool // switch hiding output when printing to terminal InsecureSecrets bool // switch hiding output when printing to terminal
@ -40,12 +42,14 @@ type Config struct {
UsernsMode string // user namespace to use UsernsMode string // user namespace to use
ContainerArchitecture string // Desired OS/architecture platform for running containers ContainerArchitecture string // Desired OS/architecture platform for running containers
ContainerDaemonSocket string // Path to Docker daemon socket ContainerDaemonSocket string // Path to Docker daemon socket
ContainerOptions string // Options for the job container
UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true
GitHubInstance string // GitHub instance to use, default "github.com" GitHubInstance string // GitHub instance to use, default "github.com"
ContainerCapAdd []string // list of kernel capabilities to add to the containers ContainerCapAdd []string // list of kernel capabilities to add to the containers
ContainerCapDrop []string // list of kernel capabilities to remove from the containers ContainerCapDrop []string // list of kernel capabilities to remove from the containers
AutoRemove bool // controls if the container is automatically removed upon workflow completion AutoRemove bool // controls if the container is automatically removed upon workflow completion
ArtifactServerPath string // the path where the artifact server stores uploads ArtifactServerPath string // the path where the artifact server stores uploads
ArtifactServerAddr string // the address the artifact server binds to
ArtifactServerPort string // the port the artifact server binds to ArtifactServerPort string // the port the artifact server binds to
NoSkipCheckout bool // do not skip actions/checkout NoSkipCheckout bool // do not skip actions/checkout
RemoteName string // remote name in local git repo config RemoteName string // remote name in local git repo config
@ -62,9 +66,14 @@ type Config struct {
JobLoggerLevel *log.Level // the level of job logger JobLoggerLevel *log.Level // the level of job logger
} }
type caller struct {
runContext *RunContext
}
type runnerImpl struct { type runnerImpl struct {
config *Config config *Config
eventJSON string eventJSON string
caller *caller // the job calling this runner (caller of a reusable workflow)
} }
// New Creates a new Runner // New Creates a new Runner
@ -73,40 +82,46 @@ func New(runnerConfig *Config) (Runner, error) {
config: runnerConfig, config: runnerConfig,
} }
return runner.configure()
}
func (runner *runnerImpl) configure() (Runner, error) {
runner.eventJSON = "{}" runner.eventJSON = "{}"
if runnerConfig.EventJSON != "" { if runner.config.EventJSON != "" {
runner.eventJSON = runnerConfig.EventJSON runner.eventJSON = runner.config.EventJSON
} else if runnerConfig.EventPath != "" { } else if runner.config.EventPath != "" {
log.Debugf("Reading event.json from %s", runner.config.EventPath) log.Debugf("Reading event.json from %s", runner.config.EventPath)
eventJSONBytes, err := os.ReadFile(runner.config.EventPath) eventJSONBytes, err := os.ReadFile(runner.config.EventPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
runner.eventJSON = string(eventJSONBytes) runner.eventJSON = string(eventJSONBytes)
} else if len(runner.config.Inputs) != 0 {
eventMap := map[string]map[string]string{
"inputs": runner.config.Inputs,
}
eventJSON, err := json.Marshal(eventMap)
if err != nil {
return nil, err
}
runner.eventJSON = string(eventJSON)
} }
return runner, nil return runner, nil
} }
// NewPlanExecutor ... // NewPlanExecutor ...
//
//nolint:gocyclo
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
maxJobNameLen := 0 maxJobNameLen := 0
stagePipeline := make([]common.Executor, 0) stagePipeline := make([]common.Executor, 0)
for i := range plan.Stages { for i := range plan.Stages {
s := i
stage := plan.Stages[i] stage := plan.Stages[i]
stagePipeline = append(stagePipeline, func(ctx context.Context) error { stagePipeline = append(stagePipeline, func(ctx context.Context) error {
pipeline := make([]common.Executor, 0) pipeline := make([]common.Executor, 0)
for r, run := range stage.Runs { for _, run := range stage.Runs {
stageExecutor := make([]common.Executor, 0) stageExecutor := make([]common.Executor, 0)
job := run.Job() job := run.Job()
if job.Uses != "" {
return fmt.Errorf("reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)")
}
if job.Strategy != nil { if job.Strategy != nil {
strategyRc := runner.newRunContext(ctx, run, nil) strategyRc := runner.newRunContext(ctx, run, nil)
if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
@ -134,29 +149,8 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
maxJobNameLen = len(rc.String()) maxJobNameLen = len(rc.String())
} }
stageExecutor = append(stageExecutor, func(ctx context.Context) error { stageExecutor = append(stageExecutor, func(ctx context.Context) error {
logger := common.Logger(ctx)
jobName := fmt.Sprintf("%-*s", maxJobNameLen, rc.String()) jobName := fmt.Sprintf("%-*s", maxJobNameLen, rc.String())
return rc.Executor().Finally(func(ctx context.Context) error { return rc.Executor()(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix)))
isLastRunningContainer := func(currentStage int, currentRun int) bool {
return currentStage == len(plan.Stages)-1 && currentRun == len(stage.Runs)-1
}
if runner.config.AutoRemove && isLastRunningContainer(s, r) {
var cancel context.CancelFunc
if ctx.Err() == context.Canceled {
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
}
log.Infof("Cleaning up container for job %s", rc.JobName)
if err := rc.stopJobContainer()(ctx); err != nil {
logger.Errorf("Error while cleaning container: %v", err)
}
}
return nil
})(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix)))
}) })
} }
pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...)) pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...))
@ -196,8 +190,10 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat
EventJSON: runner.eventJSON, EventJSON: runner.eventJSON,
StepResults: make(map[string]*model.StepResult), StepResults: make(map[string]*model.StepResult),
Matrix: matrix, Matrix: matrix,
caller: runner.caller,
} }
rc.ExprEval = rc.NewExpressionEvaluator(ctx) rc.ExprEval = rc.NewExpressionEvaluator(ctx)
rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) rc.Name = rc.ExprEval.Interpolate(ctx, run.String())
return rc return rc
} }

View file

@ -1,8 +1,10 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -22,6 +24,7 @@ var (
platforms map[string]string platforms map[string]string
logLevel = log.DebugLevel logLevel = log.DebugLevel
workdir = "testdata" workdir = "testdata"
secrets map[string]string
) )
func init() { func init() {
@ -42,14 +45,103 @@ func init() {
if wd, err := filepath.Abs(workdir); err == nil { if wd, err := filepath.Abs(workdir); err == nil {
workdir = wd workdir = wd
} }
secrets = map[string]string{}
}
func TestNoWorkflowsFoundByPlanner(t *testing.T) {
planner, err := model.NewWorkflowPlanner("res", true)
assert.NoError(t, err)
out := log.StandardLogger().Out
var buf bytes.Buffer
log.SetOutput(&buf)
log.SetLevel(log.DebugLevel)
plan, err := planner.PlanEvent("pull_request")
assert.NotNil(t, plan)
assert.NoError(t, err)
assert.Contains(t, buf.String(), "no workflows found by planner")
buf.Reset()
plan, err = planner.PlanAll()
assert.NotNil(t, plan)
assert.NoError(t, err)
assert.Contains(t, buf.String(), "no workflows found by planner")
log.SetOutput(out)
}
func TestGraphMissingEvent(t *testing.T) {
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-event.yml", true)
assert.NoError(t, err)
out := log.StandardLogger().Out
var buf bytes.Buffer
log.SetOutput(&buf)
log.SetLevel(log.DebugLevel)
plan, err := planner.PlanEvent("push")
assert.NoError(t, err)
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages))
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
log.SetOutput(out)
}
func TestGraphMissingFirst(t *testing.T) {
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-first.yml", true)
assert.NoError(t, err)
plan, err := planner.PlanEvent("push")
assert.EqualError(t, err, "unable to build dependency graph for no first (no-first.yml)")
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages))
}
func TestGraphWithMissing(t *testing.T) {
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/missing.yml", true)
assert.NoError(t, err)
out := log.StandardLogger().Out
var buf bytes.Buffer
log.SetOutput(&buf)
log.SetLevel(log.DebugLevel)
plan, err := planner.PlanEvent("push")
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages))
assert.EqualError(t, err, "unable to build dependency graph for missing (missing.yml)")
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
log.SetOutput(out)
}
func TestGraphWithSomeMissing(t *testing.T) {
log.SetLevel(log.DebugLevel)
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/", true)
assert.NoError(t, err)
out := log.StandardLogger().Out
var buf bytes.Buffer
log.SetOutput(&buf)
log.SetLevel(log.DebugLevel)
plan, err := planner.PlanAll()
assert.Error(t, err, "unable to build dependency graph for no first (no-first.yml)")
assert.NotNil(t, plan)
assert.Equal(t, 1, len(plan.Stages))
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
assert.Contains(t, buf.String(), "unable to build dependency graph for no first (no-first.yml)")
log.SetOutput(out)
} }
func TestGraphEvent(t *testing.T) { func TestGraphEvent(t *testing.T) {
planner, err := model.NewWorkflowPlanner("testdata/basic", true) planner, err := model.NewWorkflowPlanner("testdata/basic", true)
assert.Nil(t, err) assert.NoError(t, err)
plan := planner.PlanEvent("push") plan, err := planner.PlanEvent("push")
assert.Nil(t, err) assert.NoError(t, err)
assert.NotNil(t, plan)
assert.NotNil(t, plan.Stages)
assert.Equal(t, len(plan.Stages), 3, "stages") assert.Equal(t, len(plan.Stages), 3, "stages")
assert.Equal(t, len(plan.Stages[0].Runs), 1, "stage0.runs") assert.Equal(t, len(plan.Stages[0].Runs), 1, "stage0.runs")
assert.Equal(t, len(plan.Stages[1].Runs), 1, "stage1.runs") assert.Equal(t, len(plan.Stages[1].Runs), 1, "stage1.runs")
@ -58,8 +150,10 @@ func TestGraphEvent(t *testing.T) {
assert.Equal(t, plan.Stages[1].Runs[0].JobID, "build", "jobid") assert.Equal(t, plan.Stages[1].Runs[0].JobID, "build", "jobid")
assert.Equal(t, plan.Stages[2].Runs[0].JobID, "test", "jobid") assert.Equal(t, plan.Stages[2].Runs[0].JobID, "test", "jobid")
plan = planner.PlanEvent("release") plan, err = planner.PlanEvent("release")
assert.Equal(t, len(plan.Stages), 0, "stages") assert.NoError(t, err)
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages))
} }
type TestJobFileInfo struct { type TestJobFileInfo struct {
@ -68,6 +162,7 @@ type TestJobFileInfo struct {
eventName string eventName string
errorMessage string errorMessage string
platforms map[string]string platforms map[string]string
secrets map[string]string
} }
func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) { func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) {
@ -88,6 +183,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
ReuseContainers: false, ReuseContainers: false,
Env: cfg.Env, Env: cfg.Env,
Secrets: cfg.Secrets, Secrets: cfg.Secrets,
Inputs: cfg.Inputs,
GitHubInstance: "github.com", GitHubInstance: "github.com",
ContainerArchitecture: cfg.ContainerArchitecture, ContainerArchitecture: cfg.ContainerArchitecture,
} }
@ -98,13 +194,15 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true) planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
assert.Nil(t, err, fullWorkflowPath) assert.Nil(t, err, fullWorkflowPath)
plan := planner.PlanEvent(j.eventName) plan, err := planner.PlanEvent(j.eventName)
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
err = runner.NewPlanExecutor(plan)(ctx) if err == nil && plan != nil {
if j.errorMessage == "" { err = runner.NewPlanExecutor(plan)(ctx)
assert.Nil(t, err, fullWorkflowPath) if j.errorMessage == "" {
} else { assert.Nil(t, err, fullWorkflowPath)
assert.Error(t, err, j.errorMessage) } else {
assert.Error(t, err, j.errorMessage)
}
} }
fmt.Println("::endgroup::") fmt.Println("::endgroup::")
@ -119,81 +217,96 @@ func TestRunEvent(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
// TODO: figure out why it fails // TODO: figure out why it fails
// {workdir, "shells/custom", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, }, // custom image with pwsh // {workdir, "shells/custom", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, }, // custom image with pwsh
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-docker-url", "push", "", platforms, secrets},
{workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
// Uses // Uses
{workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite", "push", "", platforms, secrets},
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
{workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "uses-nested-composite", "push", "", platforms, secrets},
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms}, {workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms, secrets},
{workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms}, {workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}},
{workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
{workdir, "act-composite-env-test", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms, secrets},
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
// Eval // Eval
{workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrix", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
{workdir, "issue-1195", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms, secrets},
{workdir, "basic", "push", "", platforms}, {workdir, "basic", "push", "", platforms, secrets},
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
{workdir, "runs-on", "push", "", platforms}, {workdir, "runs-on", "push", "", platforms, secrets},
{workdir, "checkout", "push", "", platforms}, {workdir, "checkout", "push", "", platforms, secrets},
{workdir, "job-container", "push", "", platforms}, {workdir, "job-container", "push", "", platforms, secrets},
{workdir, "job-container-non-root", "push", "", platforms}, {workdir, "job-container-non-root", "push", "", platforms, secrets},
{workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms}, {workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms, secrets},
{workdir, "container-hostname", "push", "", platforms}, {workdir, "container-hostname", "push", "", platforms, secrets},
{workdir, "remote-action-docker", "push", "", platforms}, {workdir, "remote-action-docker", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}}, // Test if this works with non root container {workdir, "remote-action-js-node-user", "push", "", platforms, secrets}, // Test if this works with non root container
{workdir, "matrix", "push", "", platforms}, {workdir, "matrix", "push", "", platforms, secrets},
{workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms, secrets},
{workdir, "commands", "push", "", platforms}, {workdir, "matrix-exitcode", "push", "Job 'test' failed", platforms, secrets},
{workdir, "workdir", "push", "", platforms}, {workdir, "commands", "push", "", platforms, secrets},
{workdir, "defaults-run", "push", "", platforms}, {workdir, "workdir", "push", "", platforms, secrets},
{workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms, secrets},
{workdir, "issue-597", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms, secrets},
{workdir, "issue-598", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms, secrets},
{workdir, "if-env-act", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms, secrets},
{workdir, "env-and-path", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms, secrets},
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "env-and-path", "push", "", platforms, secrets},
{workdir, "outputs", "push", "", platforms}, {workdir, "environment-files", "push", "", platforms, secrets},
{workdir, "networking", "push", "", platforms}, {workdir, "GITHUB_STATE", "push", "", platforms, secrets},
{workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "environment-files-parser-bug", "push", "", platforms, secrets},
{workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "outputs", "push", "", platforms, secrets},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "networking", "push", "", platforms, secrets},
{workdir, "actions-environment-and-context-tests", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms, secrets},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms, secrets},
{workdir, "evalenv", "push", "", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms}, {workdir, "actions-environment-and-context-tests", "push", "", platforms, secrets},
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms}, {workdir, "evalenv", "push", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms}, {workdir, "docker-action-custom-path", "push", "", platforms, secrets},
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner {workdir, "GITHUB_ENV-use-in-env-ctx", "push", "", platforms, secrets},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms, secrets},
{workdir, "job-needs-context-contains-result", "push", "", platforms, secrets},
{"../model/testdata", "strategy", "push", "", platforms, secrets}, // TODO: move all testdata into pkg so we can validate it with planner and runner
// {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes // {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes
{"../model/testdata", "container-volumes", "push", "", platforms}, {"../model/testdata", "container-volumes", "push", "", platforms, secrets},
{workdir, "path-handling", "push", "", platforms, secrets},
{workdir, "do-not-leak-step-env-in-composite", "push", "", platforms, secrets},
{workdir, "set-env-step-env-override", "push", "", platforms, secrets},
{workdir, "set-env-new-env-file-per-step", "push", "", platforms, secrets},
{workdir, "no-panic-on-invalid-composite-action", "push", "jobs failed due to invalid action", platforms, secrets},
} }
for _, table := range tables { for _, table := range tables {
t.Run(table.workflowPath, func(t *testing.T) { t.Run(table.workflowPath, func(t *testing.T) {
config := &Config{} config := &Config{
Secrets: table.secrets,
}
eventFile := filepath.Join(workdir, table.workflowPath, "event.json") eventFile := filepath.Join(workdir, table.workflowPath, "event.json")
if _, err := os.Stat(eventFile); err == nil { if _, err := os.Stat(eventFile); err == nil {
@ -221,51 +334,51 @@ func TestRunEventHostEnvironment(t *testing.T) {
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
{workdir, "shells/pwsh", "push", "", platforms}, {workdir, "shells/pwsh", "push", "", platforms, secrets},
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", platforms}, {workdir, "shells/python", "push", "", platforms, secrets},
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
// Uses // Uses
{workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite", "push", "", platforms, secrets},
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
{workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "uses-nested-composite", "push", "", platforms, secrets},
{workdir, "act-composite-env-test", "push", "", platforms}, {workdir, "act-composite-env-test", "push", "", platforms, secrets},
// Eval // Eval
{workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrix", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
{workdir, "issue-1195", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms, secrets},
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
{workdir, "runs-on", "push", "", platforms}, {workdir, "runs-on", "push", "", platforms, secrets},
{workdir, "checkout", "push", "", platforms}, {workdir, "checkout", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms, secrets},
{workdir, "matrix", "push", "", platforms}, {workdir, "matrix", "push", "", platforms, secrets},
{workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms, secrets},
{workdir, "commands", "push", "", platforms}, {workdir, "commands", "push", "", platforms, secrets},
{workdir, "defaults-run", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms, secrets},
{workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms, secrets},
{workdir, "issue-597", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms, secrets},
{workdir, "issue-598", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms, secrets},
{workdir, "if-env-act", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms, secrets},
{workdir, "env-and-path", "push", "", platforms}, {workdir, "env-and-path", "push", "", platforms, secrets},
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
{workdir, "outputs", "push", "", platforms}, {workdir, "outputs", "push", "", platforms, secrets},
{workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms, secrets},
{workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms, secrets},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
{workdir, "evalenv", "push", "", platforms}, {workdir, "evalenv", "push", "", platforms, secrets},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
}...) }...)
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -274,16 +387,22 @@ func TestRunEventHostEnvironment(t *testing.T) {
} }
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
{workdir, "windows-prepend-path", "push", "", platforms}, {workdir, "windows-prepend-path", "push", "", platforms, secrets},
{workdir, "windows-add-env", "push", "", platforms}, {workdir, "windows-add-env", "push", "", platforms, secrets},
}...) }...)
} else { } else {
platforms := map[string]string{ platforms := map[string]string{
"self-hosted": "-self-hosted", "self-hosted": "-self-hosted",
"ubuntu-latest": "-self-hosted",
} }
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
{workdir, "nix-prepend-path", "push", "", platforms}, {workdir, "nix-prepend-path", "push", "", platforms, secrets},
{workdir, "inputs-via-env-context", "push", "", platforms, secrets},
{workdir, "do-not-leak-step-env-in-composite", "push", "", platforms, secrets},
{workdir, "set-env-step-env-override", "push", "", platforms, secrets},
{workdir, "set-env-new-env-file-per-step", "push", "", platforms, secrets},
{workdir, "no-panic-on-invalid-composite-action", "push", "jobs failed due to invalid action", platforms, secrets},
}...) }...)
} }
@ -303,17 +422,17 @@ func TestDryrunEvent(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-docker-url", "push", "", platforms, secrets},
{workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
} }
for _, table := range tables { for _, table := range tables {
@ -323,6 +442,30 @@ func TestDryrunEvent(t *testing.T) {
} }
} }
func TestDockerActionForcePullForceRebuild(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
config := &Config{
ForcePull: true,
ForceRebuild: true,
}
tables := []TestJobFileInfo{
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
}
for _, table := range tables {
t.Run(table.workflowPath, func(t *testing.T) {
table.runTest(ctx, t, config)
})
}
}
func TestRunDifferentArchitecture(t *testing.T) { func TestRunDifferentArchitecture(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")
@ -339,6 +482,17 @@ func TestRunDifferentArchitecture(t *testing.T) {
tjfi.runTest(context.Background(), t, &Config{ContainerArchitecture: "linux/arm64"}) tjfi.runTest(context.Background(), t, &Config{ContainerArchitecture: "linux/arm64"})
} }
type maskJobLoggerFactory struct {
Output bytes.Buffer
}
func (f *maskJobLoggerFactory) WithJobLogger() *log.Logger {
logger := log.New()
logger.SetOutput(io.MultiWriter(&f.Output, os.Stdout))
logger.SetLevel(log.DebugLevel)
return logger
}
func TestMaskValues(t *testing.T) { func TestMaskValues(t *testing.T) {
assertNoSecret := func(text string, secret string) { assertNoSecret := func(text string, secret string) {
index := strings.Index(text, "composite secret") index := strings.Index(text, "composite secret")
@ -362,9 +516,9 @@ func TestMaskValues(t *testing.T) {
platforms: platforms, platforms: platforms,
} }
output := captureOutput(t, func() { logger := &maskJobLoggerFactory{}
tjfi.runTest(context.Background(), t, &Config{}) tjfi.runTest(WithJobLoggerFactory(common.WithLogger(context.Background(), logger.WithJobLogger()), logger), t, &Config{})
}) output := logger.Output.String()
assertNoSecret(output, "secret value") assertNoSecret(output, "secret value")
assertNoSecret(output, "YWJjCg==") assertNoSecret(output, "YWJjCg==")
@ -392,6 +546,27 @@ func TestRunEventSecrets(t *testing.T) {
tjfi.runTest(context.Background(), t, &Config{Secrets: secrets, Env: env}) tjfi.runTest(context.Background(), t, &Config{Secrets: secrets, Env: env})
} }
func TestRunActionInputs(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
workflowPath := "input-from-cli"
tjfi := TestJobFileInfo{
workdir: workdir,
workflowPath: workflowPath,
eventName: "workflow_dispatch",
errorMessage: "",
platforms: platforms,
}
inputs := map[string]string{
"SOME_INPUT": "input",
}
tjfi.runTest(context.Background(), t, &Config{Inputs: inputs})
}
func TestRunEventPullRequest(t *testing.T) { func TestRunEventPullRequest(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")

View file

@ -44,16 +44,16 @@ func (s stepStage) String() string {
return "Unknown" return "Unknown"
} }
func (s stepStage) getStepName(stepModel *model.Step) string { func processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error {
switch s { env := map[string]string{}
case stepStagePre: err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx)
return fmt.Sprintf("pre-%s", stepModel.ID) if err != nil {
case stepStageMain: return err
return stepModel.ID
case stepStagePost:
return fmt.Sprintf("post-%s", stepModel.ID)
} }
return "unknown" for k, v := range env {
setter(ctx, map[string]string{"name": k}, v)
}
return nil
} }
func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor { func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor {
@ -63,13 +63,16 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
stepModel := step.getStepModel() stepModel := step.getStepModel()
ifExpression := step.getIfExpression(ctx, stage) ifExpression := step.getIfExpression(ctx, stage)
rc.CurrentStep = stage.getStepName(stepModel) rc.CurrentStep = stepModel.ID
rc.StepResults[rc.CurrentStep] = &model.StepResult{ stepResult := &model.StepResult{
Outcome: model.StepStatusSuccess, Outcome: model.StepStatusSuccess,
Conclusion: model.StepStatusSuccess, Conclusion: model.StepStatusSuccess,
Outputs: make(map[string]string), Outputs: make(map[string]string),
} }
if stage == stepStageMain {
rc.StepResults[rc.CurrentStep] = stepResult
}
err := setupEnv(ctx, step) err := setupEnv(ctx, step)
if err != nil { if err != nil {
@ -78,15 +81,15 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
runStep, err := isStepEnabled(ctx, ifExpression, step, stage) runStep, err := isStepEnabled(ctx, ifExpression, step, stage)
if err != nil { if err != nil {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure stepResult.Conclusion = model.StepStatusFailure
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure stepResult.Outcome = model.StepStatusFailure
return err return err
} }
if !runStep { if !runStep {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped stepResult.Conclusion = model.StepStatusSkipped
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped stepResult.Outcome = model.StepStatusSkipped
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression) logger.WithField("stepResult", stepResult.Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
return nil return nil
} }
@ -98,58 +101,79 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
// Prepare and clean Runner File Commands // Prepare and clean Runner File Commands
actPath := rc.JobContainer.GetActPath() actPath := rc.JobContainer.GetActPath()
outputFileCommand := path.Join("workflow", "outputcmd.txt") outputFileCommand := path.Join("workflow", "outputcmd.txt")
stateFileCommand := path.Join("workflow", "statecmd.txt")
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand) (*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
stateFileCommand := path.Join("workflow", "statecmd.txt")
(*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand) (*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand)
pathFileCommand := path.Join("workflow", "pathcmd.txt")
(*step.getEnv())["GITHUB_PATH"] = path.Join(actPath, pathFileCommand)
envFileCommand := path.Join("workflow", "envs.txt")
(*step.getEnv())["GITHUB_ENV"] = path.Join(actPath, envFileCommand)
summaryFileCommand := path.Join("workflow", "SUMMARY.md")
(*step.getEnv())["GITHUB_STEP_SUMMARY"] = path.Join(actPath, summaryFileCommand)
_ = rc.JobContainer.Copy(actPath, &container.FileEntry{ _ = rc.JobContainer.Copy(actPath, &container.FileEntry{
Name: outputFileCommand, Name: outputFileCommand,
Mode: 0666, Mode: 0o666,
}, &container.FileEntry{ }, &container.FileEntry{
Name: stateFileCommand, Name: stateFileCommand,
Mode: 0o666,
}, &container.FileEntry{
Name: pathFileCommand,
Mode: 0o666,
}, &container.FileEntry{
Name: envFileCommand,
Mode: 0666, Mode: 0666,
}, &container.FileEntry{
Name: summaryFileCommand,
Mode: 0o666,
})(ctx) })(ctx)
err = executor(ctx) err = executor(ctx)
if err == nil { if err == nil {
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Infof(" \u2705 Success - %s %s", stage, stepString) logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
} else { } else {
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure stepResult.Outcome = model.StepStatusFailure
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage) continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage)
if parseErr != nil { if parseErr != nil {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure stepResult.Conclusion = model.StepStatusFailure
return parseErr return parseErr
} }
if continueOnError { if continueOnError {
logger.Infof("Failed but continue next step") logger.Infof("Failed but continue next step")
err = nil err = nil
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess stepResult.Conclusion = model.StepStatusSuccess
} else { } else {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure stepResult.Conclusion = model.StepStatusFailure
} }
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString) logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
} }
// Process Runner File Commands // Process Runner File Commands
orgerr := err orgerr := err
state := map[string]string{} err = processRunnerEnvFileCommand(ctx, envFileCommand, rc, rc.setEnv)
err = rc.JobContainer.UpdateFromEnv(path.Join(actPath, stateFileCommand), &state)(ctx)
if err != nil { if err != nil {
return err return err
} }
for k, v := range state { err = processRunnerEnvFileCommand(ctx, stateFileCommand, rc, rc.saveState)
rc.saveState(ctx, map[string]string{"name": k}, v)
}
output := map[string]string{}
err = rc.JobContainer.UpdateFromEnv(path.Join(actPath, outputFileCommand), &output)(ctx)
if err != nil { if err != nil {
return err return err
} }
for k, v := range output { err = processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput)
rc.setOutput(ctx, map[string]string{"name": k}, v) if err != nil {
return err
}
err = rc.UpdateExtraPath(ctx, path.Join(actPath, pathFileCommand))
if err != nil {
return err
} }
if orgerr != nil { if orgerr != nil {
return orgerr return orgerr
@ -162,24 +186,22 @@ func setupEnv(ctx context.Context, step step) error {
rc := step.getRunContext() rc := step.getRunContext()
mergeEnv(ctx, step) mergeEnv(ctx, step)
err := rc.JobContainer.UpdateFromImageEnv(step.getEnv())(ctx)
if err != nil {
return err
}
err = rc.JobContainer.UpdateFromEnv((*step.getEnv())["GITHUB_ENV"], step.getEnv())(ctx)
if err != nil {
return err
}
err = rc.JobContainer.UpdateFromPath(step.getEnv())(ctx)
if err != nil {
return err
}
// merge step env last, since it should not be overwritten // merge step env last, since it should not be overwritten
mergeIntoMap(step.getEnv(), step.getStepModel().GetEnv()) mergeIntoMap(step.getEnv(), step.getStepModel().GetEnv())
exprEval := rc.NewExpressionEvaluator(ctx) exprEval := rc.NewExpressionEvaluator(ctx)
for k, v := range *step.getEnv() { for k, v := range *step.getEnv() {
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v) if !strings.HasPrefix(k, "INPUT_") {
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
}
}
// after we have an evaluated step context, update the expressions evaluator with a new env context
// you can use step level env in the with property of a uses construct
exprEval = rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv())
for k, v := range *step.getEnv() {
if strings.HasPrefix(k, "INPUT_") {
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
}
} }
common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv()) common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv())
@ -199,14 +221,6 @@ func mergeEnv(ctx context.Context, step step) {
mergeIntoMap(env, rc.GetEnv()) mergeIntoMap(env, rc.GetEnv())
} }
path := rc.JobContainer.GetPathVariableName()
if (*env)[path] == "" {
(*env)[path] = rc.JobContainer.DefaultPathVariable()
}
if rc.ExtraPath != nil && len(rc.ExtraPath) > 0 {
(*env)[path] = rc.JobContainer.JoinPathVariable(append(rc.ExtraPath, (*env)[path])...)
}
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env) rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env)
} }

View file

@ -1,7 +1,9 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"io"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -67,7 +69,7 @@ func TestStepActionLocalTest(t *testing.T) {
salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything). salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything).
Return(&model.Action{}, nil) Return(&model.Action{}, nil)
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -75,14 +77,6 @@ func TestStepActionLocalTest(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -91,6 +85,8 @@ func TestStepActionLocalTest(t *testing.T) {
return nil return nil
}) })
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(func(ctx context.Context) error { salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -107,13 +103,12 @@ func TestStepActionLocalTest(t *testing.T) {
func TestStepActionLocalPost(t *testing.T) { func TestStepActionLocalPost(t *testing.T) {
table := []struct { table := []struct {
name string name string
stepModel *model.Step stepModel *model.Step
actionModel *model.Action actionModel *model.Action
initialStepResults map[string]*model.StepResult initialStepResults map[string]*model.StepResult
expectedPostStepResult *model.StepResult err error
err error mocks struct {
mocks struct {
env bool env bool
exec bool exec bool
} }
@ -138,11 +133,6 @@ func TestStepActionLocalPost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSuccess,
Outcome: model.StepStatusSuccess,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -171,11 +161,6 @@ func TestStepActionLocalPost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSuccess,
Outcome: model.StepStatusSuccess,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -204,16 +189,11 @@ func TestStepActionLocalPost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSkipped,
Outcome: model.StepStatusSkipped,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
}{ }{
env: true, env: false,
exec: false, exec: false,
}, },
}, },
@ -238,7 +218,6 @@ func TestStepActionLocalPost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: nil,
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -277,11 +256,6 @@ func TestStepActionLocalPost(t *testing.T) {
} }
sal.RunContext.ExprEval = sal.RunContext.NewExpressionEvaluator(ctx) sal.RunContext.ExprEval = sal.RunContext.NewExpressionEvaluator(ctx)
if tt.mocks.env {
cm.On("UpdateFromImageEnv", &sal.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &sal.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromPath", &sal.env).Return(func(ctx context.Context) error { return nil })
}
if tt.mocks.exec { if tt.mocks.exec {
suffixMatcher := func(suffix string) interface{} { suffixMatcher := func(suffix string) interface{} {
return mock.MatchedBy(func(array []string) bool { return mock.MatchedBy(func(array []string) bool {
@ -294,6 +268,10 @@ func TestStepActionLocalPost(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -301,12 +279,14 @@ func TestStepActionLocalPost(t *testing.T) {
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
err := sal.post()(ctx) err := sal.post()(ctx)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
assert.Equal(t, tt.expectedPostStepResult, sal.RunContext.StepResults["post-step"]) assert.Equal(t, sal.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
cm.AssertExpectations(t) cm.AssertExpectations(t)
}) })
} }

View file

@ -11,11 +11,11 @@ import (
"regexp" "regexp"
"strings" "strings"
gogit "github.com/go-git/go-git/v5"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git" "github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
gogit "github.com/go-git/go-git/v5"
) )
type stepActionRemote struct { type stepActionRemote struct {
@ -197,6 +197,7 @@ func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunCon
// was already created during the pre stage) // was already created during the pre stage)
env := evaluateCompositeInputAndEnv(ctx, sar.RunContext, sar) env := evaluateCompositeInputAndEnv(ctx, sar.RunContext, sar)
sar.compositeRunContext.Env = env sar.compositeRunContext.Env = env
sar.compositeRunContext.ExtraPath = sar.RunContext.ExtraPath
} }
return sar.compositeRunContext return sar.compositeRunContext
} }

View file

@ -1,18 +1,20 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"io"
"strings" "strings"
"testing" "testing"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/model"
) )
type stepActionRemoteMocks struct { type stepActionRemoteMocks struct {
@ -163,11 +165,6 @@ func TestStepActionRemote(t *testing.T) {
}) })
} }
if tt.mocks.env {
cm.On("UpdateFromImageEnv", &sar.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &sar.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromPath", &sar.env).Return(func(ctx context.Context) error { return nil })
}
if tt.mocks.read { if tt.mocks.read {
sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
} }
@ -178,6 +175,10 @@ func TestStepActionRemote(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -185,6 +186,8 @@ func TestStepActionRemote(t *testing.T) {
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
err := sar.pre()(ctx) err := sar.pre()(ctx)
@ -412,14 +415,14 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
func TestStepActionRemotePost(t *testing.T) { func TestStepActionRemotePost(t *testing.T) {
table := []struct { table := []struct {
name string name string
stepModel *model.Step stepModel *model.Step
actionModel *model.Action actionModel *model.Action
initialStepResults map[string]*model.StepResult initialStepResults map[string]*model.StepResult
expectedEnv map[string]string IntraActionState map[string]map[string]string
expectedPostStepResult *model.StepResult expectedEnv map[string]string
err error err error
mocks struct { mocks struct {
env bool env bool
exec bool exec bool
} }
@ -442,19 +445,16 @@ func TestStepActionRemotePost(t *testing.T) {
Conclusion: model.StepStatusSuccess, Conclusion: model.StepStatusSuccess,
Outcome: model.StepStatusSuccess, Outcome: model.StepStatusSuccess,
Outputs: map[string]string{}, Outputs: map[string]string{},
State: map[string]string{ },
"key": "value", },
}, IntraActionState: map[string]map[string]string{
"step": {
"key": "value",
}, },
}, },
expectedEnv: map[string]string{ expectedEnv: map[string]string{
"STATE_key": "value", "STATE_key": "value",
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSuccess,
Outcome: model.StepStatusSuccess,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -483,11 +483,6 @@ func TestStepActionRemotePost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSuccess,
Outcome: model.StepStatusSuccess,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -516,11 +511,6 @@ func TestStepActionRemotePost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: &model.StepResult{
Conclusion: model.StepStatusSkipped,
Outcome: model.StepStatusSkipped,
Outputs: map[string]string{},
},
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -550,7 +540,6 @@ func TestStepActionRemotePost(t *testing.T) {
Outputs: map[string]string{}, Outputs: map[string]string{},
}, },
}, },
expectedPostStepResult: nil,
mocks: struct { mocks: struct {
env bool env bool
exec bool exec bool
@ -582,18 +571,14 @@ func TestStepActionRemotePost(t *testing.T) {
}, },
}, },
}, },
StepResults: tt.initialStepResults, StepResults: tt.initialStepResults,
IntraActionState: tt.IntraActionState,
}, },
Step: tt.stepModel, Step: tt.stepModel,
action: tt.actionModel, action: tt.actionModel,
} }
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
if tt.mocks.env {
cm.On("UpdateFromImageEnv", &sar.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &sar.env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromPath", &sar.env).Return(func(ctx context.Context) error { return nil })
}
if tt.mocks.exec { if tt.mocks.exec {
cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err }) cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
@ -601,6 +586,10 @@ func TestStepActionRemotePost(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -608,6 +597,8 @@ func TestStepActionRemotePost(t *testing.T) {
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
err := sar.post()(ctx) err := sar.post()(ctx)
@ -618,7 +609,8 @@ func TestStepActionRemotePost(t *testing.T) {
assert.Equal(t, value, sar.env[key]) assert.Equal(t, value, sar.env[key])
} }
} }
assert.Equal(t, tt.expectedPostStepResult, sar.RunContext.StepResults["post-step"]) // Enshure that StepResults is nil in this test
assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
cm.AssertExpectations(t) cm.AssertExpectations(t)
}) })
} }

View file

@ -1,7 +1,9 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"io"
"testing" "testing"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
@ -55,18 +57,6 @@ func TestStepDockerMain(t *testing.T) {
} }
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx) sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("Pull", false).Return(func(ctx context.Context) error { cm.On("Pull", false).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -91,6 +81,10 @@ func TestStepDockerMain(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -99,6 +93,8 @@ func TestStepDockerMain(t *testing.T) {
return nil return nil
}) })
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
err := sd.main()(ctx) err := sd.main()(ctx)
assert.Nil(t, err) assert.Nil(t, err)

View file

@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
@ -30,6 +31,7 @@ func (sr *stepRun) main() common.Executor {
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor( return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
sr.setupShellCommandExecutor(), sr.setupShellCommandExecutor(),
func(ctx context.Context) error { func(ctx context.Context) error {
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.Step.WorkingDirectory)(ctx) return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.Step.WorkingDirectory)(ctx)
}, },
)) ))
@ -71,7 +73,7 @@ func (sr *stepRun) setupShellCommandExecutor() common.Executor {
rc := sr.getRunContext() rc := sr.getRunContext()
return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{ return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
Name: scriptName, Name: scriptName,
Mode: 0755, Mode: 0o755,
Body: script, Body: script,
})(ctx) })(ctx)
} }

View file

@ -1,20 +1,23 @@
package runner package runner
import ( import (
"bytes"
"context" "context"
"io"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
) )
func TestStepRun(t *testing.T) { func TestStepRun(t *testing.T) {
cm := &containerMock{} cm := &containerMock{}
fileEntry := &container.FileEntry{ fileEntry := &container.FileEntry{
Name: "workflow/1.sh", Name: "workflow/1.sh",
Mode: 0755, Mode: 0o755,
Body: "\ncmd\n", Body: "\ncmd\n",
} }
@ -53,7 +56,7 @@ func TestStepRun(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -61,14 +64,6 @@ func TestStepRun(t *testing.T) {
return nil return nil
}) })
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil return nil
}) })
@ -79,6 +74,8 @@ func TestStepRun(t *testing.T) {
ctx := context.Background() ctx := context.Background()
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
err := sr.main()(ctx) err := sr.main()(ctx)
assert.Nil(t, err) assert.Nil(t, err)

View file

@ -134,7 +134,6 @@ func TestSetupEnv(t *testing.T) {
Env: map[string]string{ Env: map[string]string{
"RC_KEY": "rcvalue", "RC_KEY": "rcvalue",
}, },
ExtraPath: []string{"/path/to/extra/file"},
JobContainer: cm, JobContainer: cm,
} }
step := &model.Step{ step := &model.Step{
@ -142,19 +141,13 @@ func TestSetupEnv(t *testing.T) {
"STEP_WITH": "with-value", "STEP_WITH": "with-value",
}, },
} }
env := map[string]string{ env := map[string]string{}
"PATH": "",
}
sm.On("getRunContext").Return(rc) sm.On("getRunContext").Return(rc)
sm.On("getGithubContext").Return(rc) sm.On("getGithubContext").Return(rc)
sm.On("getStepModel").Return(step) sm.On("getStepModel").Return(step)
sm.On("getEnv").Return(&env) sm.On("getEnv").Return(&env)
cm.On("UpdateFromImageEnv", &env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &env).Return(func(ctx context.Context) error { return nil })
cm.On("UpdateFromPath", &env).Return(func(ctx context.Context) error { return nil })
err := setupEnv(context.Background(), sm) err := setupEnv(context.Background(), sm)
assert.Nil(t, err) assert.Nil(t, err)
@ -178,13 +171,11 @@ func TestSetupEnv(t *testing.T) {
"GITHUB_ACTION_REPOSITORY": "", "GITHUB_ACTION_REPOSITORY": "",
"GITHUB_API_URL": "https:///api/v3", "GITHUB_API_URL": "https:///api/v3",
"GITHUB_BASE_REF": "", "GITHUB_BASE_REF": "",
"GITHUB_ENV": "/var/run/act/workflow/envs.txt",
"GITHUB_EVENT_NAME": "", "GITHUB_EVENT_NAME": "",
"GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json", "GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json",
"GITHUB_GRAPHQL_URL": "https:///api/graphql", "GITHUB_GRAPHQL_URL": "https:///api/graphql",
"GITHUB_HEAD_REF": "", "GITHUB_HEAD_REF": "",
"GITHUB_JOB": "", "GITHUB_JOB": "1",
"GITHUB_PATH": "/var/run/act/workflow/paths.txt",
"GITHUB_RETENTION_DAYS": "0", "GITHUB_RETENTION_DAYS": "0",
"GITHUB_RUN_ID": "runId", "GITHUB_RUN_ID": "runId",
"GITHUB_RUN_NUMBER": "1", "GITHUB_RUN_NUMBER": "1",
@ -192,7 +183,6 @@ func TestSetupEnv(t *testing.T) {
"GITHUB_TOKEN": "", "GITHUB_TOKEN": "",
"GITHUB_WORKFLOW": "", "GITHUB_WORKFLOW": "",
"INPUT_STEP_WITH": "with-value", "INPUT_STEP_WITH": "with-value",
"PATH": "/path/to/extra/file:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"RC_KEY": "rcvalue", "RC_KEY": "rcvalue",
"RUNNER_PERFLOG": "/dev/null", "RUNNER_PERFLOG": "/dev/null",
"RUNNER_TRACKING_ID": "", "RUNNER_TRACKING_ID": "",

View file

@ -0,0 +1,82 @@
name: reusable
on:
workflow_call:
inputs:
string_required:
required: true
type: string
string_optional:
required: false
type: string
default: string
bool_required:
required: true
type: boolean
bool_optional:
required: false
type: boolean
default: true
number_required:
required: true
type: number
number_optional:
required: false
type: number
default: ${{ 1 }}
outputs:
output:
description: "A workflow output"
value: ${{ jobs.reusable_workflow_job.outputs.job-output }}
jobs:
reusable_workflow_job:
runs-on: ubuntu-latest
steps:
- name: test required string
run: |
echo inputs.string_required=${{ inputs.string_required }}
[[ "${{ inputs.string_required == 'string' }}" = "true" ]] || exit 1
- name: test optional string
run: |
echo inputs.string_optional=${{ inputs.string_optional }}
[[ "${{ inputs.string_optional == 'string' }}" = "true" ]] || exit 1
- name: test required bool
run: |
echo inputs.bool_required=${{ inputs.bool_required }}
[[ "${{ inputs.bool_required }}" = "true" ]] || exit 1
- name: test optional bool
run: |
echo inputs.bool_optional=${{ inputs.bool_optional }}
[[ "${{ inputs.bool_optional }}" = "true" ]] || exit 1
- name: test required number
run: |
echo inputs.number_required=${{ inputs.number_required }}
[[ "${{ inputs.number_required == 1 }}" = "true" ]] || exit 1
- name: test optional number
run: |
echo inputs.number_optional=${{ inputs.number_optional }}
[[ "${{ inputs.number_optional == 1 }}" = "true" ]] || exit 1
- name: test secret
run: |
echo secrets.secret=${{ secrets.secret }}
[[ "${{ secrets.secret == 'keep_it_private' }}" = "true" ]] || exit 1
- name: test github.event_name is never workflow_call
run: |
echo github.event_name=${{ github.event_name }}
[[ "${{ github.event_name != 'workflow_call' }}" = "true" ]] || exit 1
- name: test output
id: output_test
run: |
echo "value=${{ inputs.string_required }}" >> $GITHUB_OUTPUT
outputs:
job-output: ${{ steps.output_test.outputs.value }}

View file

@ -0,0 +1,27 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
env:
MYGLOBALENV3: myglobalval3
steps:
- run: |
echo MYGLOBALENV1=myglobalval1 > $GITHUB_ENV
echo "::set-env name=MYGLOBALENV2::myglobalval2"
- uses: nektos/act-test-actions/script@main
with:
main: |
env
[[ "$MYGLOBALENV1" = "${{ env.MYGLOBALENV1 }}" ]]
[[ "$MYGLOBALENV1" = "${{ env.MYGLOBALENV1ALIAS }}" ]]
[[ "$MYGLOBALENV1" = "$MYGLOBALENV1ALIAS" ]]
[[ "$MYGLOBALENV2" = "${{ env.MYGLOBALENV2 }}" ]]
[[ "$MYGLOBALENV2" = "${{ env.MYGLOBALENV2ALIAS }}" ]]
[[ "$MYGLOBALENV2" = "$MYGLOBALENV2ALIAS" ]]
[[ "$MYGLOBALENV3" = "${{ env.MYGLOBALENV3 }}" ]]
[[ "$MYGLOBALENV3" = "${{ env.MYGLOBALENV3ALIAS }}" ]]
[[ "$MYGLOBALENV3" = "$MYGLOBALENV3ALIAS" ]]
env:
MYGLOBALENV1ALIAS: ${{ env.MYGLOBALENV1 }}
MYGLOBALENV2ALIAS: ${{ env.MYGLOBALENV2 }}
MYGLOBALENV3ALIAS: ${{ env.MYGLOBALENV3 }}

View file

@ -0,0 +1,48 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/script@main
with:
pre: |
env
echo mystate0=mystateval > $GITHUB_STATE
echo "::save-state name=mystate1::mystateval"
main: |
env
echo mystate2=mystateval > $GITHUB_STATE
echo "::save-state name=mystate3::mystateval"
post: |
env
[ "$STATE_mystate0" = "mystateval" ]
[ "$STATE_mystate1" = "mystateval" ]
[ "$STATE_mystate2" = "mystateval" ]
[ "$STATE_mystate3" = "mystateval" ]
test-id-collision-bug:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/script@main
id: script
with:
pre: |
env
echo mystate0=mystateval > $GITHUB_STATE
echo "::save-state name=mystate1::mystateval"
main: |
env
echo mystate2=mystateval > $GITHUB_STATE
echo "::save-state name=mystate3::mystateval"
post: |
env
[ "$STATE_mystate0" = "mystateval" ]
[ "$STATE_mystate1" = "mystateval" ]
[ "$STATE_mystate2" = "mystateval" ]
[ "$STATE_mystate3" = "mystateval" ]
- uses: nektos/act-test-actions/script@main
id: pre-script
with:
main: |
env
echo mystate0=mystateerror > $GITHUB_STATE
echo "::save-state name=mystate1::mystateerror"

View file

@ -11,3 +11,5 @@ jobs:
- uses: './actions-environment-and-context-tests/docker' - uses: './actions-environment-and-context-tests/docker'
- uses: 'nektos/act-test-actions/js@main' - uses: 'nektos/act-test-actions/js@main'
- uses: 'nektos/act-test-actions/docker@main' - uses: 'nektos/act-test-actions/docker@main'
- uses: 'nektos/act-test-actions/docker-file@main'
- uses: 'nektos/act-test-actions/docker-relative-context/action@main'

View file

@ -0,0 +1,18 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
steps:
- run: |
runs:
using: composite
steps:
- run: exit 1
shell: bash
if: env.LEAK_ENV != 'val'
shell: cp {0} action.yml
- uses: ./
env:
LEAK_ENV: val
- run: exit 1
if: env.LEAK_ENV == 'val'

View file

@ -0,0 +1,12 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
steps:
- run: |
FROM ubuntu:latest
ENV PATH="/opt/texlive/texdir/bin/x86_64-linuxmusl:${PATH}"
ENV ORG_PATH="${PATH}"
ENTRYPOINT [ "bash", "-c", "echo \"PATH=$PATH\" && echo \"ORG_PATH=$ORG_PATH\" && [[ \"$PATH\" = \"$ORG_PATH\" ]]" ]
shell: mv {0} Dockerfile
- uses: ./

View file

@ -0,0 +1,13 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
steps:
- run: |
echo "test<<World" > $GITHUB_ENV
echo "x=Thats really Weird" >> $GITHUB_ENV
echo "World" >> $GITHUB_ENV
- if: env.test != 'x=Thats really Weird'
run: exit 1
- if: env.x == 'Thats really Weird' # This assert is triggered by the broken impl of act
run: exit 1

View file

@ -0,0 +1,101 @@
name: environment-files
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: "Append to $GITHUB_PATH"
run: |
echo "$HOME/someFolder" >> $GITHUB_PATH
- name: "Append some more to $GITHUB_PATH"
run: |
echo "$HOME/someOtherFolder" >> $GITHUB_PATH
- name: "Check PATH"
run: |
echo "${PATH}"
if [[ ! "${PATH}" =~ .*"$HOME/"someOtherFolder.*"$HOME/"someFolder.* ]]; then
echo "${PATH} doesn't match .*someOtherFolder.*someFolder.*"
exit 1
fi
- name: "Prepend"
run: |
if ls | grep -q 'called ls' ; then
echo 'ls was overridden already?'
exit 2
fi
path_add=$(mktemp -d)
cat > $path_add/ls <<LS
#!/bin/sh
echo 'called ls'
LS
chmod +x $path_add/ls
echo $path_add >> $GITHUB_PATH
- name: "Verify prepend"
run: |
if ! ls | grep -q 'called ls' ; then
echo 'ls was not overridden'
exit 2
fi
- name: "Write single line env to $GITHUB_ENV"
run: |
echo "KEY=value" >> $GITHUB_ENV
- name: "Check single line env"
run: |
if [[ "${KEY}" != "value" ]]; then
echo "${KEY} doesn't == 'value'"
exit 1
fi
- name: "Write single line env with more than one 'equals' signs to $GITHUB_ENV"
run: |
echo "KEY=value=anothervalue" >> $GITHUB_ENV
- name: "Check single line env"
run: |
if [[ "${KEY}" != "value=anothervalue" ]]; then
echo "${KEY} doesn't == 'value=anothervalue'"
exit 1
fi
- name: "Write multiline env to $GITHUB_ENV"
run: |
echo 'KEY2<<EOF' >> $GITHUB_ENV
echo value2 >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: "Check multiline line env"
run: |
if [[ "${KEY2}" != "value2" ]]; then
echo "${KEY2} doesn't == 'value'"
exit 1
fi
- name: "Write multiline env with UUID to $GITHUB_ENV"
run: |
echo 'KEY3<<ghadelimiter_b8273c6d-d535-419a-a010-b0aaac240e36' >> $GITHUB_ENV
echo value3 >> $GITHUB_ENV
echo 'ghadelimiter_b8273c6d-d535-419a-a010-b0aaac240e36' >> $GITHUB_ENV
- name: "Check multiline env with UUID to $GITHUB_ENV"
run: |
if [[ "${KEY3}" != "value3" ]]; then
echo "${KEY3} doesn't == 'value3'"
exit 1
fi
- name: "Write single line output to $GITHUB_OUTPUT"
id: write-single-output
run: |
echo "KEY=value" >> $GITHUB_OUTPUT
- name: "Check single line output"
run: |
if [[ "${{ steps.write-single-output.outputs.KEY }}" != "value" ]]; then
echo "${{ steps.write-single-output.outputs.KEY }} doesn't == 'value'"
exit 1
fi
- name: "Write multiline output to $GITHUB_OUTPUT"
id: write-multi-output
run: |
echo 'KEY2<<EOF' >> $GITHUB_OUTPUT
echo value2 >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: "Check multiline output"
run: |
if [[ "${{ steps.write-multi-output.outputs.KEY2 }}" != "value2" ]]; then
echo "${{ steps.write-multi-output.outputs.KEY2 }} doesn't == 'value2'"
exit 1
fi

View file

@ -0,0 +1,33 @@
name: environment variables
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Test on job level
run: |
echo \$UPPER=$UPPER
echo \$upper=$upper
echo \$LOWER=$LOWER
echo \$lower=$lower
[[ "$UPPER" = "UPPER" ]] || exit 1
[[ "$upper" = "" ]] || exit 1
[[ "$LOWER" = "" ]] || exit 1
[[ "$lower" = "lower" ]] || exit 1
- name: Test on step level
run: |
echo \$UPPER=$UPPER
echo \$upper=$upper
echo \$LOWER=$LOWER
echo \$lower=$lower
[[ "$UPPER" = "upper" ]] || exit 1
[[ "$upper" = "" ]] || exit 1
[[ "$LOWER" = "" ]] || exit 1
[[ "$lower" = "LOWER" ]] || exit 1
env:
UPPER: upper
lower: LOWER
env:
UPPER: UPPER
lower: lower

View file

@ -0,0 +1,21 @@
on:
workflow_dispatch:
inputs:
NAME:
description: "A random input name for the workflow"
type: string
required: true
SOME_VALUE:
description: "Some other input to pass"
type: string
required: true
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Test with inputs
run: |
[ -z "${{ github.event.inputs.SOME_INPUT }}" ] && exit 1 || exit 0

View file

@ -0,0 +1,8 @@
inputs:
test-env-input: {}
runs:
using: composite
steps:
- run: |
exit ${{ inputs.test-env-input == env.test-env-input && '0' || '1'}}
shell: bash

View file

@ -0,0 +1,15 @@
on: push
jobs:
test-inputs-via-env-context:
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
- uses: ./inputs-via-env-context
with:
test-env-input: ${{ env.test-env-input }}
env:
test-env-input: ${{ github.event_name }}/${{ github.run_id }}
- run: |
exit ${{ env.test-env-input == format('{0}/{1}', github.event_name, github.run_id) && '0' || '1' }}
env:
test-env-input: ${{ github.event_name }}/${{ github.run_id }}

View file

@ -0,0 +1,16 @@
name: missing
on: push
jobs:
second:
runs-on: ubuntu-latest
needs: first
steps:
- run: echo How did you get here?
shell: bash
standalone:
runs-on: ubuntu-latest
steps:
- run: echo Hello world
shell: bash

View file

@ -0,0 +1,8 @@
name: no event
jobs:
stuck:
runs-on: ubuntu-latest
steps:
- run: echo How did you get here?
shell: bash

View file

@ -0,0 +1,10 @@
name: no first
on: push
jobs:
second:
runs-on: ubuntu-latest
needs: first
steps:
- run: echo How did you get here?
shell: bash

View file

@ -0,0 +1,15 @@
on:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: exit 0
assert:
needs: test
if: |
( always() && !cancelled() ) && (
( needs.test.result != 'success' || !success() ) )
runs-on: ubuntu-latest
steps:
- run: exit 1

View file

@ -0,0 +1,16 @@
name: test
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
val: ["success", "failure"]
fail-fast: false
steps:
- name: test
run: |
echo "Expected job result: ${{ matrix.val }}"
[[ "${{ matrix.val }}" = "success" ]] || exit 1

View file

@ -0,0 +1,29 @@
on: push
jobs:
local-invalid-step:
runs-on: ubuntu-latest
steps:
- run: |
runs:
using: composite
steps:
- name: Foo
- uses: Foo/Bar
shell: cp {0} action.yml
- uses: ./
local-missing-steps:
runs-on: ubuntu-latest
steps:
- run: |
runs:
using: composite
shell: cp {0} action.yml
- uses: ./
remote-invalid-step:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/invalid-composite-action/invalid-step@main
remote-missing-steps:
runs-on: ubuntu-latest
steps:
- uses: nektos/act-test-actions/invalid-composite-action/missing-steps@main

View file

@ -0,0 +1,21 @@
name: output action
description: output action
inputs:
input:
description: some input
required: false
outputs:
job-output:
description: some output
value: ${{ steps.gen-out.outputs.step-output }}
runs:
using: composite
steps:
- name: run step
id: gen-out
run: |
echo "::set-output name=step-output::"
shell: bash

View file

@ -0,0 +1,39 @@
name: path tests
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Append to $GITHUB_PATH"
run: |
echo "/opt/hostedtoolcache/node/18.99/x64/bin" >> $GITHUB_PATH
- name: test path (after setup)
run: |
if ! echo "$PATH" |grep "/opt/hostedtoolcache/node/18.*/\(x64\|arm64\)/bin" ; then
echo "Node binaries not in path: $PATH"
exit 1
fi
- id: action-with-output
uses: ./path-handling/
- name: test path (after local action)
run: |
if ! echo "$PATH" |grep "/opt/hostedtoolcache/node/18.*/\(x64\|arm64\)/bin" ; then
echo "Node binaries not in path: $PATH"
exit 1
fi
- uses: nektos/act-test-actions/composite@main
with:
input: some input
- name: test path (after remote action)
run: |
if ! echo "$PATH" |grep "/opt/hostedtoolcache/node/18.*/\(x64\|arm64\)/bin" ; then
echo "Node binaries not in path: $PATH"
exit 1
fi

View file

@ -0,0 +1,30 @@
name: remote-action-js
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:16-buster-slim
options: --user node
steps:
- name: check permissions of env files
id: test
run: |
echo "USER: $(id -un) expected: node"
[[ "$(id -un)" = "node" ]]
echo "TEST=Value" >> $GITHUB_OUTPUT
shell: bash
- name: check if file command worked
if: steps.test.outputs.test != 'Value'
run: |
echo "steps.test.outputs.test=${{ steps.test.outputs.test || 'missing value!' }}"
exit 1
shell: bash
- uses: actions/hello-world-javascript-action@v1
with:
who-to-greet: 'Mona the Octocat'
- uses: cloudposse/actions/github/slash-command-dispatch@0.14.0

View file

@ -0,0 +1,15 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
env:
MY_ENV: test
steps:
- run: exit 1
if: env.MY_ENV != 'test'
- run: echo "MY_ENV=test2" > $GITHUB_ENV
- run: exit 1
if: env.MY_ENV != 'test2'
- run: echo "MY_ENV=returnedenv" > $GITHUB_ENV
- run: exit 1
if: env.MY_ENV != 'returnedenv'

View file

@ -0,0 +1,24 @@
on: push
jobs:
_:
runs-on: ubuntu-latest
env:
MY_ENV: test
steps:
- run: exit 1
if: env.MY_ENV != 'test'
- run: |
runs:
using: composite
steps:
- run: exit 1
shell: bash
if: env.MY_ENV != 'val'
- run: echo "MY_ENV=returnedenv" > $GITHUB_ENV
shell: bash
shell: cp {0} action.yml
- uses: ./
env:
MY_ENV: val
- run: exit 1
if: env.MY_ENV != 'returnedenv'

View file

@ -9,23 +9,22 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
# The output of actions/setup-node@v2 seems to fail the workflow - uses: actions/setup-node@v3
# - uses: actions/setup-node@v2 with:
# with: node-version: '16'
# node-version: '16' - run: |
# - run: | console.log(process.version);
# console.log(process.version); console.log("Hi from node");
# console.log("Hi from node"); console.log("${{ inputs.test_input_optional }}");
# console.log("${{ inputs.test_input_optional }}"); if("${{ inputs.test_input_optional }}" !== "Test") {
# if("${{ inputs.test_input_optional }}" !== "Test") { console.log("Invalid input test_input_optional expected \"Test\" as value");
# console.log("Invalid input test_input_optional expected \"Test\" as value"); process.exit(1);
# process.exit(1); }
# } if(!process.version.startsWith('v16')) {
# if(!process.version.startsWith('v16')) { console.log("Expected node v16, but got " + process.version);
# console.log("Expected node v16, but got " + process.version); process.exit(1);
# process.exit(1); }
# } shell: node {0}
# shell: node {0}
- uses: ./uses-composite/composite_action - uses: ./uses-composite/composite_action
id: composite id: composite
with: with:

View file

@ -0,0 +1,36 @@
name: local-reusable-workflows
on: pull_request
jobs:
reusable-workflow:
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
number_required: 1
secrets:
secret: keep_it_private
reusable-workflow-with-inherited-secrets:
uses: ./.github/workflows/local-reusable-workflow.yml
with:
string_required: string
bool_required: ${{ true }}
number_required: 1
secrets: inherit
output-test:
runs-on: ubuntu-latest
needs:
- reusable-workflow
- reusable-workflow-with-inherited-secrets
steps:
- name: output with secrets map
run: |
echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }}
[[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1
- name: output with inherited secrets
run: |
echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }}
[[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1

View file

@ -2,8 +2,34 @@ on: push
jobs: jobs:
reusable-workflow: reusable-workflow:
uses: nektos/act-tests/.github/workflows/reusable-workflow.yml@master uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
with: with:
username: mona string_required: string
bool_required: ${{ true }}
number_required: 1
secrets: secrets:
envPATH: ${{ secrets.envPAT }} secret: keep_it_private
reusable-workflow-with-inherited-secrets:
uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
with:
string_required: string
bool_required: ${{ true }}
number_required: 1
secrets: inherit
output-test:
runs-on: ubuntu-latest
needs:
- reusable-workflow
- reusable-workflow-with-inherited-secrets
steps:
- name: output with secrets map
run: |
echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }}
[[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1
- name: output with inherited secrets
run: |
echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }}
[[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1

View file

@ -0,0 +1,18 @@
package workflowpattern
import "fmt"
type TraceWriter interface {
Info(string, ...interface{})
}
type EmptyTraceWriter struct{}
func (*EmptyTraceWriter) Info(string, ...interface{}) {
}
type StdOutTraceWriter struct{}
func (*StdOutTraceWriter) Info(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
}

View file

@ -0,0 +1,196 @@
package workflowpattern
import (
"fmt"
"regexp"
"strings"
)
type WorkflowPattern struct {
Pattern string
Negative bool
Regex *regexp.Regexp
}
func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
negative := false
pattern := rawpattern
if strings.HasPrefix(rawpattern, "!") {
negative = true
pattern = rawpattern[1:]
}
rpattern, err := PatternToRegex(pattern)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(rpattern)
if err != nil {
return nil, err
}
return &WorkflowPattern{
Pattern: pattern,
Negative: negative,
Regex: regex,
}, nil
}
//nolint:gocyclo
func PatternToRegex(pattern string) (string, error) {
var rpattern strings.Builder
rpattern.WriteString("^")
pos := 0
errors := map[int]string{}
for pos < len(pattern) {
switch pattern[pos] {
case '*':
if pos+1 < len(pattern) && pattern[pos+1] == '*' {
if pos+2 < len(pattern) && pattern[pos+2] == '/' {
rpattern.WriteString("(.+/)?")
pos += 3
} else {
rpattern.WriteString(".*")
pos += 2
}
} else {
rpattern.WriteString("[^/]*")
pos++
}
case '+', '?':
if pos > 0 {
rpattern.WriteByte(pattern[pos])
} else {
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
}
pos++
case '[':
rpattern.WriteByte(pattern[pos])
pos++
if pos < len(pattern) && pattern[pos] == ']' {
errors[pos] = "Unexpected empty brackets '[]'"
pos++
break
}
validChar := func(a, b, test byte) bool {
return test >= a && test <= b
}
startPos := pos
for pos < len(pattern) && pattern[pos] != ']' {
switch pattern[pos] {
case '-':
if pos <= startPos || pos+1 >= len(pattern) {
errors[pos] = "Invalid range"
pos++
break
}
validRange := func(a, b byte) bool {
return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1]
}
if !validRange('A', 'z') && !validRange('0', '9') {
errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9"
pos++
break
}
rpattern.WriteString(pattern[pos : pos+2])
pos += 2
default:
if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) {
errors[pos] = "Ranges can only include a-z, A-Z and 0-9"
pos++
break
}
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
pos++
}
}
if pos >= len(pattern) || pattern[pos] != ']' {
errors[pos] = "Missing closing bracket ']' after '['"
pos++
}
rpattern.WriteString("]")
pos++
case '\\':
if pos+1 >= len(pattern) {
errors[pos] = "Missing symbol after \\"
pos++
break
}
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]})))
pos += 2
default:
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
pos++
}
}
if len(errors) > 0 {
var errorMessage strings.Builder
for position, err := range errors {
if errorMessage.Len() > 0 {
errorMessage.WriteString(", ")
}
errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err))
}
return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String())
}
rpattern.WriteString("$")
return rpattern.String(), nil
}
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
ret := []*WorkflowPattern{}
for _, pattern := range patterns {
cp, err := CompilePattern(pattern)
if err != nil {
return nil, err
}
ret = append(ret, cp)
}
return ret, nil
}
// returns true if the workflow should be skipped paths/branches
func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
if len(sequence) == 0 {
return false
}
for _, file := range input {
matched := false
for _, item := range sequence {
if item.Regex.MatchString(file) {
pattern := item.Pattern
if item.Negative {
matched = false
traceWriter.Info("%s excluded by pattern %s", file, pattern)
} else {
matched = true
traceWriter.Info("%s included by pattern %s", file, pattern)
}
}
}
if matched {
return false
}
}
return true
}
// returns true if the workflow should be skipped paths-ignore/branches-ignore
func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
if len(sequence) == 0 {
return false
}
for _, file := range input {
matched := false
for _, item := range sequence {
if item.Regex.MatchString(file) == !item.Negative {
pattern := item.Pattern
traceWriter.Info("%s ignored by pattern %s", file, pattern)
matched = true
break
}
}
if !matched {
return false
}
}
return true
}

View file

@ -0,0 +1,414 @@
package workflowpattern
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMatchPattern(t *testing.T) {
kases := []struct {
inputs []string
patterns []string
skipResult bool
filterResult bool
}{
{
patterns: []string{"*"},
inputs: []string{"path/with/slash"},
skipResult: true,
filterResult: false,
},
{
patterns: []string{"path/a", "path/b", "path/c"},
inputs: []string{"meta", "path/b", "otherfile"},
skipResult: false,
filterResult: false,
},
{
patterns: []string{"path/a", "path/b", "path/c"},
inputs: []string{"path/b"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"path/a", "path/b", "path/c"},
inputs: []string{"path/c", "path/b"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"path/a", "path/b", "path/c"},
inputs: []string{"path/c", "path/b", "path/a"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"path/a", "path/b", "path/c"},
inputs: []string{"path/c", "path/b", "path/d", "path/a"},
skipResult: false,
filterResult: false,
},
{
patterns: []string{},
inputs: []string{},
skipResult: false,
filterResult: false,
},
{
patterns: []string{"\\!file"},
inputs: []string{"!file"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"escape\\\\backslash"},
inputs: []string{"escape\\backslash"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{".yml"},
inputs: []string{"fyml"},
skipResult: true,
filterResult: false,
},
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags
{
patterns: []string{"feature/*"},
inputs: []string{"feature/my-branch"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"feature/*"},
inputs: []string{"feature/your-branch"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"feature/**"},
inputs: []string{"feature/beta-a/my-branch"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"feature/**"},
inputs: []string{"feature/beta-a/my-branch"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"feature/**"},
inputs: []string{"feature/mona/the/octocat"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"main", "releases/mona-the-octocat"},
inputs: []string{"main"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"main", "releases/mona-the-octocat"},
inputs: []string{"releases/mona-the-octocat"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*"},
inputs: []string{"main"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*"},
inputs: []string{"releases"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**"},
inputs: []string{"all/the/branches"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**"},
inputs: []string{"every/tag"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*feature"},
inputs: []string{"mona-feature"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*feature"},
inputs: []string{"feature"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*feature"},
inputs: []string{"ver-10-feature"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"v2*"},
inputs: []string{"v2"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"v2*"},
inputs: []string{"v2.0"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"v2*"},
inputs: []string{"v2.9"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"v[12].[0-9]+.[0-9]+"},
inputs: []string{"v1.10.1"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"v[12].[0-9]+.[0-9]+"},
inputs: []string{"v2.0.0"},
skipResult: false,
filterResult: true,
},
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths
{
patterns: []string{"*"},
inputs: []string{"README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*"},
inputs: []string{"server.rb"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.jsx?"},
inputs: []string{"page.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.jsx?"},
inputs: []string{"page.jsx"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**"},
inputs: []string{"all/the/files.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.js"},
inputs: []string{"app.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.js"},
inputs: []string{"index.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**.js"},
inputs: []string{"index.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**.js"},
inputs: []string{"js/index.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**.js"},
inputs: []string{"src/js/app.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/*"},
inputs: []string{"docs/README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/*"},
inputs: []string{"docs/file.txt"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/**"},
inputs: []string{"docs/README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/**"},
inputs: []string{"docs/mona/octocat.txt"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/**/*.md"},
inputs: []string{"docs/README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/**/*.md"},
inputs: []string{"docs/mona/hello-world.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"docs/**/*.md"},
inputs: []string{"docs/a/markdown/file.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/docs/**"},
inputs: []string{"docs/hello.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/docs/**"},
inputs: []string{"dir/docs/my-file.txt"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/docs/**"},
inputs: []string{"space/docs/plan/space.doc"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/README.md"},
inputs: []string{"README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/README.md"},
inputs: []string{"js/README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/*src/**"},
inputs: []string{"a/src/app.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/*src/**"},
inputs: []string{"my-src/code/js/app.js"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/*-post.md"},
inputs: []string{"my-post.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/*-post.md"},
inputs: []string{"path/their-post.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/migrate-*.sql"},
inputs: []string{"migrate-10909.sql"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/migrate-*.sql"},
inputs: []string{"db/migrate-v1.0.sql"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"**/migrate-*.sql"},
inputs: []string{"db/sept/migrate-v1.sql"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md"},
inputs: []string{"hello.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md"},
inputs: []string{"README.md"},
skipResult: true,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md"},
inputs: []string{"docs/hello.md"},
skipResult: true,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md", "README*"},
inputs: []string{"hello.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md", "README*"},
inputs: []string{"README.md"},
skipResult: false,
filterResult: true,
},
{
patterns: []string{"*.md", "!README.md", "README*"},
inputs: []string{"README.doc"},
skipResult: false,
filterResult: true,
},
}
for _, kase := range kases {
t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) {
patterns, err := CompilePatterns(kase.patterns...)
assert.NoError(t, err)
assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult")
assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult")
})
}
}