Compare commits
21 Commits
nektos/v0.
...
v0.234.2
Author | SHA1 | Date | |
---|---|---|---|
|
dca7801682 | ||
|
4b99ed8916 | ||
|
e46ede1b17 | ||
|
1ba076d321 | ||
|
0efa2d5e63 | ||
|
0a37a03f2e | ||
|
88cce47022 | ||
|
7920109e89 | ||
|
4cacc14d22 | ||
|
c6b8548d35 | ||
|
64cae197a4 | ||
|
7fb84a54a8 | ||
|
70cc6c017b | ||
|
d7e9ea75fc | ||
|
b9c20dcaa4 | ||
|
97629ae8af | ||
|
b9a9812ad9 | ||
|
113c3e98fb | ||
|
7815eec33b | ||
|
c051090583 | ||
|
0fa1fe0310 |
2
.github/actions/choco/Dockerfile
vendored
2
.github/actions/choco/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.16
|
||||||
|
|
||||||
ARG CHOCOVERSION=1.1.0
|
ARG CHOCOVERSION=1.1.0
|
||||||
|
|
||||||
|
77
.github/actions/run-tests/action.yml
vendored
77
.github/actions/run-tests/action.yml
vendored
@@ -1,77 +0,0 @@
|
|||||||
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.4.0
|
- uses: golangci/golangci-lint-action@v3.3.1
|
||||||
with:
|
with:
|
||||||
version: v1.47.2
|
version: v1.47.2
|
||||||
- uses: megalinter/megalinter/flavors/go@v6.20.0
|
- uses: megalinter/megalinter/flavors/go@v6.15.0
|
||||||
env:
|
env:
|
||||||
DEFAULT_BRANCH: master
|
DEFAULT_BRANCH: master
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -50,10 +50,7 @@ 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-
|
||||||
- name: Run Tests
|
- run: go test -v -cover -coverprofile=coverage.txt -covermode=atomic -timeout 15m ./...
|
||||||
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:
|
||||||
@@ -76,11 +73,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Run Tests
|
- run: go test -v -run ^TestRunEventHostEnvironment$ ./...
|
||||||
uses: ./.github/actions/run-tests
|
# TODO merge coverage with test-linux
|
||||||
with:
|
|
||||||
filter: '^TestRunEventHostEnvironment$'
|
|
||||||
upload-logs-name: logs-${{ matrix.os }}
|
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name: snapshot
|
name: snapshot
|
||||||
@@ -99,7 +93,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
- name: GoReleaser
|
- name: GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v3
|
||||||
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@v4
|
uses: goreleaser/goreleaser-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --rm-dist
|
||||||
@@ -39,29 +39,3 @@ 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@v7
|
- uses: actions/stale@v6
|
||||||
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: 180
|
days-before-stale: 30
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ coverage.txt
|
|||||||
|
|
||||||
# megalinter
|
# megalinter
|
||||||
report/
|
report/
|
||||||
|
act
|
||||||
|
@@ -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|VERSION)
|
FILTER_REGEX_EXCLUDE: (.*testdata/*|install.sh|pkg/container/docker_cli.go|pkg/container/DOCKER_LICENSE)
|
||||||
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,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 The Gitea Authors
|
||||||
Copyright (c) 2019
|
Copyright (c) 2019
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
6
Makefile
6
Makefile
@@ -96,11 +96,7 @@ 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
|
||||||
@@ -109,5 +105,3 @@ snapshot:
|
|||||||
--rm-dist \
|
--rm-dist \
|
||||||
--single-target \
|
--single-target \
|
||||||
--snapshot
|
--snapshot
|
||||||
|
|
||||||
.PHONY: clean all
|
|
||||||
|
116
README.md
116
README.md
@@ -1,3 +1,17 @@
|
|||||||
|
## Naming rules:
|
||||||
|
|
||||||
|
Branches:
|
||||||
|
|
||||||
|
- `main`: default branch, contains custom changes.
|
||||||
|
- `nektos/master`: mirror for `master` of [nektos/act](https://github.com/nektos/act/).
|
||||||
|
|
||||||
|
Tags:
|
||||||
|
|
||||||
|
- `vX.YZ.*`: based on `nektos/vX.Y.Z`, contains custom changes.
|
||||||
|
- `nektos/vX.Y.Z`: mirror for `vX.Y.Z` of [nektos/act](https://github.com/nektos/act/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# Overview [](https://github.com/nektos/act/actions) [](https://gitter.im/nektos/act?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://goreportcard.com/report/github.com/nektos/act) [](https://github.com/jonico/awesome-runners)
|
# Overview [](https://github.com/nektos/act/actions) [](https://gitter.im/nektos/act?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://goreportcard.com/report/github.com/nektos/act) [](https://github.com/jonico/awesome-runners)
|
||||||
@@ -71,14 +85,6 @@ choco install act-cli
|
|||||||
scoop install act
|
scoop install act
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/) (Windows)
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
[](https://aur.archlinux.org/packages/act/)
|
[](https://aur.archlinux.org/packages/act/)
|
||||||
@@ -116,14 +122,6 @@ 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
|
||||||
@@ -131,7 +129,7 @@ gh extension install nektos/gh-act
|
|||||||
Run this command in your terminal:
|
Run this command in your terminal:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
|
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual download
|
### Manual download
|
||||||
@@ -179,6 +177,49 @@ 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.
|
||||||
@@ -315,41 +356,10 @@ 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
|
||||||
@@ -381,7 +391,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
|
||||||
|
|
||||||
@@ -407,14 +417,6 @@ 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
|
||||||
|
@@ -17,14 +17,12 @@ 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
|
||||||
@@ -32,7 +30,6 @@ 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
|
||||||
@@ -40,7 +37,6 @@ 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
|
||||||
@@ -87,8 +83,3 @@ 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
150
cmd/notices.go
@@ -1,150 +0,0 @@
|
|||||||
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,7 +12,6 @@ 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"
|
||||||
@@ -31,14 +30,13 @@ 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: setup(input),
|
PersistentPreRun: setupLogging,
|
||||||
PersistentPostRun: cleanup(input),
|
Version: version,
|
||||||
Version: version,
|
SilenceUsage: true,
|
||||||
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")
|
||||||
@@ -49,12 +47,11 @@ 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", true, "pull docker image(s) even if already present")
|
rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull 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.forceRebuild, "rebuild", "", false, "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")
|
||||||
@@ -77,14 +74,11 @@ 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.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 (will only bind to localhost).")
|
||||||
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())
|
||||||
|
|
||||||
@@ -99,21 +93,18 @@ 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
|
||||||
for _, fileName := range []string{"act/actrc", configFileName} {
|
if xdg, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok && xdg != "" {
|
||||||
if foundConfig, err := xdg.SearchConfigFile(fileName); foundConfig != "" && err == nil {
|
actrcXdg = filepath.Join(xdg, ".actrc")
|
||||||
actrcXdg = foundConfig
|
} else {
|
||||||
break
|
actrcXdg = filepath.Join(home, ".config", ".actrc")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{
|
return []string{
|
||||||
filepath.Join(home, configFileName),
|
filepath.Join(home, ".actrc"),
|
||||||
actrcXdg,
|
actrcXdg,
|
||||||
filepath.Join(".", configFileName),
|
filepath.Join(".", ".actrc"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,37 +241,13 @@ func readArgsFile(file string, split bool) []string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(inputs *Input) func(*cobra.Command, []string) {
|
func setupLogging(cmd *cobra.Command, _ []string) {
|
||||||
return func(cmd *cobra.Command, _ []string) {
|
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||||
verbose, _ := cmd.Flags().GetBool("verbose")
|
if verbose {
|
||||||
if verbose {
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
}
|
|
||||||
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)
|
||||||
@@ -317,14 +284,18 @@ 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)
|
||||||
_ = parseEnvs(input.envs, envs)
|
if input.envs != nil {
|
||||||
|
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)
|
||||||
@@ -358,7 +329,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])
|
||||||
@@ -370,35 +341,23 @@ 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, plannerErr = planner.PlanJob(jobID)
|
filterPlan = 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, plannerErr = planner.PlanEvent(filterEventName)
|
filterPlan = planner.PlanEvent(filterEventName)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Preparing plan with all jobs")
|
log.Debugf("Preparing plan with all jobs")
|
||||||
filterPlan, plannerErr = planner.PlanAll()
|
filterPlan = planner.PlanAll()
|
||||||
}
|
|
||||||
if filterPlan == nil && plannerErr != nil {
|
|
||||||
return plannerErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if list {
|
if list {
|
||||||
err = printList(filterPlan)
|
return printList(filterPlan)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return plannerErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph {
|
if graph {
|
||||||
err = drawGraph(filterPlan)
|
return drawGraph(filterPlan)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return plannerErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// plan with triggered jobs
|
// plan with triggered jobs
|
||||||
@@ -426,13 +385,10 @@ 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, plannerErr = planner.PlanJob(jobID)
|
plan = planner.PlanJob(jobID)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Planning jobs for event: %s", eventName)
|
log.Debugf("Planning jobs for event: %s", eventName)
|
||||||
plan, plannerErr = planner.PlanEvent(eventName)
|
plan = 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
|
||||||
@@ -458,19 +414,6 @@ 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{
|
||||||
@@ -487,7 +430,6 @@ 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(),
|
||||||
@@ -495,14 +437,12 @@ 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,
|
||||||
@@ -514,28 +454,20 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort)
|
cancel := artifacts.Serve(ctx, input.artifactServerPath, 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 {
|
||||||
err = watchAndRun(ctx, r.NewPlanExecutor(plan))
|
return 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
|
||||||
})
|
})
|
||||||
err = executor(ctx)
|
return executor(ctx)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return plannerErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +492,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 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"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(actrc)
|
f, err := os.Create(actrc)
|
||||||
|
60
go.mod
60
go.mod
@@ -5,77 +5,79 @@ 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 v23.0.1+incompatible
|
github.com/docker/cli v20.10.21+incompatible
|
||||||
github.com/docker/distribution v2.8.1+incompatible
|
github.com/docker/distribution v2.8.1+incompatible
|
||||||
github.com/docker/docker v23.0.1+incompatible
|
github.com/docker/docker v20.10.21+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.4.1
|
github.com/go-git/go-billy/v5 v5.3.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.5.1
|
github.com/joho/godotenv v1.4.0
|
||||||
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.17
|
github.com/mattn/go-isatty v0.0.16
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/moby/buildkit v0.11.4
|
github.com/moby/buildkit v0.10.6
|
||||||
github.com/moby/patternmatcher v0.5.0
|
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc2
|
github.com/opencontainers/selinux v1.10.2
|
||||||
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.23
|
github.com/rhysd/actionlint v1.6.22
|
||||||
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.6.0
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||||
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/containerd v1.6.18 // indirect
|
github.com/containerd/cgroups v1.0.3 // 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.7.0 // indirect
|
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.4.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/google/go-cmp v0.5.9 // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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.14 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
github.com/moby/sys/mount v0.3.1 // indirect
|
||||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
|
github.com/moby/sys/mountinfo v0.6.0 // 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.3 // indirect
|
github.com/opencontainers/runc v1.1.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.3 // indirect
|
github.com/rivo/uniseg v0.3.4 // 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 v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect
|
||||||
golang.org/x/crypto v0.2.0 // indirect
|
go.opencensus.io v0.23.0 // indirect
|
||||||
golang.org/x/net v0.7.0 // indirect
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
||||||
golang.org/x/text v0.7.0 // indirect
|
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // 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,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -10,8 +9,7 @@ import (
|
|||||||
"github.com/nektos/act/cmd"
|
"github.com/nektos/act/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed VERSION
|
var version = "v0.2.27-dev" // Manually bump after tagging next release
|
||||||
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,34 +46,28 @@ type ResponseMessage struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WritableFile interface {
|
type MkdirFS interface {
|
||||||
io.WriteCloser
|
fs.FS
|
||||||
|
MkdirAll(path string, perm fs.FileMode) error
|
||||||
|
Open(name string) (fs.File, error)
|
||||||
|
OpenAtEnd(name string) (fs.File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WriteFS interface {
|
type MkdirFsImpl struct {
|
||||||
OpenWritable(name string) (WritableFile, error)
|
dir string
|
||||||
OpenAppendable(name string) (WritableFile, error)
|
fs.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
type readWriteFSImpl struct {
|
func (fsys MkdirFsImpl) MkdirAll(path string, perm fs.FileMode) error {
|
||||||
|
return os.MkdirAll(fsys.dir+"/"+path, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
|
func (fsys MkdirFsImpl) Open(name string) (fs.File, error) {
|
||||||
return os.Open(name)
|
return os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
|
func (fsys MkdirFsImpl) OpenAtEnd(name string) (fs.File, error) {
|
||||||
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
file, err := os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR, 0644)
|
||||||
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
|
||||||
@@ -83,16 +77,13 @@ func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, 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 safeResolve(baseDir string, relPath string) string {
|
func uploads(router *httprouter.Router, fsys MkdirFS) {
|
||||||
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")
|
||||||
|
|
||||||
@@ -117,15 +108,19 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
|
|||||||
itemPath += gzipExtension
|
itemPath += gzipExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
safeRunPath := safeResolve(baseDir, runID)
|
filePath := fmt.Sprintf("%s/%s", runID, itemPath)
|
||||||
safePath := safeResolve(safeRunPath, itemPath)
|
|
||||||
|
|
||||||
file, err := func() (WritableFile, error) {
|
err := fsys.MkdirAll(path.Dir(filePath), os.ModePerm)
|
||||||
|
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.OpenAppendable(safePath)
|
return fsys.OpenAtEnd(filePath)
|
||||||
}
|
}
|
||||||
return fsys.OpenWritable(safePath)
|
return fsys.Open(filePath)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,13 +170,11 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
|
func downloads(router *httprouter.Router, 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")
|
||||||
|
|
||||||
safePath := safeResolve(baseDir, runID)
|
entries, err := fs.ReadDir(fsys, runID)
|
||||||
|
|
||||||
entries, err := fs.ReadDir(fsys, safePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -211,12 +204,12 @@ func downloads(router *httprouter.Router, baseDir string, 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")
|
||||||
safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
|
dirPath := fmt.Sprintf("%s/%s", container, itemPath)
|
||||||
|
|
||||||
var files []ContainerItem
|
var files []ContainerItem
|
||||||
err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error {
|
err := fs.WalkDir(fsys, dirPath, func(path string, entry fs.DirEntry, err error) error {
|
||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
rel, err := filepath.Rel(safePath, path)
|
rel, err := filepath.Rel(dirPath, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -225,7 +218,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
|
|||||||
rel = strings.TrimSuffix(rel, gzipExtension)
|
rel = strings.TrimSuffix(rel, gzipExtension)
|
||||||
|
|
||||||
files = append(files, ContainerItem{
|
files = append(files, ContainerItem{
|
||||||
Path: filepath.Join(itemPath, rel),
|
Path: fmt.Sprintf("%s/%s", 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),
|
||||||
})
|
})
|
||||||
@@ -252,12 +245,10 @@ func downloads(router *httprouter.Router, baseDir string, 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:]
|
||||||
|
|
||||||
safePath := safeResolve(baseDir, path)
|
file, err := fsys.Open(path)
|
||||||
|
|
||||||
file, err := fsys.Open(safePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// try gzip file
|
// try gzip file
|
||||||
file, err = fsys.Open(safePath + gzipExtension)
|
file, err = fsys.Open(path + gzipExtension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -271,7 +262,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc {
|
func Serve(ctx context.Context, artifactPath string, port string) context.CancelFunc {
|
||||||
serverContext, cancel := context.WithCancel(ctx)
|
serverContext, cancel := context.WithCancel(ctx)
|
||||||
logger := common.Logger(serverContext)
|
logger := common.Logger(serverContext)
|
||||||
|
|
||||||
@@ -282,19 +273,20 @@ func Serve(ctx context.Context, artifactPath string, addr string, port string) c
|
|||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
|
|
||||||
logger.Debugf("Artifacts base path '%s'", artifactPath)
|
logger.Debugf("Artifacts base path '%s'", artifactPath)
|
||||||
fsys := readWriteFSImpl{}
|
fs := os.DirFS(artifactPath)
|
||||||
uploads(router, artifactPath, fsys)
|
uploads(router, MkdirFsImpl{artifactPath, fs})
|
||||||
downloads(router, artifactPath, fsys)
|
downloads(router, fs)
|
||||||
|
ip := common.GetOutboundIP().String()
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%s", addr, port),
|
Addr: fmt.Sprintf("%s:%s", ip, 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", addr, port)
|
logger.Infof("Start server on http://%s:%s", ip, 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,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -20,43 +21,44 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type writableMapFile struct {
|
type MapFsImpl struct {
|
||||||
fstest.MapFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *writableMapFile) Write(data []byte) (int, error) {
|
|
||||||
f.Data = data
|
|
||||||
return len(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *writableMapFile) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type writeMapFS struct {
|
|
||||||
fstest.MapFS
|
fstest.MapFS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
|
func (fsys MapFsImpl) MkdirAll(path string, perm fs.FileMode) error {
|
||||||
var file = &writableMapFile{
|
// mocked no-op
|
||||||
MapFile: fstest.MapFile{
|
return nil
|
||||||
Data: []byte("content2"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fsys.MapFS[name] = &file.MapFile
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
|
type WritableFile struct {
|
||||||
var file = &writableMapFile{
|
fs.File
|
||||||
MapFile: fstest.MapFile{
|
fsys fstest.MapFS
|
||||||
Data: []byte("content2"),
|
path string
|
||||||
},
|
}
|
||||||
}
|
|
||||||
fsys.MapFS[name] = &file.MapFile
|
|
||||||
|
|
||||||
return file, nil
|
func (file WritableFile) Write(data []byte) (int, error) {
|
||||||
|
file.fsys[file.path].Data = data
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fsys MapFsImpl) Open(path string) (fs.File, error) {
|
||||||
|
var file = fstest.MapFile{
|
||||||
|
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) {
|
||||||
|
var file = fstest.MapFile{
|
||||||
|
Data: []byte("content2"),
|
||||||
|
}
|
||||||
|
fsys.MapFS[path] = &file
|
||||||
|
|
||||||
|
result, err := fsys.MapFS.Open(path)
|
||||||
|
return WritableFile{result, fsys.MapFS, path}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewArtifactUploadPrepare(t *testing.T) {
|
func TestNewArtifactUploadPrepare(t *testing.T) {
|
||||||
@@ -65,7 +67,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, "artifact/server/path", writeMapFS{memfs})
|
uploads(router, MapFsImpl{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()
|
||||||
@@ -91,7 +93,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, "artifact/server/path", writeMapFS{memfs})
|
uploads(router, MapFsImpl{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()
|
||||||
@@ -109,7 +111,7 @@ func TestArtifactUploadBlob(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal("success", response.Message)
|
assert.Equal("success", response.Message)
|
||||||
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
|
assert.Equal("content", string(memfs["1/some/file"].Data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFinalizeArtifactUpload(t *testing.T) {
|
func TestFinalizeArtifactUpload(t *testing.T) {
|
||||||
@@ -118,7 +120,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, "artifact/server/path", writeMapFS{memfs})
|
uploads(router, MapFsImpl{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()
|
||||||
@@ -142,13 +144,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{
|
||||||
"artifact/server/path/1/file.txt": {
|
"1/file.txt": {
|
||||||
Data: []byte(""),
|
Data: []byte(""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
downloads(router, "artifact/server/path", memfs)
|
downloads(router, 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()
|
||||||
@@ -174,13 +176,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{
|
||||||
"artifact/server/path/1/some/file": {
|
"1/some/file": {
|
||||||
Data: []byte(""),
|
Data: []byte(""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
downloads(router, "artifact/server/path", memfs)
|
downloads(router, 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()
|
||||||
@@ -198,7 +200,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)
|
||||||
}
|
}
|
||||||
@@ -207,13 +209,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{
|
||||||
"artifact/server/path/1/some/file": {
|
"1/some/file": {
|
||||||
Data: []byte("content"),
|
Data: []byte("content"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
downloads(router, "artifact/server/path", memfs)
|
downloads(router, 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()
|
||||||
@@ -238,8 +240,7 @@ type TestJobFileInfo struct {
|
|||||||
containerArchitecture string
|
containerArchitecture string
|
||||||
}
|
}
|
||||||
|
|
||||||
var artifactsPath = path.Join(os.TempDir(), "test-artifacts")
|
var aritfactsPath = 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) {
|
||||||
@@ -249,7 +250,7 @@ func TestArtifactFlow(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
cancel := Serve(ctx, artifactsPath, artifactsAddr, artifactsPort)
|
cancel := Serve(ctx, aritfactsPath, artifactsPort)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
platforms := map[string]string{
|
platforms := map[string]string{
|
||||||
@@ -258,7 +259,6 @@ 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(artifactsPath); err != nil {
|
if err := os.RemoveAll(aritfactsPath); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,8 +286,7 @@ 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: artifactsPath,
|
ArtifactServerPath: aritfactsPath,
|
||||||
ArtifactServerAddr: artifactsAddr,
|
|
||||||
ArtifactServerPort: artifactsPort,
|
ArtifactServerPort: artifactsPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,96 +296,15 @@ 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, err := planner.PlanEvent(tjfi.eventName)
|
plan := 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.Nil(t, plan)
|
assert.Error(t, err, tjfi.errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
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,19 +7,20 @@ 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 (
|
||||||
@@ -54,40 +55,41 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if head.Hash().IsZero() {
|
bts, err := os.ReadFile(filepath.Join(gitDir, "HEAD"))
|
||||||
return "", "", fmt.Errorf("HEAD sha1 could not be resolved")
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := head.Hash().String()
|
var ref = strings.TrimSpace(strings.TrimPrefix(string(bts), "ref:"))
|
||||||
|
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", hash)
|
logger.Debugf("Found revision: %s", refBuf)
|
||||||
return hash[:7], strings.TrimSpace(hash), nil
|
return string(refBuf[:7]), strings.TrimSpace(string(refBuf)), 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
|
||||||
@@ -98,58 +100,28 @@ 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 = ""
|
||||||
repo, err := git.PlainOpenWithOptions(
|
r, err := git.PlainOpen(filepath.Join(gitDir, ".."))
|
||||||
file,
|
if err == nil {
|
||||||
&git.PlainOpenOptions{
|
iter, err := r.References()
|
||||||
DetectDotGit: true,
|
if err == nil {
|
||||||
EnableDotGitCommonDir: true,
|
for {
|
||||||
},
|
r, err := iter.Next()
|
||||||
)
|
if r == nil || err != nil {
|
||||||
|
break
|
||||||
if err != nil {
|
}
|
||||||
return "", err
|
// logger.Debugf("Reference: name=%s sha=%s", r.Name().String(), r.Hash().String())
|
||||||
}
|
if r.Hash().String() == ref {
|
||||||
|
if r.Name().IsTag() {
|
||||||
iter, err := repo.References()
|
refTag = r.Name().String()
|
||||||
if err != nil {
|
}
|
||||||
return "", err
|
if r.Name().IsBranch() {
|
||||||
}
|
refBranch = r.Name().String()
|
||||||
|
}
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
iter.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -157,7 +129,39 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
|||||||
return refBranch, nil
|
return refBranch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("failed to identify reference (tag/branch) for the checked-out revision '%s'", ref)
|
// If the above doesn't work, fall back to the old way
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -175,27 +179,26 @@ 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) {
|
||||||
repo, err := git.PlainOpenWithOptions(
|
gitDir, err := findGitDirectory(file)
|
||||||
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)
|
||||||
|
|
||||||
remote, err := repo.Remote(remoteName)
|
gitconfig, err := ini.InsensitiveLoad(fmt.Sprintf("%s/config", gitDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
remote, err := gitconfig.GetSection(fmt.Sprintf(`remote "%s"`, remoteName))
|
||||||
if len(remote.Config().URLs) < 1 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("remote '%s' exists but has no URL", remoteName)
|
return "", err
|
||||||
}
|
}
|
||||||
|
urlKey, err := remote.GetKey("url")
|
||||||
return remote.Config().URLs[0], nil
|
if err != 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) {
|
||||||
@@ -219,6 +222,35 @@ 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
|
||||||
@@ -260,7 +292,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
if err = os.Chmod(input.Dir, 0755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -82,19 +82,12 @@ 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("-C", basedir, "remote", "add", "origin", remoteURL)
|
err = gitCmd("config", "-f", fmt.Sprintf("%s/.git/config", basedir), "--add", "remote.origin.url", 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) {
|
||||||
@@ -167,7 +160,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, 0o755))
|
require.NoError(t, os.MkdirAll(dir, 0755))
|
||||||
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)
|
||||||
|
@@ -25,3 +25,24 @@ func Logger(ctx context.Context) logrus.FieldLogger {
|
|||||||
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
|
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
|
||||||
return context.WithValue(ctx, loggerContextKeyVal, logger)
|
return context.WithValue(ctx, loggerContextKeyVal, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type loggerHookKey string
|
||||||
|
|
||||||
|
const loggerHookKeyVal = loggerHookKey("logrus.Hook")
|
||||||
|
|
||||||
|
// LoggerHook returns the appropriate logger hook for current context
|
||||||
|
// the hook affects job logger, not global logger
|
||||||
|
func LoggerHook(ctx context.Context) logrus.Hook {
|
||||||
|
val := ctx.Value(loggerHookKeyVal)
|
||||||
|
if val != nil {
|
||||||
|
if hook, ok := val.(logrus.Hook); ok {
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLoggerHook adds a value to the context for the logger hook
|
||||||
|
func WithLoggerHook(ctx context.Context, hook logrus.Hook) context.Context {
|
||||||
|
return context.WithValue(ctx, loggerHookKeyVal, hook)
|
||||||
|
}
|
||||||
|
@@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,5 +1,3 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -38,24 +36,3 @@ 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,5 +1,3 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,14 +8,22 @@ 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 {
|
||||||
@@ -41,17 +47,15 @@ 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, input.Dockerfile)
|
buildContext, err = createBuildContext(ctx, input.ContextDir, "Dockerfile")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -97,8 +101,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, _ := patternmatcher.Matches(".dockerignore", excludes)
|
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
|
||||||
keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
|
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
|
||||||
if keepThem1 || keepThem2 {
|
if keepThem1 || keepThem2 {
|
||||||
includes = append(includes, ".dockerignore", relDockerfile)
|
includes = append(includes, ".dockerignore", relDockerfile)
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
//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: maximum retry count must be an integer",
|
"always:2:3": "invalid restart policy format",
|
||||||
"on-failure:invalid": "invalid restart policy format: maximum retry count must be an integer",
|
"on-failure:invalid": "maximum retry count must be an integer",
|
||||||
}
|
}
|
||||||
valids := map[string]container.RestartPolicy{
|
valids := map[string]container.RestartPolicy{
|
||||||
"": {},
|
"": {},
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/api/types/filters"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageExistsLocally returns a boolean indicating if an image with the
|
// ImageExistsLocally returns a boolean indicating if an image with the
|
||||||
@@ -19,15 +17,33 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
filters := filters.NewArgs()
|
||||||
if client.IsErrNotFound(err) {
|
filters.Add("reference", imageName)
|
||||||
return false, nil
|
|
||||||
} else if err != nil {
|
imageListOptions := types.ImageListOptions{
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, err := cli.ImageList(ctx, imageListOptions)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if platform == "" || platform == "any" || fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
|
if len(images) > 0 {
|
||||||
return true, nil
|
if platform == "any" || platform == "" {
|
||||||
|
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
|
||||||
@@ -36,25 +52,38 @@ 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()
|
|
||||||
|
|
||||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
filters := filters.NewArgs()
|
||||||
if client.IsErrNotFound(err) {
|
filters.Add("reference", imageName)
|
||||||
return false, nil
|
|
||||||
} else if err != nil {
|
imageListOptions := types.ImageListOptions{
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, err := cli.ImageList(ctx, imageListOptions)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = cli.ImageRemove(ctx, inspectImage.ID, types.ImageRemoveOptions{
|
if len(images) > 0 {
|
||||||
Force: force,
|
for _, v := range images {
|
||||||
PruneChildren: pruneChildren,
|
if _, err = cli.ImageRemove(ctx, v.ID, types.ImageRemoveOptions{
|
||||||
}); err != nil {
|
Force: force,
|
||||||
return false, err
|
PruneChildren: pruneChildren,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,6 +12,15 @@ 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,9 +1,8 @@
|
|||||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -39,6 +38,53 @@ 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)
|
||||||
@@ -144,13 +190,17 @@ 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 parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun)
|
return cr.extractEnv(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),
|
||||||
@@ -363,16 +413,10 @@ 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
|
||||||
@@ -434,6 +478,7 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
|
|||||||
NetworkMode: container.NetworkMode(input.NetworkMode),
|
NetworkMode: container.NetworkMode(input.NetworkMode),
|
||||||
Privileged: input.Privileged,
|
Privileged: input.Privileged,
|
||||||
UsernsMode: container.UsernsMode(input.UsernsMode),
|
UsernsMode: container.UsernsMode(input.UsernsMode),
|
||||||
|
AutoRemove: input.AutoRemove,
|
||||||
}
|
}
|
||||||
logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
|
logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
|
||||||
|
|
||||||
@@ -455,6 +500,59 @@ 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 {
|
||||||
@@ -487,6 +585,31 @@ 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)
|
||||||
@@ -583,7 +706,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(strings.TrimSpace(found), 10, 32)
|
id, err := strconv.ParseInt(found[:len(found)-1], 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,57 +0,0 @@
|
|||||||
//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,5 +1,3 @@
|
|||||||
//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), 0o777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(fdestpath), 0777); 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", 0o777)
|
_ = fs.MkdirAll("mygitrepo/.git", 0777)
|
||||||
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,13 +15,14 @@ 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 {
|
||||||
@@ -49,7 +50,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)), 0o777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0777); 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 {
|
||||||
@@ -340,7 +341,77 @@ 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 {
|
||||||
return parseEnvFile(e, srcPath, env)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -383,32 +454,10 @@ 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": goOsToActionOs(runtime.GOOS),
|
"os": runtime.GOOS,
|
||||||
"arch": goArchToActionArch(runtime.GOARCH),
|
"arch": runtime.GOARCH,
|
||||||
"temp": e.TmpDir,
|
"temp": e.TmpDir,
|
||||||
"tool_cache": e.ToolCache,
|
"tool_cache": e.ToolCache,
|
||||||
}
|
}
|
||||||
|
@@ -1,60 +0,0 @@
|
|||||||
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,7 +14,6 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -203,9 +202,6 @@ 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,21 +15,15 @@ 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]Needs
|
Needs map[string]map[string]map[string]string
|
||||||
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
|
||||||
@@ -156,11 +150,6 @@ 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":
|
||||||
@@ -372,16 +361,8 @@ 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("Compare not implemented for types: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
|
return nil, fmt.Errorf("TODO: evaluateCompare not implemented! left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -69,11 +69,6 @@ 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{
|
||||||
@@ -560,7 +555,6 @@ 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"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,12 +593,11 @@ func TestContexts(t *testing.T) {
|
|||||||
Matrix: map[string]interface{}{
|
Matrix: map[string]interface{}{
|
||||||
"os": "Linux",
|
"os": "Linux",
|
||||||
},
|
},
|
||||||
Needs: map[string]Needs{
|
Needs: map[string]map[string]map[string]string{
|
||||||
"job-id": {
|
"job-id": {
|
||||||
Outputs: map[string]string{
|
"outputs": {
|
||||||
"output-name": "value",
|
"output-name": "value",
|
||||||
},
|
},
|
||||||
Result: "success",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Inputs: map[string]interface{}{
|
Inputs: map[string]interface{}{
|
||||||
|
185
pkg/jobparser/evaluator.go
Normal file
185
pkg/jobparser/evaluator.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpressionEvaluator is copied from runner.expressionEvaluator,
|
||||||
|
// to avoid unnecessary dependencies
|
||||||
|
type ExpressionEvaluator struct {
|
||||||
|
interpreter exprparser.Interpreter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpressionEvaluator(interpreter exprparser.Interpreter) *ExpressionEvaluator {
|
||||||
|
return &ExpressionEvaluator{interpreter: interpreter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
|
||||||
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
||||||
|
|
||||||
|
return evaluated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
|
||||||
|
var in string
|
||||||
|
if err := node.Decode(&in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
expr, _ := rewriteSubExpression(in, false)
|
||||||
|
res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return node.Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateMappingYamlNode(node *yaml.Node) error {
|
||||||
|
// GitHub has this undocumented feature to merge maps, called insert directive
|
||||||
|
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
||||||
|
for i := 0; i < len(node.Content)/2; {
|
||||||
|
k := node.Content[i*2]
|
||||||
|
v := node.Content[i*2+1]
|
||||||
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var sk string
|
||||||
|
// Merge the nested map of the insert directive
|
||||||
|
if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
|
||||||
|
node.Content = append(append(node.Content[:i*2], v.Content...), node.Content[(i+1)*2:]...)
|
||||||
|
i += len(v.Content) / 2
|
||||||
|
} else {
|
||||||
|
if err := ee.EvaluateYamlNode(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateSequenceYamlNode(node *yaml.Node) error {
|
||||||
|
for i := 0; i < len(node.Content); {
|
||||||
|
v := node.Content[i]
|
||||||
|
// Preserve nested sequences
|
||||||
|
wasseq := v.Kind == yaml.SequenceNode
|
||||||
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// GitHub has this undocumented feature to merge sequences / arrays
|
||||||
|
// We have a nested sequence via evaluation, merge the arrays
|
||||||
|
if v.Kind == yaml.SequenceNode && !wasseq {
|
||||||
|
node.Content = append(append(node.Content[:i], v.Content...), node.Content[i+1:]...)
|
||||||
|
i += len(v.Content)
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) EvaluateYamlNode(node *yaml.Node) error {
|
||||||
|
switch node.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
return ee.evaluateScalarYamlNode(node)
|
||||||
|
case yaml.MappingNode:
|
||||||
|
return ee.evaluateMappingYamlNode(node)
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
return ee.evaluateSequenceYamlNode(node)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) Interpolate(in string) string {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
expr, _ := rewriteSubExpression(in, true)
|
||||||
|
evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := evaluated.(string)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeFormatString(in string) string {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
strPattern := regexp.MustCompile("(?:''|[^'])*'")
|
||||||
|
pos := 0
|
||||||
|
exprStart := -1
|
||||||
|
strStart := -1
|
||||||
|
var results []string
|
||||||
|
formatOut := ""
|
||||||
|
for pos < len(in) {
|
||||||
|
if strStart > -1 {
|
||||||
|
matches := strPattern.FindStringIndex(in[pos:])
|
||||||
|
if matches == nil {
|
||||||
|
panic("unclosed string.")
|
||||||
|
}
|
||||||
|
|
||||||
|
strStart = -1
|
||||||
|
pos += matches[1]
|
||||||
|
} else if exprStart > -1 {
|
||||||
|
exprEnd := strings.Index(in[pos:], "}}")
|
||||||
|
strStart = strings.Index(in[pos:], "'")
|
||||||
|
|
||||||
|
if exprEnd > -1 && strStart > -1 {
|
||||||
|
if exprEnd < strStart {
|
||||||
|
strStart = -1
|
||||||
|
} else {
|
||||||
|
exprEnd = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exprEnd > -1 {
|
||||||
|
formatOut += fmt.Sprintf("{%d}", len(results))
|
||||||
|
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
|
||||||
|
pos += exprEnd + 2
|
||||||
|
exprStart = -1
|
||||||
|
} else if strStart > -1 {
|
||||||
|
pos += strStart + 1
|
||||||
|
} else {
|
||||||
|
panic("unclosed expression.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exprStart = strings.Index(in[pos:], "${{")
|
||||||
|
if exprStart != -1 {
|
||||||
|
formatOut += escapeFormatString(in[pos : pos+exprStart])
|
||||||
|
exprStart = pos + exprStart + 3
|
||||||
|
pos = exprStart
|
||||||
|
} else {
|
||||||
|
formatOut += escapeFormatString(in[pos:])
|
||||||
|
pos = len(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
|
||||||
|
return out, nil
|
||||||
|
}
|
80
pkg/jobparser/interpeter.go
Normal file
80
pkg/jobparser/interpeter.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewInterpeter returns an interpeter used in the server,
|
||||||
|
// need github, needs, strategy, matrix, inputs context only,
|
||||||
|
// see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
||||||
|
func NewInterpeter(
|
||||||
|
jobID string,
|
||||||
|
job *model.Job,
|
||||||
|
matrix map[string]interface{},
|
||||||
|
gitCtx *model.GithubContext,
|
||||||
|
results map[string]*JobResult,
|
||||||
|
) exprparser.Interpreter {
|
||||||
|
strategy := make(map[string]interface{})
|
||||||
|
if job.Strategy != nil {
|
||||||
|
strategy["fail-fast"] = job.Strategy.FailFast
|
||||||
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||||
|
}
|
||||||
|
|
||||||
|
run := &model.Run{
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{},
|
||||||
|
},
|
||||||
|
JobID: jobID,
|
||||||
|
}
|
||||||
|
for id, result := range results {
|
||||||
|
need := yaml.Node{}
|
||||||
|
_ = need.Encode(result.Needs)
|
||||||
|
run.Workflow.Jobs[id] = &model.Job{
|
||||||
|
RawNeeds: need,
|
||||||
|
Result: result.Result,
|
||||||
|
Outputs: result.Outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := run.Workflow.Jobs
|
||||||
|
jobNeeds := run.Job().Needs()
|
||||||
|
|
||||||
|
using := map[string]map[string]map[string]string{}
|
||||||
|
for _, need := range jobNeeds {
|
||||||
|
if v, ok := jobs[need]; ok {
|
||||||
|
using[need] = map[string]map[string]string{
|
||||||
|
"outputs": v.Outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
|
Github: gitCtx,
|
||||||
|
Env: nil, // no need
|
||||||
|
Job: nil, // no need
|
||||||
|
Steps: nil, // no need
|
||||||
|
Runner: nil, // no need
|
||||||
|
Secrets: nil, // no need
|
||||||
|
Strategy: strategy,
|
||||||
|
Matrix: matrix,
|
||||||
|
Needs: using,
|
||||||
|
Inputs: nil, // not supported yet
|
||||||
|
}
|
||||||
|
|
||||||
|
config := exprparser.Config{
|
||||||
|
Run: run,
|
||||||
|
WorkingDir: "", // WorkingDir is used for the function hashFiles, but it's not needed in the server
|
||||||
|
Context: "job",
|
||||||
|
}
|
||||||
|
|
||||||
|
return exprparser.NewInterpeter(ee, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobResult is the minimum requirement of job results for Interpeter
|
||||||
|
type JobResult struct {
|
||||||
|
Needs []string
|
||||||
|
Result string
|
||||||
|
Outputs map[string]string
|
||||||
|
}
|
153
pkg/jobparser/jobparser.go
Normal file
153
pkg/jobparser/jobparser.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||||
|
origin, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("model.ReadWorkflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow := &SingleWorkflow{}
|
||||||
|
if err := yaml.Unmarshal(content, workflow); err != nil {
|
||||||
|
return nil, fmt.Errorf("yaml.Unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := &parseContext{}
|
||||||
|
for _, o := range options {
|
||||||
|
o(pc)
|
||||||
|
}
|
||||||
|
results := map[string]*JobResult{}
|
||||||
|
for id, job := range origin.Jobs {
|
||||||
|
results[id] = &JobResult{
|
||||||
|
Needs: job.Needs(),
|
||||||
|
Result: pc.jobResults[id],
|
||||||
|
Outputs: nil, // not supported yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []*SingleWorkflow
|
||||||
|
for id, job := range workflow.Jobs {
|
||||||
|
for _, matrix := range getMatrixes(origin.GetJob(id)) {
|
||||||
|
job := job.Clone()
|
||||||
|
if job.Name == "" {
|
||||||
|
job.Name = id
|
||||||
|
}
|
||||||
|
job.Name = nameWithMatrix(job.Name, matrix)
|
||||||
|
job.Strategy.RawMatrix = encodeMatrix(matrix)
|
||||||
|
evaluator := NewExpressionEvaluator(NewInterpeter(id, origin.GetJob(id), matrix, pc.gitContext, results))
|
||||||
|
runsOn := origin.GetJob(id).RunsOn()
|
||||||
|
for i, v := range runsOn {
|
||||||
|
runsOn[i] = evaluator.Interpolate(v)
|
||||||
|
}
|
||||||
|
job.RawRunsOn = encodeRunsOn(runsOn)
|
||||||
|
job.EraseNeeds() // there will be only one job in SingleWorkflow, it cannot have needs
|
||||||
|
ret = append(ret, &SingleWorkflow{
|
||||||
|
Name: workflow.Name,
|
||||||
|
RawOn: workflow.RawOn,
|
||||||
|
Env: workflow.Env,
|
||||||
|
Jobs: map[string]*Job{id: job},
|
||||||
|
Defaults: workflow.Defaults,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortWorkflows(ret)
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithJobResults(results map[string]string) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.jobResults = results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithGitContext(context *model.GithubContext) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.gitContext = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseContext struct {
|
||||||
|
jobResults map[string]string
|
||||||
|
gitContext *model.GithubContext
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseOption func(c *parseContext)
|
||||||
|
|
||||||
|
func getMatrixes(job *model.Job) []map[string]interface{} {
|
||||||
|
ret := job.GetMatrixes()
|
||||||
|
sort.Slice(ret, func(i, j int) bool {
|
||||||
|
return matrixName(ret[i]) < matrixName(ret[j])
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeMatrix(matrix map[string]interface{}) yaml.Node {
|
||||||
|
if len(matrix) == 0 {
|
||||||
|
return yaml.Node{}
|
||||||
|
}
|
||||||
|
value := map[string][]interface{}{}
|
||||||
|
for k, v := range matrix {
|
||||||
|
value[k] = []interface{}{v}
|
||||||
|
}
|
||||||
|
node := yaml.Node{}
|
||||||
|
_ = node.Encode(value)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeRunsOn(runsOn []string) yaml.Node {
|
||||||
|
node := yaml.Node{}
|
||||||
|
if len(runsOn) == 1 {
|
||||||
|
_ = node.Encode(runsOn[0])
|
||||||
|
} else {
|
||||||
|
_ = node.Encode(runsOn)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func nameWithMatrix(name string, m map[string]interface{}) string {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name + " " + matrixName(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matrixName(m map[string]interface{}) string {
|
||||||
|
ks := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
vs := make([]string, 0, len(m))
|
||||||
|
for _, v := range ks {
|
||||||
|
vs = append(vs, fmt.Sprint(m[v]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("(%s)", strings.Join(vs, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortWorkflows(wfs []*SingleWorkflow) {
|
||||||
|
sort.Slice(wfs, func(i, j int) bool {
|
||||||
|
ki := ""
|
||||||
|
for k := range wfs[i].Jobs {
|
||||||
|
ki = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
kj := ""
|
||||||
|
for k := range wfs[j].Jobs {
|
||||||
|
kj = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return ki < kj
|
||||||
|
})
|
||||||
|
}
|
65
pkg/jobparser/jobparser_test.go
Normal file
65
pkg/jobparser/jobparser_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed testdata
|
||||||
|
var f embed.FS
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options []ParseOption
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multiple_jobs",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple_matrix",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has_needs",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
content, err := f.ReadFile(filepath.Join("testdata", tt.name+".in.yaml"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
want, err := f.ReadFile(filepath.Join("testdata", tt.name+".out.yaml"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
got, err := Parse(content, tt.options...)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
for _, v := range got {
|
||||||
|
if builder.Len() > 0 {
|
||||||
|
builder.WriteString("---\n")
|
||||||
|
}
|
||||||
|
encoder := yaml.NewEncoder(builder)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
_ = encoder.Encode(v)
|
||||||
|
}
|
||||||
|
assert.Equal(t, string(want), builder.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
207
pkg/jobparser/model.go
Normal file
207
pkg/jobparser/model.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SingleWorkflow is a workflow with single job and single matrix
|
||||||
|
type SingleWorkflow struct {
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
RawOn yaml.Node `yaml:"on,omitempty"`
|
||||||
|
Env map[string]string `yaml:"env,omitempty"`
|
||||||
|
Jobs map[string]*Job `yaml:"jobs,omitempty"`
|
||||||
|
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) Job() (string, *Job) {
|
||||||
|
for k, v := range w.Jobs {
|
||||||
|
return k, v
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||||
|
return yaml.Marshal(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||||
|
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||||
|
Env yaml.Node `yaml:"env,omitempty"`
|
||||||
|
If yaml.Node `yaml:"if,omitempty"`
|
||||||
|
Steps []*Step `yaml:"steps,omitempty"`
|
||||||
|
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||||
|
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||||
|
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||||
|
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||||
|
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||||
|
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||||
|
Uses string `yaml:"uses,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) Clone() *Job {
|
||||||
|
if j == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Job{
|
||||||
|
Name: j.Name,
|
||||||
|
RawNeeds: j.RawNeeds,
|
||||||
|
RawRunsOn: j.RawRunsOn,
|
||||||
|
Env: j.Env,
|
||||||
|
If: j.If,
|
||||||
|
Steps: j.Steps,
|
||||||
|
TimeoutMinutes: j.TimeoutMinutes,
|
||||||
|
Services: j.Services,
|
||||||
|
Strategy: j.Strategy,
|
||||||
|
RawContainer: j.RawContainer,
|
||||||
|
Defaults: j.Defaults,
|
||||||
|
Outputs: j.Outputs,
|
||||||
|
Uses: j.Uses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) Needs() []string {
|
||||||
|
return (&model.Job{RawNeeds: j.RawNeeds}).Needs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) EraseNeeds() {
|
||||||
|
j.RawNeeds = yaml.Node{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) RunsOn() []string {
|
||||||
|
return (&model.Job{RawRunsOn: j.RawRunsOn}).RunsOn()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
ID string `yaml:"id,omitempty"`
|
||||||
|
If yaml.Node `yaml:"if,omitempty"`
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
Uses string `yaml:"uses,omitempty"`
|
||||||
|
Run string `yaml:"run,omitempty"`
|
||||||
|
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||||
|
Shell string `yaml:"shell,omitempty"`
|
||||||
|
Env yaml.Node `yaml:"env,omitempty"`
|
||||||
|
With map[string]string `yaml:"with,omitempty"`
|
||||||
|
ContinueOnError bool `yaml:"continue-on-error,omitempty"`
|
||||||
|
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String gets the name of step
|
||||||
|
func (s *Step) String() string {
|
||||||
|
return (&model.Step{
|
||||||
|
ID: s.ID,
|
||||||
|
Name: s.Name,
|
||||||
|
Uses: s.Uses,
|
||||||
|
Run: s.Run,
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerSpec struct {
|
||||||
|
Image string `yaml:"image,omitempty"`
|
||||||
|
Env map[string]string `yaml:"env,omitempty"`
|
||||||
|
Ports []string `yaml:"ports,omitempty"`
|
||||||
|
Volumes []string `yaml:"volumes,omitempty"`
|
||||||
|
Options string `yaml:"options,omitempty"`
|
||||||
|
Credentials map[string]string `yaml:"credentials,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy struct {
|
||||||
|
FailFastString string `yaml:"fail-fast,omitempty"`
|
||||||
|
MaxParallelString string `yaml:"max-parallel,omitempty"`
|
||||||
|
RawMatrix yaml.Node `yaml:"matrix,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Defaults struct {
|
||||||
|
Run RunDefaults `yaml:"run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunDefaults struct {
|
||||||
|
Shell string `yaml:"shell,omitempty"`
|
||||||
|
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Name string
|
||||||
|
Acts map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||||
|
switch rawOn.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
var val string
|
||||||
|
err := rawOn.Decode(&val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []*Event{
|
||||||
|
{Name: val},
|
||||||
|
}, nil
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
var val []interface{}
|
||||||
|
err := rawOn.Decode(&val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := make([]*Event, 0, len(val))
|
||||||
|
for _, v := range val {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
res = append(res, &Event{Name: t})
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid type %T", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
case yaml.MappingNode:
|
||||||
|
var val map[string]interface{}
|
||||||
|
err := rawOn.Decode(&val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := make([]*Event, 0, len(val))
|
||||||
|
for k, v := range val {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
Acts: map[string][]string{},
|
||||||
|
})
|
||||||
|
case []string:
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
Acts: map[string][]string{},
|
||||||
|
})
|
||||||
|
case map[string]interface{}:
|
||||||
|
acts := make(map[string][]string, len(t))
|
||||||
|
for act, branches := range t {
|
||||||
|
switch b := branches.(type) {
|
||||||
|
case string:
|
||||||
|
acts[act] = []string{b}
|
||||||
|
case []string:
|
||||||
|
acts[act] = b
|
||||||
|
case []interface{}:
|
||||||
|
acts[act] = make([]string, len(b))
|
||||||
|
for i, v := range b {
|
||||||
|
acts[act][i] = v.(string)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %#v", branches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
Acts: acts,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %#v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind)
|
||||||
|
}
|
||||||
|
}
|
184
pkg/jobparser/model_test.go
Normal file
184
pkg/jobparser/model_test.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRawOn(t *testing.T) {
|
||||||
|
kases := []struct {
|
||||||
|
input string
|
||||||
|
result []*Event
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "on: issue_comment",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "issue_comment",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: "on:\n - push\n - pull_request",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pull_request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n branches:\n - master",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"branches": {
|
||||||
|
"master",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "branch_protection_rule",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"types": {
|
||||||
|
"created",
|
||||||
|
"deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "project",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"types": {
|
||||||
|
"created",
|
||||||
|
"deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "milestone",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"types": {
|
||||||
|
"opened",
|
||||||
|
"deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "pull_request",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"types": {
|
||||||
|
"opened",
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"releases/**",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"branches": {
|
||||||
|
"main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pull_request",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"types": {
|
||||||
|
"opened",
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"**",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n branches:\n - 'main'\n - 'releases/**'",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"branches": {
|
||||||
|
"main",
|
||||||
|
"releases/**",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n tags:\n - v1.**",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
Acts: map[string][]string{
|
||||||
|
"tags": {
|
||||||
|
"v1.**",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on: [pull_request, workflow_dispatch]",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "pull_request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "workflow_dispatch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, kase := range kases {
|
||||||
|
t.Run(kase.input, func(t *testing.T) {
|
||||||
|
origin, err := model.ReadWorkflow(strings.NewReader(kase.input))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
events, err := ParseRawOn(&origin.RawOn)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, kase.result, events, fmt.Sprintf("%#v", events))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
pkg/jobparser/testdata/has_needs.in.yaml
vendored
Normal file
16
pkg/jobparser/testdata/has_needs.in.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
job2:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: job1
|
||||||
|
job3:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: [job1, job2]
|
23
pkg/jobparser/testdata/has_needs.out.yaml
vendored
Normal file
23
pkg/jobparser/testdata/has_needs.out.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job3:
|
||||||
|
name: job3
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
14
pkg/jobparser/testdata/multiple_jobs.in.yaml
vendored
Normal file
14
pkg/jobparser/testdata/multiple_jobs.in.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
job2:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
job3:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
23
pkg/jobparser/testdata/multiple_jobs.out.yaml
vendored
Normal file
23
pkg/jobparser/testdata/multiple_jobs.out.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job3:
|
||||||
|
name: job3
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
13
pkg/jobparser/testdata/multiple_matrix.in.yaml
vendored
Normal file
13
pkg/jobparser/testdata/multiple_matrix.in.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-20.04]
|
||||||
|
version: [1.17, 1.18, 1.19]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
101
pkg/jobparser/testdata/multiple_matrix.out.yaml
vendored
Normal file
101
pkg/jobparser/testdata/multiple_matrix.out.yaml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.17)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.18)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.19)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.17)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.18)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.19)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
@@ -42,6 +42,8 @@ const (
|
|||||||
ActionRunsUsingDocker = "docker"
|
ActionRunsUsingDocker = "docker"
|
||||||
// ActionRunsUsingComposite for running composite
|
// ActionRunsUsingComposite for running composite
|
||||||
ActionRunsUsingComposite = "composite"
|
ActionRunsUsingComposite = "composite"
|
||||||
|
// ActionRunsUsingGo for running with go
|
||||||
|
ActionRunsUsingGo = "go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActionRuns are a field in Action
|
// ActionRuns are a field in Action
|
||||||
|
@@ -3,7 +3,6 @@ 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"
|
||||||
@@ -90,22 +89,26 @@ 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) SetRef(ctx context.Context, defaultBranch string, repoPath string) {
|
func (ghc *GithubContext) SetRefAndSha(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 = fmt.Sprintf("refs/tags/%s", asString(nestedMapLookup(ghc.Event, "release", "tag_name")))
|
ghc.Ref = 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 != "" {
|
||||||
@@ -133,23 +136,6 @@ func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repo
|
|||||||
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)
|
||||||
@@ -160,51 +146,3 @@ func (ghc *GithubContext) SetSha(ctx context.Context, repoPath 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 TestSetRef(t *testing.T) {
|
func TestSetRefAndSha(t *testing.T) {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
oldFindGitRef := findGitRef
|
oldFindGitRef := findGitRef
|
||||||
@@ -29,31 +29,38 @@ func TestSetRef(t *testing.T) {
|
|||||||
eventName string
|
eventName string
|
||||||
event map[string]interface{}
|
event map[string]interface{}
|
||||||
ref string
|
ref string
|
||||||
refName string
|
sha string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
eventName: "pull_request_target",
|
eventName: "pull_request_target",
|
||||||
event: map[string]interface{}{},
|
event: map[string]interface{}{
|
||||||
ref: "refs/heads/master",
|
"pull_request": map[string]interface{}{
|
||||||
refName: "master",
|
"base": map[string]interface{}{
|
||||||
|
"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",
|
||||||
refName: "1234/merge",
|
sha: "1234fakesha",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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",
|
||||||
refName: "somebranch",
|
sha: "deployment-sha",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
eventName: "release",
|
eventName: "release",
|
||||||
@@ -62,16 +69,18 @@ func TestSetRef(t *testing.T) {
|
|||||||
"tag_name": "v1.0.0",
|
"tag_name": "v1.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ref: "refs/tags/v1.0.0",
|
ref: "v1.0.0",
|
||||||
refName: "v1.0.0",
|
sha: "1234fakesha",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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",
|
||||||
refName: "somebranch",
|
sha: "push-sha",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
eventName: "unknown",
|
eventName: "unknown",
|
||||||
@@ -80,14 +89,14 @@ func TestSetRef(t *testing.T) {
|
|||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ref: "refs/heads/main",
|
ref: "refs/heads/main",
|
||||||
refName: "main",
|
sha: "1234fakesha",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
eventName: "no-event",
|
eventName: "no-event",
|
||||||
event: map[string]interface{}{},
|
event: map[string]interface{}{},
|
||||||
ref: "refs/heads/master",
|
ref: "refs/heads/master",
|
||||||
refName: "master",
|
sha: "1234fakesha",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +108,10 @@ func TestSetRef(t *testing.T) {
|
|||||||
Event: table.event,
|
Event: table.event,
|
||||||
}
|
}
|
||||||
|
|
||||||
ghc.SetRef(context.Background(), "main", "/some/dir")
|
ghc.SetRefAndSha(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.refName, ghc.RefName)
|
assert.Equal(t, table.sha, ghc.Sha)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,96 +125,9 @@ func TestSetRef(t *testing.T) {
|
|||||||
Event: map[string]interface{}{},
|
Event: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ghc.SetRef(context.Background(), "", "/some/dir")
|
ghc.SetRefAndSha(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, error)
|
PlanEvent(eventName string) *Plan
|
||||||
PlanJob(jobName string) (*Plan, error)
|
PlanJob(jobName string) *Plan
|
||||||
PlanAll() (*Plan, error)
|
PlanAll() *Plan
|
||||||
GetEvents() []string
|
GetEvents() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,81 +164,59 @@ func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, e
|
|||||||
return wp, nil
|
return wp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CombineWorkflowPlanner combines workflows to a WorkflowPlanner
|
||||||
|
func CombineWorkflowPlanner(workflows ...*Workflow) WorkflowPlanner {
|
||||||
|
return &workflowPlanner{
|
||||||
|
workflows: workflows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type workflowPlanner struct {
|
type workflowPlanner struct {
|
||||||
workflows []*Workflow
|
workflows []*Workflow
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func (wp *workflowPlanner) PlanEvent(eventName string) *Plan {
|
||||||
plan := new(Plan)
|
plan := new(Plan)
|
||||||
if len(wp.workflows) == 0 {
|
if len(wp.workflows) == 0 {
|
||||||
log.Debug("no workflows found by planner")
|
log.Debugf("no events found for workflow: %s", eventName)
|
||||||
return plan, nil
|
|
||||||
}
|
}
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for _, w := range wp.workflows {
|
for _, w := range wp.workflows {
|
||||||
events := w.On()
|
for _, e := range 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 {
|
||||||
stages, err := createStages(w, w.GetJobIDs()...)
|
plan.mergeStages(createStages(w, w.GetJobIDs()...))
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
lastErr = err
|
|
||||||
} else {
|
|
||||||
plan.mergeStages(stages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return plan, lastErr
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func (wp *workflowPlanner) PlanJob(jobName string) *Plan {
|
||||||
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 {
|
||||||
stages, err := createStages(w, jobName)
|
plan.mergeStages(createStages(w, jobName))
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
lastErr = err
|
|
||||||
} else {
|
|
||||||
plan.mergeStages(stages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return plan, lastErr
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func (wp *workflowPlanner) PlanAll() *Plan {
|
||||||
plan := new(Plan)
|
plan := new(Plan)
|
||||||
if len(wp.workflows) == 0 {
|
if len(wp.workflows) == 0 {
|
||||||
log.Debug("no workflows found by planner")
|
log.Debugf("no jobs found for loaded workflows")
|
||||||
return plan, nil
|
|
||||||
}
|
}
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for _, w := range wp.workflows {
|
for _, w := range wp.workflows {
|
||||||
stages, err := createStages(w, w.GetJobIDs()...)
|
plan.mergeStages(createStages(w, w.GetJobIDs()...))
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
lastErr = err
|
|
||||||
} else {
|
|
||||||
plan.mergeStages(stages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, lastErr
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEvents gets all the events in the workflows file
|
// GetEvents gets all the events in the workflows file
|
||||||
@@ -311,7 +289,7 @@ func (p *Plan) mergeStages(stages []*Stage) {
|
|||||||
p.Stages = newStages
|
p.Stages = newStages
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
|
func createStages(w *Workflow, jobIDs ...string) []*Stage {
|
||||||
// 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 {
|
||||||
@@ -328,8 +306,6 @@ func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
|
|||||||
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 {
|
||||||
@@ -345,16 +321,12 @@ func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(stage.Runs) == 0 {
|
if len(stage.Runs) == 0 {
|
||||||
return nil, fmt.Errorf("unable to build dependency graph for %s (%s)", w.Name, w.File)
|
log.Fatalf("Unable to build dependency graph!")
|
||||||
}
|
}
|
||||||
stages = append(stages, stage)
|
stages = append(stages, stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stages) == 0 && err != nil {
|
return stages
|
||||||
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,4 +42,5 @@ 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
|
||||||
}
|
}
|
||||||
|
@@ -67,6 +67,30 @@ func (w *Workflow) OnEvent(event string) interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Workflow) OnSchedule() []string {
|
||||||
|
schedules := w.OnEvent("schedule")
|
||||||
|
if schedules == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := schedules.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
allSchedules := []string{}
|
||||||
|
for _, v := range val {
|
||||||
|
for k, cron := range v.(map[string]interface{}) {
|
||||||
|
if k != "cron" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allSchedules = append(allSchedules, cron.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allSchedules
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
type WorkflowDispatchInput struct {
|
type WorkflowDispatchInput struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
Required bool `yaml:"required"`
|
Required bool `yaml:"required"`
|
||||||
@@ -100,48 +124,6 @@ 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"`
|
||||||
@@ -157,8 +139,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,34 +193,6 @@ 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
|
||||||
@@ -504,6 +456,7 @@ type ContainerSpec struct {
|
|||||||
|
|
||||||
// Step is the structure of one step in a job
|
// Step is the structure of one step in a job
|
||||||
type Step struct {
|
type Step struct {
|
||||||
|
Number int `yaml:"-"`
|
||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
If yaml.Node `yaml:"if"`
|
If yaml.Node `yaml:"if"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
@@ -530,8 +483,16 @@ 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 {
|
||||||
return environment(s.Env)
|
env := 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
|
||||||
@@ -550,7 +511,7 @@ func (s *Step) GetEnv() map[string]string {
|
|||||||
func (s *Step) ShellCommand() string {
|
func (s *Step) ShellCommand() string {
|
||||||
shellCommand := ""
|
shellCommand := ""
|
||||||
|
|
||||||
//Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
|
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
|
||||||
switch s.Shell {
|
switch s.Shell {
|
||||||
case "", "bash":
|
case "", "bash":
|
||||||
shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
|
shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
|
||||||
|
@@ -7,6 +7,88 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestReadWorkflow_ScheduleEvent(t *testing.T) {
|
||||||
|
yaml := `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 5 * * 1,3'
|
||||||
|
- cron: '30 5 * * 2,4'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: ./actions/docker-url
|
||||||
|
`
|
||||||
|
|
||||||
|
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
|
||||||
|
schedules := workflow.OnEvent("schedule")
|
||||||
|
assert.Len(t, schedules, 2)
|
||||||
|
|
||||||
|
newSchedules := workflow.OnSchedule()
|
||||||
|
assert.Len(t, newSchedules, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, "30 5 * * 1,3", newSchedules[0])
|
||||||
|
assert.Equal(t, "30 5 * * 2,4", newSchedules[1])
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
test: '30 5 * * 1,3'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: ./actions/docker-url
|
||||||
|
`
|
||||||
|
|
||||||
|
workflow, err = ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
|
||||||
|
newSchedules = workflow.OnSchedule()
|
||||||
|
assert.Len(t, newSchedules, 0)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: ./actions/docker-url
|
||||||
|
`
|
||||||
|
|
||||||
|
workflow, err = ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
|
||||||
|
newSchedules = workflow.OnSchedule()
|
||||||
|
assert.Len(t, newSchedules, 0)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on: [push, tag]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: ./actions/docker-url
|
||||||
|
`
|
||||||
|
|
||||||
|
workflow, err = ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
|
||||||
|
newSchedules = workflow.OnSchedule()
|
||||||
|
assert.Len(t, newSchedules, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadWorkflow_StringEvent(t *testing.T) {
|
func TestReadWorkflow_StringEvent(t *testing.T) {
|
||||||
yaml := `
|
yaml := `
|
||||||
name: local-action-docker-url
|
name: local-action-docker-url
|
||||||
@@ -241,8 +323,7 @@ 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, err := w.PlanJob("strategy-only-max-parallel")
|
p := 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,7 +14,6 @@ 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,9 +29,10 @@ 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 actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
type (
|
||||||
|
actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||||
type fileWriter func(filename string, data []byte, perm fs.FileMode) error
|
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,8 +156,6 @@ 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
|
||||||
@@ -171,6 +169,13 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
return execAsComposite(step)(ctx)
|
return execAsComposite(step)(ctx)
|
||||||
|
case model.ActionRunsUsingGo:
|
||||||
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
containerArgs := []string{"go", "run", path.Join(containerActionDir, action.Runs.Main)}
|
||||||
|
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||||
|
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
||||||
model.ActionRunsUsingDocker,
|
model.ActionRunsUsingDocker,
|
||||||
@@ -223,17 +228,14 @@ 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, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
contextDir := filepath.Join(basedir, action.Runs.Main)
|
||||||
|
|
||||||
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
|
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -263,7 +265,6 @@ 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,
|
||||||
@@ -295,7 +296,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(forcePull),
|
stepContainer.Pull(rc.Config.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),
|
||||||
@@ -356,10 +357,7 @@ 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,
|
||||||
@@ -367,25 +365,25 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
|
|||||||
Image: image,
|
Image: image,
|
||||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||||
Name: createContainerName(rc.jobContainerName(), stepModel.ID),
|
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: networkMode,
|
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
|
||||||
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,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
||||||
state, ok := rc.IntraActionState[step.getStepModel().ID]
|
stepResult := rc.StepResults[step.getStepModel().ID]
|
||||||
if ok {
|
if stepResult != nil {
|
||||||
for name, value := range state {
|
for name, value := range stepResult.State {
|
||||||
envName := fmt.Sprintf("STATE_%s", name)
|
envName := fmt.Sprintf("STATE_%s", name)
|
||||||
(*env)[envName] = value
|
(*env)[envName] = value
|
||||||
}
|
}
|
||||||
@@ -475,7 +473,7 @@ func runPreStep(step actionStep) common.Executor {
|
|||||||
var actionPath string
|
var actionPath string
|
||||||
if _, ok := step.(*stepActionRemote); ok {
|
if _, ok := step.(*stepActionRemote); ok {
|
||||||
actionPath = newRemoteAction(stepModel.Uses).Path
|
actionPath = newRemoteAction(stepModel.Uses).Path
|
||||||
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
|
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-"))
|
||||||
} else {
|
} else {
|
||||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
||||||
actionPath = ""
|
actionPath = ""
|
||||||
@@ -497,8 +495,6 @@ 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:
|
||||||
@@ -506,10 +502,7 @@ func runPreStep(step actionStep) common.Executor {
|
|||||||
step.getCompositeRunContext(ctx)
|
step.getCompositeRunContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
|
return step.getCompositeSteps().pre(ctx)
|
||||||
return steps.pre(ctx)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("missing steps in composite action")
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
@@ -566,7 +559,7 @@ func runPostStep(step actionStep) common.Executor {
|
|||||||
var actionPath string
|
var actionPath string
|
||||||
if _, ok := step.(*stepActionRemote); ok {
|
if _, ok := step.(*stepActionRemote); ok {
|
||||||
actionPath = newRemoteAction(stepModel.Uses).Path
|
actionPath = newRemoteAction(stepModel.Uses).Path
|
||||||
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
|
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-"))
|
||||||
} else {
|
} else {
|
||||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
||||||
actionPath = ""
|
actionPath = ""
|
||||||
@@ -589,8 +582,6 @@ 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:
|
||||||
@@ -598,10 +589,7 @@ func runPostStep(step actionStep) common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
|
return step.getCompositeSteps().post(ctx)
|
||||||
return steps.post(ctx)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("missing steps in composite action")
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
@@ -66,7 +66,6 @@ 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,
|
||||||
@@ -86,10 +85,6 @@ 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)
|
||||||
@@ -104,14 +99,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -135,6 +122,7 @@ func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
|
|||||||
if step.ID == "" {
|
if step.ID == "" {
|
||||||
step.ID = fmt.Sprintf("%d", i)
|
step.ID = fmt.Sprintf("%d", i)
|
||||||
}
|
}
|
||||||
|
step.Number = i
|
||||||
|
|
||||||
// create a copy of the step, since this composite action could
|
// create a copy of the step, since this composite action could
|
||||||
// run multiple times and we might modify the instance
|
// run multiple times and we might modify the instance
|
||||||
|
@@ -201,11 +201,10 @@ 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": {
|
||||||
"name": "state value",
|
State: map[string]string{
|
||||||
|
"name": "state value",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
66
pkg/runner/command.go
Normal file → Executable file
66
pkg/runner/command.go
Normal file → Executable file
@@ -16,27 +16,22 @@ 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 {
|
||||||
command, kvPairs, arg, ok := tryParseRawActionCommand(line)
|
var command string
|
||||||
if !ok {
|
var kvPairs map[string]string
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +66,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -82,17 +75,11 @@ 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) {
|
||||||
name := kvPairs["name"]
|
common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", kvPairs["name"], arg)
|
||||||
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[name] = arg
|
rc.Env[kvPairs["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)
|
||||||
@@ -114,13 +101,7 @@ 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)
|
||||||
extraPath := []string{arg}
|
rc.ExtraPath = append(rc.ExtraPath, 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 {
|
||||||
@@ -166,16 +147,13 @@ 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) {
|
||||||
stepID := rc.CurrentStep
|
if rc.CurrentStep != "" {
|
||||||
if stepID != "" {
|
stepResult := rc.StepResults[rc.CurrentStep]
|
||||||
if rc.IntraActionState == nil {
|
if stepResult != nil {
|
||||||
rc.IntraActionState = map[string]map[string]string{}
|
if stepResult.State == nil {
|
||||||
|
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[0])
|
a.Equal("/boo", rc.ExtraPath[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
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[0])
|
a.Equal("/boo", rc.ExtraPath[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddmask(t *testing.T) {
|
func TestAddmask(t *testing.T) {
|
||||||
@@ -177,7 +177,11 @@ 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()
|
||||||
@@ -185,5 +189,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.IntraActionState["step"]["state-name"])
|
assert.Equal(t, "state-value", rc.StepResults["step"].State["state-name"])
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ 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"
|
||||||
@@ -50,6 +49,11 @@ 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)
|
||||||
@@ -59,17 +63,7 @@ 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,14 +21,8 @@ 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]exprparser.Needs)
|
using := make(map[string]map[string]map[string]string)
|
||||||
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()
|
||||||
@@ -41,26 +35,8 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
|||||||
jobNeeds := rc.Run.Job().Needs()
|
jobNeeds := rc.Run.Job().Needs()
|
||||||
|
|
||||||
for _, needs := range jobNeeds {
|
for _, needs := range jobNeeds {
|
||||||
using[needs] = exprparser.Needs{
|
using[needs] = map[string]map[string]string{
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,13 +46,12 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
|||||||
|
|
||||||
ee := &exprparser.EvaluationEnvironment{
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
Github: ghc,
|
Github: ghc,
|
||||||
Env: env,
|
Env: rc.GetEnv(),
|
||||||
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: getWorkflowSecrets(ctx, rc),
|
Secrets: rc.Config.Secrets,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
Matrix: rc.Matrix,
|
Matrix: rc.Matrix,
|
||||||
Needs: using,
|
Needs: using,
|
||||||
@@ -107,11 +82,10 @@ 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]exprparser.Needs)
|
using := make(map[string]map[string]map[string]string)
|
||||||
for _, needs := range jobNeeds {
|
for _, needs := range jobNeeds {
|
||||||
using[needs] = exprparser.Needs{
|
using[needs] = map[string]map[string]string{
|
||||||
Outputs: jobs[needs].Outputs,
|
"outputs": jobs[needs].Outputs,
|
||||||
Result: jobs[needs].Result,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +97,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: getWorkflowSecrets(ctx, rc),
|
Secrets: rc.Config.Secrets,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
Matrix: rc.Matrix,
|
Matrix: rc.Matrix,
|
||||||
Needs: using,
|
Needs: using,
|
||||||
@@ -337,8 +311,6 @@ 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()
|
||||||
@@ -371,54 +343,3 @@ 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
|
|
||||||
}
|
|
||||||
|
@@ -62,6 +62,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
if stepModel.ID == "" {
|
if stepModel.ID == "" {
|
||||||
stepModel.ID = fmt.Sprintf("%d", i)
|
stepModel.ID = fmt.Sprintf("%d", i)
|
||||||
}
|
}
|
||||||
|
stepModel.Number = i
|
||||||
|
|
||||||
step, err := sf.newStep(stepModel, rc)
|
step, err := sf.newStep(stepModel, rc)
|
||||||
|
|
||||||
@@ -95,18 +96,21 @@ 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)
|
||||||
var err error
|
if jobError != nil {
|
||||||
if rc.Config.AutoRemove || jobError == nil {
|
info.result("failure")
|
||||||
// always allow 1 min for stopping and removing the runner, even if we were cancelled
|
logger.WithField("jobResult", "failure").Infof("\U0001F3C1 Job failed")
|
||||||
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
} else {
|
||||||
defer cancel()
|
err := info.stopContainer()(ctx)
|
||||||
err = info.stopContainer()(ctx)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info.result("success")
|
||||||
|
logger.WithField("jobResult", "success").Infof("\U0001F3C1 Job succeeded")
|
||||||
}
|
}
|
||||||
setJobResult(ctx, info, rc, jobError == nil)
|
|
||||||
setJobOutputs(ctx, rc)
|
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
pipeline := make([]common.Executor, 0)
|
pipeline := make([]common.Executor, 0)
|
||||||
@@ -119,7 +123,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(common.WithLogger(context.Background(), common.Logger(ctx)), 5*time.Minute)
|
ctx, cancel = context.WithTimeout(WithJobLogger(context.Background(), rc.Run.JobID, rc.String(), rc.Config, &rc.Masks, rc.Matrix), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
return postExecutor(ctx)
|
return postExecutor(ctx)
|
||||||
@@ -128,52 +132,9 @@ 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.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())
|
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())
|
||||||
|
|
||||||
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
||||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||||
|
@@ -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, secrets},
|
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms},
|
||||||
{workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
|
{workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
|
||||||
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
|
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
|
||||||
{workdir, "uses-github-root", "push", "", platforms, secrets},
|
{workdir, "uses-github-root", "push", "", platforms},
|
||||||
{workdir, "uses-github-path", "push", "", platforms, secrets},
|
{workdir, "uses-github-path", "push", "", platforms},
|
||||||
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
{workdir, "uses-docker-url", "push", "", platforms},
|
||||||
{workdir, "uses-github-full-sha", "push", "", platforms, secrets},
|
{workdir, "uses-github-full-sha", "push", "", 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, "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, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets},
|
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms},
|
||||||
}
|
}
|
||||||
// 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,48 +57,34 @@ 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 {
|
||||||
ctx = WithMasks(ctx, masks)
|
mux.Lock()
|
||||||
|
defer mux.Unlock()
|
||||||
|
|
||||||
var logger *logrus.Logger
|
var formatter logrus.Formatter
|
||||||
if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
|
if config.JSONLogger {
|
||||||
logger = jobLoggerFactory.WithJobLogger()
|
formatter = &jobLogJSONFormatter{
|
||||||
} else {
|
formatter: &logrus.JSONFormatter{},
|
||||||
var formatter logrus.Formatter
|
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
||||||
if config.JSONLogger {
|
}
|
||||||
formatter = &logrus.JSONFormatter{}
|
} else {
|
||||||
} else {
|
formatter = &jobLogFormatter{
|
||||||
mux.Lock()
|
color: colors[nextColor%len(colors)],
|
||||||
defer mux.Unlock()
|
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
||||||
nextColor++
|
|
||||||
formatter = &jobLogFormatter{
|
|
||||||
color: colors[nextColor%len(colors)],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = logrus.New()
|
|
||||||
logger.SetOutput(os.Stdout)
|
|
||||||
logger.SetLevel(logrus.GetLevel())
|
|
||||||
logger.SetFormatter(formatter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.SetFormatter(&maskedFormatter{
|
nextColor++
|
||||||
Formatter: logger.Formatter,
|
ctx = WithMasks(ctx, masks)
|
||||||
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
|
||||||
})
|
logger := logrus.New()
|
||||||
|
if hook := common.LoggerHook(ctx); hook != nil {
|
||||||
|
logger.AddHook(hook)
|
||||||
|
}
|
||||||
|
logger.SetFormatter(formatter)
|
||||||
|
logger.SetOutput(os.Stdout)
|
||||||
|
logger.SetLevel(logrus.TraceLevel) // to be aware of steps
|
||||||
rtn := logger.WithFields(logrus.Fields{
|
rtn := logger.WithFields(logrus.Fields{
|
||||||
"job": jobName,
|
"job": jobName,
|
||||||
"jobID": jobID,
|
"jobID": jobID,
|
||||||
@@ -131,11 +117,12 @@ func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context
|
|||||||
}).WithContext(ctx))
|
}).WithContext(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func withStepLogger(ctx context.Context, stepID string, stepName string, stageName string) context.Context {
|
func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stageName string) context.Context {
|
||||||
rtn := common.Logger(ctx).WithFields(logrus.Fields{
|
rtn := common.Logger(ctx).WithFields(logrus.Fields{
|
||||||
"step": stepName,
|
"stepNumber": stepNumber,
|
||||||
"stepID": []string{stepID},
|
"step": stepName,
|
||||||
"stage": stageName,
|
"stepID": []string{stepID},
|
||||||
|
"stage": stageName,
|
||||||
})
|
})
|
||||||
return common.WithLogger(ctx, rtn)
|
return common.WithLogger(ctx, rtn)
|
||||||
}
|
}
|
||||||
@@ -166,22 +153,16 @@ func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type maskedFormatter struct {
|
|
||||||
logrus.Formatter
|
|
||||||
masker entryProcessor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
||||||
return f.Formatter.Format(f.masker(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
type jobLogFormatter struct {
|
type jobLogFormatter struct {
|
||||||
color int
|
color int
|
||||||
|
masker entryProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -248,3 +229,12 @@ 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))
|
||||||
|
}
|
||||||
|
@@ -1,129 +0,0 @@
|
|||||||
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,27 +1,25 @@
|
|||||||
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"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
"github.com/opencontainers/selinux/go-selinux"
|
"github.com/opencontainers/selinux/go-selinux"
|
||||||
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"
|
||||||
@@ -35,11 +33,9 @@ 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
|
||||||
@@ -48,7 +44,6 @@ 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) {
|
||||||
@@ -61,13 +56,7 @@ type MappableOutput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) String() string {
|
func (rc *RunContext) String() string {
|
||||||
name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
|
return 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
|
||||||
@@ -86,7 +75,7 @@ func (rc *RunContext) GetEnv() map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) jobContainerName() string {
|
func (rc *RunContext) jobContainerName() string {
|
||||||
return createContainerName("act", rc.String())
|
return createSimpleContainerName(rc.Config.ContainerNamePrefix, "WORKFLOW-"+rc.Run.Workflow.Name, "JOB-"+rc.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the binds and mounts for the container, resolving paths as appopriate
|
// Returns the binds and mounts for the container, resolving paths as appopriate
|
||||||
@@ -156,15 +145,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, 0o777); err != nil {
|
if err := os.MkdirAll(actPath, 0777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path := filepath.Join(miscpath, "hostexecutor")
|
path := filepath.Join(miscpath, "hostexecutor")
|
||||||
if err := os.MkdirAll(path, 0o777); err != nil {
|
if err := os.MkdirAll(path, 0777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
runnerTmp := filepath.Join(miscpath, "tmp")
|
runnerTmp := filepath.Join(miscpath, "tmp")
|
||||||
if err := os.MkdirAll(runnerTmp, 0o777); err != nil {
|
if err := os.MkdirAll(runnerTmp, 0777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
toolCache := filepath.Join(cacheDir, "tool_cache")
|
toolCache := filepath.Join(cacheDir, "tool_cache")
|
||||||
@@ -180,28 +169,29 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
StdOut: logWriter,
|
StdOut: logWriter,
|
||||||
}
|
}
|
||||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
rc.Env["RUNNER_TOOL_CACHE"] = toolCache
|
||||||
if v, ok := v.(string); ok {
|
rc.Env["RUNNER_OS"] = runtime.GOOS
|
||||||
rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
|
rc.Env["RUNNER_ARCH"] = runtime.GOARCH
|
||||||
}
|
rc.Env["RUNNER_TEMP"] = runnerTmp
|
||||||
}
|
|
||||||
for _, env := range os.Environ() {
|
for _, env := range os.Environ() {
|
||||||
if k, v, ok := strings.Cut(env, "="); ok {
|
i := strings.Index(env, "=")
|
||||||
// don't override
|
if i > 0 {
|
||||||
if _, ok := rc.Env[k]; !ok {
|
rc.Env[env[0:i]] = env[i+1:]
|
||||||
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: 0o644,
|
Mode: 0644,
|
||||||
Body: rc.EventJSON,
|
Body: rc.EventJSON,
|
||||||
}, &container.FileEntry{
|
}, &container.FileEntry{
|
||||||
Name: "workflow/envs.txt",
|
Name: "workflow/envs.txt",
|
||||||
Mode: 0o666,
|
Mode: 0666,
|
||||||
|
Body: "",
|
||||||
|
}, &container.FileEntry{
|
||||||
|
Name: "workflow/paths.txt",
|
||||||
|
Mode: 0666,
|
||||||
Body: "",
|
Body: "",
|
||||||
}),
|
}),
|
||||||
)(ctx)
|
)(ctx)
|
||||||
@@ -236,7 +226,6 @@ 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()
|
||||||
@@ -252,7 +241,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
|
|
||||||
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
||||||
Cmd: nil,
|
Cmd: nil,
|
||||||
Entrypoint: []string{"tail", "-f", "/dev/null"},
|
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
||||||
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
||||||
Image: image,
|
Image: image,
|
||||||
Username: username,
|
Username: username,
|
||||||
@@ -260,7 +249,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: "host",
|
NetworkMode: rc.Config.ContainerNetworkMode,
|
||||||
Binds: binds,
|
Binds: binds,
|
||||||
Stdout: logWriter,
|
Stdout: logWriter,
|
||||||
Stderr: logWriter,
|
Stderr: logWriter,
|
||||||
@@ -268,6 +257,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
UsernsMode: rc.Config.UsernsMode,
|
UsernsMode: rc.Config.UsernsMode,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
Options: rc.options(ctx),
|
Options: rc.options(ctx),
|
||||||
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
})
|
})
|
||||||
if rc.JobContainer == nil {
|
if rc.JobContainer == nil {
|
||||||
return errors.New("Failed to create job container")
|
return errors.New("Failed to create job container")
|
||||||
@@ -278,13 +268,19 @@ 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: 0o644,
|
Mode: 0644,
|
||||||
Body: rc.EventJSON,
|
Body: rc.EventJSON,
|
||||||
}, &container.FileEntry{
|
}, &container.FileEntry{
|
||||||
Name: "workflow/envs.txt",
|
Name: "workflow/envs.txt",
|
||||||
Mode: 0o666,
|
Mode: 0666,
|
||||||
|
Body: "",
|
||||||
|
}, &container.FileEntry{
|
||||||
|
Name: "workflow/paths.txt",
|
||||||
|
Mode: 0666,
|
||||||
Body: "",
|
Body: "",
|
||||||
}),
|
}),
|
||||||
)(ctx)
|
)(ctx)
|
||||||
@@ -297,51 +293,6 @@ 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 {
|
||||||
@@ -384,18 +335,14 @@ 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 {
|
||||||
if rc.IsHostEnv(ctx) {
|
image := rc.platformImage(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()
|
||||||
}
|
}
|
||||||
@@ -423,25 +370,16 @@ 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 {
|
||||||
res, err := rc.isEnabled(ctx)
|
isEnabled, err := rc.isEnabled(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if res {
|
|
||||||
return executor(ctx)
|
if isEnabled {
|
||||||
|
return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,9 +396,19 @@ func (rc *RunContext) platformImage(ctx context.Context) string {
|
|||||||
common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String())
|
common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, runnerLabel := range job.RunsOn() {
|
runsOn := job.RunsOn()
|
||||||
platformName := rc.ExprEval.Interpolate(ctx, runnerLabel)
|
for i, v := range runsOn {
|
||||||
image := rc.Config.Platforms[strings.ToLower(platformName)]
|
runsOn[i] = rc.ExprEval.Interpolate(ctx, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pick := rc.Config.PlatformPicker; pick != nil {
|
||||||
|
if image := pick(runsOn); image != "" {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runnerLabel := range runsOn {
|
||||||
|
image := rc.Config.Platforms[strings.ToLower(runnerLabel)]
|
||||||
if image != "" {
|
if image != "" {
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
@@ -473,7 +421,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 rc.Config.ContainerOptions
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Options
|
return c.Options
|
||||||
@@ -491,10 +439,6 @@ 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 {
|
||||||
@@ -520,17 +464,44 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
|||||||
return rtnMap
|
return rtnMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated: use createSimpleContainerName
|
||||||
func createContainerName(parts ...string) string {
|
func createContainerName(parts ...string) string {
|
||||||
name := strings.Join(parts, "-")
|
name := make([]string, 0)
|
||||||
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||||
name = pattern.ReplaceAllString(name, "-")
|
partLen := (30 / len(parts)) - 1
|
||||||
name = strings.ReplaceAll(name, "--", "-")
|
for i, part := range parts {
|
||||||
hash := sha256.Sum256([]byte(name))
|
if i == len(parts)-1 {
|
||||||
|
name = append(name, pattern.ReplaceAllString(part, "-"))
|
||||||
|
} else {
|
||||||
|
// If any part has a '-<number>' on the end it is likely part of a matrix job.
|
||||||
|
// Let's preserve the number to prevent clashes in container names.
|
||||||
|
re := regexp.MustCompile("-[0-9]+$")
|
||||||
|
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, "-"), "-"), "--", "-")
|
||||||
|
}
|
||||||
|
|
||||||
// SHA256 is 64 hex characters. So trim name to 63 characters to make room for the hash and separator
|
func createSimpleContainerName(parts ...string) string {
|
||||||
trimmedName := strings.Trim(trimToLen(name, 63), "-")
|
pattern := regexp.MustCompile("[^a-zA-Z0-9-]")
|
||||||
|
name := make([]string, 0, len(parts))
|
||||||
return fmt.Sprintf("%s-%x", trimmedName, hash)
|
for _, v := range parts {
|
||||||
|
v = pattern.ReplaceAllString(v, "-")
|
||||||
|
v = strings.Trim(v, "-")
|
||||||
|
for strings.Contains(v, "--") {
|
||||||
|
v = strings.ReplaceAll(v, "--", "-")
|
||||||
|
}
|
||||||
|
if v != "" {
|
||||||
|
name = append(name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(name, "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimToLen(s string, l int) string {
|
func trimToLen(s string, l int) string {
|
||||||
@@ -571,20 +542,11 @@ 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"
|
||||||
@@ -613,24 +575,58 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
|||||||
ghc.Actor = "nektos/act"
|
ghc.Actor = "nektos/act"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preset := rc.Config.PresetGitHubContext; preset != nil {
|
||||||
|
ghc.Event = preset.Event
|
||||||
|
ghc.RunID = preset.RunID
|
||||||
|
ghc.RunNumber = preset.RunNumber
|
||||||
|
ghc.Actor = preset.Actor
|
||||||
|
ghc.Repository = preset.Repository
|
||||||
|
ghc.EventName = preset.EventName
|
||||||
|
ghc.Sha = preset.Sha
|
||||||
|
ghc.Ref = preset.Ref
|
||||||
|
ghc.RefName = preset.RefName
|
||||||
|
ghc.RefType = preset.RefType
|
||||||
|
ghc.HeadRef = preset.HeadRef
|
||||||
|
ghc.BaseRef = preset.BaseRef
|
||||||
|
ghc.Token = preset.Token
|
||||||
|
ghc.RepositoryOwner = preset.RepositoryOwner
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ghc.SetBaseAndHeadRef()
|
if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" {
|
||||||
repoPath := rc.Config.Workdir
|
ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
|
||||||
ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath)
|
ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
|
||||||
if ghc.Ref == "" {
|
|
||||||
ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath)
|
|
||||||
}
|
|
||||||
if ghc.Sha == "" {
|
|
||||||
ghc.SetSha(ctx, repoPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ghc.SetRefTypeAndName()
|
ghc.SetRefAndSha(ctx, rc.Config.DefaultBranch, repoPath)
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
@@ -661,6 +657,15 @@ 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
|
||||||
|
|
||||||
@@ -680,6 +685,8 @@ 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
|
||||||
@@ -698,34 +705,27 @@ 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_JOB"] = github.Job
|
env["GITHUB_SERVER_URL"] = "https://github.com"
|
||||||
|
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" {
|
||||||
defaultServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
|
hasProtocol := strings.HasPrefix(rc.Config.GitHubInstance, "http://") || strings.HasPrefix(rc.Config.GitHubInstance, "https://")
|
||||||
defaultAPIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
|
if hasProtocol {
|
||||||
defaultGraphqlURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
|
env["GITHUB_SERVER_URL"] = 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
|
||||||
if env["GITHUB_SERVER_URL"] == "" {
|
} else {
|
||||||
env["GITHUB_SERVER_URL"] = defaultServerURL
|
env["GITHUB_SERVER_URL"] = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
|
||||||
}
|
env["GITHUB_API_URL"] = fmt.Sprintf("https://%s/api/v1", rc.Config.GitHubInstance)
|
||||||
|
env["GITHUB_GRAPHQL_URL"] = "" // disable graphql url because Gitea doesn't support that
|
||||||
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 +754,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/", rc.Config.ArtifactServerAddr, rc.Config.ArtifactServerPort)
|
actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", common.GetOutboundIP().String(), rc.Config.ArtifactServerPort)
|
||||||
}
|
}
|
||||||
env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL
|
env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL
|
||||||
|
|
||||||
|
@@ -144,7 +144,6 @@ 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},
|
||||||
@@ -365,7 +364,6 @@ 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())
|
||||||
|
|
||||||
@@ -394,7 +392,6 @@ 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) {
|
||||||
@@ -413,7 +410,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: "refs/tags/tag-name"},
|
{event: "release", json: `{"release": {"tag_name": "tag-name"}}`, ref: "tag-name"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, data := range table {
|
for _, data := range table {
|
||||||
@@ -624,3 +621,24 @@ func TestRunContextGetEnv(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_createSimpleContainerName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
parts []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
parts: []string{"a--a", "BB正", "c-C"},
|
||||||
|
want: "a-a_BB_c-C",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parts: []string{"a-a", "", "-"},
|
||||||
|
want: "a-a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(strings.Join(tt.parts, " "), func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, createSimpleContainerName(tt.parts...), "createSimpleContainerName(%v)", tt.parts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,9 +2,9 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ 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
|
||||||
@@ -41,29 +40,30 @@ 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
|
||||||
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
||||||
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
||||||
}
|
|
||||||
|
|
||||||
type caller struct {
|
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
|
||||||
runContext *RunContext
|
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
||||||
|
ContainerNamePrefix string // the prefix of container name
|
||||||
|
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
||||||
|
ContainerNetworkMode string // the network mode of job containers
|
||||||
|
DefaultActionInstance string // the default actions web site
|
||||||
|
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -72,44 +72,40 @@ func New(runnerConfig *Config) (Runner, error) {
|
|||||||
config: runnerConfig,
|
config: runnerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
return runner.configure()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (runner *runnerImpl) configure() (Runner, error) {
|
|
||||||
runner.eventJSON = "{}"
|
runner.eventJSON = "{}"
|
||||||
if runner.config.EventPath != "" {
|
if runnerConfig.EventJSON != "" {
|
||||||
|
runner.eventJSON = runnerConfig.EventJSON
|
||||||
|
} else if runnerConfig.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 _, run := range stage.Runs {
|
for r, 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 {
|
||||||
@@ -137,8 +133,29 @@ 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()(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix)))
|
return rc.Executor().Finally(func(ctx context.Context) error {
|
||||||
|
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...))
|
||||||
@@ -178,10 +195,8 @@ 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,10 +1,8 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -24,7 +22,6 @@ 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() {
|
||||||
@@ -45,103 +42,14 @@ 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.NoError(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
plan, err := planner.PlanEvent("push")
|
plan := planner.PlanEvent("push")
|
||||||
assert.NoError(t, err)
|
assert.Nil(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")
|
||||||
@@ -150,10 +58,8 @@ 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, err = planner.PlanEvent("release")
|
plan = planner.PlanEvent("release")
|
||||||
assert.NoError(t, err)
|
assert.Equal(t, len(plan.Stages), 0, "stages")
|
||||||
assert.NotNil(t, plan)
|
|
||||||
assert.Equal(t, 0, len(plan.Stages))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestJobFileInfo struct {
|
type TestJobFileInfo struct {
|
||||||
@@ -162,7 +68,6 @@ 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) {
|
||||||
@@ -183,7 +88,6 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -194,15 +98,13 @@ 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, err := planner.PlanEvent(j.eventName)
|
plan := planner.PlanEvent(j.eventName)
|
||||||
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
|
|
||||||
if err == nil && plan != nil {
|
err = runner.NewPlanExecutor(plan)(ctx)
|
||||||
err = runner.NewPlanExecutor(plan)(ctx)
|
if j.errorMessage == "" {
|
||||||
if j.errorMessage == "" {
|
assert.Nil(t, err, fullWorkflowPath)
|
||||||
assert.Nil(t, err, fullWorkflowPath)
|
} else {
|
||||||
} else {
|
assert.Error(t, err, j.errorMessage)
|
||||||
assert.Error(t, err, j.errorMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("::endgroup::")
|
fmt.Println("::endgroup::")
|
||||||
@@ -217,96 +119,81 @@ func TestRunEvent(t *testing.T) {
|
|||||||
|
|
||||||
tables := []TestJobFileInfo{
|
tables := []TestJobFileInfo{
|
||||||
// Shells
|
// Shells
|
||||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
{workdir, "shells/defaults", "push", "", platforms},
|
||||||
// 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"}, secrets}, // custom image with pwsh
|
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh
|
||||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
{workdir, "shells/bash", "push", "", platforms},
|
||||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python
|
||||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
{workdir, "shells/sh", "push", "", platforms},
|
||||||
|
|
||||||
// Local action
|
// Local action
|
||||||
{workdir, "local-action-docker-url", "push", "", platforms, secrets},
|
{workdir, "local-action-docker-url", "push", "", platforms},
|
||||||
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
|
{workdir, "local-action-dockerfile", "push", "", platforms},
|
||||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
|
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms},
|
||||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
{workdir, "local-action-js", "push", "", platforms},
|
||||||
|
|
||||||
// Uses
|
// Uses
|
||||||
{workdir, "uses-composite", "push", "", platforms, secrets},
|
{workdir, "uses-composite", "push", "", platforms},
|
||||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms},
|
||||||
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
{workdir, "uses-nested-composite", "push", "", platforms},
|
||||||
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms, secrets},
|
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms},
|
||||||
{workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}},
|
{workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms},
|
||||||
{workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
|
{workdir, "uses-docker-url", "push", "", platforms},
|
||||||
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
{workdir, "act-composite-env-test", "push", "", platforms},
|
||||||
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
|
||||||
|
|
||||||
// Eval
|
// Eval
|
||||||
{workdir, "evalmatrix", "push", "", platforms, secrets},
|
{workdir, "evalmatrix", "push", "", platforms},
|
||||||
{workdir, "evalmatrixneeds", "push", "", platforms, secrets},
|
{workdir, "evalmatrixneeds", "push", "", platforms},
|
||||||
{workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
|
{workdir, "evalmatrixneeds2", "push", "", platforms},
|
||||||
{workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
|
{workdir, "evalmatrix-merge-map", "push", "", platforms},
|
||||||
{workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
|
{workdir, "evalmatrix-merge-array", "push", "", platforms},
|
||||||
{workdir, "issue-1195", "push", "", platforms, secrets},
|
{workdir, "issue-1195", "push", "", platforms},
|
||||||
|
|
||||||
{workdir, "basic", "push", "", platforms, secrets},
|
{workdir, "basic", "push", "", platforms},
|
||||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
|
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms},
|
||||||
{workdir, "runs-on", "push", "", platforms, secrets},
|
{workdir, "runs-on", "push", "", platforms},
|
||||||
{workdir, "checkout", "push", "", platforms, secrets},
|
{workdir, "checkout", "push", "", platforms},
|
||||||
{workdir, "job-container", "push", "", platforms, secrets},
|
{workdir, "job-container", "push", "", platforms},
|
||||||
{workdir, "job-container-non-root", "push", "", platforms, secrets},
|
{workdir, "job-container-non-root", "push", "", platforms},
|
||||||
{workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms, secrets},
|
{workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms},
|
||||||
{workdir, "container-hostname", "push", "", platforms, secrets},
|
{workdir, "container-hostname", "push", "", platforms},
|
||||||
{workdir, "remote-action-docker", "push", "", platforms, secrets},
|
{workdir, "remote-action-docker", "push", "", platforms},
|
||||||
{workdir, "remote-action-js", "push", "", platforms, secrets},
|
{workdir, "remote-action-js", "push", "", platforms},
|
||||||
{workdir, "remote-action-js-node-user", "push", "", platforms, secrets}, // Test if this works with non root container
|
{workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}}, // Test if this works with non root container
|
||||||
{workdir, "matrix", "push", "", platforms, secrets},
|
{workdir, "matrix", "push", "", platforms},
|
||||||
{workdir, "matrix-include-exclude", "push", "", platforms, secrets},
|
{workdir, "matrix-include-exclude", "push", "", platforms},
|
||||||
{workdir, "matrix-exitcode", "push", "Job 'test' failed", platforms, secrets},
|
{workdir, "commands", "push", "", platforms},
|
||||||
{workdir, "commands", "push", "", platforms, secrets},
|
{workdir, "workdir", "push", "", platforms},
|
||||||
{workdir, "workdir", "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, "environment-files", "push", "", platforms, secrets},
|
{workdir, "outputs", "push", "", platforms},
|
||||||
{workdir, "GITHUB_STATE", "push", "", platforms, secrets},
|
{workdir, "networking", "push", "", platforms},
|
||||||
{workdir, "environment-files-parser-bug", "push", "", platforms, secrets},
|
{workdir, "steps-context/conclusion", "push", "", platforms},
|
||||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
|
{workdir, "steps-context/outcome", "push", "", platforms},
|
||||||
{workdir, "outputs", "push", "", platforms, secrets},
|
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
||||||
{workdir, "networking", "push", "", platforms, secrets},
|
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
||||||
{workdir, "steps-context/conclusion", "push", "", platforms, secrets},
|
{workdir, "actions-environment-and-context-tests", "push", "", platforms},
|
||||||
{workdir, "steps-context/outcome", "push", "", platforms, secrets},
|
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
|
||||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
|
{workdir, "evalenv", "push", "", platforms},
|
||||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
|
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
|
||||||
{workdir, "actions-environment-and-context-tests", "push", "", platforms, secrets},
|
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms},
|
||||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
|
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms},
|
||||||
{workdir, "evalenv", "push", "", platforms, secrets},
|
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms},
|
||||||
{workdir, "docker-action-custom-path", "push", "", platforms, secrets},
|
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms},
|
||||||
{workdir, "GITHUB_ENV-use-in-env-ctx", "push", "", platforms, secrets},
|
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner
|
||||||
{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, secrets},
|
{"../model/testdata", "container-volumes", "push", "", platforms},
|
||||||
{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 {
|
||||||
@@ -334,51 +221,51 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
|||||||
|
|
||||||
tables = append(tables, []TestJobFileInfo{
|
tables = append(tables, []TestJobFileInfo{
|
||||||
// Shells
|
// Shells
|
||||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
{workdir, "shells/defaults", "push", "", platforms},
|
||||||
{workdir, "shells/pwsh", "push", "", platforms, secrets},
|
{workdir, "shells/pwsh", "push", "", platforms},
|
||||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
{workdir, "shells/bash", "push", "", platforms},
|
||||||
{workdir, "shells/python", "push", "", platforms, secrets},
|
{workdir, "shells/python", "push", "", platforms},
|
||||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
{workdir, "shells/sh", "push", "", platforms},
|
||||||
|
|
||||||
// Local action
|
// Local action
|
||||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
{workdir, "local-action-js", "push", "", platforms},
|
||||||
|
|
||||||
// Uses
|
// Uses
|
||||||
{workdir, "uses-composite", "push", "", platforms, secrets},
|
{workdir, "uses-composite", "push", "", platforms},
|
||||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms},
|
||||||
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
{workdir, "uses-nested-composite", "push", "", platforms},
|
||||||
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
{workdir, "act-composite-env-test", "push", "", platforms},
|
||||||
|
|
||||||
// Eval
|
// Eval
|
||||||
{workdir, "evalmatrix", "push", "", platforms, secrets},
|
{workdir, "evalmatrix", "push", "", platforms},
|
||||||
{workdir, "evalmatrixneeds", "push", "", platforms, secrets},
|
{workdir, "evalmatrixneeds", "push", "", platforms},
|
||||||
{workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
|
{workdir, "evalmatrixneeds2", "push", "", platforms},
|
||||||
{workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
|
{workdir, "evalmatrix-merge-map", "push", "", platforms},
|
||||||
{workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
|
{workdir, "evalmatrix-merge-array", "push", "", platforms},
|
||||||
{workdir, "issue-1195", "push", "", platforms, secrets},
|
{workdir, "issue-1195", "push", "", platforms},
|
||||||
|
|
||||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
|
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms},
|
||||||
{workdir, "runs-on", "push", "", platforms, secrets},
|
{workdir, "runs-on", "push", "", platforms},
|
||||||
{workdir, "checkout", "push", "", platforms, secrets},
|
{workdir, "checkout", "push", "", platforms},
|
||||||
{workdir, "remote-action-js", "push", "", platforms, secrets},
|
{workdir, "remote-action-js", "push", "", platforms},
|
||||||
{workdir, "matrix", "push", "", platforms, secrets},
|
{workdir, "matrix", "push", "", platforms},
|
||||||
{workdir, "matrix-include-exclude", "push", "", platforms, secrets},
|
{workdir, "matrix-include-exclude", "push", "", platforms},
|
||||||
{workdir, "commands", "push", "", platforms, secrets},
|
{workdir, "commands", "push", "", platforms},
|
||||||
{workdir, "defaults-run", "push", "", platforms, secrets},
|
{workdir, "defaults-run", "push", "", platforms},
|
||||||
{workdir, "composite-fail-with-output", "push", "", platforms, secrets},
|
{workdir, "composite-fail-with-output", "push", "", platforms},
|
||||||
{workdir, "issue-597", "push", "", platforms, secrets},
|
{workdir, "issue-597", "push", "", platforms},
|
||||||
{workdir, "issue-598", "push", "", platforms, secrets},
|
{workdir, "issue-598", "push", "", platforms},
|
||||||
{workdir, "if-env-act", "push", "", platforms, secrets},
|
{workdir, "if-env-act", "push", "", platforms},
|
||||||
{workdir, "env-and-path", "push", "", platforms, secrets},
|
{workdir, "env-and-path", "push", "", platforms},
|
||||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
|
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms},
|
||||||
{workdir, "outputs", "push", "", platforms, secrets},
|
{workdir, "outputs", "push", "", platforms},
|
||||||
{workdir, "steps-context/conclusion", "push", "", platforms, secrets},
|
{workdir, "steps-context/conclusion", "push", "", platforms},
|
||||||
{workdir, "steps-context/outcome", "push", "", platforms, secrets},
|
{workdir, "steps-context/outcome", "push", "", platforms},
|
||||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
|
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
||||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
|
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
||||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
|
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
|
||||||
{workdir, "evalenv", "push", "", platforms, secrets},
|
{workdir, "evalenv", "push", "", platforms},
|
||||||
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
|
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -387,22 +274,16 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tables = append(tables, []TestJobFileInfo{
|
tables = append(tables, []TestJobFileInfo{
|
||||||
{workdir, "windows-prepend-path", "push", "", platforms, secrets},
|
{workdir, "windows-prepend-path", "push", "", platforms},
|
||||||
{workdir, "windows-add-env", "push", "", platforms, secrets},
|
{workdir, "windows-add-env", "push", "", platforms},
|
||||||
}...)
|
}...)
|
||||||
} 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, secrets},
|
{workdir, "nix-prepend-path", "push", "", platforms},
|
||||||
{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},
|
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,17 +303,17 @@ func TestDryrunEvent(t *testing.T) {
|
|||||||
|
|
||||||
tables := []TestJobFileInfo{
|
tables := []TestJobFileInfo{
|
||||||
// Shells
|
// Shells
|
||||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
{workdir, "shells/defaults", "push", "", platforms},
|
||||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
|
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh
|
||||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
{workdir, "shells/bash", "push", "", platforms},
|
||||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python
|
||||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
{workdir, "shells/sh", "push", "", platforms},
|
||||||
|
|
||||||
// Local action
|
// Local action
|
||||||
{workdir, "local-action-docker-url", "push", "", platforms, secrets},
|
{workdir, "local-action-docker-url", "push", "", platforms},
|
||||||
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
|
{workdir, "local-action-dockerfile", "push", "", platforms},
|
||||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
|
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms},
|
||||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
{workdir, "local-action-js", "push", "", platforms},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
@@ -442,30 +323,6 @@ 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")
|
||||||
@@ -482,17 +339,6 @@ 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")
|
||||||
@@ -516,9 +362,9 @@ func TestMaskValues(t *testing.T) {
|
|||||||
platforms: platforms,
|
platforms: platforms,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := &maskJobLoggerFactory{}
|
output := captureOutput(t, func() {
|
||||||
tjfi.runTest(WithJobLoggerFactory(common.WithLogger(context.Background(), logger.WithJobLogger()), logger), t, &Config{})
|
tjfi.runTest(context.Background(), t, &Config{})
|
||||||
output := logger.Output.String()
|
})
|
||||||
|
|
||||||
assertNoSecret(output, "secret value")
|
assertNoSecret(output, "secret value")
|
||||||
assertNoSecret(output, "YWJjCg==")
|
assertNoSecret(output, "YWJjCg==")
|
||||||
@@ -546,27 +392,6 @@ 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 processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error {
|
func (s stepStage) getStepName(stepModel *model.Step) string {
|
||||||
env := map[string]string{}
|
switch s {
|
||||||
err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx)
|
case stepStagePre:
|
||||||
if err != nil {
|
return fmt.Sprintf("pre-%s", stepModel.ID)
|
||||||
return err
|
case stepStageMain:
|
||||||
|
return stepModel.ID
|
||||||
|
case stepStagePost:
|
||||||
|
return fmt.Sprintf("post-%s", stepModel.ID)
|
||||||
}
|
}
|
||||||
for k, v := range env {
|
return "unknown"
|
||||||
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,16 +63,13 @@ 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 = stepModel.ID
|
rc.CurrentStep = stage.getStepName(stepModel)
|
||||||
|
|
||||||
stepResult := &model.StepResult{
|
rc.StepResults[rc.CurrentStep] = &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 {
|
||||||
@@ -81,15 +78,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 {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !runStep {
|
if !runStep {
|
||||||
stepResult.Conclusion = model.StepStatusSkipped
|
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
|
||||||
stepResult.Outcome = model.StepStatusSkipped
|
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,79 +98,58 @@ 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")
|
||||||
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
|
|
||||||
|
|
||||||
stateFileCommand := path.Join("workflow", "statecmd.txt")
|
stateFileCommand := path.Join("workflow", "statecmd.txt")
|
||||||
|
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
|
||||||
(*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: 0o666,
|
|
||||||
}, &container.FileEntry{
|
|
||||||
Name: stateFileCommand,
|
|
||||||
Mode: 0o666,
|
|
||||||
}, &container.FileEntry{
|
|
||||||
Name: pathFileCommand,
|
|
||||||
Mode: 0o666,
|
|
||||||
}, &container.FileEntry{
|
|
||||||
Name: envFileCommand,
|
|
||||||
Mode: 0666,
|
Mode: 0666,
|
||||||
}, &container.FileEntry{
|
}, &container.FileEntry{
|
||||||
Name: summaryFileCommand,
|
Name: stateFileCommand,
|
||||||
Mode: 0o666,
|
Mode: 0666,
|
||||||
})(ctx)
|
})(ctx)
|
||||||
|
|
||||||
err = executor(ctx)
|
err = executor(ctx)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
|
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
|
||||||
} else {
|
} else {
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].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 {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].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
|
||||||
stepResult.Conclusion = model.StepStatusSuccess
|
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess
|
||||||
} else {
|
} else {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
|
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
|
||||||
}
|
}
|
||||||
// Process Runner File Commands
|
// Process Runner File Commands
|
||||||
orgerr := err
|
orgerr := err
|
||||||
err = processRunnerEnvFileCommand(ctx, envFileCommand, rc, rc.setEnv)
|
state := map[string]string{}
|
||||||
|
err = rc.JobContainer.UpdateFromEnv(path.Join(actPath, stateFileCommand), &state)(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = processRunnerEnvFileCommand(ctx, stateFileCommand, rc, rc.saveState)
|
for k, v := range state {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
err = processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput)
|
for k, v := range output {
|
||||||
if err != nil {
|
rc.setOutput(ctx, map[string]string{"name": k}, v)
|
||||||
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
|
||||||
@@ -186,22 +162,24 @@ 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() {
|
||||||
if !strings.HasPrefix(k, "INPUT_") {
|
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
|
||||||
(*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())
|
||||||
@@ -221,6 +199,14 @@ 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,9 +1,7 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -69,7 +67,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("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
|
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -77,6 +75,14 @@ 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
|
||||||
})
|
})
|
||||||
@@ -85,8 +91,6 @@ 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
|
||||||
})
|
})
|
||||||
@@ -103,12 +107,13 @@ 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
|
||||||
err error
|
expectedPostStepResult *model.StepResult
|
||||||
mocks struct {
|
err error
|
||||||
|
mocks struct {
|
||||||
env bool
|
env bool
|
||||||
exec bool
|
exec bool
|
||||||
}
|
}
|
||||||
@@ -133,6 +138,11 @@ 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
|
||||||
@@ -161,6 +171,11 @@ 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
|
||||||
@@ -189,11 +204,16 @@ 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: false,
|
env: true,
|
||||||
exec: false,
|
exec: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -218,6 +238,7 @@ 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
|
||||||
@@ -256,6 +277,11 @@ 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 {
|
||||||
@@ -268,10 +294,6 @@ 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
|
||||||
})
|
})
|
||||||
@@ -279,14 +301,12 @@ 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, sal.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
|
assert.Equal(t, tt.expectedPostStepResult, sal.RunContext.StepResults["post-step"])
|
||||||
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 {
|
||||||
@@ -30,9 +30,7 @@ type stepActionRemote struct {
|
|||||||
remoteAction *remoteAction
|
remoteAction *remoteAction
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
||||||
stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
|
||||||
)
|
|
||||||
|
|
||||||
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
@@ -46,15 +44,12 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses)
|
return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses)
|
||||||
}
|
}
|
||||||
|
|
||||||
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
|
|
||||||
|
|
||||||
github := sar.getGithubContext(ctx)
|
github := sar.getGithubContext(ctx)
|
||||||
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||||
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
|
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
|
|
||||||
for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
|
for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
|
||||||
if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
|
if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
|
||||||
sar.remoteAction.URL = "github.com"
|
sar.remoteAction.URL = "github.com"
|
||||||
@@ -62,12 +57,18 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
||||||
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
||||||
URL: sar.remoteAction.CloneURL(),
|
URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
|
||||||
Ref: sar.remoteAction.Ref,
|
Ref: sar.remoteAction.Ref,
|
||||||
Dir: actionDir,
|
Dir: actionDir,
|
||||||
Token: github.Token,
|
Token: "", /*
|
||||||
|
Shouldn't provide token when cloning actions,
|
||||||
|
the token comes from the instance which triggered the task,
|
||||||
|
however, it might be not the same instance which provides actions.
|
||||||
|
For GitHub, they are the same, always github.com.
|
||||||
|
But for Gitea, tasks triggered by a.com can clone actions from b.com.
|
||||||
|
*/
|
||||||
})
|
})
|
||||||
var ntErr common.Executor
|
var ntErr common.Executor
|
||||||
if err := gitClone(ctx); err != nil {
|
if err := gitClone(ctx); err != nil {
|
||||||
@@ -122,7 +123,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
|||||||
return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore)(ctx)
|
return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore)(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
||||||
|
|
||||||
return sar.runAction(sar, actionDir, sar.remoteAction)(ctx)
|
return sar.runAction(sar, actionDir, sar.remoteAction)(ctx)
|
||||||
}),
|
}),
|
||||||
@@ -181,7 +182,7 @@ func (sar *stepActionRemote) getActionModel() *model.Action {
|
|||||||
|
|
||||||
func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunContext {
|
func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunContext {
|
||||||
if sar.compositeRunContext == nil {
|
if sar.compositeRunContext == nil {
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
||||||
actionLocation := path.Join(actionDir, sar.remoteAction.Path)
|
actionLocation := path.Join(actionDir, sar.remoteAction.Path)
|
||||||
_, containerActionDir := getContainerActionPaths(sar.getStepModel(), actionLocation, sar.RunContext)
|
_, containerActionDir := getContainerActionPaths(sar.getStepModel(), actionLocation, sar.RunContext)
|
||||||
|
|
||||||
@@ -196,7 +197,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -213,8 +213,15 @@ type remoteAction struct {
|
|||||||
Ref string
|
Ref string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *remoteAction) CloneURL() string {
|
func (ra *remoteAction) CloneURL(defaultURL string) string {
|
||||||
return fmt.Sprintf("https://%s/%s/%s", ra.URL, ra.Org, ra.Repo)
|
u := ra.URL
|
||||||
|
if u == "" {
|
||||||
|
u = defaultURL
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||||
|
u = "https://" + u
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s/%s", u, ra.Org, ra.Repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *remoteAction) IsCheckout() bool {
|
func (ra *remoteAction) IsCheckout() bool {
|
||||||
@@ -225,6 +232,26 @@ func (ra *remoteAction) IsCheckout() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newRemoteAction(action string) *remoteAction {
|
func newRemoteAction(action string) *remoteAction {
|
||||||
|
// support http(s)://host/owner/repo@v3
|
||||||
|
for _, schema := range []string{"https://", "http://"} {
|
||||||
|
if strings.HasPrefix(action, schema) {
|
||||||
|
splits := strings.SplitN(strings.TrimPrefix(action, schema), "/", 2)
|
||||||
|
if len(splits) != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := parseAction(splits[1])
|
||||||
|
if ret == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret.URL = schema + splits[0]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAction(action string) *remoteAction {
|
||||||
// GitHub's document[^] describes:
|
// GitHub's document[^] describes:
|
||||||
// > We strongly recommend that you include the version of
|
// > We strongly recommend that you include the version of
|
||||||
// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
|
// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
|
||||||
@@ -240,20 +267,6 @@ func newRemoteAction(action string) *remoteAction {
|
|||||||
Repo: matches[2],
|
Repo: matches[2],
|
||||||
Path: matches[4],
|
Path: matches[4],
|
||||||
Ref: matches[6],
|
Ref: matches[6],
|
||||||
URL: "github.com",
|
URL: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func safeFilename(s string) string {
|
|
||||||
return strings.NewReplacer(
|
|
||||||
`<`, "-",
|
|
||||||
`>`, "-",
|
|
||||||
`:`, "-",
|
|
||||||
`"`, "-",
|
|
||||||
`/`, "-",
|
|
||||||
`\`, "-",
|
|
||||||
`|`, "-",
|
|
||||||
`?`, "-",
|
|
||||||
`*`, "-",
|
|
||||||
).Replace(s)
|
|
||||||
}
|
|
||||||
|
@@ -1,20 +1,18 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"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"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionRemoteMocks struct {
|
type stepActionRemoteMocks struct {
|
||||||
@@ -165,6 +163,11 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -175,10 +178,6 @@ 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
|
||||||
})
|
})
|
||||||
@@ -186,8 +185,6 @@ 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)
|
||||||
@@ -415,14 +412,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
|
||||||
IntraActionState map[string]map[string]string
|
expectedEnv map[string]string
|
||||||
expectedEnv map[string]string
|
expectedPostStepResult *model.StepResult
|
||||||
err error
|
err error
|
||||||
mocks struct {
|
mocks struct {
|
||||||
env bool
|
env bool
|
||||||
exec bool
|
exec bool
|
||||||
}
|
}
|
||||||
@@ -445,16 +442,19 @@ 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,6 +483,11 @@ 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
|
||||||
@@ -511,6 +516,11 @@ 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
|
||||||
@@ -540,6 +550,7 @@ 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
|
||||||
@@ -571,14 +582,18 @@ 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 })
|
||||||
|
|
||||||
@@ -586,10 +601,6 @@ 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
|
||||||
})
|
})
|
||||||
@@ -597,8 +608,6 @@ 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)
|
||||||
@@ -609,30 +618,102 @@ func TestStepActionRemotePost(t *testing.T) {
|
|||||||
assert.Equal(t, value, sar.env[key])
|
assert.Equal(t, value, sar.env[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enshure that StepResults is nil in this test
|
assert.Equal(t, tt.expectedPostStepResult, sar.RunContext.StepResults["post-step"])
|
||||||
assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
|
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_safeFilename(t *testing.T) {
|
func Test_newRemoteAction(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
s string
|
action string
|
||||||
want string
|
want *remoteAction
|
||||||
|
wantCloneURL string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
s: "https://test.com/test/",
|
action: "actions/heroku@main",
|
||||||
want: "https---test.com-test-",
|
want: &remoteAction{
|
||||||
|
URL: "",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "heroku",
|
||||||
|
Path: "",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "https://github.com/actions/heroku",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
s: `<>:"/\|?*`,
|
action: "actions/aws/ec2@main",
|
||||||
want: "---------",
|
want: &remoteAction{
|
||||||
|
URL: "",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "aws",
|
||||||
|
Path: "ec2",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "https://github.com/actions/aws",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "./.github/actions/my-action", // it's valid for GitHub, but act don't support it
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "docker://alpine:3.8", // it's valid for GitHub, but act don't support it
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "https://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it
|
||||||
|
want: &remoteAction{
|
||||||
|
URL: "https://gitea.com",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "heroku",
|
||||||
|
Path: "",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "https://gitea.com/actions/heroku",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "https://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it
|
||||||
|
want: &remoteAction{
|
||||||
|
URL: "https://gitea.com",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "aws",
|
||||||
|
Path: "ec2",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "https://gitea.com/actions/aws",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "http://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it
|
||||||
|
want: &remoteAction{
|
||||||
|
URL: "http://gitea.com",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "heroku",
|
||||||
|
Path: "",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "http://gitea.com/actions/heroku",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "http://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it
|
||||||
|
want: &remoteAction{
|
||||||
|
URL: "http://gitea.com",
|
||||||
|
Org: "actions",
|
||||||
|
Repo: "aws",
|
||||||
|
Path: "ec2",
|
||||||
|
Ref: "main",
|
||||||
|
},
|
||||||
|
wantCloneURL: "http://gitea.com/actions/aws",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.s, func(t *testing.T) {
|
t.Run(tt.action, func(t *testing.T) {
|
||||||
assert.Equalf(t, tt.want, safeFilename(tt.s), "safeFilename(%v)", tt.s)
|
got := newRemoteAction(tt.action)
|
||||||
|
assert.Equalf(t, tt.want, got, "newRemoteAction(%v)", tt.action)
|
||||||
|
cloneURL := ""
|
||||||
|
if got != nil {
|
||||||
|
cloneURL = got.CloneURL("github.com")
|
||||||
|
}
|
||||||
|
assert.Equalf(t, tt.wantCloneURL, cloneURL, "newRemoteAction(%v).CloneURL()", tt.action)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -120,7 +120,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd []
|
|||||||
Image: image,
|
Image: image,
|
||||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||||
Name: createContainerName(rc.jobContainerName(), step.ID),
|
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
|
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
|
||||||
@@ -130,6 +130,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd []
|
|||||||
Privileged: rc.Config.Privileged,
|
Privileged: rc.Config.Privileged,
|
||||||
UsernsMode: rc.Config.UsernsMode,
|
UsernsMode: rc.Config.UsernsMode,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/container"
|
"github.com/nektos/act/pkg/container"
|
||||||
@@ -57,6 +55,18 @@ 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
|
||||||
})
|
})
|
||||||
@@ -81,10 +91,6 @@ 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
|
||||||
})
|
})
|
||||||
@@ -93,8 +99,6 @@ 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,7 +6,6 @@ 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"
|
||||||
@@ -31,7 +30,6 @@ 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)
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
@@ -73,7 +71,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: 0o755,
|
Mode: 0755,
|
||||||
Body: script,
|
Body: script,
|
||||||
})(ctx)
|
})(ctx)
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,20 @@
|
|||||||
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: 0o755,
|
Mode: 0755,
|
||||||
Body: "\ncmd\n",
|
Body: "\ncmd\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +53,7 @@ func TestStepRun(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
|
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,6 +61,14 @@ 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
|
||||||
})
|
})
|
||||||
@@ -74,8 +79,6 @@ 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,6 +134,7 @@ 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{
|
||||||
@@ -141,13 +142,19 @@ 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)
|
||||||
|
|
||||||
@@ -171,11 +178,13 @@ 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": "1",
|
"GITHUB_JOB": "",
|
||||||
|
"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",
|
||||||
@@ -183,6 +192,7 @@ 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": "",
|
||||||
|
@@ -1,82 +0,0 @@
|
|||||||
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 }}
|
|
@@ -1,27 +0,0 @@
|
|||||||
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
48
pkg/runner/testdata/GITHUB_STATE/push.yml
vendored
@@ -1,48 +0,0 @@
|
|||||||
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,5 +11,3 @@ 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'
|
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
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'
|
|
@@ -1,12 +0,0 @@
|
|||||||
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: ./
|
|
@@ -1,13 +0,0 @@
|
|||||||
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
101
pkg/runner/testdata/environment-files/push.yaml
vendored
@@ -1,101 +0,0 @@
|
|||||||
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
|
|
@@ -1,33 +0,0 @@
|
|||||||
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
21
pkg/runner/testdata/input-from-cli/input.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
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
|
|
@@ -1,8 +0,0 @@
|
|||||||
inputs:
|
|
||||||
test-env-input: {}
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- run: |
|
|
||||||
exit ${{ inputs.test-env-input == env.test-env-input && '0' || '1'}}
|
|
||||||
shell: bash
|
|
@@ -1,15 +0,0 @@
|
|||||||
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
16
pkg/runner/testdata/issue-1595/missing.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
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
8
pkg/runner/testdata/issue-1595/no-event.yml
vendored
@@ -1,8 +0,0 @@
|
|||||||
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
10
pkg/runner/testdata/issue-1595/no-first.yml
vendored
@@ -1,10 +0,0 @@
|
|||||||
name: no first
|
|
||||||
on: push
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
second:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: first
|
|
||||||
steps:
|
|
||||||
- run: echo How did you get here?
|
|
||||||
shell: bash
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user