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
|
||||
|
||||
|
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:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
check-latest: true
|
||||
- uses: golangci/golangci-lint-action@v3.4.0
|
||||
- uses: golangci/golangci-lint-action@v3.3.1
|
||||
with:
|
||||
version: v1.47.2
|
||||
- uses: megalinter/megalinter/flavors/go@v6.20.0
|
||||
- uses: megalinter/megalinter/flavors/go@v6.15.0
|
||||
env:
|
||||
DEFAULT_BRANCH: master
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -50,10 +50,7 @@ jobs:
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run Tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
upload-logs-name: logs-linux
|
||||
- run: go test -v -cover -coverprofile=coverage.txt -covermode=atomic -timeout 15m ./...
|
||||
- name: Upload Codecov report
|
||||
uses: codecov/codecov-action@v3.1.1
|
||||
with:
|
||||
@@ -76,11 +73,8 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
check-latest: true
|
||||
- name: Run Tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
filter: '^TestRunEventHostEnvironment$'
|
||||
upload-logs-name: logs-${{ matrix.os }}
|
||||
- run: go test -v -run ^TestRunEventHostEnvironment$ ./...
|
||||
# TODO merge coverage with test-linux
|
||||
|
||||
snapshot:
|
||||
name: snapshot
|
||||
@@ -99,7 +93,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: release --snapshot --rm-dist
|
||||
|
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
@@ -39,29 +39,3 @@ jobs:
|
||||
version: ${{ github.ref }}
|
||||
apiKey: ${{ secrets.CHOCO_APIKEY }}
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
- uses: actions/stale@v6
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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'
|
||||
remove-stale-when-updated: 'True'
|
||||
operations-per-run: 500
|
||||
days-before-stale: 180
|
||||
days-before-stale: 30
|
||||
days-before-close: 14
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ coverage.txt
|
||||
|
||||
# megalinter
|
||||
report/
|
||||
act
|
||||
|
@@ -14,7 +14,7 @@ DISABLE_LINTERS:
|
||||
- MARKDOWN_MARKDOWN_LINK_CHECK
|
||||
- REPOSITORY_CHECKOV
|
||||
- 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
|
||||
PARALLEL: false
|
||||
PRINT_ALPACA: false
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fix"],
|
||||
"go.testTimeout": "300s",
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 The Gitea Authors
|
||||
Copyright (c) 2019
|
||||
|
||||
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"
|
||||
@exit 1
|
||||
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 push origin master
|
||||
git push origin v$(NEW_VERSION)
|
||||
|
||||
.PHONY: snapshot
|
||||
@@ -109,5 +105,3 @@ snapshot:
|
||||
--rm-dist \
|
||||
--single-target \
|
||||
--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)
|
||||
@@ -71,14 +85,6 @@ choco install act-cli
|
||||
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)
|
||||
|
||||
[](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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### Bash script
|
||||
@@ -131,7 +129,7 @@ gh extension install nektos/gh-act
|
||||
Run this command in your terminal:
|
||||
|
||||
```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
|
||||
@@ -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.
|
||||
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 [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"
|
||||
```
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
**You cannot use this method in job level if conditions, see [Skipping jobs](#skipping-jobs)**
|
||||
|
||||
```yml
|
||||
- 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.
|
||||
|
||||
# Pass Inputs to Manually Triggered Workflows
|
||||
## Pass Inputs to Manually Triggered Workflows
|
||||
|
||||
Example workflow file
|
||||
|
||||
@@ -407,14 +417,6 @@ jobs:
|
||||
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`
|
||||
|
||||
```json
|
||||
|
@@ -17,14 +17,12 @@ type Input struct {
|
||||
bindWorkdir bool
|
||||
secrets []string
|
||||
envs []string
|
||||
inputs []string
|
||||
platforms []string
|
||||
dryrun bool
|
||||
forcePull bool
|
||||
forceRebuild bool
|
||||
noOutput bool
|
||||
envfile string
|
||||
inputfile string
|
||||
secretfile string
|
||||
insecureSecrets bool
|
||||
defaultBranch string
|
||||
@@ -32,7 +30,6 @@ type Input struct {
|
||||
usernsMode string
|
||||
containerArchitecture string
|
||||
containerDaemonSocket string
|
||||
containerOptions string
|
||||
noWorkflowRecurse bool
|
||||
useGitIgnore bool
|
||||
githubInstance string
|
||||
@@ -40,7 +37,6 @@ type Input struct {
|
||||
containerCapDrop []string
|
||||
autoRemove bool
|
||||
artifactServerPath string
|
||||
artifactServerAddr string
|
||||
artifactServerPort string
|
||||
jsonLogger bool
|
||||
noSkipCheckout bool
|
||||
@@ -87,8 +83,3 @@ func (i *Input) WorkflowsPath() string {
|
||||
func (i *Input) EventPath() string {
|
||||
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")
|
||||
}
|
134
cmd/root.go
134
cmd/root.go
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/andreaskoch/go-fswatch"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
@@ -35,8 +34,7 @@ func Execute(ctx context.Context, version string) {
|
||||
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: newRunCommand(ctx, input),
|
||||
PersistentPreRun: setup(input),
|
||||
PersistentPostRun: cleanup(input),
|
||||
PersistentPreRun: setupLogging,
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -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().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.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().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.forcePull, "pull", "p", true, "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.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
|
||||
rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
|
||||
rootCmd.Flags().BoolVarP(&input.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().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().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.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.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.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.")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
rootCmd.SetArgs(args())
|
||||
|
||||
@@ -99,21 +93,18 @@ func configLocations() []string {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
configFileName := ".actrc"
|
||||
|
||||
// reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
var actrcXdg string
|
||||
for _, fileName := range []string{"act/actrc", configFileName} {
|
||||
if foundConfig, err := xdg.SearchConfigFile(fileName); foundConfig != "" && err == nil {
|
||||
actrcXdg = foundConfig
|
||||
break
|
||||
}
|
||||
if xdg, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok && xdg != "" {
|
||||
actrcXdg = filepath.Join(xdg, ".actrc")
|
||||
} else {
|
||||
actrcXdg = filepath.Join(home, ".config", ".actrc")
|
||||
}
|
||||
|
||||
return []string{
|
||||
filepath.Join(home, configFileName),
|
||||
filepath.Join(home, ".actrc"),
|
||||
actrcXdg,
|
||||
filepath.Join(".", configFileName),
|
||||
filepath.Join(".", ".actrc"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,35 +241,11 @@ func readArgsFile(file string, split bool) []string {
|
||||
return args
|
||||
}
|
||||
|
||||
func setup(inputs *Input) func(*cobra.Command, []string) {
|
||||
return func(cmd *cobra.Command, _ []string) {
|
||||
func setupLogging(cmd *cobra.Command, _ []string) {
|
||||
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||
if verbose {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
loadVersionNotices(cmd.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup(inputs *Input) func(*cobra.Command, []string) {
|
||||
return func(cmd *cobra.Command, _ []string) {
|
||||
displayNotices(inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvs(env []string, envs map[string]string) bool {
|
||||
if env != nil {
|
||||
for _, envVar := range env {
|
||||
e := strings.SplitN(envVar, `=`, 2)
|
||||
if len(e) == 2 {
|
||||
envs[e[0]] = e[1]
|
||||
} else {
|
||||
envs[e[0]] = ""
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readEnvs(path string, envs map[string]string) bool {
|
||||
@@ -317,14 +284,18 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||
|
||||
log.Debugf("Loading environment from %s", input.Envfile())
|
||||
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)
|
||||
|
||||
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())
|
||||
secrets := newSecrets(input.secrets)
|
||||
_ = readEnvs(input.Secretfile(), secrets)
|
||||
@@ -358,7 +329,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||
var filterPlan *model.Plan
|
||||
|
||||
// Determine the event name to be filtered
|
||||
var filterEventName string
|
||||
var filterEventName string = ""
|
||||
|
||||
if len(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]
|
||||
}
|
||||
|
||||
var plannerErr error
|
||||
if jobID != "" {
|
||||
log.Debugf("Preparing plan with a job: %s", jobID)
|
||||
filterPlan, plannerErr = planner.PlanJob(jobID)
|
||||
filterPlan = planner.PlanJob(jobID)
|
||||
} else if filterEventName != "" {
|
||||
log.Debugf("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, plannerErr = planner.PlanEvent(filterEventName)
|
||||
filterPlan = planner.PlanEvent(filterEventName)
|
||||
} else {
|
||||
log.Debugf("Preparing plan with all jobs")
|
||||
filterPlan, plannerErr = planner.PlanAll()
|
||||
}
|
||||
if filterPlan == nil && plannerErr != nil {
|
||||
return plannerErr
|
||||
filterPlan = planner.PlanAll()
|
||||
}
|
||||
|
||||
if list {
|
||||
err = printList(filterPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
return printList(filterPlan)
|
||||
}
|
||||
|
||||
if graph {
|
||||
err = drawGraph(filterPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
return drawGraph(filterPlan)
|
||||
}
|
||||
|
||||
// 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
|
||||
if jobID != "" {
|
||||
log.Debugf("Planning job: %s", jobID)
|
||||
plan, plannerErr = planner.PlanJob(jobID)
|
||||
plan = planner.PlanJob(jobID)
|
||||
} else {
|
||||
log.Debugf("Planning jobs for event: %s", eventName)
|
||||
plan, plannerErr = planner.PlanEvent(eventName)
|
||||
}
|
||||
if plan == nil && plannerErr != nil {
|
||||
return plannerErr
|
||||
plan = planner.PlanEvent(eventName)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
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
|
||||
config := &runner.Config{
|
||||
@@ -487,7 +430,6 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||
JSONLogger: input.jsonLogger,
|
||||
Env: envs,
|
||||
Secrets: secrets,
|
||||
Inputs: inputs,
|
||||
Token: secrets["GITHUB_TOKEN"],
|
||||
InsecureSecrets: input.insecureSecrets,
|
||||
Platforms: input.newPlatforms(),
|
||||
@@ -495,14 +437,12 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||
UsernsMode: input.usernsMode,
|
||||
ContainerArchitecture: input.containerArchitecture,
|
||||
ContainerDaemonSocket: input.containerDaemonSocket,
|
||||
ContainerOptions: input.containerOptions,
|
||||
UseGitIgnore: input.useGitIgnore,
|
||||
GitHubInstance: input.githubInstance,
|
||||
ContainerCapAdd: input.containerCapAdd,
|
||||
ContainerCapDrop: input.containerCapDrop,
|
||||
AutoRemove: input.autoRemove,
|
||||
ArtifactServerPath: input.artifactServerPath,
|
||||
ArtifactServerAddr: input.artifactServerAddr,
|
||||
ArtifactServerPort: input.artifactServerPort,
|
||||
NoSkipCheckout: input.noSkipCheckout,
|
||||
RemoteName: input.remoteName,
|
||||
@@ -514,28 +454,20 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||
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)
|
||||
if watch, err := cmd.Flags().GetBool("watch"); err != nil {
|
||||
return err
|
||||
} else if watch {
|
||||
err = watchAndRun(ctx, r.NewPlanExecutor(plan))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
return watchAndRun(ctx, r.NewPlanExecutor(plan))
|
||||
}
|
||||
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
cancel()
|
||||
return nil
|
||||
})
|
||||
err = executor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plannerErr
|
||||
return executor(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +492,7 @@ func defaultImageSurvey(actrc string) error {
|
||||
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"
|
||||
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)
|
||||
|
60
go.mod
60
go.mod
@@ -5,77 +5,79 @@ go 1.18
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/andreaskoch/go-fswatch v1.0.0
|
||||
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/docker v23.0.1+incompatible
|
||||
github.com/docker/docker v20.10.21+incompatible
|
||||
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-ini/ini v1.67.0
|
||||
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/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/moby/buildkit v0.11.4
|
||||
github.com/moby/patternmatcher v0.5.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2
|
||||
github.com/opencontainers/selinux v1.11.0
|
||||
github.com/moby/buildkit v0.10.6
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
|
||||
github.com/opencontainers/selinux v1.10.2
|
||||
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/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
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
|
||||
gotest.tools/v3 v3.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
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/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/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // 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/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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-runewidth v0.0.14 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
|
||||
github.com/moby/sys/mount v0.3.1 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.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/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/rivo/uniseg v0.3.4 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // 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/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
4
main.go
4
main.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -10,8 +9,7 @@ import (
|
||||
"github.com/nektos/act/cmd"
|
||||
)
|
||||
|
||||
//go:embed VERSION
|
||||
var version string
|
||||
var version = "v0.2.27-dev" // Manually bump after tagging next release
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
@@ -9,12 +9,12 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
@@ -46,34 +46,28 @@ type ResponseMessage struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type WritableFile interface {
|
||||
io.WriteCloser
|
||||
type MkdirFS interface {
|
||||
fs.FS
|
||||
MkdirAll(path string, perm fs.FileMode) error
|
||||
Open(name string) (fs.File, error)
|
||||
OpenAtEnd(name string) (fs.File, error)
|
||||
}
|
||||
|
||||
type WriteFS interface {
|
||||
OpenWritable(name string) (WritableFile, error)
|
||||
OpenAppendable(name string) (WritableFile, error)
|
||||
type MkdirFsImpl struct {
|
||||
dir string
|
||||
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) {
|
||||
return os.Open(name)
|
||||
func (fsys MkdirFsImpl) Open(name string) (fs.File, error) {
|
||||
return os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
||||
}
|
||||
|
||||
func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
|
||||
}
|
||||
|
||||
func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
func (fsys MkdirFsImpl) OpenAtEnd(name string) (fs.File, error) {
|
||||
file, err := os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR, 0644)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -83,16 +77,13 @@ func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
var gzipExtension = ".gz__"
|
||||
|
||||
func safeResolve(baseDir string, relPath string) string {
|
||||
return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
|
||||
}
|
||||
|
||||
func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
|
||||
func uploads(router *httprouter.Router, fsys MkdirFS) {
|
||||
router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
runID := params.ByName("runId")
|
||||
|
||||
@@ -117,15 +108,19 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
|
||||
itemPath += gzipExtension
|
||||
}
|
||||
|
||||
safeRunPath := safeResolve(baseDir, runID)
|
||||
safePath := safeResolve(safeRunPath, itemPath)
|
||||
filePath := fmt.Sprintf("%s/%s", runID, 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")
|
||||
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 {
|
||||
@@ -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) {
|
||||
runID := params.ByName("runId")
|
||||
|
||||
safePath := safeResolve(baseDir, runID)
|
||||
|
||||
entries, err := fs.ReadDir(fsys, safePath)
|
||||
entries, err := fs.ReadDir(fsys, runID)
|
||||
if err != nil {
|
||||
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) {
|
||||
container := params.ByName("container")
|
||||
itemPath := req.URL.Query().Get("itemPath")
|
||||
safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
|
||||
dirPath := fmt.Sprintf("%s/%s", container, itemPath)
|
||||
|
||||
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() {
|
||||
rel, err := filepath.Rel(safePath, path)
|
||||
rel, err := filepath.Rel(dirPath, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -225,7 +218,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
|
||||
rel = strings.TrimSuffix(rel, gzipExtension)
|
||||
|
||||
files = append(files, ContainerItem{
|
||||
Path: filepath.Join(itemPath, rel),
|
||||
Path: fmt.Sprintf("%s/%s", itemPath, rel),
|
||||
ItemType: "file",
|
||||
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) {
|
||||
path := params.ByName("path")[1:]
|
||||
|
||||
safePath := safeResolve(baseDir, path)
|
||||
|
||||
file, err := fsys.Open(safePath)
|
||||
file, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
// try gzip file
|
||||
file, err = fsys.Open(safePath + gzipExtension)
|
||||
file, err = fsys.Open(path + gzipExtension)
|
||||
if err != nil {
|
||||
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)
|
||||
logger := common.Logger(serverContext)
|
||||
|
||||
@@ -282,19 +273,20 @@ func Serve(ctx context.Context, artifactPath string, addr string, port string) c
|
||||
router := httprouter.New()
|
||||
|
||||
logger.Debugf("Artifacts base path '%s'", artifactPath)
|
||||
fsys := readWriteFSImpl{}
|
||||
uploads(router, artifactPath, fsys)
|
||||
downloads(router, artifactPath, fsys)
|
||||
fs := os.DirFS(artifactPath)
|
||||
uploads(router, MkdirFsImpl{artifactPath, fs})
|
||||
downloads(router, fs)
|
||||
ip := common.GetOutboundIP().String()
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", addr, port),
|
||||
Addr: fmt.Sprintf("%s:%s", ip, port),
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
// run server
|
||||
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 {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -20,43 +21,44 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type writableMapFile 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 {
|
||||
type MapFsImpl struct {
|
||||
fstest.MapFS
|
||||
}
|
||||
|
||||
func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
|
||||
var file = &writableMapFile{
|
||||
MapFile: fstest.MapFile{
|
||||
func (fsys MapFsImpl) MkdirAll(path string, perm fs.FileMode) error {
|
||||
// mocked no-op
|
||||
return nil
|
||||
}
|
||||
|
||||
type WritableFile struct {
|
||||
fs.File
|
||||
fsys fstest.MapFS
|
||||
path string
|
||||
}
|
||||
|
||||
func (file WritableFile) Write(data []byte) (int, error) {
|
||||
file.fsys[file.path].Data = data
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (fsys MapFsImpl) Open(path string) (fs.File, error) {
|
||||
var file = fstest.MapFile{
|
||||
Data: []byte("content2"),
|
||||
},
|
||||
}
|
||||
fsys.MapFS[name] = &file.MapFile
|
||||
fsys.MapFS[path] = &file
|
||||
|
||||
return file, nil
|
||||
result, err := fsys.MapFS.Open(path)
|
||||
return WritableFile{result, fsys.MapFS, path}, err
|
||||
}
|
||||
|
||||
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
|
||||
var file = &writableMapFile{
|
||||
MapFile: fstest.MapFile{
|
||||
func (fsys MapFsImpl) OpenAtEnd(path string) (fs.File, error) {
|
||||
var file = fstest.MapFile{
|
||||
Data: []byte("content2"),
|
||||
},
|
||||
}
|
||||
fsys.MapFS[name] = &file.MapFile
|
||||
fsys.MapFS[path] = &file
|
||||
|
||||
return file, nil
|
||||
result, err := fsys.MapFS.Open(path)
|
||||
return WritableFile{result, fsys.MapFS, path}, err
|
||||
}
|
||||
|
||||
func TestNewArtifactUploadPrepare(t *testing.T) {
|
||||
@@ -65,7 +67,7 @@ func TestNewArtifactUploadPrepare(t *testing.T) {
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
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)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -91,7 +93,7 @@ func TestArtifactUploadBlob(t *testing.T) {
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
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"))
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -109,7 +111,7 @@ func TestArtifactUploadBlob(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -118,7 +120,7 @@ func TestFinalizeArtifactUpload(t *testing.T) {
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{})
|
||||
|
||||
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)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -142,13 +144,13 @@ func TestListArtifacts(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/file.txt": {
|
||||
"1/file.txt": {
|
||||
Data: []byte(""),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
downloads(router, memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -174,13 +176,13 @@ func TestListArtifactContainer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/some/file": {
|
||||
"1/some/file": {
|
||||
Data: []byte(""),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
downloads(router, memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -198,7 +200,7 @@ func TestListArtifactContainer(t *testing.T) {
|
||||
}
|
||||
|
||||
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("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
|
||||
}
|
||||
@@ -207,13 +209,13 @@ func TestDownloadArtifactFile(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var memfs = fstest.MapFS(map[string]*fstest.MapFile{
|
||||
"artifact/server/path/1/some/file": {
|
||||
"1/some/file": {
|
||||
Data: []byte("content"),
|
||||
},
|
||||
})
|
||||
|
||||
router := httprouter.New()
|
||||
downloads(router, "artifact/server/path", memfs)
|
||||
downloads(router, memfs)
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -238,8 +240,7 @@ type TestJobFileInfo struct {
|
||||
containerArchitecture string
|
||||
}
|
||||
|
||||
var artifactsPath = path.Join(os.TempDir(), "test-artifacts")
|
||||
var artifactsAddr = "127.0.0.1"
|
||||
var aritfactsPath = path.Join(os.TempDir(), "test-artifacts")
|
||||
var artifactsPort = "12345"
|
||||
|
||||
func TestArtifactFlow(t *testing.T) {
|
||||
@@ -249,7 +250,7 @@ func TestArtifactFlow(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cancel := Serve(ctx, artifactsPath, artifactsAddr, artifactsPort)
|
||||
cancel := Serve(ctx, aritfactsPath, artifactsPort)
|
||||
defer cancel()
|
||||
|
||||
platforms := map[string]string{
|
||||
@@ -258,7 +259,6 @@ func TestArtifactFlow(t *testing.T) {
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
{"testdata", "upload-and-download", "push", "", platforms, ""},
|
||||
{"testdata", "GHSL-2023-004", "push", "", platforms, ""},
|
||||
}
|
||||
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) {
|
||||
fmt.Printf("::group::%s\n", tjfi.workflowPath)
|
||||
|
||||
if err := os.RemoveAll(artifactsPath); err != nil {
|
||||
if err := os.RemoveAll(aritfactsPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -286,8 +286,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||
ReuseContainers: false,
|
||||
ContainerArchitecture: tjfi.containerArchitecture,
|
||||
GitHubInstance: "github.com",
|
||||
ArtifactServerPath: artifactsPath,
|
||||
ArtifactServerAddr: artifactsAddr,
|
||||
ArtifactServerPath: aritfactsPath,
|
||||
ArtifactServerPort: artifactsPort,
|
||||
}
|
||||
|
||||
@@ -297,96 +296,15 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
|
||||
plan, err := planner.PlanEvent(tjfi.eventName)
|
||||
if err == nil {
|
||||
plan := planner.PlanEvent(tjfi.eventName)
|
||||
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if tjfi.errorMessage == "" {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
} else {
|
||||
assert.Error(t, err, tjfi.errorMessage)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, plan)
|
||||
}
|
||||
|
||||
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"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"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-ini/ini"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,40 +55,41 @@ func (e *Error) Commit() string {
|
||||
// FindGitRevision get the current git revision
|
||||
func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) {
|
||||
logger := common.Logger(ctx)
|
||||
|
||||
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)
|
||||
gitDir, err := findGitDirectory(file)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if head.Hash().IsZero() {
|
||||
return "", "", fmt.Errorf("HEAD sha1 could not be resolved")
|
||||
bts, err := os.ReadFile(filepath.Join(gitDir, "HEAD"))
|
||||
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)
|
||||
return hash[:7], strings.TrimSpace(hash), nil
|
||||
logger.Debugf("Found revision: %s", refBuf)
|
||||
return string(refBuf[:7]), strings.TrimSpace(string(refBuf)), nil
|
||||
}
|
||||
|
||||
// FindGitRef get the current git ref
|
||||
func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -98,36 +100,16 @@ 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.
|
||||
var refTag = ""
|
||||
var refBranch = ""
|
||||
repo, err := git.PlainOpenWithOptions(
|
||||
file,
|
||||
&git.PlainOpenOptions{
|
||||
DetectDotGit: true,
|
||||
EnableDotGitCommonDir: true,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
r, err := git.PlainOpen(filepath.Join(gitDir, ".."))
|
||||
if err == nil {
|
||||
iter, err := r.References()
|
||||
if err == nil {
|
||||
for {
|
||||
r, err := iter.Next()
|
||||
if r == nil || err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
iter, err := repo.References()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// find the reference that matches the revision's has
|
||||
err = iter.ForEach(func(r *plumbing.Reference) error {
|
||||
/* tags and branches will have the same hash
|
||||
* when a user checks out a tag, it is not mentioned explicitly
|
||||
* in the go-git package, we must identify the revision
|
||||
* then check if any tag matches that revision,
|
||||
* if so then we checked out a tag
|
||||
* else we look for branches and if matches,
|
||||
* it means we checked out a branch
|
||||
*
|
||||
* If a branches matches first we must continue and check all tags (all references)
|
||||
* in case we match with a tag later in the interation
|
||||
*/
|
||||
// logger.Debugf("Reference: name=%s sha=%s", r.Name().String(), r.Hash().String())
|
||||
if r.Hash().String() == ref {
|
||||
if r.Name().IsTag() {
|
||||
refTag = r.Name().String()
|
||||
@@ -136,20 +118,10 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||
refBranch = r.Name().String()
|
||||
}
|
||||
}
|
||||
|
||||
// we found what we where looking for
|
||||
if refTag != "" && refBranch != "" {
|
||||
return storer.ErrStop
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
iter.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// order matters here see above comment.
|
||||
if refTag != "" {
|
||||
return refTag, nil
|
||||
}
|
||||
@@ -157,7 +129,39 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||
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
|
||||
@@ -175,27 +179,26 @@ func FindGithubRepo(ctx context.Context, file, githubInstance, remoteName string
|
||||
}
|
||||
|
||||
func findGitRemoteURL(ctx context.Context, file, remoteName string) (string, error) {
|
||||
repo, err := git.PlainOpenWithOptions(
|
||||
file,
|
||||
&git.PlainOpenOptions{
|
||||
DetectDotGit: true,
|
||||
EnableDotGitCommonDir: true,
|
||||
},
|
||||
)
|
||||
gitDir, err := findGitDirectory(file)
|
||||
if err != nil {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(remote.Config().URLs) < 1 {
|
||||
return "", fmt.Errorf("remote '%s' exists but has no URL", remoteName)
|
||||
remote, err := gitconfig.GetSection(fmt.Sprintf(`remote "%s"`, remoteName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return remote.Config().URLs[0], nil
|
||||
urlKey, err := remote.GetKey("url")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
url := urlKey.String()
|
||||
return url, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
type NewGitCloneExecutorInput struct {
|
||||
URL string
|
||||
@@ -260,7 +292,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
||||
if err = os.Chmod(input.Dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@@ -82,19 +82,12 @@ func TestFindGitRemoteURL(t *testing.T) {
|
||||
assert.NoError(err)
|
||||
|
||||
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)
|
||||
|
||||
u, err := findGitRemoteURL(context.Background(), basedir, "origin")
|
||||
assert.NoError(err)
|
||||
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) {
|
||||
@@ -167,7 +160,7 @@ func TestGitFindRef(t *testing.T) {
|
||||
name := name
|
||||
t.Run(name, func(t *testing.T) {
|
||||
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, cleanGitHooks(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 {
|
||||
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
|
||||
|
||||
import (
|
||||
@@ -38,24 +36,3 @@ func LoadDockerAuthConfig(ctx context.Context, image string) (types.AuthConfig,
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
@@ -10,14 +8,22 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
|
||||
// github.com/docker/docker/builder/dockerignore is deprecated
|
||||
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||
"github.com/moby/patternmatcher"
|
||||
|
||||
"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
|
||||
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
@@ -44,14 +50,12 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
Tags: tags,
|
||||
Remove: true,
|
||||
Platform: input.Platform,
|
||||
AuthConfigs: LoadDockerAuthConfigs(ctx),
|
||||
Dockerfile: input.Dockerfile,
|
||||
}
|
||||
var buildContext io.ReadCloser
|
||||
if input.Container != nil {
|
||||
buildContext, err = input.Container.GetContainerArchive(ctx, input.ContextDir+"/.")
|
||||
} else {
|
||||
buildContext, err = createBuildContext(ctx, input.ContextDir, input.Dockerfile)
|
||||
buildContext, err = createBuildContext(ctx, input.ContextDir, "Dockerfile")
|
||||
}
|
||||
if err != nil {
|
||||
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
|
||||
// caught by validateContextDirectory above.
|
||||
var includes = []string{"."}
|
||||
keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
|
||||
keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
|
||||
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
|
||||
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
|
||||
if keepThem1 || keepThem2 {
|
||||
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
|
||||
// appended with license information.
|
||||
//
|
||||
|
@@ -663,8 +663,8 @@ func TestRunFlagsParseShmSize(t *testing.T) {
|
||||
|
||||
func TestParseRestartPolicy(t *testing.T) {
|
||||
invalids := map[string]string{
|
||||
"always:2:3": "invalid restart policy format: maximum retry count must be an integer",
|
||||
"on-failure:invalid": "invalid restart policy format: maximum retry count must be an integer",
|
||||
"always:2:3": "invalid restart policy format",
|
||||
"on-failure:invalid": "maximum retry count must be an integer",
|
||||
}
|
||||
valids := map[string]container.RestartPolicy{
|
||||
"": {},
|
||||
|
@@ -1,5 +1,3 @@
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
@@ -7,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
@@ -19,16 +17,34 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
||||
if client.IsErrNotFound(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("reference", imageName)
|
||||
|
||||
imageListOptions := types.ImageListOptions{
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
images, err := cli.ImageList(ctx, imageListOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if platform == "" || platform == "any" || fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
|
||||
if len(images) > 0 {
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
// container image architectures
|
||||
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)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
||||
if client.IsErrNotFound(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("reference", imageName)
|
||||
|
||||
imageListOptions := types.ImageListOptions{
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
images, err := cli.ImageList(ctx, imageListOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err = cli.ImageRemove(ctx, inspectImage.ID, types.ImageRemoveOptions{
|
||||
if len(images) > 0 {
|
||||
for _, v := range images {
|
||||
if _, err = cli.ImageRemove(ctx, v.ID, types.ImageRemoveOptions{
|
||||
Force: force,
|
||||
PruneChildren: pruneChildren,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
|
@@ -1,5 +1,3 @@
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
@@ -14,6 +12,15 @@ import (
|
||||
"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
|
||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
|
@@ -1,9 +1,8 @@
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
@@ -39,6 +38,53 @@ import (
|
||||
"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
|
||||
func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return common.NewPipelineExecutor(
|
||||
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)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
return config, hostConfig, nil
|
||||
@@ -434,6 +478,7 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
|
||||
NetworkMode: container.NetworkMode(input.NetworkMode),
|
||||
Privileged: input.Privileged,
|
||||
UsernsMode: container.UsernsMode(input.UsernsMode),
|
||||
AutoRemove: input.AutoRemove,
|
||||
}
|
||||
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 {
|
||||
envMap := *env
|
||||
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 {
|
||||
return func(ctx context.Context) error {
|
||||
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`)
|
||||
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 {
|
||||
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
|
||||
|
||||
import (
|
||||
|
@@ -65,7 +65,7 @@ type copyCollector struct {
|
||||
|
||||
func (cc *copyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
|
||||
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
|
||||
}
|
||||
if f == nil {
|
||||
|
@@ -76,7 +76,7 @@ func (mfs *memoryFs) Readlink(path string) (string, error) {
|
||||
|
||||
func TestIgnoredTrackedfile(t *testing.T) {
|
||||
fs := memfs.New()
|
||||
_ = fs.MkdirAll("mygitrepo/.git", 0o777)
|
||||
_ = fs.MkdirAll("mygitrepo/.git", 0777)
|
||||
dotgit, _ := fs.Chroot("mygitrepo/.git")
|
||||
worktree, _ := fs.Chroot("mygitrepo")
|
||||
repo, _ := git.Init(filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault()), worktree)
|
||||
|
@@ -2,9 +2,9 @@ package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -15,13 +15,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"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/lookpath"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type HostEnvironment struct {
|
||||
@@ -49,7 +50,7 @@ func (e *HostEnvironment) Close() common.Executor {
|
||||
func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
@@ -383,32 +454,10 @@ func (*HostEnvironment) JoinPathVariable(paths ...string) string {
|
||||
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{} {
|
||||
return map[string]interface{}{
|
||||
"os": goOsToActionOs(runtime.GOOS),
|
||||
"arch": goArchToActionArch(runtime.GOARCH),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"temp": e.TmpDir,
|
||||
"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"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/rhysd/actionlint"
|
||||
)
|
||||
@@ -203,9 +202,6 @@ func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
|
||||
|
||||
var files []string
|
||||
if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
|
||||
parts := strings.Split(sansPrefix, string(filepath.Separator))
|
||||
if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {
|
||||
|
@@ -15,21 +15,15 @@ type EvaluationEnvironment struct {
|
||||
Github *model.GithubContext
|
||||
Env map[string]string
|
||||
Job *model.JobContext
|
||||
Jobs *map[string]*model.WorkflowCallResult
|
||||
Steps map[string]*model.StepResult
|
||||
Runner map[string]interface{}
|
||||
Secrets map[string]string
|
||||
Strategy map[string]interface{}
|
||||
Matrix map[string]interface{}
|
||||
Needs map[string]Needs
|
||||
Needs map[string]map[string]map[string]string
|
||||
Inputs map[string]interface{}
|
||||
}
|
||||
|
||||
type Needs struct {
|
||||
Outputs map[string]string `json:"outputs"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Run *model.Run
|
||||
WorkingDir string
|
||||
@@ -156,11 +150,6 @@ func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableN
|
||||
return impl.env.Env, nil
|
||||
case "job":
|
||||
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":
|
||||
return impl.env.Steps, nil
|
||||
case "runner":
|
||||
@@ -372,16 +361,8 @@ func (impl *interperterImpl) compareValues(leftValue reflect.Value, rightValue r
|
||||
|
||||
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:
|
||||
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", ""},
|
||||
{`fromJSON('{}') && true`, true, "and-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{
|
||||
@@ -560,7 +555,6 @@ func TestContexts(t *testing.T) {
|
||||
{"strategy.fail-fast", true, "strategy-context"},
|
||||
{"matrix.os", "Linux", "matrix-context"},
|
||||
{"needs.job-id.outputs.output-name", "value", "needs-context"},
|
||||
{"needs.job-id.result", "success", "needs-context"},
|
||||
{"inputs.name", "value", "inputs-context"},
|
||||
}
|
||||
|
||||
@@ -599,12 +593,11 @@ func TestContexts(t *testing.T) {
|
||||
Matrix: map[string]interface{}{
|
||||
"os": "Linux",
|
||||
},
|
||||
Needs: map[string]Needs{
|
||||
Needs: map[string]map[string]map[string]string{
|
||||
"job-id": {
|
||||
Outputs: map[string]string{
|
||||
"outputs": {
|
||||
"output-name": "value",
|
||||
},
|
||||
Result: "success",
|
||||
},
|
||||
},
|
||||
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"
|
||||
// ActionRunsUsingComposite for running composite
|
||||
ActionRunsUsingComposite = "composite"
|
||||
// ActionRunsUsingGo for running with go
|
||||
ActionRunsUsingGo = "go"
|
||||
)
|
||||
|
||||
// ActionRuns are a field in Action
|
||||
|
@@ -3,7 +3,6 @@ package model
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"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 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)
|
||||
|
||||
// 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.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":
|
||||
ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"])
|
||||
case "deployment", "deployment_status":
|
||||
ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref"))
|
||||
ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
|
||||
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":
|
||||
ghc.Ref = asString(ghc.Event["ref"])
|
||||
if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
|
||||
ghc.Sha = asString(ghc.Event["after"])
|
||||
}
|
||||
default:
|
||||
defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))
|
||||
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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
_, 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"
|
||||
)
|
||||
|
||||
func TestSetRef(t *testing.T) {
|
||||
func TestSetRefAndSha(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
oldFindGitRef := findGitRef
|
||||
@@ -29,13 +29,19 @@ func TestSetRef(t *testing.T) {
|
||||
eventName string
|
||||
event map[string]interface{}
|
||||
ref string
|
||||
refName string
|
||||
sha string
|
||||
}{
|
||||
{
|
||||
eventName: "pull_request_target",
|
||||
event: map[string]interface{}{},
|
||||
event: map[string]interface{}{
|
||||
"pull_request": map[string]interface{}{
|
||||
"base": map[string]interface{}{
|
||||
"sha": "pr-base-sha",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "refs/heads/master",
|
||||
refName: "master",
|
||||
sha: "pr-base-sha",
|
||||
},
|
||||
{
|
||||
eventName: "pull_request",
|
||||
@@ -43,17 +49,18 @@ func TestSetRef(t *testing.T) {
|
||||
"number": 1234.,
|
||||
},
|
||||
ref: "refs/pull/1234/merge",
|
||||
refName: "1234/merge",
|
||||
sha: "1234fakesha",
|
||||
},
|
||||
{
|
||||
eventName: "deployment",
|
||||
event: map[string]interface{}{
|
||||
"deployment": map[string]interface{}{
|
||||
"ref": "refs/heads/somebranch",
|
||||
"sha": "deployment-sha",
|
||||
},
|
||||
},
|
||||
ref: "refs/heads/somebranch",
|
||||
refName: "somebranch",
|
||||
sha: "deployment-sha",
|
||||
},
|
||||
{
|
||||
eventName: "release",
|
||||
@@ -62,16 +69,18 @@ func TestSetRef(t *testing.T) {
|
||||
"tag_name": "v1.0.0",
|
||||
},
|
||||
},
|
||||
ref: "refs/tags/v1.0.0",
|
||||
refName: "v1.0.0",
|
||||
ref: "v1.0.0",
|
||||
sha: "1234fakesha",
|
||||
},
|
||||
{
|
||||
eventName: "push",
|
||||
event: map[string]interface{}{
|
||||
"ref": "refs/heads/somebranch",
|
||||
"after": "push-sha",
|
||||
"deleted": false,
|
||||
},
|
||||
ref: "refs/heads/somebranch",
|
||||
refName: "somebranch",
|
||||
sha: "push-sha",
|
||||
},
|
||||
{
|
||||
eventName: "unknown",
|
||||
@@ -81,13 +90,13 @@ func TestSetRef(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ref: "refs/heads/main",
|
||||
refName: "main",
|
||||
sha: "1234fakesha",
|
||||
},
|
||||
{
|
||||
eventName: "no-event",
|
||||
event: map[string]interface{}{},
|
||||
ref: "refs/heads/master",
|
||||
refName: "master",
|
||||
sha: "1234fakesha",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -99,11 +108,10 @@ func TestSetRef(t *testing.T) {
|
||||
Event: table.event,
|
||||
}
|
||||
|
||||
ghc.SetRef(context.Background(), "main", "/some/dir")
|
||||
ghc.SetRefTypeAndName()
|
||||
ghc.SetRefAndSha(context.Background(), "main", "/some/dir")
|
||||
|
||||
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{}{},
|
||||
}
|
||||
|
||||
ghc.SetRef(context.Background(), "", "/some/dir")
|
||||
ghc.SetRefAndSha(context.Background(), "", "/some/dir")
|
||||
|
||||
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
|
||||
type WorkflowPlanner interface {
|
||||
PlanEvent(eventName string) (*Plan, error)
|
||||
PlanJob(jobName string) (*Plan, error)
|
||||
PlanAll() (*Plan, error)
|
||||
PlanEvent(eventName string) *Plan
|
||||
PlanJob(jobName string) *Plan
|
||||
PlanAll() *Plan
|
||||
GetEvents() []string
|
||||
}
|
||||
|
||||
@@ -164,81 +164,59 @@ func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, e
|
||||
return wp, nil
|
||||
}
|
||||
|
||||
// CombineWorkflowPlanner combines workflows to a WorkflowPlanner
|
||||
func CombineWorkflowPlanner(workflows ...*Workflow) WorkflowPlanner {
|
||||
return &workflowPlanner{
|
||||
workflows: workflows,
|
||||
}
|
||||
}
|
||||
|
||||
type workflowPlanner struct {
|
||||
workflows []*Workflow
|
||||
}
|
||||
|
||||
// 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)
|
||||
if len(wp.workflows) == 0 {
|
||||
log.Debug("no workflows found by planner")
|
||||
return plan, nil
|
||||
log.Debugf("no events found for workflow: %s", eventName)
|
||||
}
|
||||
var lastErr error
|
||||
|
||||
for _, w := range wp.workflows {
|
||||
events := w.On()
|
||||
if len(events) == 0 {
|
||||
log.Debugf("no events found for workflow: %s", w.File)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
for _, e := range w.On() {
|
||||
if e == eventName {
|
||||
stages, err := createStages(w, w.GetJobIDs()...)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
lastErr = err
|
||||
} else {
|
||||
plan.mergeStages(stages)
|
||||
plan.mergeStages(createStages(w, w.GetJobIDs()...))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return plan, lastErr
|
||||
return plan
|
||||
}
|
||||
|
||||
// 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)
|
||||
if len(wp.workflows) == 0 {
|
||||
log.Debugf("no jobs found for workflow: %s", jobName)
|
||||
}
|
||||
var lastErr error
|
||||
|
||||
for _, w := range wp.workflows {
|
||||
stages, err := createStages(w, jobName)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
lastErr = err
|
||||
} else {
|
||||
plan.mergeStages(stages)
|
||||
plan.mergeStages(createStages(w, jobName))
|
||||
}
|
||||
}
|
||||
return plan, lastErr
|
||||
return plan
|
||||
}
|
||||
|
||||
// PlanAll builds a new run to execute in parallel all
|
||||
func (wp *workflowPlanner) PlanAll() (*Plan, error) {
|
||||
func (wp *workflowPlanner) PlanAll() *Plan {
|
||||
plan := new(Plan)
|
||||
if len(wp.workflows) == 0 {
|
||||
log.Debug("no workflows found by planner")
|
||||
return plan, nil
|
||||
log.Debugf("no jobs found for loaded workflows")
|
||||
}
|
||||
var lastErr error
|
||||
|
||||
for _, w := range wp.workflows {
|
||||
stages, err := createStages(w, w.GetJobIDs()...)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
lastErr = err
|
||||
} else {
|
||||
plan.mergeStages(stages)
|
||||
}
|
||||
plan.mergeStages(createStages(w, w.GetJobIDs()...))
|
||||
}
|
||||
|
||||
return plan, lastErr
|
||||
return plan
|
||||
}
|
||||
|
||||
// GetEvents gets all the events in the workflows file
|
||||
@@ -311,7 +289,7 @@ func (p *Plan) mergeStages(stages []*Stage) {
|
||||
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
|
||||
jobDependencies := make(map[string][]string)
|
||||
for len(jobIDs) > 0 {
|
||||
@@ -328,8 +306,6 @@ func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
|
||||
jobIDs = newJobIDs
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// next, build an execution graph
|
||||
stages := make([]*Stage, 0)
|
||||
for len(jobDependencies) > 0 {
|
||||
@@ -345,16 +321,12 @@ func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if len(stages) == 0 && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stages, nil
|
||||
return 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"`
|
||||
Conclusion stepStatus `json:"conclusion"`
|
||||
Outcome stepStatus `json:"outcome"`
|
||||
State map[string]string
|
||||
}
|
||||
|
@@ -67,6 +67,30 @@ func (w *Workflow) OnEvent(event string) interface{} {
|
||||
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 {
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
@@ -100,48 +124,6 @@ func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
|
||||
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
|
||||
type Job struct {
|
||||
Name string `yaml:"name"`
|
||||
@@ -157,8 +139,6 @@ type Job struct {
|
||||
Defaults Defaults `yaml:"defaults"`
|
||||
Outputs map[string]string `yaml:"outputs"`
|
||||
Uses string `yaml:"uses"`
|
||||
With map[string]interface{} `yaml:"with"`
|
||||
RawSecrets yaml.Node `yaml:"secrets"`
|
||||
Result string
|
||||
}
|
||||
|
||||
@@ -213,34 +193,6 @@ func (s Strategy) GetFailFast() bool {
|
||||
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
|
||||
func (j *Job) Container() *ContainerSpec {
|
||||
var val *ContainerSpec
|
||||
@@ -504,6 +456,7 @@ type ContainerSpec struct {
|
||||
|
||||
// Step is the structure of one step in a job
|
||||
type Step struct {
|
||||
Number int `yaml:"-"`
|
||||
ID string `yaml:"id"`
|
||||
If yaml.Node `yaml:"if"`
|
||||
Name string `yaml:"name"`
|
||||
@@ -530,8 +483,16 @@ func (s *Step) String() string {
|
||||
}
|
||||
|
||||
// Environments returns string-based key=value map for a step
|
||||
// Note: all keys are uppercase
|
||||
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
|
||||
|
@@ -7,6 +7,88 @@ import (
|
||||
"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) {
|
||||
yaml := `
|
||||
name: local-action-docker-url
|
||||
@@ -241,8 +323,7 @@ func TestReadWorkflow_Strategy(t *testing.T) {
|
||||
w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p, err := w.PlanJob("strategy-only-max-parallel")
|
||||
assert.NoError(t, err)
|
||||
p := w.PlanJob("strategy-only-max-parallel")
|
||||
|
||||
assert.Equal(t, len(p.Stages), 1)
|
||||
assert.Equal(t, len(p.Stages[0].Runs), 1)
|
||||
|
@@ -14,7 +14,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"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 actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||
|
||||
type fileWriter func(filename string, data []byte, perm fs.FileMode) error
|
||||
type (
|
||||
actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||
fileWriter func(filename string, data []byte, perm fs.FileMode) error
|
||||
)
|
||||
|
||||
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)}
|
||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||
|
||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||
|
||||
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||
case model.ActionRunsUsingDocker:
|
||||
location := actionLocation
|
||||
@@ -171,6 +169,13 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
||||
}
|
||||
|
||||
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:
|
||||
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
||||
model.ActionRunsUsingDocker,
|
||||
@@ -223,17 +228,14 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
|
||||
|
||||
var prepImage common.Executor
|
||||
var image string
|
||||
forcePull := false
|
||||
if strings.HasPrefix(action.Runs.Image, "docker://") {
|
||||
image = strings.TrimPrefix(action.Runs.Image, "docker://")
|
||||
// Apply forcePull only for prebuild docker images
|
||||
forcePull = rc.Config.ForcePull
|
||||
} else {
|
||||
// "-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("act-%s", strings.TrimLeft(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")
|
||||
if err != nil {
|
||||
@@ -263,7 +265,6 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
|
||||
}
|
||||
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||
ContextDir: contextDir,
|
||||
Dockerfile: fileName,
|
||||
ImageTag: image,
|
||||
Container: actionContainer,
|
||||
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)
|
||||
return common.NewPipelineExecutor(
|
||||
prepImage,
|
||||
stepContainer.Pull(forcePull),
|
||||
stepContainer.Pull(rc.Config.ForcePull),
|
||||
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
|
||||
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
|
||||
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"))
|
||||
|
||||
binds, mounts := rc.GetBindsAndMounts()
|
||||
networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
|
||||
if rc.IsHostEnv(ctx) {
|
||||
networkMode = "default"
|
||||
}
|
||||
|
||||
stepContainer := container.NewContainer(&container.NewContainerInput{
|
||||
Cmd: cmd,
|
||||
Entrypoint: entrypoint,
|
||||
@@ -367,25 +365,25 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
|
||||
Image: image,
|
||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||
Name: createContainerName(rc.jobContainerName(), stepModel.ID),
|
||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
NetworkMode: networkMode,
|
||||
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
|
||||
Binds: binds,
|
||||
Stdout: logWriter,
|
||||
Stderr: logWriter,
|
||||
Privileged: rc.Config.Privileged,
|
||||
UsernsMode: rc.Config.UsernsMode,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
Options: rc.Config.ContainerOptions,
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
})
|
||||
return stepContainer
|
||||
}
|
||||
|
||||
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
||||
state, ok := rc.IntraActionState[step.getStepModel().ID]
|
||||
if ok {
|
||||
for name, value := range state {
|
||||
stepResult := rc.StepResults[step.getStepModel().ID]
|
||||
if stepResult != nil {
|
||||
for name, value := range stepResult.State {
|
||||
envName := fmt.Sprintf("STATE_%s", name)
|
||||
(*env)[envName] = value
|
||||
}
|
||||
@@ -475,7 +473,7 @@ func runPreStep(step actionStep) common.Executor {
|
||||
var actionPath string
|
||||
if _, ok := step.(*stepActionRemote); ok {
|
||||
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 {
|
||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
||||
actionPath = ""
|
||||
@@ -497,8 +495,6 @@ func runPreStep(step actionStep) common.Executor {
|
||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
|
||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||
|
||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||
|
||||
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||
|
||||
case model.ActionRunsUsingComposite:
|
||||
@@ -506,10 +502,7 @@ func runPreStep(step actionStep) common.Executor {
|
||||
step.getCompositeRunContext(ctx)
|
||||
}
|
||||
|
||||
if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
|
||||
return steps.pre(ctx)
|
||||
}
|
||||
return fmt.Errorf("missing steps in composite action")
|
||||
return step.getCompositeSteps().pre(ctx)
|
||||
|
||||
default:
|
||||
return nil
|
||||
@@ -566,7 +559,7 @@ func runPostStep(step actionStep) common.Executor {
|
||||
var actionPath string
|
||||
if _, ok := step.(*stepActionRemote); ok {
|
||||
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 {
|
||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
||||
actionPath = ""
|
||||
@@ -589,8 +582,6 @@ func runPostStep(step actionStep) common.Executor {
|
||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
|
||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||
|
||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||
|
||||
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||
|
||||
case model.ActionRunsUsingComposite:
|
||||
@@ -598,10 +589,7 @@ func runPostStep(step actionStep) common.Executor {
|
||||
return err
|
||||
}
|
||||
|
||||
if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
|
||||
return steps.post(ctx)
|
||||
}
|
||||
return fmt.Errorf("missing steps in composite action")
|
||||
return step.getCompositeSteps().post(ctx)
|
||||
|
||||
default:
|
||||
return nil
|
||||
|
@@ -66,7 +66,6 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
|
||||
JobContainer: parent.JobContainer,
|
||||
ActionPath: actionPath,
|
||||
Env: env,
|
||||
GlobalEnv: parent.GlobalEnv,
|
||||
Masks: parent.Masks,
|
||||
ExtraPath: parent.ExtraPath,
|
||||
Parent: parent,
|
||||
@@ -86,10 +85,6 @@ func execAsComposite(step actionStep) common.Executor {
|
||||
|
||||
steps := step.getCompositeSteps()
|
||||
|
||||
if steps == nil || steps.main == nil {
|
||||
return fmt.Errorf("missing steps in composite action")
|
||||
}
|
||||
|
||||
ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
|
||||
|
||||
err := steps.main(ctx)
|
||||
@@ -104,14 +99,6 @@ func execAsComposite(step actionStep) common.Executor {
|
||||
|
||||
rc.Masks = append(rc.Masks, compositeRC.Masks...)
|
||||
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
|
||||
}
|
||||
@@ -135,6 +122,7 @@ func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
|
||||
if step.ID == "" {
|
||||
step.ID = fmt.Sprintf("%d", i)
|
||||
}
|
||||
step.Number = i
|
||||
|
||||
// create a copy of the step, since this composite action could
|
||||
// run multiple times and we might modify the instance
|
||||
|
@@ -201,14 +201,13 @@ func TestActionRunner(t *testing.T) {
|
||||
},
|
||||
CurrentStep: "post-step",
|
||||
StepResults: map[string]*model.StepResult{
|
||||
"step": {},
|
||||
},
|
||||
IntraActionState: map[string]map[string]string{
|
||||
"step": {
|
||||
State: map[string]string{
|
||||
"name": "state value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
action: &model.Action{
|
||||
Runs: model.ActionRuns{
|
||||
Using: "node16",
|
||||
|
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]+$")
|
||||
}
|
||||
|
||||
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 {
|
||||
logger := common.Logger(ctx)
|
||||
resumeCommand := ""
|
||||
return func(line string) bool {
|
||||
command, kvPairs, arg, ok := tryParseRawActionCommand(line)
|
||||
if !ok {
|
||||
var command string
|
||||
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
|
||||
}
|
||||
|
||||
@@ -71,8 +66,6 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||
case "save-state":
|
||||
logger.Infof(" \U0001f4be %s", line)
|
||||
rc.saveState(ctx, kvPairs, arg)
|
||||
case "add-matcher":
|
||||
logger.Infof(" \U00002753 add-matcher %s", arg)
|
||||
default:
|
||||
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) {
|
||||
name := kvPairs["name"]
|
||||
common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", name, arg)
|
||||
common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", kvPairs["name"], arg)
|
||||
if rc.Env == nil {
|
||||
rc.Env = make(map[string]string)
|
||||
}
|
||||
rc.Env[name] = arg
|
||||
// for composite action GITHUB_ENV and set-env passing
|
||||
if rc.GlobalEnv == nil {
|
||||
rc.GlobalEnv = map[string]string{}
|
||||
}
|
||||
rc.GlobalEnv[name] = arg
|
||||
rc.Env[kvPairs["name"]] = arg
|
||||
}
|
||||
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
|
||||
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) {
|
||||
common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg)
|
||||
extraPath := []string{arg}
|
||||
for _, v := range rc.ExtraPath {
|
||||
if v != arg {
|
||||
extraPath = append(extraPath, v)
|
||||
}
|
||||
}
|
||||
rc.ExtraPath = extraPath
|
||||
rc.ExtraPath = append(rc.ExtraPath, arg)
|
||||
}
|
||||
|
||||
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) {
|
||||
stepID := rc.CurrentStep
|
||||
if stepID != "" {
|
||||
if rc.IntraActionState == nil {
|
||||
rc.IntraActionState = map[string]map[string]string{}
|
||||
if rc.CurrentStep != "" {
|
||||
stepResult := rc.StepResults[rc.CurrentStep]
|
||||
if stepResult != nil {
|
||||
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])
|
||||
|
||||
handler("::add-path::/boo\n")
|
||||
a.Equal("/boo", rc.ExtraPath[0])
|
||||
a.Equal("/boo", rc.ExtraPath[1])
|
||||
}
|
||||
|
||||
func TestStopCommands(t *testing.T) {
|
||||
@@ -102,7 +102,7 @@ func TestAddpathADO(t *testing.T) {
|
||||
a.Equal("/zoo", rc.ExtraPath[0])
|
||||
|
||||
handler("##[add-path]/boo\n")
|
||||
a.Equal("/boo", rc.ExtraPath[0])
|
||||
a.Equal("/boo", rc.ExtraPath[1])
|
||||
}
|
||||
|
||||
func TestAddmask(t *testing.T) {
|
||||
@@ -177,7 +177,11 @@ func TestAddmaskUsemask(t *testing.T) {
|
||||
func TestSaveState(t *testing.T) {
|
||||
rc := &RunContext{
|
||||
CurrentStep: "step",
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
StepResults: map[string]*model.StepResult{
|
||||
"step": {
|
||||
State: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -185,5 +189,5 @@ func TestSaveState(t *testing.T) {
|
||||
handler := rc.commandHandler(ctx)
|
||||
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 (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"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)
|
||||
}
|
||||
|
||||
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 {
|
||||
args := cm.Called(destPath, files)
|
||||
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)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||
args := cm.Called(command, env, user, workdir)
|
||||
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
|
||||
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
|
||||
using := make(map[string]exprparser.Needs)
|
||||
using := make(map[string]map[string]map[string]string)
|
||||
strategy := make(map[string]interface{})
|
||||
if rc.Run != nil {
|
||||
job := rc.Run.Job()
|
||||
@@ -41,26 +35,8 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
||||
jobNeeds := rc.Run.Job().Needs()
|
||||
|
||||
for _, needs := range jobNeeds {
|
||||
using[needs] = exprparser.Needs{
|
||||
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
|
||||
using[needs] = map[string]map[string]string{
|
||||
"outputs": jobs[needs].Outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,13 +46,12 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
||||
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: ghc,
|
||||
Env: env,
|
||||
Env: rc.GetEnv(),
|
||||
Job: rc.getJobContext(),
|
||||
Jobs: &workflowCallResult,
|
||||
// todo: should be unavailable
|
||||
// but required to interpolate/evaluate the step outputs on the job
|
||||
Steps: rc.getStepsContext(),
|
||||
Secrets: getWorkflowSecrets(ctx, rc),
|
||||
Secrets: rc.Config.Secrets,
|
||||
Strategy: strategy,
|
||||
Matrix: rc.Matrix,
|
||||
Needs: using,
|
||||
@@ -107,11 +82,10 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.Run.Job().Needs()
|
||||
|
||||
using := make(map[string]exprparser.Needs)
|
||||
using := make(map[string]map[string]map[string]string)
|
||||
for _, needs := range jobNeeds {
|
||||
using[needs] = exprparser.Needs{
|
||||
Outputs: jobs[needs].Outputs,
|
||||
Result: jobs[needs].Result,
|
||||
using[needs] = map[string]map[string]string{
|
||||
"outputs": jobs[needs].Outputs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +97,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
|
||||
Env: *step.getEnv(),
|
||||
Job: rc.getJobContext(),
|
||||
Steps: rc.getStepsContext(),
|
||||
Secrets: getWorkflowSecrets(ctx, rc),
|
||||
Secrets: rc.Config.Secrets,
|
||||
Strategy: strategy,
|
||||
Matrix: rc.Matrix,
|
||||
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{} {
|
||||
inputs := map[string]interface{}{}
|
||||
|
||||
setupWorkflowInputs(ctx, &inputs, rc)
|
||||
|
||||
var env map[string]string
|
||||
if step != nil {
|
||||
env = *step.getEnv()
|
||||
@@ -371,54 +343,3 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
|
||||
|
||||
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 == "" {
|
||||
stepModel.ID = fmt.Sprintf("%d", i)
|
||||
}
|
||||
stepModel.Number = i
|
||||
|
||||
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 {
|
||||
logger := common.Logger(ctx)
|
||||
jobError := common.JobError(ctx)
|
||||
var err error
|
||||
if rc.Config.AutoRemove || jobError == nil {
|
||||
// always allow 1 min for stopping and removing the runner, even if we were cancelled
|
||||
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
defer cancel()
|
||||
err = info.stopContainer()(ctx)
|
||||
}
|
||||
setJobResult(ctx, info, rc, jobError == nil)
|
||||
setJobOutputs(ctx, rc)
|
||||
|
||||
if jobError != nil {
|
||||
info.result("failure")
|
||||
logger.WithField("jobResult", "failure").Infof("\U0001F3C1 Job failed")
|
||||
} else {
|
||||
err := info.stopContainer()(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.result("success")
|
||||
logger.WithField("jobResult", "success").Infof("\U0001F3C1 Job succeeded")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
pipeline := make([]common.Executor, 0)
|
||||
@@ -119,7 +123,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
if ctx.Err() == context.Canceled {
|
||||
// in case of an aborted run, we still should execute the
|
||||
// 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()
|
||||
}
|
||||
return postExecutor(ctx)
|
||||
@@ -128,52 +132,9 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
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 {
|
||||
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)
|
||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||
|
@@ -15,15 +15,15 @@ import (
|
||||
|
||||
func TestJobExecutor(t *testing.T) {
|
||||
tables := []TestJobFileInfo{
|
||||
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, secrets},
|
||||
{workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
|
||||
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
|
||||
{workdir, "uses-github-root", "push", "", platforms, secrets},
|
||||
{workdir, "uses-github-path", "push", "", platforms, secrets},
|
||||
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
||||
{workdir, "uses-github-full-sha", "push", "", platforms, secrets},
|
||||
{workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, secrets},
|
||||
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", 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},
|
||||
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
|
||||
{workdir, "uses-github-root", "push", "", platforms},
|
||||
{workdir, "uses-github-path", "push", "", platforms},
|
||||
{workdir, "uses-docker-url", "push", "", platforms},
|
||||
{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},
|
||||
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms},
|
||||
}
|
||||
// These tests are sufficient to only check syntax.
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context {
|
||||
ctx = WithMasks(ctx, masks)
|
||||
|
||||
var logger *logrus.Logger
|
||||
if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
|
||||
logger = jobLoggerFactory.WithJobLogger()
|
||||
} else {
|
||||
var formatter logrus.Formatter
|
||||
if config.JSONLogger {
|
||||
formatter = &logrus.JSONFormatter{}
|
||||
} else {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
nextColor++
|
||||
|
||||
var formatter logrus.Formatter
|
||||
if config.JSONLogger {
|
||||
formatter = &jobLogJSONFormatter{
|
||||
formatter: &logrus.JSONFormatter{},
|
||||
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
||||
}
|
||||
} else {
|
||||
formatter = &jobLogFormatter{
|
||||
color: colors[nextColor%len(colors)],
|
||||
}
|
||||
}
|
||||
|
||||
logger = logrus.New()
|
||||
logger.SetOutput(os.Stdout)
|
||||
logger.SetLevel(logrus.GetLevel())
|
||||
logger.SetFormatter(formatter)
|
||||
}
|
||||
|
||||
logger.SetFormatter(&maskedFormatter{
|
||||
Formatter: logger.Formatter,
|
||||
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nextColor++
|
||||
ctx = WithMasks(ctx, masks)
|
||||
|
||||
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{
|
||||
"job": jobName,
|
||||
"jobID": jobID,
|
||||
@@ -131,8 +117,9 @@ func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context
|
||||
}).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{
|
||||
"stepNumber": stepNumber,
|
||||
"step": stepName,
|
||||
"stepID": []string{stepID},
|
||||
"stage": stageName,
|
||||
@@ -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 {
|
||||
color int
|
||||
masker entryProcessor
|
||||
}
|
||||
|
||||
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
entry = f.masker(entry)
|
||||
|
||||
if f.isColored(entry) {
|
||||
f.printColored(b, entry)
|
||||
} else {
|
||||
@@ -248,3 +229,12 @@ func checkIfTerminal(w io.Writer) bool {
|
||||
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
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/common/git"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/exprparser"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
@@ -35,11 +33,9 @@ type RunContext struct {
|
||||
Run *model.Run
|
||||
EventJSON 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
|
||||
CurrentStep string
|
||||
StepResults map[string]*model.StepResult
|
||||
IntraActionState map[string]map[string]string
|
||||
ExprEval ExpressionEvaluator
|
||||
JobContainer container.ExecutionsEnvironment
|
||||
OutputMappings map[MappableOutput]MappableOutput
|
||||
@@ -48,7 +44,6 @@ type RunContext struct {
|
||||
Parent *RunContext
|
||||
Masks []string
|
||||
cleanUpJobContainer common.Executor
|
||||
caller *caller // job calling this RunContext (reusable workflows)
|
||||
}
|
||||
|
||||
func (rc *RunContext) AddMask(mask string) {
|
||||
@@ -61,13 +56,7 @@ type MappableOutput struct {
|
||||
}
|
||||
|
||||
func (rc *RunContext) String() string {
|
||||
name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
|
||||
if rc.caller != nil {
|
||||
// prefix the reusable workflow with the caller job
|
||||
// this is required to create unique container names
|
||||
name = fmt.Sprintf("%s/%s", rc.caller.runContext.Run.JobID, name)
|
||||
}
|
||||
return name
|
||||
return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
|
||||
}
|
||||
|
||||
// GetEnv returns the env for the context
|
||||
@@ -86,7 +75,7 @@ func (rc *RunContext) GetEnv() map[string]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
|
||||
@@ -156,15 +145,15 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
_, _ = rand.Read(randBytes)
|
||||
miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes))
|
||||
actPath := filepath.Join(miscpath, "act")
|
||||
if err := os.MkdirAll(actPath, 0o777); err != nil {
|
||||
if err := os.MkdirAll(actPath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(miscpath, "hostexecutor")
|
||||
if err := os.MkdirAll(path, 0o777); err != nil {
|
||||
if err := os.MkdirAll(path, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
runnerTmp := filepath.Join(miscpath, "tmp")
|
||||
if err := os.MkdirAll(runnerTmp, 0o777); err != nil {
|
||||
if err := os.MkdirAll(runnerTmp, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
toolCache := filepath.Join(cacheDir, "tool_cache")
|
||||
@@ -180,28 +169,29 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
StdOut: logWriter,
|
||||
}
|
||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
||||
if v, ok := v.(string); ok {
|
||||
rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
|
||||
}
|
||||
}
|
||||
rc.Env["RUNNER_TOOL_CACHE"] = toolCache
|
||||
rc.Env["RUNNER_OS"] = runtime.GOOS
|
||||
rc.Env["RUNNER_ARCH"] = runtime.GOARCH
|
||||
rc.Env["RUNNER_TEMP"] = runnerTmp
|
||||
for _, env := range os.Environ() {
|
||||
if k, v, ok := strings.Cut(env, "="); ok {
|
||||
// don't override
|
||||
if _, ok := rc.Env[k]; !ok {
|
||||
rc.Env[k] = v
|
||||
}
|
||||
i := strings.Index(env, "=")
|
||||
if i > 0 {
|
||||
rc.Env[env[0:i]] = env[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||
Name: "workflow/event.json",
|
||||
Mode: 0o644,
|
||||
Mode: 0644,
|
||||
Body: rc.EventJSON,
|
||||
}, &container.FileEntry{
|
||||
Name: "workflow/envs.txt",
|
||||
Mode: 0o666,
|
||||
Mode: 0666,
|
||||
Body: "",
|
||||
}, &container.FileEntry{
|
||||
Name: "workflow/paths.txt",
|
||||
Mode: 0666,
|
||||
Body: "",
|
||||
}),
|
||||
)(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_ARCH", container.RunnerArch(ctx)))
|
||||
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{}
|
||||
binds, mounts := rc.GetBindsAndMounts()
|
||||
@@ -252,7 +241,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
|
||||
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
||||
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),
|
||||
Image: image,
|
||||
Username: username,
|
||||
@@ -260,7 +249,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
Name: name,
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
NetworkMode: "host",
|
||||
NetworkMode: rc.Config.ContainerNetworkMode,
|
||||
Binds: binds,
|
||||
Stdout: logWriter,
|
||||
Stderr: logWriter,
|
||||
@@ -268,6 +257,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
UsernsMode: rc.Config.UsernsMode,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
Options: rc.options(ctx),
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
})
|
||||
if rc.JobContainer == nil {
|
||||
return errors.New("Failed to create job container")
|
||||
@@ -278,13 +268,19 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
rc.stopJobContainer(),
|
||||
rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
|
||||
rc.JobContainer.Start(false),
|
||||
rc.JobContainer.UpdateFromImageEnv(&rc.Env),
|
||||
rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env),
|
||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||
Name: "workflow/event.json",
|
||||
Mode: 0o644,
|
||||
Mode: 0644,
|
||||
Body: rc.EventJSON,
|
||||
}, &container.FileEntry{
|
||||
Name: "workflow/envs.txt",
|
||||
Mode: 0o666,
|
||||
Mode: 0666,
|
||||
Body: "",
|
||||
}, &container.FileEntry{
|
||||
Name: "workflow/paths.txt",
|
||||
Mode: 0666,
|
||||
Body: "",
|
||||
}),
|
||||
)(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
|
||||
func (rc *RunContext) stopJobContainer() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
@@ -384,18 +335,14 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
|
||||
|
||||
func (rc *RunContext) startContainer() common.Executor {
|
||||
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.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 {
|
||||
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
|
||||
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 {
|
||||
res, err := rc.isEnabled(ctx)
|
||||
isEnabled, err := rc.isEnabled(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res {
|
||||
return executor(ctx)
|
||||
|
||||
if isEnabled {
|
||||
return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
for _, runnerLabel := range job.RunsOn() {
|
||||
platformName := rc.ExprEval.Interpolate(ctx, runnerLabel)
|
||||
image := rc.Config.Platforms[strings.ToLower(platformName)]
|
||||
runsOn := job.RunsOn()
|
||||
for i, v := range runsOn {
|
||||
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 != "" {
|
||||
return image
|
||||
}
|
||||
@@ -473,7 +421,7 @@ func (rc *RunContext) options(ctx context.Context) string {
|
||||
job := rc.Run.Job()
|
||||
c := job.Container()
|
||||
if c == nil {
|
||||
return rc.Config.ContainerOptions
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.Options
|
||||
@@ -491,10 +439,6 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if job.Type() != model.JobTypeDefault {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
img := rc.platformImage(ctx)
|
||||
if img == "" {
|
||||
if job.RunsOn() == nil {
|
||||
@@ -520,17 +464,44 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
||||
return rtnMap
|
||||
}
|
||||
|
||||
// deprecated: use createSimpleContainerName
|
||||
func createContainerName(parts ...string) string {
|
||||
name := strings.Join(parts, "-")
|
||||
name := make([]string, 0)
|
||||
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||
name = pattern.ReplaceAllString(name, "-")
|
||||
name = strings.ReplaceAll(name, "--", "-")
|
||||
hash := sha256.Sum256([]byte(name))
|
||||
partLen := (30 / len(parts)) - 1
|
||||
for i, part := range parts {
|
||||
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
|
||||
trimmedName := strings.Trim(trimToLen(name, 63), "-")
|
||||
|
||||
return fmt.Sprintf("%s-%x", trimmedName, hash)
|
||||
func createSimpleContainerName(parts ...string) string {
|
||||
pattern := regexp.MustCompile("[^a-zA-Z0-9-]")
|
||||
name := make([]string, 0, len(parts))
|
||||
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 {
|
||||
@@ -571,20 +542,11 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
EventName: rc.Config.EventName,
|
||||
Action: rc.CurrentStep,
|
||||
Token: rc.Config.Token,
|
||||
Job: rc.Run.JobID,
|
||||
ActionPath: rc.ActionPath,
|
||||
RepositoryOwner: rc.Config.Env["GITHUB_REPOSITORY_OWNER"],
|
||||
RetentionDays: rc.Config.Env["GITHUB_RETENTION_DAYS"],
|
||||
RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"],
|
||||
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 {
|
||||
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"
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
err := json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
|
||||
err = json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
|
||||
if err != nil {
|
||||
logger.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err)
|
||||
}
|
||||
}
|
||||
|
||||
ghc.SetBaseAndHeadRef()
|
||||
repoPath := rc.Config.Workdir
|
||||
ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath)
|
||||
if ghc.Ref == "" {
|
||||
ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath)
|
||||
}
|
||||
if ghc.Sha == "" {
|
||||
ghc.SetSha(ctx, repoPath)
|
||||
if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" {
|
||||
ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
|
||||
ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -661,6 +657,15 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
||||
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{}) {
|
||||
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 {
|
||||
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_RUN_ID"] = github.RunID
|
||||
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_TYPE"] = github.RefType
|
||||
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_RETENTION_DAYS"] = github.RetentionDays
|
||||
env["RUNNER_PERFLOG"] = github.RunnerPerflog
|
||||
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" {
|
||||
defaultServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
|
||||
defaultAPIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
|
||||
defaultGraphqlURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
|
||||
hasProtocol := strings.HasPrefix(rc.Config.GitHubInstance, "http://") || strings.HasPrefix(rc.Config.GitHubInstance, "https://")
|
||||
if hasProtocol {
|
||||
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
|
||||
} else {
|
||||
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_SERVER_URL"] == "" {
|
||||
env["GITHUB_SERVER_URL"] = defaultServerURL
|
||||
}
|
||||
|
||||
if env["GITHUB_API_URL"] == "" {
|
||||
env["GITHUB_API_URL"] = defaultAPIURL
|
||||
}
|
||||
|
||||
if env["GITHUB_GRAPHQL_URL"] == "" {
|
||||
env["GITHUB_GRAPHQL_URL"] = defaultGraphqlURL
|
||||
}
|
||||
|
||||
if rc.Config.ArtifactServerPath != "" {
|
||||
@@ -754,7 +754,7 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
|
||||
func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
||||
actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL")
|
||||
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
|
||||
|
||||
|
@@ -144,7 +144,6 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
// Check github context
|
||||
{in: "github.actor == 'nektos/act'", out: true},
|
||||
{in: "github.actor == 'unknown'", out: false},
|
||||
{in: "github.job == 'job1'", out: true},
|
||||
// The special ACT flag
|
||||
{in: "${{ env.ACT }}", out: true},
|
||||
{in: "${{ !env.ACT }}", out: false},
|
||||
@@ -365,7 +364,6 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
OutputMappings: map[MappableOutput]MappableOutput{},
|
||||
}
|
||||
rc.Run.JobID = "job1"
|
||||
|
||||
ghc := rc.getGithubContext(context.Background())
|
||||
|
||||
@@ -394,7 +392,6 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
assert.Equal(t, ghc.RepositoryOwner, owner)
|
||||
assert.Equal(t, ghc.RunnerPerflog, "/dev/null")
|
||||
assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"])
|
||||
assert.Equal(t, ghc.Job, "job1")
|
||||
}
|
||||
|
||||
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: "deployment", 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 {
|
||||
@@ -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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@@ -32,7 +32,6 @@ type Config struct {
|
||||
LogOutput bool // log the output from docker run
|
||||
JSONLogger bool // use json or text logger
|
||||
Env map[string]string // env for containers
|
||||
Inputs map[string]string // manually passed action inputs
|
||||
Secrets map[string]string // list of secrets
|
||||
Token string // GitHub token
|
||||
InsecureSecrets bool // switch hiding output when printing to terminal
|
||||
@@ -41,29 +40,30 @@ type Config struct {
|
||||
UsernsMode string // user namespace to use
|
||||
ContainerArchitecture string // Desired OS/architecture platform for running containers
|
||||
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
|
||||
GitHubInstance string // GitHub instance to use, default "github.com"
|
||||
ContainerCapAdd []string // list of kernel capabilities to add to 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
|
||||
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
|
||||
NoSkipCheckout bool // do not skip actions/checkout
|
||||
RemoteName string // remote name in local git repo config
|
||||
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
||||
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
||||
}
|
||||
|
||||
type caller struct {
|
||||
runContext *RunContext
|
||||
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
|
||||
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 {
|
||||
config *Config
|
||||
eventJSON string
|
||||
caller *caller // the job calling this runner (caller of a reusable workflow)
|
||||
}
|
||||
|
||||
// New Creates a new Runner
|
||||
@@ -72,44 +72,40 @@ func New(runnerConfig *Config) (Runner, error) {
|
||||
config: runnerConfig,
|
||||
}
|
||||
|
||||
return runner.configure()
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) configure() (Runner, error) {
|
||||
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)
|
||||
eventJSONBytes, err := os.ReadFile(runner.config.EventPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// NewPlanExecutor ...
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
maxJobNameLen := 0
|
||||
|
||||
stagePipeline := make([]common.Executor, 0)
|
||||
for i := range plan.Stages {
|
||||
s := i
|
||||
stage := plan.Stages[i]
|
||||
stagePipeline = append(stagePipeline, func(ctx context.Context) error {
|
||||
pipeline := make([]common.Executor, 0)
|
||||
for _, run := range stage.Runs {
|
||||
for r, run := range stage.Runs {
|
||||
stageExecutor := make([]common.Executor, 0)
|
||||
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 {
|
||||
strategyRc := runner.newRunContext(ctx, run, 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())
|
||||
}
|
||||
stageExecutor = append(stageExecutor, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
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...))
|
||||
@@ -178,10 +195,8 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat
|
||||
EventJSON: runner.eventJSON,
|
||||
StepResults: make(map[string]*model.StepResult),
|
||||
Matrix: matrix,
|
||||
caller: runner.caller,
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
rc.Name = rc.ExprEval.Interpolate(ctx, run.String())
|
||||
|
||||
return rc
|
||||
}
|
||||
|
@@ -1,10 +1,8 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -24,7 +22,6 @@ var (
|
||||
platforms map[string]string
|
||||
logLevel = log.DebugLevel
|
||||
workdir = "testdata"
|
||||
secrets map[string]string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -45,103 +42,14 @@ func init() {
|
||||
if wd, err := filepath.Abs(workdir); err == nil {
|
||||
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) {
|
||||
planner, err := model.NewWorkflowPlanner("testdata/basic", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, err)
|
||||
|
||||
plan, err := planner.PlanEvent("push")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, plan)
|
||||
assert.NotNil(t, plan.Stages)
|
||||
plan := planner.PlanEvent("push")
|
||||
assert.Nil(t, err)
|
||||
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[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[2].Runs[0].JobID, "test", "jobid")
|
||||
|
||||
plan, err = planner.PlanEvent("release")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, plan)
|
||||
assert.Equal(t, 0, len(plan.Stages))
|
||||
plan = planner.PlanEvent("release")
|
||||
assert.Equal(t, len(plan.Stages), 0, "stages")
|
||||
}
|
||||
|
||||
type TestJobFileInfo struct {
|
||||
@@ -162,7 +68,6 @@ type TestJobFileInfo struct {
|
||||
eventName string
|
||||
errorMessage string
|
||||
platforms map[string]string
|
||||
secrets map[string]string
|
||||
}
|
||||
|
||||
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,
|
||||
Env: cfg.Env,
|
||||
Secrets: cfg.Secrets,
|
||||
Inputs: cfg.Inputs,
|
||||
GitHubInstance: "github.com",
|
||||
ContainerArchitecture: cfg.ContainerArchitecture,
|
||||
}
|
||||
@@ -194,16 +98,14 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
|
||||
plan, err := 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 {
|
||||
plan := planner.PlanEvent(j.eventName)
|
||||
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if j.errorMessage == "" {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
} else {
|
||||
assert.Error(t, err, j.errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("::endgroup::")
|
||||
}
|
||||
@@ -217,96 +119,81 @@ func TestRunEvent(t *testing.T) {
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
// Shells
|
||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
||||
{workdir, "shells/defaults", "push", "", platforms},
|
||||
// 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/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms},
|
||||
|
||||
// Local action
|
||||
{workdir, "local-action-docker-url", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-docker-url", "push", "", platforms},
|
||||
{workdir, "local-action-dockerfile", "push", "", platforms},
|
||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms},
|
||||
{workdir, "local-action-js", "push", "", platforms},
|
||||
|
||||
// Uses
|
||||
{workdir, "uses-composite", "push", "", platforms, secrets},
|
||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
||||
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
||||
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms, secrets},
|
||||
{workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}},
|
||||
{workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
|
||||
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
||||
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
||||
{workdir, "uses-composite", "push", "", platforms},
|
||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms},
|
||||
{workdir, "uses-nested-composite", "push", "", platforms},
|
||||
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms},
|
||||
{workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms},
|
||||
{workdir, "uses-docker-url", "push", "", platforms},
|
||||
{workdir, "act-composite-env-test", "push", "", platforms},
|
||||
|
||||
// Eval
|
||||
{workdir, "evalmatrix", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrixneeds", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
|
||||
{workdir, "issue-1195", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix", "push", "", platforms},
|
||||
{workdir, "evalmatrixneeds", "push", "", platforms},
|
||||
{workdir, "evalmatrixneeds2", "push", "", platforms},
|
||||
{workdir, "evalmatrix-merge-map", "push", "", platforms},
|
||||
{workdir, "evalmatrix-merge-array", "push", "", platforms},
|
||||
{workdir, "issue-1195", "push", "", platforms},
|
||||
|
||||
{workdir, "basic", "push", "", platforms, secrets},
|
||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
|
||||
{workdir, "runs-on", "push", "", platforms, secrets},
|
||||
{workdir, "checkout", "push", "", platforms, secrets},
|
||||
{workdir, "job-container", "push", "", platforms, secrets},
|
||||
{workdir, "job-container-non-root", "push", "", platforms, secrets},
|
||||
{workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms, secrets},
|
||||
{workdir, "container-hostname", "push", "", platforms, secrets},
|
||||
{workdir, "remote-action-docker", "push", "", platforms, secrets},
|
||||
{workdir, "remote-action-js", "push", "", platforms, secrets},
|
||||
{workdir, "remote-action-js-node-user", "push", "", platforms, secrets}, // Test if this works with non root container
|
||||
{workdir, "matrix", "push", "", platforms, secrets},
|
||||
{workdir, "matrix-include-exclude", "push", "", platforms, secrets},
|
||||
{workdir, "matrix-exitcode", "push", "Job 'test' failed", platforms, secrets},
|
||||
{workdir, "commands", "push", "", platforms, secrets},
|
||||
{workdir, "workdir", "push", "", platforms, secrets},
|
||||
{workdir, "defaults-run", "push", "", platforms, secrets},
|
||||
{workdir, "composite-fail-with-output", "push", "", platforms, secrets},
|
||||
{workdir, "issue-597", "push", "", platforms, secrets},
|
||||
{workdir, "issue-598", "push", "", platforms, secrets},
|
||||
{workdir, "if-env-act", "push", "", platforms, secrets},
|
||||
{workdir, "env-and-path", "push", "", platforms, secrets},
|
||||
{workdir, "environment-files", "push", "", platforms, secrets},
|
||||
{workdir, "GITHUB_STATE", "push", "", platforms, secrets},
|
||||
{workdir, "environment-files-parser-bug", "push", "", platforms, secrets},
|
||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
|
||||
{workdir, "outputs", "push", "", platforms, secrets},
|
||||
{workdir, "networking", "push", "", platforms, secrets},
|
||||
{workdir, "steps-context/conclusion", "push", "", platforms, secrets},
|
||||
{workdir, "steps-context/outcome", "push", "", platforms, secrets},
|
||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
|
||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
|
||||
{workdir, "actions-environment-and-context-tests", "push", "", platforms, secrets},
|
||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
|
||||
{workdir, "evalenv", "push", "", platforms, secrets},
|
||||
{workdir, "docker-action-custom-path", "push", "", platforms, secrets},
|
||||
{workdir, "GITHUB_ENV-use-in-env-ctx", "push", "", platforms, secrets},
|
||||
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
|
||||
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms, secrets},
|
||||
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms, secrets},
|
||||
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms, secrets},
|
||||
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms, secrets},
|
||||
{workdir, "job-needs-context-contains-result", "push", "", platforms, secrets},
|
||||
{"../model/testdata", "strategy", "push", "", platforms, secrets}, // TODO: move all testdata into pkg so we can validate it with planner and runner
|
||||
{workdir, "basic", "push", "", platforms},
|
||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms},
|
||||
{workdir, "runs-on", "push", "", platforms},
|
||||
{workdir, "checkout", "push", "", platforms},
|
||||
{workdir, "job-container", "push", "", platforms},
|
||||
{workdir, "job-container-non-root", "push", "", platforms},
|
||||
{workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms},
|
||||
{workdir, "container-hostname", "push", "", platforms},
|
||||
{workdir, "remote-action-docker", "push", "", platforms},
|
||||
{workdir, "remote-action-js", "push", "", platforms},
|
||||
{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},
|
||||
{workdir, "matrix-include-exclude", "push", "", platforms},
|
||||
{workdir, "commands", "push", "", platforms},
|
||||
{workdir, "workdir", "push", "", platforms},
|
||||
{workdir, "defaults-run", "push", "", platforms},
|
||||
{workdir, "composite-fail-with-output", "push", "", platforms},
|
||||
{workdir, "issue-597", "push", "", platforms},
|
||||
{workdir, "issue-598", "push", "", platforms},
|
||||
{workdir, "if-env-act", "push", "", platforms},
|
||||
{workdir, "env-and-path", "push", "", platforms},
|
||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms},
|
||||
{workdir, "outputs", "push", "", platforms},
|
||||
{workdir, "networking", "push", "", platforms},
|
||||
{workdir, "steps-context/conclusion", "push", "", platforms},
|
||||
{workdir, "steps-context/outcome", "push", "", platforms},
|
||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
||||
{workdir, "actions-environment-and-context-tests", "push", "", platforms},
|
||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
|
||||
{workdir, "evalenv", "push", "", platforms},
|
||||
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
|
||||
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms},
|
||||
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms},
|
||||
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms},
|
||||
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms},
|
||||
{"../model/testdata", "strategy", "push", "", platforms}, // 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
|
||||
{"../model/testdata", "container-volumes", "push", "", platforms, secrets},
|
||||
{workdir, "path-handling", "push", "", platforms, secrets},
|
||||
{workdir, "do-not-leak-step-env-in-composite", "push", "", platforms, secrets},
|
||||
{workdir, "set-env-step-env-override", "push", "", platforms, secrets},
|
||||
{workdir, "set-env-new-env-file-per-step", "push", "", platforms, secrets},
|
||||
{workdir, "no-panic-on-invalid-composite-action", "push", "jobs failed due to invalid action", platforms, secrets},
|
||||
{"../model/testdata", "container-volumes", "push", "", platforms},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
t.Run(table.workflowPath, func(t *testing.T) {
|
||||
config := &Config{
|
||||
Secrets: table.secrets,
|
||||
}
|
||||
config := &Config{}
|
||||
|
||||
eventFile := filepath.Join(workdir, table.workflowPath, "event.json")
|
||||
if _, err := os.Stat(eventFile); err == nil {
|
||||
@@ -334,51 +221,51 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
||||
|
||||
tables = append(tables, []TestJobFileInfo{
|
||||
// Shells
|
||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
||||
{workdir, "shells/pwsh", "push", "", platforms, secrets},
|
||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
||||
{workdir, "shells/python", "push", "", platforms, secrets},
|
||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
||||
{workdir, "shells/defaults", "push", "", platforms},
|
||||
{workdir, "shells/pwsh", "push", "", platforms},
|
||||
{workdir, "shells/bash", "push", "", platforms},
|
||||
{workdir, "shells/python", "push", "", platforms},
|
||||
{workdir, "shells/sh", "push", "", platforms},
|
||||
|
||||
// Local action
|
||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-js", "push", "", platforms},
|
||||
|
||||
// Uses
|
||||
{workdir, "uses-composite", "push", "", platforms, secrets},
|
||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
||||
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
||||
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
||||
{workdir, "uses-composite", "push", "", platforms},
|
||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms},
|
||||
{workdir, "uses-nested-composite", "push", "", platforms},
|
||||
{workdir, "act-composite-env-test", "push", "", platforms},
|
||||
|
||||
// Eval
|
||||
{workdir, "evalmatrix", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrixneeds", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
|
||||
{workdir, "issue-1195", "push", "", platforms, secrets},
|
||||
{workdir, "evalmatrix", "push", "", platforms},
|
||||
{workdir, "evalmatrixneeds", "push", "", platforms},
|
||||
{workdir, "evalmatrixneeds2", "push", "", platforms},
|
||||
{workdir, "evalmatrix-merge-map", "push", "", platforms},
|
||||
{workdir, "evalmatrix-merge-array", "push", "", platforms},
|
||||
{workdir, "issue-1195", "push", "", platforms},
|
||||
|
||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
|
||||
{workdir, "runs-on", "push", "", platforms, secrets},
|
||||
{workdir, "checkout", "push", "", platforms, secrets},
|
||||
{workdir, "remote-action-js", "push", "", platforms, secrets},
|
||||
{workdir, "matrix", "push", "", platforms, secrets},
|
||||
{workdir, "matrix-include-exclude", "push", "", platforms, secrets},
|
||||
{workdir, "commands", "push", "", platforms, secrets},
|
||||
{workdir, "defaults-run", "push", "", platforms, secrets},
|
||||
{workdir, "composite-fail-with-output", "push", "", platforms, secrets},
|
||||
{workdir, "issue-597", "push", "", platforms, secrets},
|
||||
{workdir, "issue-598", "push", "", platforms, secrets},
|
||||
{workdir, "if-env-act", "push", "", platforms, secrets},
|
||||
{workdir, "env-and-path", "push", "", platforms, secrets},
|
||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
|
||||
{workdir, "outputs", "push", "", platforms, secrets},
|
||||
{workdir, "steps-context/conclusion", "push", "", platforms, secrets},
|
||||
{workdir, "steps-context/outcome", "push", "", platforms, secrets},
|
||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
|
||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
|
||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
|
||||
{workdir, "evalenv", "push", "", platforms, secrets},
|
||||
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
|
||||
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms},
|
||||
{workdir, "runs-on", "push", "", platforms},
|
||||
{workdir, "checkout", "push", "", platforms},
|
||||
{workdir, "remote-action-js", "push", "", platforms},
|
||||
{workdir, "matrix", "push", "", platforms},
|
||||
{workdir, "matrix-include-exclude", "push", "", platforms},
|
||||
{workdir, "commands", "push", "", platforms},
|
||||
{workdir, "defaults-run", "push", "", platforms},
|
||||
{workdir, "composite-fail-with-output", "push", "", platforms},
|
||||
{workdir, "issue-597", "push", "", platforms},
|
||||
{workdir, "issue-598", "push", "", platforms},
|
||||
{workdir, "if-env-act", "push", "", platforms},
|
||||
{workdir, "env-and-path", "push", "", platforms},
|
||||
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms},
|
||||
{workdir, "outputs", "push", "", platforms},
|
||||
{workdir, "steps-context/conclusion", "push", "", platforms},
|
||||
{workdir, "steps-context/outcome", "push", "", platforms},
|
||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
||||
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
|
||||
{workdir, "evalenv", "push", "", platforms},
|
||||
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
|
||||
}...)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -387,22 +274,16 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
||||
}
|
||||
|
||||
tables = append(tables, []TestJobFileInfo{
|
||||
{workdir, "windows-prepend-path", "push", "", platforms, secrets},
|
||||
{workdir, "windows-add-env", "push", "", platforms, secrets},
|
||||
{workdir, "windows-prepend-path", "push", "", platforms},
|
||||
{workdir, "windows-add-env", "push", "", platforms},
|
||||
}...)
|
||||
} else {
|
||||
platforms := map[string]string{
|
||||
"self-hosted": "-self-hosted",
|
||||
"ubuntu-latest": "-self-hosted",
|
||||
}
|
||||
|
||||
tables = append(tables, []TestJobFileInfo{
|
||||
{workdir, "nix-prepend-path", "push", "", platforms, secrets},
|
||||
{workdir, "inputs-via-env-context", "push", "", platforms, secrets},
|
||||
{workdir, "do-not-leak-step-env-in-composite", "push", "", platforms, secrets},
|
||||
{workdir, "set-env-step-env-override", "push", "", platforms, secrets},
|
||||
{workdir, "set-env-new-env-file-per-step", "push", "", platforms, secrets},
|
||||
{workdir, "no-panic-on-invalid-composite-action", "push", "jobs failed due to invalid action", platforms, secrets},
|
||||
{workdir, "nix-prepend-path", "push", "", platforms},
|
||||
}...)
|
||||
}
|
||||
|
||||
@@ -422,17 +303,17 @@ func TestDryrunEvent(t *testing.T) {
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
// Shells
|
||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
||||
{workdir, "shells/defaults", "push", "", platforms},
|
||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms},
|
||||
|
||||
// Local action
|
||||
{workdir, "local-action-docker-url", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-js", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-docker-url", "push", "", platforms},
|
||||
{workdir, "local-action-dockerfile", "push", "", platforms},
|
||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms},
|
||||
{workdir, "local-action-js", "push", "", platforms},
|
||||
}
|
||||
|
||||
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) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
@@ -482,17 +339,6 @@ func TestRunDifferentArchitecture(t *testing.T) {
|
||||
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) {
|
||||
assertNoSecret := func(text string, secret string) {
|
||||
index := strings.Index(text, "composite secret")
|
||||
@@ -516,9 +362,9 @@ func TestMaskValues(t *testing.T) {
|
||||
platforms: platforms,
|
||||
}
|
||||
|
||||
logger := &maskJobLoggerFactory{}
|
||||
tjfi.runTest(WithJobLoggerFactory(common.WithLogger(context.Background(), logger.WithJobLogger()), logger), t, &Config{})
|
||||
output := logger.Output.String()
|
||||
output := captureOutput(t, func() {
|
||||
tjfi.runTest(context.Background(), t, &Config{})
|
||||
})
|
||||
|
||||
assertNoSecret(output, "secret value")
|
||||
assertNoSecret(output, "YWJjCg==")
|
||||
@@ -546,27 +392,6 @@ func TestRunEventSecrets(t *testing.T) {
|
||||
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) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
|
@@ -44,16 +44,16 @@ func (s stepStage) String() string {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error {
|
||||
env := map[string]string{}
|
||||
err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
func (s stepStage) getStepName(stepModel *model.Step) string {
|
||||
switch s {
|
||||
case stepStagePre:
|
||||
return fmt.Sprintf("pre-%s", stepModel.ID)
|
||||
case stepStageMain:
|
||||
return stepModel.ID
|
||||
case stepStagePost:
|
||||
return fmt.Sprintf("post-%s", stepModel.ID)
|
||||
}
|
||||
for k, v := range env {
|
||||
setter(ctx, map[string]string{"name": k}, v)
|
||||
}
|
||||
return nil
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outputs: make(map[string]string),
|
||||
}
|
||||
if stage == stepStageMain {
|
||||
rc.StepResults[rc.CurrentStep] = stepResult
|
||||
}
|
||||
|
||||
err := setupEnv(ctx, step)
|
||||
if err != nil {
|
||||
@@ -81,15 +78,15 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
|
||||
runStep, err := isStepEnabled(ctx, ifExpression, step, stage)
|
||||
if err != nil {
|
||||
stepResult.Conclusion = model.StepStatusFailure
|
||||
stepResult.Outcome = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
return err
|
||||
}
|
||||
|
||||
if !runStep {
|
||||
stepResult.Conclusion = model.StepStatusSkipped
|
||||
stepResult.Outcome = model.StepStatusSkipped
|
||||
logger.WithField("stepResult", stepResult.Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
||||
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,79 +98,58 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
|
||||
// Prepare and clean Runner File Commands
|
||||
actPath := rc.JobContainer.GetActPath()
|
||||
|
||||
outputFileCommand := path.Join("workflow", "outputcmd.txt")
|
||||
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
|
||||
|
||||
stateFileCommand := path.Join("workflow", "statecmd.txt")
|
||||
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
|
||||
(*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{
|
||||
Name: outputFileCommand,
|
||||
Mode: 0o666,
|
||||
}, &container.FileEntry{
|
||||
Name: stateFileCommand,
|
||||
Mode: 0o666,
|
||||
}, &container.FileEntry{
|
||||
Name: pathFileCommand,
|
||||
Mode: 0o666,
|
||||
}, &container.FileEntry{
|
||||
Name: envFileCommand,
|
||||
Mode: 0666,
|
||||
}, &container.FileEntry{
|
||||
Name: summaryFileCommand,
|
||||
Mode: 0o666,
|
||||
Name: stateFileCommand,
|
||||
Mode: 0666,
|
||||
})(ctx)
|
||||
|
||||
err = executor(ctx)
|
||||
|
||||
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 {
|
||||
stepResult.Outcome = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
|
||||
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage)
|
||||
if parseErr != nil {
|
||||
stepResult.Conclusion = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
return parseErr
|
||||
}
|
||||
|
||||
if continueOnError {
|
||||
logger.Infof("Failed but continue next step")
|
||||
err = nil
|
||||
stepResult.Conclusion = model.StepStatusSuccess
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess
|
||||
} 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
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
err = processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rc.UpdateExtraPath(ctx, path.Join(actPath, pathFileCommand))
|
||||
if err != nil {
|
||||
return err
|
||||
for k, v := range output {
|
||||
rc.setOutput(ctx, map[string]string{"name": k}, v)
|
||||
}
|
||||
if orgerr != nil {
|
||||
return orgerr
|
||||
@@ -186,23 +162,25 @@ func setupEnv(ctx context.Context, step step) error {
|
||||
rc := step.getRunContext()
|
||||
|
||||
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
|
||||
mergeIntoMap(step.getEnv(), step.getStepModel().GetEnv())
|
||||
|
||||
exprEval := rc.NewExpressionEvaluator(ctx)
|
||||
for k, v := range *step.getEnv() {
|
||||
if !strings.HasPrefix(k, "INPUT_") {
|
||||
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
|
||||
}
|
||||
}
|
||||
// after we have an evaluated step context, update the expressions evaluator with a new env context
|
||||
// you can use step level env in the with property of a uses construct
|
||||
exprEval = rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv())
|
||||
for k, v := range *step.getEnv() {
|
||||
if strings.HasPrefix(k, "INPUT_") {
|
||||
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
|
||||
}
|
||||
}
|
||||
|
||||
common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv())
|
||||
|
||||
@@ -221,6 +199,14 @@ func mergeEnv(ctx context.Context, step step) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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).
|
||||
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
|
||||
})
|
||||
|
||||
@@ -77,6 +75,14 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
@@ -85,8 +91,6 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
@@ -107,6 +111,7 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
stepModel *model.Step
|
||||
actionModel *model.Action
|
||||
initialStepResults map[string]*model.StepResult
|
||||
expectedPostStepResult *model.StepResult
|
||||
err error
|
||||
mocks struct {
|
||||
env bool
|
||||
@@ -133,6 +138,11 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -161,6 +171,11 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -189,11 +204,16 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSkipped,
|
||||
Outcome: model.StepStatusSkipped,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
}{
|
||||
env: false,
|
||||
env: true,
|
||||
exec: false,
|
||||
},
|
||||
},
|
||||
@@ -218,6 +238,7 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: nil,
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -256,6 +277,11 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
}
|
||||
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 {
|
||||
suffixMatcher := func(suffix string) interface{} {
|
||||
return mock.MatchedBy(func(array []string) bool {
|
||||
@@ -268,10 +294,6 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
}
|
||||
|
||||
err := sal.post()(ctx)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
@@ -11,11 +11,11 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/common/git"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
type stepActionRemote struct {
|
||||
@@ -30,9 +30,7 @@ type stepActionRemote struct {
|
||||
remoteAction *remoteAction
|
||||
}
|
||||
|
||||
var (
|
||||
stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
||||
)
|
||||
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
||||
|
||||
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
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)
|
||||
}
|
||||
|
||||
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
|
||||
|
||||
github := sar.getGithubContext(ctx)
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
|
||||
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
|
||||
for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
|
||||
if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
|
||||
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{
|
||||
URL: sar.remoteAction.CloneURL(),
|
||||
URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
|
||||
Ref: sar.remoteAction.Ref,
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}),
|
||||
@@ -181,7 +182,7 @@ func (sar *stepActionRemote) getActionModel() *model.Action {
|
||||
|
||||
func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunContext {
|
||||
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)
|
||||
_, 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)
|
||||
env := evaluateCompositeInputAndEnv(ctx, sar.RunContext, sar)
|
||||
sar.compositeRunContext.Env = env
|
||||
sar.compositeRunContext.ExtraPath = sar.RunContext.ExtraPath
|
||||
}
|
||||
return sar.compositeRunContext
|
||||
}
|
||||
@@ -213,8 +213,15 @@ type remoteAction struct {
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (ra *remoteAction) CloneURL() string {
|
||||
return fmt.Sprintf("https://%s/%s/%s", ra.URL, ra.Org, ra.Repo)
|
||||
func (ra *remoteAction) CloneURL(defaultURL string) string {
|
||||
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 {
|
||||
@@ -225,6 +232,26 @@ func (ra *remoteAction) IsCheckout() bool {
|
||||
}
|
||||
|
||||
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:
|
||||
// > We strongly recommend that you include the version of
|
||||
// > 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],
|
||||
Path: matches[4],
|
||||
Ref: matches[6],
|
||||
URL: "github.com",
|
||||
URL: "",
|
||||
}
|
||||
}
|
||||
|
||||
func safeFilename(s string) string {
|
||||
return strings.NewReplacer(
|
||||
`<`, "-",
|
||||
`>`, "-",
|
||||
`:`, "-",
|
||||
`"`, "-",
|
||||
`/`, "-",
|
||||
`\`, "-",
|
||||
`|`, "-",
|
||||
`?`, "-",
|
||||
`*`, "-",
|
||||
).Replace(s)
|
||||
}
|
||||
|
@@ -1,20 +1,18 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"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/git"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
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 {
|
||||
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
|
||||
})
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
}
|
||||
|
||||
err := sar.pre()(ctx)
|
||||
@@ -419,8 +416,8 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
stepModel *model.Step
|
||||
actionModel *model.Action
|
||||
initialStepResults map[string]*model.StepResult
|
||||
IntraActionState map[string]map[string]string
|
||||
expectedEnv map[string]string
|
||||
expectedPostStepResult *model.StepResult
|
||||
err error
|
||||
mocks struct {
|
||||
env bool
|
||||
@@ -445,16 +442,19 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
IntraActionState: map[string]map[string]string{
|
||||
"step": {
|
||||
State: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedEnv: map[string]string{
|
||||
"STATE_key": "value",
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -483,6 +483,11 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -511,6 +516,11 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: &model.StepResult{
|
||||
Conclusion: model.StepStatusSkipped,
|
||||
Outcome: model.StepStatusSkipped,
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -540,6 +550,7 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
Outputs: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedPostStepResult: nil,
|
||||
mocks: struct {
|
||||
env bool
|
||||
exec bool
|
||||
@@ -572,13 +583,17 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
},
|
||||
},
|
||||
StepResults: tt.initialStepResults,
|
||||
IntraActionState: tt.IntraActionState,
|
||||
},
|
||||
Step: tt.stepModel,
|
||||
action: tt.actionModel,
|
||||
}
|
||||
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 {
|
||||
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
|
||||
})
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
}
|
||||
|
||||
err := sar.post()(ctx)
|
||||
@@ -609,30 +618,102 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
assert.Equal(t, value, sar.env[key])
|
||||
}
|
||||
}
|
||||
// Enshure that StepResults is nil in this test
|
||||
assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
|
||||
assert.Equal(t, tt.expectedPostStepResult, sar.RunContext.StepResults["post-step"])
|
||||
cm.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_safeFilename(t *testing.T) {
|
||||
func Test_newRemoteAction(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
want string
|
||||
action string
|
||||
want *remoteAction
|
||||
wantCloneURL string
|
||||
}{
|
||||
{
|
||||
s: "https://test.com/test/",
|
||||
want: "https---test.com-test-",
|
||||
action: "actions/heroku@main",
|
||||
want: &remoteAction{
|
||||
URL: "",
|
||||
Org: "actions",
|
||||
Repo: "heroku",
|
||||
Path: "",
|
||||
Ref: "main",
|
||||
},
|
||||
wantCloneURL: "https://github.com/actions/heroku",
|
||||
},
|
||||
{
|
||||
s: `<>:"/\|?*`,
|
||||
want: "---------",
|
||||
action: "actions/aws/ec2@main",
|
||||
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 {
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, safeFilename(tt.s), "safeFilename(%v)", tt.s)
|
||||
t.Run(tt.action, func(t *testing.T) {
|
||||
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,
|
||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||
Name: createContainerName(rc.jobContainerName(), step.ID),
|
||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
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,
|
||||
UsernsMode: rc.Config.UsernsMode,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
})
|
||||
return stepContainer
|
||||
}
|
||||
|
@@ -1,9 +1,7 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/container"
|
||||
@@ -57,6 +55,18 @@ func TestStepDockerMain(t *testing.T) {
|
||||
}
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
@@ -81,10 +91,6 @@ func TestStepDockerMain(t *testing.T) {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
@@ -93,8 +99,6 @@ func TestStepDockerMain(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
|
||||
err := sd.main()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
@@ -31,7 +30,6 @@ func (sr *stepRun) main() common.Executor {
|
||||
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
||||
sr.setupShellCommandExecutor(),
|
||||
func(ctx context.Context) error {
|
||||
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
|
||||
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()
|
||||
return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
|
||||
Name: scriptName,
|
||||
Mode: 0o755,
|
||||
Mode: 0755,
|
||||
Body: script,
|
||||
})(ctx)
|
||||
}
|
||||
|
@@ -1,23 +1,20 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestStepRun(t *testing.T) {
|
||||
cm := &containerMock{}
|
||||
fileEntry := &container.FileEntry{
|
||||
Name: "workflow/1.sh",
|
||||
Mode: 0o755,
|
||||
Mode: 0755,
|
||||
Body: "\ncmd\n",
|
||||
}
|
||||
|
||||
@@ -56,7 +53,7 @@ func TestStepRun(t *testing.T) {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -64,6 +61,14 @@ func TestStepRun(t *testing.T) {
|
||||
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 {
|
||||
return nil
|
||||
})
|
||||
@@ -74,8 +79,6 @@ func TestStepRun(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
|
||||
err := sr.main()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
|
@@ -134,6 +134,7 @@ func TestSetupEnv(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"RC_KEY": "rcvalue",
|
||||
},
|
||||
ExtraPath: []string{"/path/to/extra/file"},
|
||||
JobContainer: cm,
|
||||
}
|
||||
step := &model.Step{
|
||||
@@ -141,13 +142,19 @@ func TestSetupEnv(t *testing.T) {
|
||||
"STEP_WITH": "with-value",
|
||||
},
|
||||
}
|
||||
env := map[string]string{}
|
||||
env := map[string]string{
|
||||
"PATH": "",
|
||||
}
|
||||
|
||||
sm.On("getRunContext").Return(rc)
|
||||
sm.On("getGithubContext").Return(rc)
|
||||
sm.On("getStepModel").Return(step)
|
||||
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)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -171,11 +178,13 @@ func TestSetupEnv(t *testing.T) {
|
||||
"GITHUB_ACTION_REPOSITORY": "",
|
||||
"GITHUB_API_URL": "https:///api/v3",
|
||||
"GITHUB_BASE_REF": "",
|
||||
"GITHUB_ENV": "/var/run/act/workflow/envs.txt",
|
||||
"GITHUB_EVENT_NAME": "",
|
||||
"GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json",
|
||||
"GITHUB_GRAPHQL_URL": "https:///api/graphql",
|
||||
"GITHUB_HEAD_REF": "",
|
||||
"GITHUB_JOB": "1",
|
||||
"GITHUB_JOB": "",
|
||||
"GITHUB_PATH": "/var/run/act/workflow/paths.txt",
|
||||
"GITHUB_RETENTION_DAYS": "0",
|
||||
"GITHUB_RUN_ID": "runId",
|
||||
"GITHUB_RUN_NUMBER": "1",
|
||||
@@ -183,6 +192,7 @@ func TestSetupEnv(t *testing.T) {
|
||||
"GITHUB_TOKEN": "",
|
||||
"GITHUB_WORKFLOW": "",
|
||||
"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",
|
||||
"RUNNER_PERFLOG": "/dev/null",
|
||||
"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: 'nektos/act-test-actions/js@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