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:
commit
1dda0aec69
97 changed files with 3928 additions and 2256 deletions
2
.github/actions/choco/Dockerfile
vendored
2
.github/actions/choco/Dockerfile
vendored
|
@ -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
77
.github/actions/run-tests/action.yml
vendored
Normal 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
|
18
.github/workflows/checks.yml
vendored
18
.github/workflows/checks.yml
vendored
|
@ -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
|
||||||
|
|
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
|
@ -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 }}
|
||||||
|
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -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
102
README.md
|
@ -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
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.2.43
|
|
@ -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
150
cmd/notices.go
Normal 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(¬ices); 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")
|
||||||
|
}
|
152
cmd/root.go
152
cmd/root.go
|
@ -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
60
go.mod
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
4
main.go
4
main.go
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
43
pkg/artifacts/testdata/GHSL-2023-004/artifacts.yml
vendored
Normal file
43
pkg/artifacts/testdata/GHSL-2023-004/artifacts.yml
vendored
Normal 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
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
73
pkg/container/container_types.go
Normal file
73
pkg/container/container_types.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
|
@ -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{
|
||||||
"": {},
|
"": {},
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
57
pkg/container/docker_stub.go
Normal file
57
pkg/container/docker_stub.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
60
pkg/container/parse_env_file.go
Normal file
60
pkg/container/parse_env_file.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()) {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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{}{
|
||||||
|
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
66
pkg/runner/command.go
Executable file → Normal 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
129
pkg/runner/reusable_workflow.go
Normal file
129
pkg/runner/reusable_workflow.go
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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": "",
|
||||||
|
|
82
pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml
vendored
Normal file
82
pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml
vendored
Normal 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 }}
|
27
pkg/runner/testdata/GITHUB_ENV-use-in-env-ctx/push.yml
vendored
Normal file
27
pkg/runner/testdata/GITHUB_ENV-use-in-env-ctx/push.yml
vendored
Normal 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 }}
|
48
pkg/runner/testdata/GITHUB_STATE/push.yml
vendored
Normal file
48
pkg/runner/testdata/GITHUB_STATE/push.yml
vendored
Normal 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"
|
|
@ -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'
|
||||||
|
|
18
pkg/runner/testdata/do-not-leak-step-env-in-composite/push.yml
vendored
Normal file
18
pkg/runner/testdata/do-not-leak-step-env-in-composite/push.yml
vendored
Normal 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'
|
12
pkg/runner/testdata/docker-action-custom-path/push.yml
vendored
Normal file
12
pkg/runner/testdata/docker-action-custom-path/push.yml
vendored
Normal 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: ./
|
13
pkg/runner/testdata/environment-files-parser-bug/push.yaml
vendored
Normal file
13
pkg/runner/testdata/environment-files-parser-bug/push.yaml
vendored
Normal 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
|
101
pkg/runner/testdata/environment-files/push.yaml
vendored
Normal file
101
pkg/runner/testdata/environment-files/push.yaml
vendored
Normal 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
|
33
pkg/runner/testdata/environment-variables/push.yml
vendored
Normal file
33
pkg/runner/testdata/environment-variables/push.yml
vendored
Normal 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
|
21
pkg/runner/testdata/input-from-cli/input.yml
vendored
Normal file
21
pkg/runner/testdata/input-from-cli/input.yml
vendored
Normal 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
|
8
pkg/runner/testdata/inputs-via-env-context/action.yml
vendored
Normal file
8
pkg/runner/testdata/inputs-via-env-context/action.yml
vendored
Normal 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
|
15
pkg/runner/testdata/inputs-via-env-context/push.yml
vendored
Normal file
15
pkg/runner/testdata/inputs-via-env-context/push.yml
vendored
Normal 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 }}
|
16
pkg/runner/testdata/issue-1595/missing.yml
vendored
Normal file
16
pkg/runner/testdata/issue-1595/missing.yml
vendored
Normal 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
|
8
pkg/runner/testdata/issue-1595/no-event.yml
vendored
Normal file
8
pkg/runner/testdata/issue-1595/no-event.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
name: no event
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stuck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo How did you get here?
|
||||||
|
shell: bash
|
10
pkg/runner/testdata/issue-1595/no-first.yml
vendored
Normal file
10
pkg/runner/testdata/issue-1595/no-first.yml
vendored
Normal 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
|
15
pkg/runner/testdata/job-needs-context-contains-result/push.yml
vendored
Normal file
15
pkg/runner/testdata/job-needs-context-contains-result/push.yml
vendored
Normal 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
|
16
pkg/runner/testdata/matrix-exitcode/push.yml
vendored
Normal file
16
pkg/runner/testdata/matrix-exitcode/push.yml
vendored
Normal 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
|
29
pkg/runner/testdata/no-panic-on-invalid-composite-action/push.yml
vendored
Normal file
29
pkg/runner/testdata/no-panic-on-invalid-composite-action/push.yml
vendored
Normal 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
|
21
pkg/runner/testdata/path-handling/action.yml
vendored
Normal file
21
pkg/runner/testdata/path-handling/action.yml
vendored
Normal 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
|
39
pkg/runner/testdata/path-handling/push.yml
vendored
Normal file
39
pkg/runner/testdata/path-handling/push.yml
vendored
Normal 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
|
30
pkg/runner/testdata/remote-action-js-node-user/push.yml
vendored
Normal file
30
pkg/runner/testdata/remote-action-js-node-user/push.yml
vendored
Normal 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
|
15
pkg/runner/testdata/set-env-new-env-file-per-step/push.yml
vendored
Normal file
15
pkg/runner/testdata/set-env-new-env-file-per-step/push.yml
vendored
Normal 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'
|
24
pkg/runner/testdata/set-env-step-env-override/push.yml
vendored
Normal file
24
pkg/runner/testdata/set-env-step-env-override/push.yml
vendored
Normal 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'
|
|
@ -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:
|
||||||
|
|
36
pkg/runner/testdata/uses-workflow/local-workflow.yml
vendored
Normal file
36
pkg/runner/testdata/uses-workflow/local-workflow.yml
vendored
Normal 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
|
32
pkg/runner/testdata/uses-workflow/push.yml
vendored
32
pkg/runner/testdata/uses-workflow/push.yml
vendored
|
@ -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
|
||||||
|
|
18
pkg/workflowpattern/trace_writer.go
Normal file
18
pkg/workflowpattern/trace_writer.go
Normal 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...)
|
||||||
|
}
|
196
pkg/workflowpattern/workflow_pattern.go
Normal file
196
pkg/workflowpattern/workflow_pattern.go
Normal 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
|
||||||
|
}
|
414
pkg/workflowpattern/workflow_pattern_test.go
Normal file
414
pkg/workflowpattern/workflow_pattern_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue