Compare commits

..

12 Commits

Author SHA1 Message Date
Jason Song
c6b8548d35 feat: support PlatformPicker 2022-11-22 16:39:19 +08:00
Jason Song
64cae197a4 Support step number 2022-11-22 16:11:35 +08:00
Jason Song
7fb84a54a8 chore: update LICENSE 2022-11-22 15:26:02 +08:00
Jason Song
70cc6c017b docs: add naming rule for git ref 2022-11-22 15:05:12 +08:00
Lunny Xiao
d7e9ea75fc disable graphql url because gitea doesn't support that 2022-11-22 14:42:48 +08:00
Jason Song
b9c20dcaa4 feat: support more options of containers 2022-11-22 14:42:12 +08:00
Jason Song
97629ae8af fix: set logger with trace level 2022-11-22 14:41:57 +08:00
Lunny Xiao
b9a9812ad9 Fix API 2022-11-22 14:22:03 +08:00
Lunny Xiao
113c3e98fb support bot site 2022-11-22 14:17:06 +08:00
Jason Song
7815eec33b Add custom enhancements 2022-11-22 14:16:35 +08:00
Jason Song
c051090583 Add description of branchs 2022-11-22 14:02:01 +08:00
fuxiaohei
0fa1fe0310 feat: add logger hook for standalone job logger 2022-11-22 14:00:13 +08:00
155 changed files with 3450 additions and 7309 deletions

View File

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

View File

@@ -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

View File

@@ -1,10 +1,6 @@
name: checks name: checks
on: [pull_request, workflow_dispatch] on: [pull_request, workflow_dispatch]
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env: env:
ACT_OWNER: ${{ github.repository_owner }} ACT_OWNER: ${{ github.repository_owner }}
ACT_REPOSITORY: ${{ github.repository }} ACT_REPOSITORY: ${{ github.repository }}
@@ -19,14 +15,14 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
- uses: golangci/golangci-lint-action@v3.4.0 - uses: golangci/golangci-lint-action@v3.3.0
with: with:
version: v1.47.2 version: v1.47.2
- uses: megalinter/megalinter/flavors/go@v6.22.2 - uses: megalinter/megalinter/flavors/go@v6.13.0
env: env:
DEFAULT_BRANCH: master DEFAULT_BRANCH: master
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -43,7 +39,7 @@ jobs:
fetch-depth: 2 fetch-depth: 2
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
@@ -54,46 +50,19 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: Run Tests - run: go test -v -cover -coverprofile=coverage.txt -covermode=atomic ./...
uses: ./.github/actions/run-tests
with:
upload-logs-name: logs-linux
- name: Run act from cli
run: go run main.go -P ubuntu-latest=node:16-buster-slim -C ./pkg/runner/testdata/ -W ./basic/push.yml
- name: Upload Codecov report - name: Upload Codecov report
uses: codecov/codecov-action@v3.1.3 uses: codecov/codecov-action@v3.1.1
with: with:
files: coverage.txt files: coverage.txt
fail_ci_if_error: true # optional (default = false) fail_ci_if_error: true # optional (default = false)
test-host:
strategy:
matrix:
os:
- windows-latest
- macos-latest
name: test-${{matrix.os}}
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: actions/setup-go@v4
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 }}
snapshot: snapshot:
name: snapshot name: snapshot
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
@@ -105,7 +74,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: GoReleaser - name: GoReleaser
uses: goreleaser/goreleaser-action@v4 uses: goreleaser/goreleaser-action@v3
with: with:
version: latest version: latest
args: release --snapshot --rm-dist args: release --snapshot --rm-dist

View File

@@ -17,8 +17,8 @@ jobs:
fetch-depth: 0 fetch-depth: 0
ref: master ref: master
token: ${{ secrets.GORELEASER_GITHUB_TOKEN }} token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
- uses: fregante/setup-git-user@v2 - uses: fregante/setup-git-user@v1
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true

View File

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

View File

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

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ coverage.txt
# megalinter # megalinter
report/ report/
act

View File

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

View File

@@ -70,7 +70,7 @@ pull_request_rules:
- 'author~=^dependabot(|-preview)\[bot\]$' - 'author~=^dependabot(|-preview)\[bot\]$'
- and: - and:
- 'approved-reviews-by=@nektos/act-maintainers' - 'approved-reviews-by=@nektos/act-maintainers'
- '#approved-reviews-by>=2' - '#approved-reviews-by>=3'
- -draft - -draft
- -merged - -merged
- -closed - -closed

View File

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

View File

@@ -1,5 +1,6 @@
MIT License MIT License
Copyright (c) 2022 The Gitea Authors
Copyright (c) 2019 Copyright (c) 2019
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View File

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

124
README.md
View File

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

View File

@@ -1 +0,0 @@
0.2.45

View File

@@ -1,27 +0,0 @@
package cmd
import (
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
)
var (
UserHomeDir string
CacheHomeDir string
)
func init() {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
UserHomeDir = home
if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
CacheHomeDir = v
} else {
CacheHomeDir = filepath.Join(UserHomeDir, ".cache")
}
}

View File

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

View File

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

View File

@@ -12,15 +12,13 @@ import (
"strings" "strings"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/adrg/xdg"
"github.com/andreaskoch/go-fswatch" "github.com/andreaskoch/go-fswatch"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/mitchellh/go-homedir"
gitignore "github.com/sabhiram/go-gitignore" gitignore "github.com/sabhiram/go-gitignore"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/nektos/act/pkg/artifactcache"
"github.com/nektos/act/pkg/artifacts" "github.com/nektos/act/pkg/artifacts"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
@@ -32,14 +30,13 @@ import (
func Execute(ctx context.Context, version string) { func Execute(ctx context.Context, version string) {
input := new(Input) input := new(Input)
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"", Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.", Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: newRunCommand(ctx, input), RunE: newRunCommand(ctx, input),
PersistentPreRun: setup(input), PersistentPreRun: setupLogging,
PersistentPostRun: cleanup(input), Version: version,
Version: version, SilenceUsage: true,
SilenceUsage: true,
} }
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change") rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
rootCmd.Flags().BoolP("list", "l", false, "list workflows") rootCmd.Flags().BoolP("list", "l", false, "list workflows")
@@ -50,12 +47,11 @@ func Execute(ctx context.Context, version string) {
rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo") rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)") rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)") rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) even if already present")
rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")
rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow") rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file") rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch") rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch")
@@ -67,7 +63,6 @@ func Execute(ctx context.Context, version string) {
rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure") rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure")
rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "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)") rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "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)")
rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "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") rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "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")
rootCmd.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13")
rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event") rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event")
rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)") rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)")
rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag") rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
@@ -79,19 +74,12 @@ func Execute(ctx context.Context, version string) {
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)") rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.") rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers") rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input")
rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.") rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "", "URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket)") rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.") rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")
rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.")
rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout") rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
rootCmd.PersistentFlags().BoolVarP(&input.noCacheServer, "no-cache-server", "", false, "Disable cache server")
rootCmd.PersistentFlags().StringVarP(&input.cacheServerPath, "cache-server-path", "", filepath.Join(CacheHomeDir, "actcache"), "Defines the path where the cache server stores caches.")
rootCmd.PersistentFlags().StringVarP(&input.cacheServerAddr, "cache-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the cache server binds.")
rootCmd.PersistentFlags().Uint16VarP(&input.cacheServerPort, "cache-server-port", "", 0, "Defines the port where the artifact server listens. 0 means a randomly available port.")
rootCmd.SetArgs(args()) rootCmd.SetArgs(args())
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
@@ -100,51 +88,26 @@ func Execute(ctx context.Context, version string) {
} }
func configLocations() []string { func configLocations() []string {
configFileName := ".actrc" home, err := homedir.Dir()
if err != nil {
log.Fatal(err)
}
// reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html // reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
var actrcXdg string var actrcXdg string
for _, fileName := range []string{"act/actrc", configFileName} { if xdg, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok && xdg != "" {
if foundConfig, err := xdg.SearchConfigFile(fileName); foundConfig != "" && err == nil { actrcXdg = filepath.Join(xdg, ".actrc")
actrcXdg = foundConfig } else {
break actrcXdg = filepath.Join(home, ".config", ".actrc")
}
} }
return []string{ return []string{
filepath.Join(UserHomeDir, configFileName), filepath.Join(home, ".actrc"),
actrcXdg, actrcXdg,
filepath.Join(".", configFileName), filepath.Join(".", ".actrc"),
} }
} }
var commonSocketPaths = []string{
"/var/run/docker.sock",
"/var/run/podman/podman.sock",
"$HOME/.colima/docker.sock",
"$XDG_RUNTIME_DIR/docker.sock",
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
// returns socket path or false if not found any
func socketLocation() (string, bool) {
if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists {
return dockerHost, true
}
for _, p := range commonSocketPaths {
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
if strings.HasPrefix(p, `\\.\`) {
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true
}
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true
}
}
return "", false
}
func args() []string { func args() []string {
actrc := configLocations() actrc := configLocations()
@@ -158,6 +121,15 @@ func args() []string {
} }
func bugReport(ctx context.Context, version string) error { func bugReport(ctx context.Context, version string) error {
var commonSocketPaths = []string{
"/var/run/docker.sock",
"/var/run/podman/podman.sock",
"$HOME/.colima/docker.sock",
"$XDG_RUNTIME_DIR/docker.sock",
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
sprintf := func(key, val string) string { sprintf := func(key, val string) string {
return fmt.Sprintf("%-24s%s\n", key, val) return fmt.Sprintf("%-24s%s\n", key, val)
} }
@@ -168,20 +140,19 @@ func bugReport(ctx context.Context, version string) error {
report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU())) report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU()))
var dockerHost string var dockerHost string
var exists bool if dockerHost = os.Getenv("DOCKER_HOST"); dockerHost == "" {
if dockerHost, exists = os.LookupEnv("DOCKER_HOST"); !exists { dockerHost = "DOCKER_HOST environment variable is unset/empty."
dockerHost = "DOCKER_HOST environment variable is not set"
} else if dockerHost == "" {
dockerHost = "DOCKER_HOST environment variable is empty."
} }
report += sprintf("Docker host:", dockerHost) report += sprintf("Docker host:", dockerHost)
report += fmt.Sprintln("Sockets found:") report += fmt.Sprintln("Sockets found:")
for _, p := range commonSocketPaths { for _, p := range commonSocketPaths {
if _, err := os.Lstat(os.ExpandEnv(p)); err != nil { if strings.HasPrefix(p, `$`) {
v := strings.Split(p, `/`)[0]
p = strings.Replace(p, v, os.Getenv(strings.TrimPrefix(v, `$`)), 1)
}
if _, err := os.Stat(p); err != nil {
continue continue
} else if _, err := os.Stat(os.ExpandEnv(p)); err != nil {
report += fmt.Sprintf("\t%s(broken)\n", p)
} else { } else {
report += fmt.Sprintf("\t%s\n", p) report += fmt.Sprintf("\t%s\n", p)
} }
@@ -270,57 +241,16 @@ func readArgsFile(file string, split bool) []string {
return args return args
} }
func setup(inputs *Input) func(*cobra.Command, []string) { func setupLogging(cmd *cobra.Command, _ []string) {
return func(cmd *cobra.Command, _ []string) { verbose, _ := cmd.Flags().GetBool("verbose")
verbose, _ := cmd.Flags().GetBool("verbose") if verbose {
if verbose { log.SetLevel(log.DebugLevel)
log.SetLevel(log.DebugLevel)
}
loadVersionNotices(cmd.Version)
} }
} }
func cleanup(inputs *Input) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, _ []string) {
displayNotices(inputs)
}
}
func parseEnvs(env []string, envs map[string]string) bool {
if env != nil {
for _, envVar := range env {
e := strings.SplitN(envVar, `=`, 2)
if len(e) == 2 {
envs[e[0]] = e[1]
} else {
envs[e[0]] = ""
}
}
return true
}
return false
}
func readYamlFile(file string) (map[string]string, error) {
content, err := os.ReadFile(file)
if err != nil {
return nil, err
}
ret := map[string]string{}
if err = yaml.Unmarshal(content, &ret); err != nil {
return nil, err
}
return ret, nil
}
func readEnvs(path string, envs map[string]string) bool { func readEnvs(path string, envs map[string]string) bool {
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
var env map[string]string env, err := godotenv.Read(path)
if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" {
env, err = readYamlFile(path)
} else {
env, err = godotenv.Read(path)
}
if err != nil { if err != nil {
log.Fatalf("Error loading from %s: %v", path, err) log.Fatalf("Error loading from %s: %v", path, err)
} }
@@ -332,36 +262,6 @@ func readEnvs(path string, envs map[string]string) bool {
return false return false
} }
func parseMatrix(matrix []string) map[string]map[string]bool {
// each matrix entry should be of the form - string:string
r := regexp.MustCompile(":")
matrixes := make(map[string]map[string]bool)
for _, m := range matrix {
matrix := r.Split(m, 2)
if len(matrix) < 2 {
log.Fatalf("Invalid matrix format. Failed to parse %s", m)
} else {
if _, ok := matrixes[matrix[0]]; !ok {
matrixes[matrix[0]] = make(map[string]bool)
}
matrixes[matrix[0]][matrix[1]] = true
}
}
return matrixes
}
func isDockerHostURI(daemonPath string) bool {
if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 {
scheme := daemonPath[:protoIndex]
if strings.IndexFunc(scheme, func(r rune) bool {
return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
}) == -1 {
return true
}
}
return false
}
//nolint:gocyclo //nolint:gocyclo
func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error { func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
@@ -373,54 +273,33 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
return bugReport(ctx, cmd.Version) return bugReport(ctx, cmd.Version)
} }
// Prefer DOCKER_HOST, don't override it
socketPath, hasDockerHost := os.LookupEnv("DOCKER_HOST")
if !hasDockerHost {
// a - in containerDaemonSocket means don't mount, preserve this value
// otherwise if input.containerDaemonSocket is a filepath don't use it as socketPath
skipMount := input.containerDaemonSocket == "-" || !isDockerHostURI(input.containerDaemonSocket)
if input.containerDaemonSocket != "" && !skipMount {
socketPath = input.containerDaemonSocket
} else {
socket, found := socketLocation()
if !found {
log.Errorln("daemon Docker Engine socket not found and containerDaemonSocket option was not set")
} else {
socketPath = socket
}
if !skipMount {
input.containerDaemonSocket = socketPath
}
}
os.Setenv("DOCKER_HOST", socketPath)
}
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" { if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
l := log.New() l := log.New()
l.SetFormatter(&log.TextFormatter{ l.SetFormatter(&log.TextFormatter{
DisableQuote: true, DisableQuote: true,
DisableTimestamp: true, DisableTimestamp: true,
}) })
l.Warnf(" \U000026A0 You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. \U000026A0 \n") l.Warnf(" \U000026A0 You are using Apple M1 chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. \U000026A0 \n")
} }
log.Debugf("Loading environment from %s", input.Envfile()) log.Debugf("Loading environment from %s", input.Envfile())
envs := make(map[string]string) envs := make(map[string]string)
_ = parseEnvs(input.envs, envs) if input.envs != nil {
for _, envVar := range input.envs {
e := strings.SplitN(envVar, `=`, 2)
if len(e) == 2 {
envs[e[0]] = e[1]
} else {
envs[e[0]] = ""
}
}
}
_ = readEnvs(input.Envfile(), envs) _ = readEnvs(input.Envfile(), envs)
log.Debugf("Loading action inputs from %s", input.Inputfile())
inputs := make(map[string]string)
_ = parseEnvs(input.inputs, inputs)
_ = readEnvs(input.Inputfile(), inputs)
log.Debugf("Loading secrets from %s", input.Secretfile()) log.Debugf("Loading secrets from %s", input.Secretfile())
secrets := newSecrets(input.secrets) secrets := newSecrets(input.secrets)
_ = readEnvs(input.Secretfile(), secrets) _ = readEnvs(input.Secretfile(), secrets)
matrixes := parseMatrix(input.matrix)
log.Debugf("Evaluated matrix inclusions: %v", matrixes)
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse) planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse)
if err != nil { if err != nil {
return err return err
@@ -450,7 +329,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
var filterPlan *model.Plan var filterPlan *model.Plan
// Determine the event name to be filtered // Determine the event name to be filtered
var filterEventName string var filterEventName string = ""
if len(args) > 0 { if len(args) > 0 {
log.Debugf("Using first passed in arguments event for filtering: %s", args[0]) log.Debugf("Using first passed in arguments event for filtering: %s", args[0])
@@ -462,35 +341,23 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
filterEventName = events[0] filterEventName = events[0]
} }
var plannerErr error
if jobID != "" { if jobID != "" {
log.Debugf("Preparing plan with a job: %s", jobID) log.Debugf("Preparing plan with a job: %s", jobID)
filterPlan, plannerErr = planner.PlanJob(jobID) filterPlan = planner.PlanJob(jobID)
} else if filterEventName != "" { } else if filterEventName != "" {
log.Debugf("Preparing plan for a event: %s", filterEventName) log.Debugf("Preparing plan for a event: %s", filterEventName)
filterPlan, plannerErr = planner.PlanEvent(filterEventName) filterPlan = planner.PlanEvent(filterEventName)
} else { } else {
log.Debugf("Preparing plan with all jobs") log.Debugf("Preparing plan with all jobs")
filterPlan, plannerErr = planner.PlanAll() filterPlan = planner.PlanAll()
}
if filterPlan == nil && plannerErr != nil {
return plannerErr
} }
if list { if list {
err = printList(filterPlan) return printList(filterPlan)
if err != nil {
return err
}
return plannerErr
} }
if graph { if graph {
err = drawGraph(filterPlan) return drawGraph(filterPlan)
if err != nil {
return err
}
return plannerErr
} }
// plan with triggered jobs // plan with triggered jobs
@@ -518,13 +385,10 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
// build the plan for this run // build the plan for this run
if jobID != "" { if jobID != "" {
log.Debugf("Planning job: %s", jobID) log.Debugf("Planning job: %s", jobID)
plan, plannerErr = planner.PlanJob(jobID) plan = planner.PlanJob(jobID)
} else { } else {
log.Debugf("Planning jobs for event: %s", eventName) log.Debugf("Planning jobs for event: %s", eventName)
plan, plannerErr = planner.PlanEvent(eventName) plan = planner.PlanEvent(eventName)
}
if plan == nil && plannerErr != nil {
return plannerErr
} }
// check to see if the main branch was defined // check to see if the main branch was defined
@@ -550,19 +414,6 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
input.platforms = readArgsFile(cfgLocations[0], true) input.platforms = readArgsFile(cfgLocations[0], true)
} }
} }
deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`."
if input.privileged {
log.Warnf(deprecationWarning, "privileged", "--privileged")
}
if len(input.usernsMode) > 0 {
log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode))
}
if len(input.containerCapAdd) > 0 {
log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd))
}
if len(input.containerCapDrop) > 0 {
log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop))
}
// run the plan // run the plan
config := &runner.Config{ config := &runner.Config{
@@ -579,7 +430,6 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
JSONLogger: input.jsonLogger, JSONLogger: input.jsonLogger,
Env: envs, Env: envs,
Secrets: secrets, Secrets: secrets,
Inputs: inputs,
Token: secrets["GITHUB_TOKEN"], Token: secrets["GITHUB_TOKEN"],
InsecureSecrets: input.insecureSecrets, InsecureSecrets: input.insecureSecrets,
Platforms: input.newPlatforms(), Platforms: input.newPlatforms(),
@@ -587,60 +437,37 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
UsernsMode: input.usernsMode, UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture, ContainerArchitecture: input.containerArchitecture,
ContainerDaemonSocket: input.containerDaemonSocket, ContainerDaemonSocket: input.containerDaemonSocket,
ContainerOptions: input.containerOptions,
UseGitIgnore: input.useGitIgnore, UseGitIgnore: input.useGitIgnore,
GitHubInstance: input.githubInstance, GitHubInstance: input.githubInstance,
ContainerCapAdd: input.containerCapAdd, ContainerCapAdd: input.containerCapAdd,
ContainerCapDrop: input.containerCapDrop, ContainerCapDrop: input.containerCapDrop,
AutoRemove: input.autoRemove, AutoRemove: input.autoRemove,
ArtifactServerPath: input.artifactServerPath, ArtifactServerPath: input.artifactServerPath,
ArtifactServerAddr: input.artifactServerAddr,
ArtifactServerPort: input.artifactServerPort, ArtifactServerPort: input.artifactServerPort,
NoSkipCheckout: input.noSkipCheckout, NoSkipCheckout: input.noSkipCheckout,
RemoteName: input.remoteName, RemoteName: input.remoteName,
ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom, ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom,
ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom, ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom,
Matrix: matrixes,
} }
r, err := runner.New(config) r, err := runner.New(config)
if err != nil { if err != nil {
return err return err
} }
cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort) cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort)
const cacheURLKey = "ACTIONS_CACHE_URL"
var cacheHandler *artifactcache.Handler
if !input.noCacheServer && envs[cacheURLKey] == "" {
var err error
cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerAddr, input.cacheServerPort, common.Logger(ctx))
if err != nil {
return err
}
envs[cacheURLKey] = cacheHandler.ExternalURL() + "/"
}
ctx = common.WithDryrun(ctx, input.dryrun) ctx = common.WithDryrun(ctx, input.dryrun)
if watch, err := cmd.Flags().GetBool("watch"); err != nil { if watch, err := cmd.Flags().GetBool("watch"); err != nil {
return err return err
} else if watch { } else if watch {
err = watchAndRun(ctx, r.NewPlanExecutor(plan)) return watchAndRun(ctx, r.NewPlanExecutor(plan))
if err != nil {
return err
}
return plannerErr
} }
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
cancel() cancel()
_ = cacheHandler.Close()
return nil return nil
}) })
err = executor(ctx) return executor(ctx)
if err != nil {
return err
}
return plannerErr
} }
} }
@@ -665,7 +492,7 @@ func defaultImageSurvey(actrc string) error {
case "Medium": case "Medium":
option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n" option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n"
case "Micro": case "Micro":
option = "-P ubuntu-latest=node:16-buster-slim\n-P ubuntu-22.04=node:16-bullseye-slim\n-P ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n" option = "-P ubuntu-latest=node:16-buster-slim\n-P -P ubuntu-22.04=node:16-bullseye-slim\n ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n"
} }
f, err := os.Create(actrc) f, err := os.Create(actrc)
@@ -688,47 +515,45 @@ func defaultImageSurvey(actrc string) error {
} }
func watchAndRun(ctx context.Context, fn common.Executor) error { func watchAndRun(ctx context.Context, fn common.Executor) error {
recurse := true
checkIntervalInSeconds := 2
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {
return err return err
} }
ignoreFile := filepath.Join(dir, ".gitignore") var ignore *gitignore.GitIgnore
ignore := &gitignore.GitIgnore{} if _, err := os.Stat(filepath.Join(dir, ".gitignore")); !os.IsNotExist(err) {
if info, err := os.Stat(ignoreFile); err == nil && !info.IsDir() { ignore, _ = gitignore.CompileIgnoreFile(filepath.Join(dir, ".gitignore"))
ignore, err = gitignore.CompileIgnoreFile(ignoreFile) } else {
if err != nil { ignore = &gitignore.GitIgnore{}
return fmt.Errorf("compile %q: %w", ignoreFile, err)
}
} }
folderWatcher := fswatch.NewFolderWatcher( folderWatcher := fswatch.NewFolderWatcher(
dir, dir,
true, recurse,
ignore.MatchesPath, ignore.MatchesPath,
2, // 2 seconds checkIntervalInSeconds,
) )
folderWatcher.Start() folderWatcher.Start()
defer folderWatcher.Stop()
// run once before watching go func() {
if err := fn(ctx); err != nil { for folderWatcher.IsRunning() {
return err if err = fn(ctx); err != nil {
} break
}
for folderWatcher.IsRunning() { log.Debugf("Watching %s for changes", dir)
log.Debugf("Watching %s for changes", dir) for changes := range folderWatcher.ChangeDetails() {
select { log.Debugf("%s", changes.String())
case <-ctx.Done(): if err = fn(ctx); err != nil {
return nil break
case changes := <-folderWatcher.ChangeDetails(): }
log.Debugf("%s", changes.String()) log.Debugf("Watching %s for changes", dir)
if err := fn(ctx); err != nil {
return err
} }
} }
} }()
<-ctx.Done()
return nil folderWatcher.Stop()
return err
} }

89
go.mod
View File

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

1123
go.sum

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,8 +0,0 @@
// Package artifactcache provides a cache handler for the runner.
//
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
//
// TODO: Authorization
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
package artifactcache

View File

@@ -1,488 +0,0 @@
package artifactcache
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
"github.com/timshannon/bolthold"
"go.etcd.io/bbolt"
"github.com/nektos/act/pkg/common"
)
const (
urlBase = "/_apis/artifactcache"
)
type Handler struct {
db *bolthold.Store
storage *Storage
router *httprouter.Router
listener net.Listener
server *http.Server
logger logrus.FieldLogger
gcing int32 // TODO: use atomic.Bool when we can use Go 1.19
gcAt time.Time
outboundIP string
}
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
h := &Handler{}
if logger == nil {
discard := logrus.New()
discard.Out = io.Discard
logger = discard
}
logger = logger.WithField("module", "artifactcache")
h.logger = logger
if dir == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
dir = filepath.Join(home, ".cache", "actcache")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
db, err := bolthold.Open(filepath.Join(dir, "bolt.db"), 0o644, &bolthold.Options{
Encoder: json.Marshal,
Decoder: json.Unmarshal,
Options: &bbolt.Options{
Timeout: 5 * time.Second,
NoGrowSync: bbolt.DefaultOptions.NoGrowSync,
FreelistType: bbolt.DefaultOptions.FreelistType,
},
})
if err != nil {
return nil, err
}
h.db = db
storage, err := NewStorage(filepath.Join(dir, "cache"))
if err != nil {
return nil, err
}
h.storage = storage
if outboundIP != "" {
h.outboundIP = outboundIP
} else if ip := common.GetOutboundIP(); ip == nil {
return nil, fmt.Errorf("unable to determine outbound IP address")
} else {
h.outboundIP = ip.String()
}
router := httprouter.New()
router.GET(urlBase+"/cache", h.middleware(h.find))
router.POST(urlBase+"/caches", h.middleware(h.reserve))
router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload))
router.POST(urlBase+"/caches/:id", h.middleware(h.commit))
router.GET(urlBase+"/artifacts/:id", h.middleware(h.get))
router.POST(urlBase+"/clean", h.middleware(h.clean))
h.router = router
h.gcCache()
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
if err != nil {
return nil, err
}
server := &http.Server{
ReadHeaderTimeout: 2 * time.Second,
Handler: router,
}
go func() {
if err := server.Serve(listener); err != nil && errors.Is(err, net.ErrClosed) {
logger.Errorf("http serve: %v", err)
}
}()
h.listener = listener
h.server = server
return h, nil
}
func (h *Handler) ExternalURL() string {
// TODO: make the external url configurable if necessary
return fmt.Sprintf("http://%s:%d",
h.outboundIP,
h.listener.Addr().(*net.TCPAddr).Port)
}
func (h *Handler) Close() error {
if h == nil {
return nil
}
var retErr error
if h.server != nil {
err := h.server.Close()
if err != nil {
retErr = err
}
h.server = nil
}
if h.listener != nil {
err := h.listener.Close()
if errors.Is(err, net.ErrClosed) {
err = nil
}
if err != nil {
retErr = err
}
h.listener = nil
}
if h.db != nil {
err := h.db.Close()
if err != nil {
retErr = err
}
h.db = nil
}
return retErr
}
// GET /_apis/artifactcache/cache
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
keys := strings.Split(r.URL.Query().Get("keys"), ",")
// cache keys are case insensitive
for i, key := range keys {
keys[i] = strings.ToLower(key)
}
version := r.URL.Query().Get("version")
cache, err := h.findCache(keys, version)
if err != nil {
h.responseJSON(w, r, 500, err)
return
}
if cache == nil {
h.responseJSON(w, r, 204)
return
}
if ok, err := h.storage.Exist(cache.ID); err != nil {
h.responseJSON(w, r, 500, err)
return
} else if !ok {
_ = h.db.Delete(cache.ID, cache)
h.responseJSON(w, r, 204)
return
}
h.responseJSON(w, r, 200, map[string]any{
"result": "hit",
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
"cacheKey": cache.Key,
})
}
// POST /_apis/artifactcache/caches
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
api := &Request{}
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
h.responseJSON(w, r, 400, err)
return
}
// cache keys are case insensitive
api.Key = strings.ToLower(api.Key)
cache := api.ToCache()
cache.FillKeyVersionHash()
if err := h.db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil {
if !errors.Is(err, bolthold.ErrNotFound) {
h.responseJSON(w, r, 500, err)
return
}
} else {
h.responseJSON(w, r, 400, fmt.Errorf("already exist"))
return
}
now := time.Now().Unix()
cache.CreatedAt = now
cache.UsedAt = now
if err := h.db.Insert(bolthold.NextSequence(), cache); err != nil {
h.responseJSON(w, r, 500, err)
return
}
// write back id to db
if err := h.db.Update(cache.ID, cache); err != nil {
h.responseJSON(w, r, 500, err)
return
}
h.responseJSON(w, r, 200, map[string]any{
"cacheId": cache.ID,
})
}
// PATCH /_apis/artifactcache/caches/:id
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
return
}
cache := &Cache{}
if err := h.db.Get(id, cache); err != nil {
if errors.Is(err, bolthold.ErrNotFound) {
h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
return
}
h.responseJSON(w, r, 500, err)
return
}
if cache.Complete {
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
}
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
if err != nil {
h.responseJSON(w, r, 400, err)
return
}
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
h.responseJSON(w, r, 500, err)
}
h.useCache(id)
h.responseJSON(w, r, 200)
}
// POST /_apis/artifactcache/caches/:id
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
return
}
cache := &Cache{}
if err := h.db.Get(id, cache); err != nil {
if errors.Is(err, bolthold.ErrNotFound) {
h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
return
}
h.responseJSON(w, r, 500, err)
return
}
if cache.Complete {
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
}
if err := h.storage.Commit(cache.ID, cache.Size); err != nil {
h.responseJSON(w, r, 500, err)
return
}
cache.Complete = true
if err := h.db.Update(cache.ID, cache); err != nil {
h.responseJSON(w, r, 500, err)
return
}
h.responseJSON(w, r, 200)
}
// GET /_apis/artifactcache/artifacts/:id
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
return
}
h.useCache(id)
h.storage.Serve(w, r, uint64(id))
}
// POST /_apis/artifactcache/clean
func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// TODO: don't support force deleting cache entries
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
h.responseJSON(w, r, 200)
}
func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
h.logger.Debugf("%s %s", r.Method, r.RequestURI)
handler(w, r, params)
go h.gcCache()
}
}
// if not found, return (nil, nil) instead of an error.
func (h *Handler) findCache(keys []string, version string) (*Cache, error) {
if len(keys) == 0 {
return nil, nil
}
key := keys[0] // the first key is for exact match.
cache := &Cache{
Key: key,
Version: version,
}
cache.FillKeyVersionHash()
if err := h.db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil {
if !errors.Is(err, bolthold.ErrNotFound) {
return nil, err
}
} else if cache.Complete {
return cache, nil
}
stop := fmt.Errorf("stop")
for _, prefix := range keys[1:] {
found := false
if err := h.db.ForEach(bolthold.Where("Key").Ge(prefix).And("Version").Eq(version).SortBy("Key"), func(v *Cache) error {
if !strings.HasPrefix(v.Key, prefix) {
return stop
}
if v.Complete {
cache = v
found = true
return stop
}
return nil
}); err != nil {
if !errors.Is(err, stop) {
return nil, err
}
}
if found {
return cache, nil
}
}
return nil, nil
}
func (h *Handler) useCache(id int64) {
cache := &Cache{}
if err := h.db.Get(id, cache); err != nil {
return
}
cache.UsedAt = time.Now().Unix()
_ = h.db.Update(cache.ID, cache)
}
func (h *Handler) gcCache() {
if atomic.LoadInt32(&h.gcing) != 0 {
return
}
if !atomic.CompareAndSwapInt32(&h.gcing, 0, 1) {
return
}
defer atomic.StoreInt32(&h.gcing, 0)
if time.Since(h.gcAt) < time.Hour {
h.logger.Debugf("skip gc: %v", h.gcAt.String())
return
}
h.gcAt = time.Now()
h.logger.Debugf("gc: %v", h.gcAt.String())
const (
keepUsed = 30 * 24 * time.Hour
keepUnused = 7 * 24 * time.Hour
keepTemp = 5 * time.Minute
)
var caches []*Cache
if err := h.db.Find(&caches, bolthold.Where("UsedAt").Lt(time.Now().Add(-keepTemp).Unix())); err != nil {
h.logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
if cache.Complete {
continue
}
h.storage.Remove(cache.ID)
if err := h.db.Delete(cache.ID, cache); err != nil {
h.logger.Warnf("delete cache: %v", err)
continue
}
h.logger.Infof("deleted cache: %+v", cache)
}
}
caches = caches[:0]
if err := h.db.Find(&caches, bolthold.Where("UsedAt").Lt(time.Now().Add(-keepUnused).Unix())); err != nil {
h.logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
h.storage.Remove(cache.ID)
if err := h.db.Delete(cache.ID, cache); err != nil {
h.logger.Warnf("delete cache: %v", err)
continue
}
h.logger.Infof("deleted cache: %+v", cache)
}
}
caches = caches[:0]
if err := h.db.Find(&caches, bolthold.Where("CreatedAt").Lt(time.Now().Add(-keepUsed).Unix())); err != nil {
h.logger.Warnf("find caches: %v", err)
} else {
for _, cache := range caches {
h.storage.Remove(cache.ID)
if err := h.db.Delete(cache.ID, cache); err != nil {
h.logger.Warnf("delete cache: %v", err)
continue
}
h.logger.Infof("deleted cache: %+v", cache)
}
}
}
func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int, v ...any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var data []byte
if len(v) == 0 || v[0] == nil {
data, _ = json.Marshal(struct{}{})
} else if err, ok := v[0].(error); ok {
h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
data, _ = json.Marshal(map[string]any{
"error": err.Error(),
})
} else {
data, _ = json.Marshal(v[0])
}
w.WriteHeader(code)
_, _ = w.Write(data)
}
func parseContentRange(s string) (int64, int64, error) {
// support the format like "bytes 11-22/*" only
s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
s1, s2, _ := strings.Cut(s, "-")
start, err := strconv.ParseInt(s1, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
}
stop, err := strconv.ParseInt(s2, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse %q: %w", s, err)
}
return start, stop, nil
}

View File

@@ -1,469 +0,0 @@
package artifactcache
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
)
func TestHandler(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, nil)
require.NoError(t, err)
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
defer func() {
t.Run("inpect db", func(t *testing.T) {
require.NoError(t, handler.db.Bolt().View(func(tx *bbolt.Tx) error {
return tx.Bucket([]byte("Cache")).ForEach(func(k, v []byte) error {
t.Logf("%s: %s", k, v)
return nil
})
}))
})
t.Run("close", func(t *testing.T) {
require.NoError(t, handler.Close())
assert.Nil(t, handler.server)
assert.Nil(t, handler.listener)
assert.Nil(t, handler.db)
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
assert.Error(t, err)
})
}()
t.Run("get not exist", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
})
t.Run("reserve and upload", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
uploadCacheNormally(t, base, key, version, content)
})
t.Run("clean", func(t *testing.T) {
resp, err := http.Post(fmt.Sprintf("%s/clean", base), "", nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
})
t.Run("reserve with bad request", func(t *testing.T) {
body := []byte(`invalid json`)
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
})
t.Run("duplicate reserve", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
}
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("upload with bad id", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/invalid_id", base), bytes.NewReader(nil))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
})
t.Run("upload without reserve", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, 1000), bytes.NewReader(nil))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
})
t.Run("upload with complete", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
var id uint64
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("upload with invalid range", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
var id uint64
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes xx-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("commit with bad id", func(t *testing.T) {
{
resp, err := http.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("commit with not exist id", func(t *testing.T) {
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("duplicate commit", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
var id uint64
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("commit early", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
var id uint64
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content[:50]))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-59/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
assert.Equal(t, 500, resp.StatusCode)
}
})
t.Run("get with bad id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/invalid_id", base))
require.NoError(t, err)
require.Equal(t, 400, resp.StatusCode)
})
t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
})
t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
})
t.Run("get with multiple keys", func(t *testing.T) {
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
key := strings.ToLower(t.Name())
keys := [3]string{
key + "_a",
key + "_a_b",
key + "_a_b_c",
}
contents := [3][]byte{
make([]byte, 100),
make([]byte, 200),
make([]byte, 300),
}
for i := range contents {
_, err := rand.Read(contents[i])
require.NoError(t, err)
uploadCacheNormally(t, base, keys[i], version, contents[i])
}
reqKeys := strings.Join([]string{
key + "_a_b_x",
key + "_a_b",
key + "_a",
}, ",")
var archiveLocation string
{
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
got := struct {
Result string `json:"result"`
ArchiveLocation string `json:"archiveLocation"`
CacheKey string `json:"cacheKey"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, "hit", got.Result)
assert.Equal(t, keys[1], got.CacheKey)
archiveLocation = got.ArchiveLocation
}
{
resp, err := http.Get(archiveLocation) //nolint:gosec
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
got, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, contents[1], got)
}
})
t.Run("case insensitive", func(t *testing.T) {
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
key := strings.ToLower(t.Name())
content := make([]byte, 100)
_, err := rand.Read(content)
require.NoError(t, err)
uploadCacheNormally(t, base, key+"_ABC", version, content)
{
reqKey := key + "_aBc"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
got := struct {
Result string `json:"result"`
ArchiveLocation string `json:"archiveLocation"`
CacheKey string `json:"cacheKey"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, "hit", got.Result)
assert.Equal(t, key+"_abc", got.CacheKey)
}
})
}
func uploadCacheNormally(t *testing.T, base, key, version string, content []byte) {
var id uint64
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: int64(len(content)),
})
require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
{
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
var archiveLocation string
{
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
got := struct {
Result string `json:"result"`
ArchiveLocation string `json:"archiveLocation"`
CacheKey string `json:"cacheKey"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, "hit", got.Result)
assert.Equal(t, strings.ToLower(key), got.CacheKey)
archiveLocation = got.ArchiveLocation
}
{
resp, err := http.Get(archiveLocation) //nolint:gosec
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
got, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, content, got)
}
}

View File

@@ -1,38 +0,0 @@
package artifactcache
import (
"crypto/sha256"
"fmt"
)
type Request struct {
Key string `json:"key" `
Version string `json:"version"`
Size int64 `json:"cacheSize"`
}
func (c *Request) ToCache() *Cache {
if c == nil {
return nil
}
return &Cache{
Key: c.Key,
Version: c.Version,
Size: c.Size,
}
}
type Cache struct {
ID uint64 `json:"id" boltholdKey:"ID"`
Key string `json:"key" boltholdIndex:"Key"`
Version string `json:"version" boltholdIndex:"Version"`
KeyVersionHash string `json:"keyVersionHash" boltholdUnique:"KeyVersionHash"`
Size int64 `json:"cacheSize"`
Complete bool `json:"complete"`
UsedAt int64 `json:"usedAt" boltholdIndex:"UsedAt"`
CreatedAt int64 `json:"createdAt" boltholdIndex:"CreatedAt"`
}
func (c *Cache) FillKeyVersionHash() {
c.KeyVersionHash = fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s:%s", c.Key, c.Version))))
}

View File

@@ -1,126 +0,0 @@
package artifactcache
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)
type Storage struct {
rootDir string
}
func NewStorage(rootDir string) (*Storage, error) {
if err := os.MkdirAll(rootDir, 0o755); err != nil {
return nil, err
}
return &Storage{
rootDir: rootDir,
}, nil
}
func (s *Storage) Exist(id uint64) (bool, error) {
name := s.filename(id)
if _, err := os.Stat(name); os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *Storage) Write(id uint64, offset int64, reader io.Reader) error {
name := s.tempName(id, offset)
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
return err
}
file, err := os.Create(name)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, reader)
return err
}
func (s *Storage) Commit(id uint64, size int64) error {
defer func() {
_ = os.RemoveAll(s.tempDir(id))
}()
name := s.filename(id)
tempNames, err := s.tempNames(id)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
return err
}
file, err := os.Create(name)
if err != nil {
return err
}
defer file.Close()
var written int64
for _, v := range tempNames {
f, err := os.Open(v)
if err != nil {
return err
}
n, err := io.Copy(file, f)
_ = f.Close()
if err != nil {
return err
}
written += n
}
if written != size {
_ = file.Close()
_ = os.Remove(name)
return fmt.Errorf("broken file: %v != %v", written, size)
}
return nil
}
func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id uint64) {
name := s.filename(id)
http.ServeFile(w, r, name)
}
func (s *Storage) Remove(id uint64) {
_ = os.Remove(s.filename(id))
_ = os.RemoveAll(s.tempDir(id))
}
func (s *Storage) filename(id uint64) string {
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
}
func (s *Storage) tempDir(id uint64) string {
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
}
func (s *Storage) tempName(id uint64, offset int64) string {
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
}
func (s *Storage) tempNames(id uint64) ([]string, error) {
dir := s.tempDir(id)
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var names []string
for _, v := range files {
if !v.IsDir() {
names = append(names, filepath.Join(dir, v.Name()))
}
}
return names, nil
}

View File

@@ -1,30 +0,0 @@
# Copied from https://github.com/actions/cache#example-cache-workflow
name: Caching Primes
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: env
- uses: actions/checkout@v3
- name: Cache Primes
id: cache-primes
uses: actions/cache@v3
with:
path: prime-numbers
key: ${{ runner.os }}-primes-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-primes
${{ runner.os }}
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
run: cat /proc/sys/kernel/random/uuid > prime-numbers
- name: Use Prime Numbers
run: cat prime-numbers

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -25,3 +25,24 @@ func Logger(ctx context.Context) logrus.FieldLogger {
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context { func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
return context.WithValue(ctx, loggerContextKeyVal, logger) return context.WithValue(ctx, loggerContextKeyVal, logger)
} }
type loggerHookKey string
const loggerHookKeyVal = loggerHookKey("logrus.Hook")
// LoggerHook returns the appropriate logger hook for current context
// the hook affects job logger, not global logger
func LoggerHook(ctx context.Context) logrus.Hook {
val := ctx.Value(loggerHookKeyVal)
if val != nil {
if hook, ok := val.(logrus.Hook); ok {
return hook
}
}
return nil
}
// WithLoggerHook adds a value to the context for the logger hook
func WithLoggerHook(ctx context.Context, hook logrus.Hook) context.Context {
return context.WithValue(ctx, loggerHookKeyVal, hook)
}

View File

@@ -2,74 +2,20 @@ package common
import ( import (
"net" "net"
"sort"
"strings" log "github.com/sirupsen/logrus"
) )
// GetOutboundIP returns an outbound IP address of this machine. // https://stackoverflow.com/a/37382208
// It tries to access the internet and returns the local IP address of the connection. // Get preferred outbound ip of this machine
// If the machine cannot access the internet, it returns a preferred IP address from network interfaces.
// It returns nil if no IP address is found.
func GetOutboundIP() net.IP { func GetOutboundIP() net.IP {
// See https://stackoverflow.com/a/37382208
conn, err := net.Dial("udp", "8.8.8.8:80") conn, err := net.Dial("udp", "8.8.8.8:80")
if err == nil { if err != nil {
defer conn.Close() log.Fatal(err)
return conn.LocalAddr().(*net.UDPAddr).IP
} }
defer conn.Close()
// So the machine cannot access the internet. Pick an IP address from network interfaces. localAddr := conn.LocalAddr().(*net.UDPAddr)
if ifs, err := net.Interfaces(); err == nil {
type IP struct {
net.IP
net.Interface
}
var ips []IP
for _, i := range ifs {
if addrs, err := i.Addrs(); err == nil {
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip.IsGlobalUnicast() {
ips = append(ips, IP{ip, i})
}
}
}
}
if len(ips) > 1 {
sort.Slice(ips, func(i, j int) bool {
ifi := ips[i].Interface
ifj := ips[j].Interface
// ethernet is preferred return localAddr.IP
if vi, vj := strings.HasPrefix(ifi.Name, "e"), strings.HasPrefix(ifj.Name, "e"); vi != vj {
return vi
}
ipi := ips[i].IP
ipj := ips[j].IP
// IPv4 is preferred
if vi, vj := ipi.To4() != nil, ipj.To4() != nil; vi != vj {
return vi
}
// en0 is preferred to en1
if ifi.Name != ifj.Name {
return ifi.Name < ifj.Name
}
// fallback
return ipi.String() < ipj.String()
})
return ips[0].IP
}
}
return nil
} }

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
@@ -24,6 +22,59 @@ type dockerMessage struct {
const logPrefix = " \U0001F433 " const logPrefix = " \U0001F433 "
/*
func logDockerOutput(ctx context.Context, dockerResponse io.Reader) {
logger := common.Logger(ctx)
if entry, ok := logger.(*logrus.Entry); ok {
w := entry.Writer()
_, err := stdcopy.StdCopy(w, w, dockerResponse)
if err != nil {
logrus.Error(err)
}
} else if lgr, ok := logger.(*logrus.Logger); ok {
w := lgr.Writer()
_, err := stdcopy.StdCopy(w, w, dockerResponse)
if err != nil {
logrus.Error(err)
}
} else {
logrus.Errorf("Unable to get writer from logger (type=%T)", logger)
}
}
*/
/*
func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) {
/*
out := os.Stdout
go func() {
<-ctx.Done()
//fmt.Println()
}()
_, err := io.Copy(out, dockerResponse)
if err != nil {
logrus.Error(err)
}
* /
logger := common.Logger(ctx)
reader := bufio.NewReader(dockerResponse)
for {
if ctx.Err() != nil {
break
}
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
logger.Debugf("%s\n", line)
}
}
*/
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error { func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
if dockerResponse == nil { if dockerResponse == nil {
return nil return nil

View File

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

View File

@@ -1,9 +1,8 @@
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows))
package container package container
import ( import (
"archive/tar" "archive/tar"
"bufio"
"bytes" "bytes"
"context" "context"
"errors" "errors"
@@ -39,8 +38,55 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
) )
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string
Username string
Password string
Entrypoint []string
Cmd []string
WorkingDir string
Env []string
Binds []string
Mounts map[string]string
Name string
Stdout io.Writer
Stderr io.Writer
NetworkMode string
Privileged bool
UsernsMode string
Platform string
Options string
AutoRemove bool
}
// FileEntry is a file to copy to a container
type FileEntry struct {
Name string
Mode int64
Body string
}
// Container for managing docker run containers
type Container interface {
Create(capAdd []string, capDrop []string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor
CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor
Exec(command []string, env map[string]string, user, workdir string) common.Executor
UpdateFromEnv(srcPath string, env *map[string]string) common.Executor
UpdateFromImageEnv(env *map[string]string) common.Executor
UpdateFromPath(env *map[string]string) common.Executor
Remove() common.Executor
Close() common.Executor
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
}
// NewContainer creates a reference to a container // NewContainer creates a reference to a container
func NewContainer(input *NewContainerInput) ExecutionsEnvironment { func NewContainer(input *NewContainerInput) Container {
cr := new(containerReference) cr := new(containerReference)
cr.input = input cr.input = input
return cr return cr
@@ -144,13 +190,17 @@ func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath s
} }
func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
return parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun) return cr.extractEnv(srcPath, env).IfNot(common.Dryrun)
} }
func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor { func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor {
return cr.extractFromImageEnv(env).IfNot(common.Dryrun) return cr.extractFromImageEnv(env).IfNot(common.Dryrun)
} }
func (cr *containerReference) UpdateFromPath(env *map[string]string) common.Executor {
return cr.extractPath(env).IfNot(common.Dryrun)
}
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor { func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir), common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
@@ -185,10 +235,12 @@ type containerReference struct {
input *NewContainerInput input *NewContainerInput
UID int UID int
GID int GID int
LinuxContainerEnvironmentExtensions
} }
func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) { func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) {
// TODO: this should maybe need to be a global option, not hidden in here?
// though i'm not sure how that works out when there's another Executor :D
// I really would like something that works on OSX native for eg
dockerHost := os.Getenv("DOCKER_HOST") dockerHost := os.Getenv("DOCKER_HOST")
if strings.HasPrefix(dockerHost, "ssh://") { if strings.HasPrefix(dockerHost, "ssh://") {
@@ -345,12 +397,6 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err)
} }
if len(copts.netMode.Value()) == 0 {
if err = copts.netMode.Set("host"); err != nil {
return nil, nil, fmt.Errorf("Cannot parse networkmode=host. This is an internal error and should not happen: '%w'", err)
}
}
containerConfig, err := parse(flags, copts, "") containerConfig, err := parse(flags, copts, "")
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err)
@@ -366,16 +412,10 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig) logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
hostConfig.Binds = append(hostConfig.Binds, containerConfig.HostConfig.Binds...)
hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...)
binds := hostConfig.Binds
mounts := hostConfig.Mounts
err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride) err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err)
} }
hostConfig.Binds = binds
hostConfig.Mounts = mounts
logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig) logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
return config, hostConfig, nil return config, hostConfig, nil
@@ -437,6 +477,7 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
NetworkMode: container.NetworkMode(input.NetworkMode), NetworkMode: container.NetworkMode(input.NetworkMode),
Privileged: input.Privileged, Privileged: input.Privileged,
UsernsMode: container.UsernsMode(input.UsernsMode), UsernsMode: container.UsernsMode(input.UsernsMode),
AutoRemove: input.AutoRemove,
} }
logger.Debugf("Common container.HostConfig ==> %+v", hostConfig) logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
@@ -458,6 +499,59 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
} }
} }
var singleLineEnvPattern, multiLineEnvPattern *regexp.Regexp
func (cr *containerReference) extractEnv(srcPath string, env *map[string]string) common.Executor {
if singleLineEnvPattern == nil {
// Single line pattern matches:
// SOME_VAR=data=moredata
// SOME_VAR=datamoredata
singleLineEnvPattern = regexp.MustCompile(`^([^=]*)\=(.*)$`)
multiLineEnvPattern = regexp.MustCompile(`^([^<]+)<<([\w-]+)$`)
}
localEnv := *env
return func(ctx context.Context) error {
envTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath)
if err != nil {
return nil
}
defer envTar.Close()
reader := tar.NewReader(envTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return fmt.Errorf("failed to read tar archive: %w", err)
}
s := bufio.NewScanner(reader)
multiLineEnvKey := ""
multiLineEnvDelimiter := ""
multiLineEnvContent := ""
for s.Scan() {
line := s.Text()
if singleLineEnv := singleLineEnvPattern.FindStringSubmatch(line); singleLineEnv != nil {
localEnv[singleLineEnv[1]] = singleLineEnv[2]
}
if line == multiLineEnvDelimiter {
localEnv[multiLineEnvKey] = multiLineEnvContent
multiLineEnvKey, multiLineEnvDelimiter, multiLineEnvContent = "", "", ""
}
if multiLineEnvKey != "" && multiLineEnvDelimiter != "" {
if multiLineEnvContent != "" {
multiLineEnvContent += "\n"
}
multiLineEnvContent += line
}
if multiLineEnvStart := multiLineEnvPattern.FindStringSubmatch(line); multiLineEnvStart != nil {
multiLineEnvKey = multiLineEnvStart[1]
multiLineEnvDelimiter = multiLineEnvStart[2]
}
}
env = &localEnv
return nil
}
}
func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor { func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor {
envMap := *env envMap := *env
return func(ctx context.Context) error { return func(ctx context.Context) error {
@@ -490,6 +584,31 @@ func (cr *containerReference) extractFromImageEnv(env *map[string]string) common
} }
} }
func (cr *containerReference) extractPath(env *map[string]string) common.Executor {
localEnv := *env
return func(ctx context.Context) error {
pathTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, localEnv["GITHUB_PATH"])
if err != nil {
return fmt.Errorf("failed to copy from container: %w", err)
}
defer pathTar.Close()
reader := tar.NewReader(pathTar)
_, err = reader.Next()
if err != nil && err != io.EOF {
return fmt.Errorf("failed to read tar archive: %w", err)
}
s := bufio.NewScanner(reader)
for s.Scan() {
line := s.Text()
localEnv["PATH"] = fmt.Sprintf("%s:%s", line, localEnv["PATH"])
}
env = &localEnv
return nil
}
}
func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor { func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@@ -586,7 +705,7 @@ func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Exe
} }
exp := regexp.MustCompile(`\d+\n`) exp := regexp.MustCompile(`\d+\n`)
found := exp.FindString(sid) found := exp.FindString(sid)
id, err := strconv.ParseInt(strings.TrimSpace(found), 10, 32) id, err := strconv.ParseInt(found[:len(found)-1], 10, 32)
if err != nil { if err != nil {
return nil return nil
} }

View File

@@ -163,6 +163,3 @@ func TestDockerExecFailure(t *testing.T) {
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
} }
// Type assert containerReference implements ExecutionsEnvironment
var _ ExecutionsEnvironment = &containerReference{}

View File

@@ -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
}
}

View File

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

View File

@@ -1,15 +0,0 @@
package container
import "context"
type ExecutionsEnvironment interface {
Container
ToContainerPath(string) string
GetActPath() string
GetPathVariableName() string
DefaultPathVariable() string
JoinPathVariable(...string) string
GetRunnerContext(ctx context.Context) map[string]interface{}
// On windows PATH and Path are the same key
IsEnvironmentCaseInsensitive() bool
}

View File

@@ -59,29 +59,6 @@ func (tc tarCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string,
return nil return nil
} }
type copyCollector struct {
DstDir string
}
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 {
return err
}
if f == nil {
return os.Symlink(linkName, fdestpath)
}
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode())
if err != nil {
return err
}
defer df.Close()
if _, err := io.Copy(df, f); err != nil {
return err
}
return nil
}
type fileCollector struct { type fileCollector struct {
Ignorer gitignore.Matcher Ignorer gitignore.Matcher
SrcPath string SrcPath string

View File

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

View File

@@ -1,425 +0,0 @@
package container
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"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"
)
type HostEnvironment struct {
Path string
TmpDir string
ToolCache string
Workdir string
ActPath string
CleanUp func()
StdOut io.Writer
}
func (e *HostEnvironment) Create(capAdd []string, capDrop []string) common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func (e *HostEnvironment) Close() common.Executor {
return func(ctx context.Context) error {
return nil
}
}
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 {
return err
}
if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil {
return err
}
}
return nil
}
}
func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
srcPrefix := filepath.Dir(srcPath)
if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
srcPrefix += string(filepath.Separator)
}
logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
var ignorer gitignore.Matcher
if useGitIgnore {
ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
if err != nil {
logger.Debugf("Error loading .gitignore: %v", err)
}
ignorer = gitignore.NewMatcher(ps)
}
fc := &fileCollector{
Fs: &defaultFs{},
Ignorer: ignorer,
SrcPath: srcPath,
SrcPrefix: srcPrefix,
Handler: &copyCollector{
DstDir: destPath,
},
}
return filepath.Walk(srcPath, fc.collectFiles(ctx, []string{}))
}
}
func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
defer tw.Close()
srcPath = filepath.Clean(srcPath)
fi, err := os.Lstat(srcPath)
if err != nil {
return nil, err
}
tc := &tarCollector{
TarWriter: tw,
}
if fi.IsDir() {
srcPrefix := filepath.Dir(srcPath)
if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
srcPrefix += string(filepath.Separator)
}
fc := &fileCollector{
Fs: &defaultFs{},
SrcPath: srcPath,
SrcPrefix: srcPrefix,
Handler: tc,
}
err = filepath.Walk(srcPath, fc.collectFiles(ctx, []string{}))
if err != nil {
return nil, err
}
} else {
var f io.ReadCloser
var linkname string
if fi.Mode()&fs.ModeSymlink != 0 {
linkname, err = os.Readlink(srcPath)
if err != nil {
return nil, err
}
} else {
f, err = os.Open(srcPath)
if err != nil {
return nil, err
}
defer f.Close()
}
err := tc.WriteFile(fi.Name(), fi, linkname, f)
if err != nil {
return nil, err
}
}
return io.NopCloser(buf), nil
}
func (e *HostEnvironment) Pull(forcePull bool) common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func (e *HostEnvironment) Start(attach bool) common.Executor {
return func(ctx context.Context) error {
return nil
}
}
type ptyWriter struct {
Out io.Writer
AutoStop bool
dirtyLine bool
}
func (w *ptyWriter) Write(buf []byte) (int, error) {
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
n, err := w.Out.Write(buf[:len(buf)-1])
if err != nil {
return n, err
}
if w.dirtyLine || len(buf) > 1 && buf[len(buf)-2] != '\n' {
_, _ = w.Out.Write([]byte("\n"))
return n, io.EOF
}
return n, io.EOF
}
w.dirtyLine = strings.LastIndex(string(buf), "\n") < len(buf)-1
return w.Out.Write(buf)
}
type localEnv struct {
env map[string]string
}
func (l *localEnv) Getenv(name string) string {
if runtime.GOOS == "windows" {
for k, v := range l.env {
if strings.EqualFold(name, k) {
return v
}
}
return ""
}
return l.env[name]
}
func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) {
f, err := lookpath.LookPath2(cmd, &localEnv{env: env})
if err != nil {
err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH"
if _, _err := writer.Write([]byte(err + "\n")); _err != nil {
return "", fmt.Errorf("%v: %w", err, _err)
}
return "", errors.New(err)
}
return f, nil
}
func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) {
ppty, tty, err := openPty()
if err != nil {
return nil, nil, err
}
if term.IsTerminal(int(tty.Fd())) {
_, err := term.MakeRaw(int(tty.Fd()))
if err != nil {
ppty.Close()
tty.Close()
return nil, nil, err
}
}
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
cmd.SysProcAttr = getSysProcAttr(cmdline, true)
return ppty, tty, nil
}
func writeKeepAlive(ppty io.Writer) {
c := 1
var err error
for c == 1 && err == nil {
c, err = ppty.Write([]byte{4})
<-time.After(time.Second)
}
}
func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFunc) {
defer func() {
finishLog()
}()
if _, err := io.Copy(writer, ppty); err != nil {
return
}
}
func (e *HostEnvironment) UpdateFromImageEnv(env *map[string]string) common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func getEnvListFromMap(env map[string]string) []string {
envList := make([]string, 0)
for k, v := range env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
}
return envList
}
func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, user, workdir string) error {
envList := getEnvListFromMap(env)
var wd string
if workdir != "" {
if filepath.IsAbs(workdir) {
wd = workdir
} else {
wd = filepath.Join(e.Path, workdir)
}
} else {
wd = e.Path
}
f, err := lookupPathHost(command[0], env, e.StdOut)
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, f)
cmd.Path = f
cmd.Args = command
cmd.Stdin = nil
cmd.Stdout = e.StdOut
cmd.Env = envList
cmd.Stderr = e.StdOut
cmd.Dir = wd
cmd.SysProcAttr = getSysProcAttr(cmdline, false)
var ppty *os.File
var tty *os.File
defer func() {
if ppty != nil {
ppty.Close()
}
if tty != nil {
tty.Close()
}
}()
if true /* allocate Terminal */ {
var err error
ppty, tty, err = setupPty(cmd, cmdline)
if err != nil {
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
}
}
writer := &ptyWriter{Out: e.StdOut}
logctx, finishLog := context.WithCancel(context.Background())
if ppty != nil {
go copyPtyOutput(writer, ppty, finishLog)
} else {
finishLog()
}
if ppty != nil {
go writeKeepAlive(ppty)
}
err = cmd.Run()
if err != nil {
return err
}
if tty != nil {
writer.AutoStop = true
if _, err := tty.Write([]byte("\x04")); err != nil {
common.Logger(ctx).Debug("Failed to write EOT")
}
}
<-logctx.Done()
if ppty != nil {
ppty.Close()
ppty = nil
}
return err
}
func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor {
return func(ctx context.Context) error {
if err := e.exec(ctx, command, "" /*cmdline*/, env, user, workdir); err != nil {
select {
case <-ctx.Done():
return fmt.Errorf("this step has been cancelled: %w", err)
default:
return err
}
}
return nil
}
}
func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
return parseEnvFile(e, srcPath, env)
}
func (e *HostEnvironment) Remove() common.Executor {
return func(ctx context.Context) error {
if e.CleanUp != nil {
e.CleanUp()
}
return os.RemoveAll(e.Path)
}
}
func (e *HostEnvironment) ToContainerPath(path string) string {
if bp, err := filepath.Rel(e.Workdir, path); err != nil {
return filepath.Join(e.Path, bp)
} else if filepath.Clean(e.Workdir) == filepath.Clean(path) {
return e.Path
}
return path
}
func (e *HostEnvironment) GetActPath() string {
return e.ActPath
}
func (*HostEnvironment) GetPathVariableName() string {
if runtime.GOOS == "plan9" {
return "path"
} else if runtime.GOOS == "windows" {
return "Path" // Actually we need a case insensitive map
}
return "PATH"
}
func (e *HostEnvironment) DefaultPathVariable() string {
v, _ := os.LookupEnv(e.GetPathVariableName())
return v
}
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),
"temp": e.TmpDir,
"tool_cache": e.ToolCache,
}
}
func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
org := e.StdOut
e.StdOut = stdout
return org, org
}
func (*HostEnvironment) IsEnvironmentCaseInsensitive() bool {
return runtime.GOOS == "windows"
}

View File

@@ -1,4 +0,0 @@
package container
// Type assert HostEnvironment implements ExecutionsEnvironment
var _ ExecutionsEnvironment = &HostEnvironment{}

View File

@@ -1,77 +0,0 @@
package container
import (
"context"
"path/filepath"
"regexp"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
)
type LinuxContainerEnvironmentExtensions struct {
}
// Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
// For use in docker volumes and binds
func (*LinuxContainerEnvironmentExtensions) ToContainerPath(path string) string {
if runtime.GOOS == "windows" && strings.Contains(path, "/") {
log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
return ""
}
abspath, err := filepath.Abs(path)
if err != nil {
log.Error(err)
return ""
}
// Test if the path is a windows path
windowsPathRegex := regexp.MustCompile(`^([a-zA-Z]):\\(.+)$`)
windowsPathComponents := windowsPathRegex.FindStringSubmatch(abspath)
// Return as-is if no match
if windowsPathComponents == nil {
return abspath
}
// Convert to WSL2-compatible path if it is a windows path
// NOTE: Cannot use filepath because it will use the wrong path separators assuming we want the path to be windows
// based if running on Windows, and because we are feeding this to Docker, GoLang auto-path-translate doesn't work.
driveLetter := strings.ToLower(windowsPathComponents[1])
translatedPath := strings.ReplaceAll(windowsPathComponents[2], `\`, `/`)
// Should make something like /mnt/c/Users/person/My Folder/MyActProject
result := strings.Join([]string{"/mnt", driveLetter, translatedPath}, `/`)
return result
}
func (*LinuxContainerEnvironmentExtensions) GetActPath() string {
return "/var/run/act"
}
func (*LinuxContainerEnvironmentExtensions) GetPathVariableName() string {
return "PATH"
}
func (*LinuxContainerEnvironmentExtensions) DefaultPathVariable() string {
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
func (*LinuxContainerEnvironmentExtensions) JoinPathVariable(paths ...string) string {
return strings.Join(paths, ":")
}
func (*LinuxContainerEnvironmentExtensions) GetRunnerContext(ctx context.Context) map[string]interface{} {
return map[string]interface{}{
"os": "Linux",
"arch": RunnerArch(ctx),
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
}
}
func (*LinuxContainerEnvironmentExtensions) IsEnvironmentCaseInsensitive() bool {
return false
}

View File

@@ -1,71 +0,0 @@
package container
import (
"fmt"
"os"
"runtime"
"strings"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestContainerPath(t *testing.T) {
type containerPathJob struct {
destinationPath string
sourcePath string
workDir string
}
linuxcontainerext := &LinuxContainerEnvironmentExtensions{}
if runtime.GOOS == "windows" {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}
rootDrive := os.Getenv("SystemDrive")
rootDriveLetter := strings.ReplaceAll(strings.ToLower(rootDrive), `:`, "")
for _, v := range []containerPathJob{
{"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""},
{"/mnt/f/work/dir", `F:\work\dir`, ""},
{"/mnt/c/windows/to/unix", "windows\\to\\unix", fmt.Sprintf("%s\\", rootDrive)},
{fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", fmt.Sprintf("%s\\", rootDrive)},
} {
if v.workDir != "" {
if err := os.Chdir(v.workDir); err != nil {
log.Error(err)
t.Fail()
}
}
assert.Equal(t, v.destinationPath, linuxcontainerext.ToContainerPath(v.sourcePath))
}
if err := os.Chdir(cwd); err != nil {
log.Error(err)
}
} else {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}
for _, v := range []containerPathJob{
{"/home/act/go/src/github.com/nektos/act", "/home/act/go/src/github.com/nektos/act", ""},
{"/home/act", `/home/act/`, ""},
{cwd, ".", ""},
} {
assert.Equal(t, v.destinationPath, linuxcontainerext.ToContainerPath(v.sourcePath))
}
}
}
type typeAssertMockContainer struct {
Container
LinuxContainerEnvironmentExtensions
}
// Type assert Container + LinuxContainerEnvironmentExtensions implements ExecutionsEnvironment
var _ ExecutionsEnvironment = &typeAssertMockContainer{}

View File

@@ -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
}
}

View File

@@ -1,26 +0,0 @@
//go:build (!windows && !plan9 && !openbsd) || (!windows && !plan9 && !mips64)
package container
import (
"os"
"syscall"
"github.com/creack/pty"
)
func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
if tty {
return &syscall.SysProcAttr{
Setsid: true,
Setctty: true,
}
}
return &syscall.SysProcAttr{
Setpgid: true,
}
}
func openPty() (*os.File, *os.File, error) {
return pty.Open()
}

View File

@@ -1,17 +0,0 @@
package container
import (
"errors"
"os"
"syscall"
)
func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{
Setpgid: true,
}
}
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}

View File

@@ -1,17 +0,0 @@
package container
import (
"errors"
"os"
"syscall"
)
func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{
Rfork: syscall.RFNOTEG,
}
}
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}

View File

@@ -1,15 +0,0 @@
package container
import (
"errors"
"os"
"syscall"
)
func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{CmdLine: cmdLine, CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
}
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}

View File

@@ -6,15 +6,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"github.com/rhysd/actionlint" "github.com/rhysd/actionlint"
) )
@@ -181,40 +178,25 @@ func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error)
} }
func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) { func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
var ps []gitignore.Pattern var filepaths []string
const cwdPrefix = "." + string(filepath.Separator)
const excludeCwdPrefix = "!" + cwdPrefix
for _, path := range paths { for _, path := range paths {
if path.Kind() == reflect.String { if path.Kind() == reflect.String {
cleanPath := path.String() filepaths = append(filepaths, path.String())
if strings.HasPrefix(cleanPath, cwdPrefix) {
cleanPath = cleanPath[len(cwdPrefix):]
} else if strings.HasPrefix(cleanPath, excludeCwdPrefix) {
cleanPath = "!" + cleanPath[len(excludeCwdPrefix):]
}
ps = append(ps, gitignore.ParsePattern(cleanPath, nil))
} else { } else {
return "", fmt.Errorf("Non-string path passed to hashFiles") return "", fmt.Errorf("Non-string path passed to hashFiles")
} }
} }
matcher := gitignore.NewMatcher(ps)
var files []string var files []string
if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error {
for i := range filepaths {
newFiles, err := filepath.Glob(filepath.Join(impl.config.WorkingDir, filepaths[i]))
if err != nil { if err != nil {
return err return "", fmt.Errorf("Unable to glob.Glob: %v", err)
} }
sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
parts := strings.Split(sansPrefix, string(filepath.Separator)) files = append(files, newFiles...)
if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return "", fmt.Errorf("Unable to filepath.Walk: %v", err)
} }
if len(files) == 0 { if len(files) == 0 {

View File

@@ -188,11 +188,7 @@ func TestFunctionHashFiles(t *testing.T) {
{"hashFiles('**/non-extant-files') }}", "", "hash-non-existing-file"}, {"hashFiles('**/non-extant-files') }}", "", "hash-non-existing-file"},
{"hashFiles('**/non-extant-files', '**/more-non-extant-files') }}", "", "hash-multiple-non-existing-files"}, {"hashFiles('**/non-extant-files', '**/more-non-extant-files') }}", "", "hash-multiple-non-existing-files"},
{"hashFiles('./for-hashing-1.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-single-file"}, {"hashFiles('./for-hashing-1.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-single-file"},
{"hashFiles('./for-hashing-*.txt') }}", "8e5935e7e13368cd9688fe8f48a0955293676a021562582c7e848dafe13fb046", "hash-multiple-files"}, {"hashFiles('./for-hashing-*') }}", "8e5935e7e13368cd9688fe8f48a0955293676a021562582c7e848dafe13fb046", "hash-multiple-files"},
{"hashFiles('./for-hashing-*.txt', '!./for-hashing-2.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-negative-pattern"},
{"hashFiles('./for-hashing-**') }}", "c418ba693753c84115ced0da77f876cddc662b9054f4b129b90f822597ee2f94", "hash-multiple-files-and-directories"},
{"hashFiles('./for-hashing-3/**') }}", "6f5696b546a7a9d6d42a449dc9a56bef244aaa826601ef27466168846139d2c2", "hash-nested-directories"},
{"hashFiles('./for-hashing-3/**/nested-data.txt') }}", "8ecadfb49f7f978d0a9f3a957e9c8da6cc9ab871f5203b5d9f9d1dc87d8af18c", "hash-nested-directories-2"},
} }
env := &EvaluationEnvironment{} env := &EvaluationEnvironment{}

View File

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

View File

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

View File

@@ -1 +0,0 @@
Knock knock!

View File

@@ -1 +0,0 @@
Anybody home?

185
pkg/jobparser/evaluator.go Normal file
View 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
}

View File

@@ -0,0 +1,81 @@
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
}

152
pkg/jobparser/jobparser.go Normal file
View File

@@ -0,0 +1,152 @@
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)
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
})
}

View 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())
})
}
}

123
pkg/jobparser/model.go Normal file
View File

@@ -0,0 +1,123 @@
package jobparser
import (
"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"`
}

View 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]

View 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

View 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

View 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

View 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

View 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

View File

@@ -1,27 +0,0 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,18 +0,0 @@
package lookpath
import "os"
type Env interface {
Getenv(name string) string
}
type defaultEnv struct {
}
func (*defaultEnv) Getenv(name string) string {
return os.Getenv(name)
}
func LookPath(file string) (string, error) {
return LookPath2(file, &defaultEnv{})
}

View File

@@ -1,10 +0,0 @@
package lookpath
type Error struct {
Name string
Err error
}
func (e *Error) Error() string {
return e.Err.Error()
}

View File

@@ -1,23 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build js && wasm
package lookpath
import (
"errors"
)
// ErrNotFound is the error resulting if a path search failed to find an executable file.
var ErrNotFound = errors.New("executable file not found in $PATH")
// LookPath searches for an executable named file in the
// directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
// The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) {
// Wasm can not execute processes, so act as if there are no executables at all.
return "", &Error{file, ErrNotFound}
}

View File

@@ -1,56 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package lookpath
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
// ErrNotFound is the error resulting if a path search failed to find an executable file.
var ErrNotFound = errors.New("executable file not found in $path")
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return fs.ErrPermission
}
// LookPath searches for an executable named file in the
// directories named by the path environment variable.
// If file begins with "/", "#", "./", or "../", it is tried
// directly and the path is not consulted.
// The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) {
// skip the path lookup for these prefixes
skip := []string{"/", "#", "./", "../"}
for _, p := range skip {
if strings.HasPrefix(file, p) {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &Error{file, err}
}
}
path := lenv.Getenv("path")
for _, dir := range filepath.SplitList(path) {
path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &Error{file, ErrNotFound}
}

View File

@@ -1,59 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package lookpath
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
// ErrNotFound is the error resulting if a path search failed to find an executable file.
var ErrNotFound = errors.New("executable file not found in $PATH")
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return fs.ErrPermission
}
// LookPath searches for an executable named file in the
// directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
// The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) {
// NOTE(rsc): I wish we could use the Plan 9 behavior here
// (only bypass the path if file begins with / or ./ or ../)
// but that would not match all the Unix shells.
if strings.Contains(file, "/") {
err := findExecutable(file)
if err == nil {
return file, nil
}
return "", &Error{file, err}
}
path := lenv.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil {
return path, nil
}
}
return "", &Error{file, ErrNotFound}
}

View File

@@ -1,94 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package lookpath
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
// ErrNotFound is the error resulting if a path search failed to find an executable file.
var ErrNotFound = errors.New("executable file not found in %PATH%")
func chkStat(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if d.IsDir() {
return fs.ErrPermission
}
return nil
}
func hasExt(file string) bool {
i := strings.LastIndex(file, ".")
if i < 0 {
return false
}
return strings.LastIndexAny(file, `:\/`) < i
}
func findExecutable(file string, exts []string) (string, error) {
if len(exts) == 0 {
return file, chkStat(file)
}
if hasExt(file) {
if chkStat(file) == nil {
return file, nil
}
}
for _, e := range exts {
if f := file + e; chkStat(f) == nil {
return f, nil
}
}
return "", fs.ErrNotExist
}
// LookPath searches for an executable named file in the
// directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
// LookPath also uses PATHEXT environment variable to match
// a suitable candidate.
// The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) {
var exts []string
x := lenv.Getenv(`PATHEXT`)
if x != "" {
for _, e := range strings.Split(strings.ToLower(x), `;`) {
if e == "" {
continue
}
if e[0] != '.' {
e = "." + e
}
exts = append(exts, e)
}
} else {
exts = []string{".com", ".exe", ".bat", ".cmd"}
}
if strings.ContainsAny(file, `:\/`) {
if f, err := findExecutable(file, exts); err == nil {
return f, nil
} else {
return "", &Error{file, err}
}
}
if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
return f, nil
}
path := lenv.Getenv("path")
for _, dir := range filepath.SplitList(path) {
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
return f, nil
}
}
return "", &Error{file, ErrNotFound}
}

View File

@@ -3,7 +3,6 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git" "github.com/nektos/act/pkg/common/git"
@@ -36,9 +35,6 @@ type GithubContext struct {
RetentionDays string `json:"retention_days"` RetentionDays string `json:"retention_days"`
RunnerPerflog string `json:"runner_perflog"` RunnerPerflog string `json:"runner_perflog"`
RunnerTrackingID string `json:"runner_tracking_id"` RunnerTrackingID string `json:"runner_tracking_id"`
ServerURL string `json:"server_url"`
APIURL string `json:"api_url"`
GraphQLURL string `json:"graphql_url"`
} }
func asString(v interface{}) string { func asString(v interface{}) string {
@@ -93,22 +89,26 @@ func withDefaultBranch(ctx context.Context, b string, event map[string]interface
var findGitRef = git.FindGitRef var findGitRef = git.FindGitRef
var findGitRevision = git.FindGitRevision var findGitRevision = git.FindGitRevision
func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repoPath string) { func (ghc *GithubContext) SetRefAndSha(ctx context.Context, defaultBranch string, repoPath string) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
switch ghc.EventName { switch ghc.EventName {
case "pull_request_target": case "pull_request_target":
ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef) ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef)
ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
case "pull_request", "pull_request_review", "pull_request_review_comment": case "pull_request", "pull_request_review", "pull_request_review_comment":
ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"]) ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"])
case "deployment", "deployment_status": case "deployment", "deployment_status":
ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref")) ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref"))
ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
case "release": case "release":
ghc.Ref = fmt.Sprintf("refs/tags/%s", asString(nestedMapLookup(ghc.Event, "release", "tag_name"))) ghc.Ref = asString(nestedMapLookup(ghc.Event, "release", "tag_name"))
case "push", "create", "workflow_dispatch": case "push", "create", "workflow_dispatch":
ghc.Ref = asString(ghc.Event["ref"]) ghc.Ref = asString(ghc.Event["ref"])
if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
ghc.Sha = asString(ghc.Event["after"])
}
default: default:
defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch")) defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))
if defaultBranch != "" { if defaultBranch != "" {
@@ -136,23 +136,6 @@ func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repo
ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))) ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch")))
} }
} }
}
func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) {
logger := common.Logger(ctx)
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
switch ghc.EventName {
case "pull_request_target":
ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
case "deployment", "deployment_status":
ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
case "push", "create", "workflow_dispatch":
if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
ghc.Sha = asString(ghc.Event["after"])
}
}
if ghc.Sha == "" { if ghc.Sha == "" {
_, sha, err := findGitRevision(ctx, repoPath) _, sha, err := findGitRevision(ctx, repoPath)
@@ -163,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"))
}
}
}

View File

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

View File

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

View File

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

View File

@@ -58,8 +58,9 @@ func (w *Workflow) On() []string {
func (w *Workflow) OnEvent(event string) interface{} { func (w *Workflow) OnEvent(event string) interface{} {
if w.RawOn.Kind == yaml.MappingNode { if w.RawOn.Kind == yaml.MappingNode {
var val map[string]interface{} var val map[string]interface{}
if !decodeNode(w.RawOn, &val) { err := w.RawOn.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
return val[event] return val[event]
} }
@@ -84,55 +85,16 @@ func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
} }
var val map[string]yaml.Node var val map[string]yaml.Node
if !decodeNode(w.RawOn, &val) { err := w.RawOn.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
var config WorkflowDispatch var config WorkflowDispatch
node := val["workflow_dispatch"] node := val["workflow_dispatch"]
if !decodeNode(node, &config) { err = node.Decode(&config)
return nil if err != nil {
} log.Fatal(err)
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 {
// The callers expect for "on: workflow_call" and "on: [ workflow_call ]" a non nil return value
return &WorkflowCall{}
}
var val map[string]yaml.Node
if !decodeNode(w.RawOn, &val) {
return &WorkflowCall{}
}
var config WorkflowCall
node := val["workflow_call"]
if !decodeNode(node, &config) {
return &WorkflowCall{}
} }
return &config return &config
@@ -153,8 +115,6 @@ type Job struct {
Defaults Defaults `yaml:"defaults"` Defaults Defaults `yaml:"defaults"`
Outputs map[string]string `yaml:"outputs"` Outputs map[string]string `yaml:"outputs"`
Uses string `yaml:"uses"` Uses string `yaml:"uses"`
With map[string]interface{} `yaml:"with"`
RawSecrets yaml.Node `yaml:"secrets"`
Result string Result string
} }
@@ -209,45 +169,21 @@ func (s Strategy) GetFailFast() bool {
return failFast return failFast
} }
func (j *Job) InheritSecrets() bool {
if j.RawSecrets.Kind != yaml.ScalarNode {
return false
}
var val string
if !decodeNode(j.RawSecrets, &val) {
return false
}
return val == "inherit"
}
func (j *Job) Secrets() map[string]string {
if j.RawSecrets.Kind != yaml.MappingNode {
return nil
}
var val map[string]string
if !decodeNode(j.RawSecrets, &val) {
return nil
}
return val
}
// Container details for the job // Container details for the job
func (j *Job) Container() *ContainerSpec { func (j *Job) Container() *ContainerSpec {
var val *ContainerSpec var val *ContainerSpec
switch j.RawContainer.Kind { switch j.RawContainer.Kind {
case yaml.ScalarNode: case yaml.ScalarNode:
val = new(ContainerSpec) val = new(ContainerSpec)
if !decodeNode(j.RawContainer, &val.Image) { err := j.RawContainer.Decode(&val.Image)
return nil if err != nil {
log.Fatal(err)
} }
case yaml.MappingNode: case yaml.MappingNode:
val = new(ContainerSpec) val = new(ContainerSpec)
if !decodeNode(j.RawContainer, val) { err := j.RawContainer.Decode(val)
return nil if err != nil {
log.Fatal(err)
} }
} }
return val return val
@@ -258,14 +194,16 @@ func (j *Job) Needs() []string {
switch j.RawNeeds.Kind { switch j.RawNeeds.Kind {
case yaml.ScalarNode: case yaml.ScalarNode:
var val string var val string
if !decodeNode(j.RawNeeds, &val) { err := j.RawNeeds.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
return []string{val} return []string{val}
case yaml.SequenceNode: case yaml.SequenceNode:
var val []string var val []string
if !decodeNode(j.RawNeeds, &val) { err := j.RawNeeds.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
return val return val
} }
@@ -277,14 +215,16 @@ func (j *Job) RunsOn() []string {
switch j.RawRunsOn.Kind { switch j.RawRunsOn.Kind {
case yaml.ScalarNode: case yaml.ScalarNode:
var val string var val string
if !decodeNode(j.RawRunsOn, &val) { err := j.RawRunsOn.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
return []string{val} return []string{val}
case yaml.SequenceNode: case yaml.SequenceNode:
var val []string var val []string
if !decodeNode(j.RawRunsOn, &val) { err := j.RawRunsOn.Decode(&val)
return nil if err != nil {
log.Fatal(err)
} }
return val return val
} }
@@ -294,8 +234,8 @@ func (j *Job) RunsOn() []string {
func environment(yml yaml.Node) map[string]string { func environment(yml yaml.Node) map[string]string {
env := make(map[string]string) env := make(map[string]string)
if yml.Kind == yaml.MappingNode { if yml.Kind == yaml.MappingNode {
if !decodeNode(yml, &env) { if err := yml.Decode(&env); err != nil {
return nil log.Fatal(err)
} }
} }
return env return env
@@ -310,8 +250,8 @@ func (j *Job) Environment() map[string]string {
func (j *Job) Matrix() map[string][]interface{} { func (j *Job) Matrix() map[string][]interface{} {
if j.Strategy.RawMatrix.Kind == yaml.MappingNode { if j.Strategy.RawMatrix.Kind == yaml.MappingNode {
var val map[string][]interface{} var val map[string][]interface{}
if !decodeNode(j.Strategy.RawMatrix, &val) { if err := j.Strategy.RawMatrix.Decode(&val); err != nil {
return nil log.Fatal(err)
} }
return val return val
} }
@@ -322,7 +262,7 @@ func (j *Job) Matrix() map[string][]interface{} {
// It skips includes and hard fails excludes for non-existing keys // It skips includes and hard fails excludes for non-existing keys
// //
//nolint:gocyclo //nolint:gocyclo
func (j *Job) GetMatrixes() ([]map[string]interface{}, error) { func (j *Job) GetMatrixes() []map[string]interface{} {
matrixes := make([]map[string]interface{}, 0) matrixes := make([]map[string]interface{}, 0)
if j.Strategy != nil { if j.Strategy != nil {
j.Strategy.FailFast = j.Strategy.GetFailFast() j.Strategy.FailFast = j.Strategy.GetFailFast()
@@ -373,7 +313,7 @@ func (j *Job) GetMatrixes() ([]map[string]interface{}, error) {
excludes = append(excludes, e) excludes = append(excludes, e)
} else { } else {
// We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include // We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include
return nil, fmt.Errorf("the workflow is not valid. Matrix exclude key %q does not match any key within the matrix", k) log.Fatalf("The workflow is not valid. Matrix exclude key '%s' does not match any key within the matrix", k)
} }
} }
} }
@@ -418,7 +358,7 @@ func (j *Job) GetMatrixes() ([]map[string]interface{}, error) {
} else { } else {
matrixes = append(matrixes, make(map[string]interface{})) matrixes = append(matrixes, make(map[string]interface{}))
} }
return matrixes, nil return matrixes
} }
func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool { func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
@@ -440,42 +380,6 @@ func commonKeysMatch2(a map[string]interface{}, b map[string]interface{}, m map[
return true return true
} }
// JobType describes what type of job we are about to run
type JobType int
const (
// StepTypeRun is all steps that have a `run` attribute
JobTypeDefault JobType = iota
// StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
JobTypeReusableWorkflowLocal
// JobTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
JobTypeReusableWorkflowRemote
)
func (j JobType) String() string {
switch j {
case JobTypeDefault:
return "default"
case JobTypeReusableWorkflowLocal:
return "local-reusable-workflow"
case JobTypeReusableWorkflowRemote:
return "remote-reusable-workflow"
}
return "unknown"
}
// Type returns the type of the job
func (j *Job) Type() JobType {
if strings.HasPrefix(j.Uses, "./.github/workflows") && (strings.HasSuffix(j.Uses, ".yml") || strings.HasSuffix(j.Uses, ".yaml")) {
return JobTypeReusableWorkflowLocal
} else if !strings.HasPrefix(j.Uses, "./") && strings.Contains(j.Uses, ".github/workflows") && (strings.Contains(j.Uses, ".yml@") || strings.Contains(j.Uses, ".yaml@")) {
return JobTypeReusableWorkflowRemote
}
return JobTypeDefault
}
// ContainerSpec is the specification of the container to use for the job // ContainerSpec is the specification of the container to use for the job
type ContainerSpec struct { type ContainerSpec struct {
Image string `yaml:"image"` Image string `yaml:"image"`
@@ -492,6 +396,7 @@ type ContainerSpec struct {
// Step is the structure of one step in a job // Step is the structure of one step in a job
type Step struct { type Step struct {
Number int `yaml:"-"`
ID string `yaml:"id"` ID string `yaml:"id"`
If yaml.Node `yaml:"if"` If yaml.Node `yaml:"if"`
Name string `yaml:"name"` Name string `yaml:"name"`
@@ -518,8 +423,16 @@ func (s *Step) String() string {
} }
// Environments returns string-based key=value map for a step // Environments returns string-based key=value map for a step
// Note: all keys are uppercase
func (s *Step) Environment() map[string]string { func (s *Step) Environment() map[string]string {
return environment(s.Env) env := environment(s.Env)
for k, v := range env {
delete(env, k)
env[strings.ToUpper(k)] = v
}
return env
} }
// GetEnv gets the env for a step // GetEnv gets the env for a step
@@ -547,7 +460,7 @@ func (s *Step) ShellCommand() string {
case "python": case "python":
shellCommand = "python {0}" shellCommand = "python {0}"
case "sh": case "sh":
shellCommand = "sh -e {0}" shellCommand = "sh -e -c {0}"
case "cmd": case "cmd":
shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\"" shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
case "powershell": case "powershell":
@@ -574,12 +487,6 @@ const (
// StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo // StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
StepTypeUsesActionRemote StepTypeUsesActionRemote
// StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
StepTypeReusableWorkflowLocal
// StepTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
StepTypeReusableWorkflowRemote
// StepTypeInvalid is for steps that have invalid step action // StepTypeInvalid is for steps that have invalid step action
StepTypeInvalid StepTypeInvalid
) )
@@ -596,10 +503,6 @@ func (s StepType) String() string {
return "remote-action" return "remote-action"
case StepTypeUsesDockerURL: case StepTypeUsesDockerURL:
return "docker" return "docker"
case StepTypeReusableWorkflowLocal:
return "local-reusable-workflow"
case StepTypeReusableWorkflowRemote:
return "remote-reusable-workflow"
} }
return "unknown" return "unknown"
} }
@@ -617,10 +520,6 @@ func (s *Step) Type() StepType {
return StepTypeRun return StepTypeRun
} else if strings.HasPrefix(s.Uses, "docker://") { } else if strings.HasPrefix(s.Uses, "docker://") {
return StepTypeUsesDockerURL return StepTypeUsesDockerURL
} else if strings.HasPrefix(s.Uses, "./.github/workflows") && (strings.HasSuffix(s.Uses, ".yml") || strings.HasSuffix(s.Uses, ".yaml")) {
return StepTypeReusableWorkflowLocal
} else if !strings.HasPrefix(s.Uses, "./") && strings.Contains(s.Uses, ".github/workflows") && (strings.Contains(s.Uses, ".yml@") || strings.Contains(s.Uses, ".yaml@")) {
return StepTypeReusableWorkflowRemote
} else if strings.HasPrefix(s.Uses, "./") { } else if strings.HasPrefix(s.Uses, "./") {
return StepTypeUsesActionLocal return StepTypeUsesActionLocal
} }
@@ -658,17 +557,3 @@ func (w *Workflow) GetJobIDs() []string {
} }
return ids return ids
} }
var OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) {
log.Fatalf("Failed to decode node %v into %T: %v", node, out, err)
}
func decodeNode(node yaml.Node, out interface{}) bool {
if err := node.Decode(out); err != nil {
if OnDecodeNodeError != nil {
OnDecodeNodeError(node, out, err)
}
return false
}
return true
}

View File

@@ -138,31 +138,6 @@ jobs:
}) })
} }
func TestReadWorkflow_JobTypes(t *testing.T) {
yaml := `
name: invalid job definition
jobs:
default-job:
runs-on: ubuntu-latest
steps:
- run: echo
remote-reusable-workflow:
runs-on: ubuntu-latest
uses: remote/repo/.github/workflows/workflow.yml@main
local-reusable-workflow:
runs-on: ubuntu-latest
uses: ./.github/workflows/workflow.yml
`
workflow, err := ReadWorkflow(strings.NewReader(yaml))
assert.NoError(t, err, "read workflow should succeed")
assert.Len(t, workflow.Jobs, 3)
assert.Equal(t, workflow.Jobs["default-job"].Type(), JobTypeDefault)
assert.Equal(t, workflow.Jobs["remote-reusable-workflow"].Type(), JobTypeReusableWorkflowRemote)
assert.Equal(t, workflow.Jobs["local-reusable-workflow"].Type(), JobTypeReusableWorkflowLocal)
}
func TestReadWorkflow_StepsTypes(t *testing.T) { func TestReadWorkflow_StepsTypes(t *testing.T) {
yaml := ` yaml := `
name: invalid step definition name: invalid step definition
@@ -241,8 +216,7 @@ func TestReadWorkflow_Strategy(t *testing.T) {
w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true) w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true)
assert.NoError(t, err) assert.NoError(t, err)
p, err := w.PlanJob("strategy-only-max-parallel") p := w.PlanJob("strategy-only-max-parallel")
assert.NoError(t, err)
assert.Equal(t, len(p.Stages), 1) assert.Equal(t, len(p.Stages), 1)
assert.Equal(t, len(p.Stages[0].Runs), 1) assert.Equal(t, len(p.Stages[0].Runs), 1)
@@ -250,33 +224,25 @@ func TestReadWorkflow_Strategy(t *testing.T) {
wf := p.Stages[0].Runs[0].Workflow wf := p.Stages[0].Runs[0].Workflow
job := wf.Jobs["strategy-only-max-parallel"] job := wf.Jobs["strategy-only-max-parallel"]
matrixes, err := job.GetMatrixes() assert.Equal(t, job.GetMatrixes(), []map[string]interface{}{{}})
assert.NoError(t, err)
assert.Equal(t, matrixes, []map[string]interface{}{{}})
assert.Equal(t, job.Matrix(), map[string][]interface{}(nil)) assert.Equal(t, job.Matrix(), map[string][]interface{}(nil))
assert.Equal(t, job.Strategy.MaxParallel, 2) assert.Equal(t, job.Strategy.MaxParallel, 2)
assert.Equal(t, job.Strategy.FailFast, true) assert.Equal(t, job.Strategy.FailFast, true)
job = wf.Jobs["strategy-only-fail-fast"] job = wf.Jobs["strategy-only-fail-fast"]
matrixes, err = job.GetMatrixes() assert.Equal(t, job.GetMatrixes(), []map[string]interface{}{{}})
assert.NoError(t, err)
assert.Equal(t, matrixes, []map[string]interface{}{{}})
assert.Equal(t, job.Matrix(), map[string][]interface{}(nil)) assert.Equal(t, job.Matrix(), map[string][]interface{}(nil))
assert.Equal(t, job.Strategy.MaxParallel, 4) assert.Equal(t, job.Strategy.MaxParallel, 4)
assert.Equal(t, job.Strategy.FailFast, false) assert.Equal(t, job.Strategy.FailFast, false)
job = wf.Jobs["strategy-no-matrix"] job = wf.Jobs["strategy-no-matrix"]
matrixes, err = job.GetMatrixes() assert.Equal(t, job.GetMatrixes(), []map[string]interface{}{{}})
assert.NoError(t, err)
assert.Equal(t, matrixes, []map[string]interface{}{{}})
assert.Equal(t, job.Matrix(), map[string][]interface{}(nil)) assert.Equal(t, job.Matrix(), map[string][]interface{}(nil))
assert.Equal(t, job.Strategy.MaxParallel, 2) assert.Equal(t, job.Strategy.MaxParallel, 2)
assert.Equal(t, job.Strategy.FailFast, false) assert.Equal(t, job.Strategy.FailFast, false)
job = wf.Jobs["strategy-all"] job = wf.Jobs["strategy-all"]
matrixes, err = job.GetMatrixes() assert.Equal(t, job.GetMatrixes(),
assert.NoError(t, err)
assert.Equal(t, matrixes,
[]map[string]interface{}{ []map[string]interface{}{
{"datacenter": "site-c", "node-version": "14.x", "site": "staging"}, {"datacenter": "site-c", "node-version": "14.x", "site": "staging"},
{"datacenter": "site-c", "node-version": "16.x", "site": "staging"}, {"datacenter": "site-c", "node-version": "16.x", "site": "staging"},

View File

@@ -14,7 +14,6 @@ import (
"strings" "strings"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
@@ -31,7 +30,6 @@ type actionStep interface {
type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
type actionYamlReader func(filename string) (io.Reader, io.Closer, error) type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
type fileWriter func(filename string, data []byte, perm fs.FileMode) error type fileWriter func(filename string, data []byte, perm fs.FileMode) error
type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor
@@ -63,7 +61,7 @@ func readActionImpl(ctx context.Context, step *model.Step, actionDir string, act
if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil { if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
return nil, err return nil, err
} }
err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0o400) err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400)
if err2 != nil { if err2 != nil {
return nil, err2 return nil, err2
} }
@@ -156,8 +154,6 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingDocker: case model.ActionRunsUsingDocker:
location := actionLocation location := actionLocation
@@ -223,17 +219,14 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
var prepImage common.Executor var prepImage common.Executor
var image string var image string
forcePull := false
if strings.HasPrefix(action.Runs.Image, "docker://") { if strings.HasPrefix(action.Runs.Image, "docker://") {
image = strings.TrimPrefix(action.Runs.Image, "docker://") image = strings.TrimPrefix(action.Runs.Image, "docker://")
// Apply forcePull only for prebuild docker images
forcePull = rc.Config.ForcePull
} else { } else {
// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names // "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest") image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-")) image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
image = strings.ToLower(image) image = strings.ToLower(image)
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image)) contextDir := filepath.Join(basedir, action.Runs.Main)
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any") anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
if err != nil { if err != nil {
@@ -263,7 +256,6 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
} }
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{ prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir, ContextDir: contextDir,
Dockerfile: fileName,
ImageTag: image, ImageTag: image,
Container: actionContainer, Container: actionContainer,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
@@ -295,7 +287,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName string, based
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint) stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
prepImage, prepImage,
stepContainer.Pull(forcePull), stepContainer.Pull(rc.Config.ForcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true), stepContainer.Start(true),
@@ -319,13 +311,13 @@ func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[
inputs[k] = eval.Interpolate(ctx, v) inputs[k] = eval.Interpolate(ctx, v)
} }
} }
mergeIntoMap(step, step.getEnv(), inputs) mergeIntoMap(step.getEnv(), inputs)
stepEE := rc.NewStepExpressionEvaluator(ctx, step) stepEE := rc.NewStepExpressionEvaluator(ctx, step)
for i, v := range *cmd { for i, v := range *cmd {
(*cmd)[i] = stepEE.Interpolate(ctx, v) (*cmd)[i] = stepEE.Interpolate(ctx, v)
} }
mergeIntoMap(step, step.getEnv(), action.Runs.Env) mergeIntoMap(step.getEnv(), action.Runs.Env)
ee := rc.NewStepExpressionEvaluator(ctx, step) ee := rc.NewStepExpressionEvaluator(ctx, step)
for k, v := range *step.getEnv() { for k, v := range *step.getEnv() {
@@ -356,36 +348,33 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
binds, mounts := rc.GetBindsAndMounts() binds, mounts := rc.GetBindsAndMounts()
networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
if rc.IsHostEnv(ctx) {
networkMode = "default"
}
stepContainer := container.NewContainer(&container.NewContainerInput{ stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd, Cmd: cmd,
Entrypoint: entrypoint, Entrypoint: entrypoint,
WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir), WorkingDir: rc.Config.ContainerWorkdir(),
Image: image, Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"], Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"], Password: rc.Config.Secrets["DOCKER_PASSWORD"],
Name: createContainerName(rc.jobContainerName(), stepModel.ID), Name: createContainerName(rc.jobContainerName(), stepModel.ID),
Env: envList, Env: envList,
Mounts: mounts, Mounts: mounts,
NetworkMode: networkMode, NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
Binds: binds, Binds: binds,
Stdout: logWriter, Stdout: logWriter,
Stderr: logWriter, Stderr: logWriter,
Privileged: rc.Config.Privileged, Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
Options: rc.Config.ContainerOptions, AutoRemove: rc.Config.AutoRemove,
}) })
return stepContainer return stepContainer
} }
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) { func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
state, ok := rc.IntraActionState[step.getStepModel().ID] stepResult := rc.StepResults[step.getStepModel().ID]
if ok { if stepResult != nil {
for name, value := range state { for name, value := range stepResult.State {
envName := fmt.Sprintf("STATE_%s", name) envName := fmt.Sprintf("STATE_%s", name)
(*env)[envName] = value (*env)[envName] = value
} }
@@ -408,11 +397,11 @@ func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext)
containerActionDir := "." containerActionDir := "."
if step.Type() != model.StepTypeUsesActionRemote { if step.Type() != model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir) actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
containerActionDir = rc.JobContainer.ToContainerPath(rc.Config.Workdir) + "/" + actionName containerActionDir = rc.Config.ContainerWorkdir() + "/" + actionName
actionName = "./" + actionName actionName = "./" + actionName
} else if step.Type() == model.StepTypeUsesActionRemote { } else if step.Type() == model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir()) actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
containerActionDir = rc.JobContainer.GetActPath() + "/actions/" + actionName containerActionDir = ActPath + "/actions/" + actionName
} }
if actionName == "" { if actionName == "" {
@@ -475,7 +464,7 @@ func runPreStep(step actionStep) common.Executor {
var actionPath string var actionPath string
if _, ok := step.(*stepActionRemote); ok { if _, ok := step.(*stepActionRemote); ok {
actionPath = newRemoteAction(stepModel.Uses).Path actionPath = newRemoteAction(stepModel.Uses).Path
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses)) actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-"))
} else { } else {
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses) actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
actionPath = "" actionPath = ""
@@ -497,8 +486,6 @@ func runPreStep(step actionStep) common.Executor {
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingComposite: case model.ActionRunsUsingComposite:
@@ -506,10 +493,7 @@ func runPreStep(step actionStep) common.Executor {
step.getCompositeRunContext(ctx) step.getCompositeRunContext(ctx)
} }
if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil { return step.getCompositeSteps().pre(ctx)
return steps.pre(ctx)
}
return fmt.Errorf("missing steps in composite action")
default: default:
return nil return nil
@@ -566,7 +550,7 @@ func runPostStep(step actionStep) common.Executor {
var actionPath string var actionPath string
if _, ok := step.(*stepActionRemote); ok { if _, ok := step.(*stepActionRemote); ok {
actionPath = newRemoteAction(stepModel.Uses).Path actionPath = newRemoteAction(stepModel.Uses).Path
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses)) actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-"))
} else { } else {
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses) actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
actionPath = "" actionPath = ""
@@ -589,8 +573,6 @@ func runPostStep(step actionStep) common.Executor {
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)} containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
logger.Debugf("executing remote job container: %s", containerArgs) logger.Debugf("executing remote job container: %s", containerArgs)
rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingComposite: case model.ActionRunsUsingComposite:
@@ -598,10 +580,7 @@ func runPostStep(step actionStep) common.Executor {
return err return err
} }
if steps := step.getCompositeSteps(); steps != nil && steps.post != nil { return step.getCompositeSteps().post(ctx)
return steps.post(ctx)
}
return fmt.Errorf("missing steps in composite action")
default: default:
return nil return nil

View File

@@ -66,13 +66,10 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
JobContainer: parent.JobContainer, JobContainer: parent.JobContainer,
ActionPath: actionPath, ActionPath: actionPath,
Env: env, Env: env,
GlobalEnv: parent.GlobalEnv,
Masks: parent.Masks, Masks: parent.Masks,
ExtraPath: parent.ExtraPath, ExtraPath: parent.ExtraPath,
Parent: parent, Parent: parent,
EventJSON: parent.EventJSON,
} }
compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
return compositerc return compositerc
} }
@@ -86,10 +83,6 @@ func execAsComposite(step actionStep) common.Executor {
steps := step.getCompositeSteps() steps := step.getCompositeSteps()
if steps == nil || steps.main == nil {
return fmt.Errorf("missing steps in composite action")
}
ctx = WithCompositeLogger(ctx, &compositeRC.Masks) ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
err := steps.main(ctx) err := steps.main(ctx)
@@ -104,16 +97,6 @@ func execAsComposite(step actionStep) common.Executor {
rc.Masks = append(rc.Masks, compositeRC.Masks...) rc.Masks = append(rc.Masks, compositeRC.Masks...)
rc.ExtraPath = compositeRC.ExtraPath rc.ExtraPath = compositeRC.ExtraPath
// compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
mergeIntoMap := mergeIntoMapCaseSensitive
if rc.JobContainer.IsEnvironmentCaseInsensitive() {
mergeIntoMap = mergeIntoMapCaseInsensitive
}
if rc.GlobalEnv == nil {
rc.GlobalEnv = map[string]string{}
}
mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv)
mergeIntoMap(rc.Env, compositeRC.GlobalEnv)
return err return err
} }
@@ -137,6 +120,7 @@ func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
if step.ID == "" { if step.ID == "" {
step.ID = fmt.Sprintf("%d", i) step.ID = fmt.Sprintf("%d", i)
} }
step.Number = i
// create a copy of the step, since this composite action could // create a copy of the step, since this composite action could
// run multiple times and we might modify the instance // run multiple times and we might modify the instance

View File

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

72
pkg/runner/command.go Normal file → Executable file
View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser" "github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -21,47 +22,21 @@ type ExpressionEvaluator interface {
// NewExpressionEvaluator creates a new evaluator // NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator { func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator {
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
}
func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) ExpressionEvaluator {
var workflowCallResult map[string]*model.WorkflowCallResult
// todo: cleanup EvaluationEnvironment creation // todo: cleanup EvaluationEnvironment creation
using := make(map[string]exprparser.Needs) job := rc.Run.Job()
strategy := make(map[string]interface{}) strategy := make(map[string]interface{})
if rc.Run != nil { if job.Strategy != nil {
job := rc.Run.Job() strategy["fail-fast"] = job.Strategy.FailFast
if job != nil && job.Strategy != nil { strategy["max-parallel"] = job.Strategy.MaxParallel
strategy["fail-fast"] = job.Strategy.FailFast }
strategy["max-parallel"] = job.Strategy.MaxParallel
}
jobs := rc.Run.Workflow.Jobs jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs() jobNeeds := rc.Run.Job().Needs()
for _, needs := range jobNeeds { using := make(map[string]map[string]map[string]string)
using[needs] = exprparser.Needs{ for _, needs := range jobNeeds {
Outputs: jobs[needs].Outputs, using[needs] = map[string]map[string]string{
Result: jobs[needs].Result, "outputs": jobs[needs].Outputs,
}
}
// only setup jobs context in case of workflow_call
// and existing expression evaluator (this means, jobs are at
// least ready to run)
if rc.caller != nil && rc.ExprEval != nil {
workflowCallResult = map[string]*model.WorkflowCallResult{}
for jobName, job := range jobs {
result := model.WorkflowCallResult{
Outputs: map[string]string{},
}
for k, v := range job.Outputs {
result.Outputs[k] = v
}
workflowCallResult[jobName] = &result
}
} }
} }
@@ -70,21 +45,23 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
ee := &exprparser.EvaluationEnvironment{ ee := &exprparser.EvaluationEnvironment{
Github: ghc, Github: ghc,
Env: env, Env: rc.GetEnv(),
Job: rc.getJobContext(), Job: rc.getJobContext(),
Jobs: &workflowCallResult,
// todo: should be unavailable // todo: should be unavailable
// but required to interpolate/evaluate the step outputs on the job // but required to interpolate/evaluate the step outputs on the job
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: getWorkflowSecrets(ctx, rc), Runner: map[string]interface{}{
"os": "Linux",
"arch": container.RunnerArch(ctx),
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
},
Secrets: rc.Config.Secrets,
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
Inputs: inputs, Inputs: inputs,
} }
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
return expressionEvaluator{ return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{ interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run, Run: rc.Run,
@@ -107,11 +84,10 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
jobs := rc.Run.Workflow.Jobs jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs() jobNeeds := rc.Run.Job().Needs()
using := make(map[string]exprparser.Needs) using := make(map[string]map[string]map[string]string)
for _, needs := range jobNeeds { for _, needs := range jobNeeds {
using[needs] = exprparser.Needs{ using[needs] = map[string]map[string]string{
Outputs: jobs[needs].Outputs, "outputs": jobs[needs].Outputs,
Result: jobs[needs].Result,
} }
} }
@@ -119,11 +95,17 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
inputs := getEvaluatorInputs(ctx, rc, step, ghc) inputs := getEvaluatorInputs(ctx, rc, step, ghc)
ee := &exprparser.EvaluationEnvironment{ ee := &exprparser.EvaluationEnvironment{
Github: step.getGithubContext(ctx), Github: step.getGithubContext(ctx),
Env: *step.getEnv(), Env: *step.getEnv(),
Job: rc.getJobContext(), Job: rc.getJobContext(),
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: getWorkflowSecrets(ctx, rc), Runner: map[string]interface{}{
"os": "Linux",
"arch": container.RunnerArch(ctx),
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
},
Secrets: rc.Config.Secrets,
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
@@ -131,9 +113,6 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
// but required to interpolate/evaluate the inputs in actions/composite // but required to interpolate/evaluate the inputs in actions/composite
Inputs: inputs, Inputs: inputs,
} }
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
return expressionEvaluator{ return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{ interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run, Run: rc.Run,
@@ -337,8 +316,6 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} { func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} {
inputs := map[string]interface{}{} inputs := map[string]interface{}{}
setupWorkflowInputs(ctx, &inputs, rc)
var env map[string]string var env map[string]string
if step != nil { if step != nil {
env = *step.getEnv() env = *step.getEnv()
@@ -354,71 +331,18 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
if ghc.EventName == "workflow_dispatch" { if ghc.EventName == "workflow_dispatch" {
config := rc.Run.Workflow.WorkflowDispatchConfig() config := rc.Run.Workflow.WorkflowDispatchConfig()
if config != nil && config.Inputs != nil { for k, v := range config.Inputs {
for k, v := range config.Inputs { value := nestedMapLookup(ghc.Event, "inputs", k)
value := nestedMapLookup(ghc.Event, "inputs", k) if value == nil {
if value == nil { value = v.Default
value = v.Default }
} if v.Type == "boolean" {
if v.Type == "boolean" { inputs[k] = value == "true"
inputs[k] = value == "true" } else {
} else { inputs[k] = value
inputs[k] = value
}
} }
} }
} }
return inputs return inputs
} }
func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc *RunContext) {
if rc.caller != nil {
config := rc.Run.Workflow.WorkflowCallConfig()
for name, input := range config.Inputs {
value := rc.caller.runContext.Run.Job().With[name]
if value != nil {
if str, ok := value.(string); ok {
// evaluate using the calling RunContext (outside)
value = rc.caller.runContext.ExprEval.Interpolate(ctx, str)
}
}
if value == nil && config != nil && config.Inputs != nil {
value = input.Default
if rc.ExprEval != nil {
if str, ok := value.(string); ok {
// evaluate using the called RunContext (inside)
value = rc.ExprEval.Interpolate(ctx, str)
}
}
}
(*inputs)[name] = value
}
}
}
func getWorkflowSecrets(ctx context.Context, rc *RunContext) map[string]string {
if rc.caller != nil {
job := rc.caller.runContext.Run.Job()
secrets := job.Secrets()
if secrets == nil && job.InheritSecrets() {
secrets = rc.caller.runContext.Config.Secrets
}
if secrets == nil {
secrets = map[string]string{}
}
for k, v := range secrets {
secrets[k] = rc.caller.runContext.ExprEval.Interpolate(ctx, v)
}
return secrets
}
return rc.Config.Secrets
}

View File

@@ -117,6 +117,7 @@ func TestEvaluateRunContext(t *testing.T) {
{"github.run_id", "1", ""}, {"github.run_id", "1", ""},
{"github.run_number", "1", ""}, {"github.run_number", "1", ""},
{"job.status", "success", ""}, {"job.status", "success", ""},
{"runner.os", "Linux", ""},
{"matrix.os", "Linux", ""}, {"matrix.os", "Linux", ""},
{"matrix.foo", "bar", ""}, {"matrix.foo", "bar", ""},
{"env.key", "value", ""}, {"env.key", "value", ""},

View File

@@ -38,20 +38,6 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
return common.NewDebugExecutor("No steps found") return common.NewDebugExecutor("No steps found")
} }
preSteps = append(preSteps, func(ctx context.Context) error {
// Have to be skipped for some Tests
if rc.Run == nil {
return nil
}
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
// evaluate environment variables since they can contain
// GitHub's special environment variables.
for k, v := range rc.GetEnv() {
rc.Env[k] = rc.ExprEval.Interpolate(ctx, v)
}
return nil
})
for i, stepModel := range infoSteps { for i, stepModel := range infoSteps {
stepModel := stepModel stepModel := stepModel
if stepModel == nil { if stepModel == nil {
@@ -62,6 +48,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
if stepModel.ID == "" { if stepModel.ID == "" {
stepModel.ID = fmt.Sprintf("%d", i) stepModel.ID = fmt.Sprintf("%d", i)
} }
stepModel.Number = i
step, err := sf.newStep(stepModel, rc) step, err := sf.newStep(stepModel, rc)
@@ -95,18 +82,21 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
} }
postExecutor = postExecutor.Finally(func(ctx context.Context) error { postExecutor = postExecutor.Finally(func(ctx context.Context) error {
logger := common.Logger(ctx)
jobError := common.JobError(ctx) jobError := common.JobError(ctx)
var err error if jobError != nil {
if rc.Config.AutoRemove || jobError == nil { info.result("failure")
// always allow 1 min for stopping and removing the runner, even if we were cancelled logger.WithField("jobResult", "failure").Infof("\U0001F3C1 Job failed")
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute) } else {
defer cancel() err := info.stopContainer()(ctx)
err = info.stopContainer()(ctx) if err != nil {
return err
}
info.result("success")
logger.WithField("jobResult", "success").Infof("\U0001F3C1 Job succeeded")
} }
setJobResult(ctx, info, rc, jobError == nil)
setJobOutputs(ctx, rc)
return err return nil
}) })
pipeline := make([]common.Executor, 0) pipeline := make([]common.Executor, 0)
@@ -119,7 +109,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
if ctx.Err() == context.Canceled { if ctx.Err() == context.Canceled {
// in case of an aborted run, we still should execute the // in case of an aborted run, we still should execute the
// post steps to allow cleanup. // post steps to allow cleanup.
ctx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), 5*time.Minute) ctx, cancel = context.WithTimeout(WithJobLogger(context.Background(), rc.Run.JobID, rc.String(), rc.Config, &rc.Masks, rc.Matrix), 5*time.Minute)
defer cancel() defer cancel()
} }
return postExecutor(ctx) return postExecutor(ctx)
@@ -128,52 +118,9 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
Finally(info.closeContainer())) Finally(info.closeContainer()))
} }
func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) {
logger := common.Logger(ctx)
jobResult := "success"
// we have only one result for a whole matrix build, so we need
// to keep an existing result state if we run a matrix
if len(info.matrix()) > 0 && rc.Run.Job().Result != "" {
jobResult = rc.Run.Job().Result
}
if !success {
jobResult = "failure"
}
info.result(jobResult)
if rc.caller != nil {
// set reusable workflow job result
rc.caller.runContext.result(jobResult)
}
jobResultMessage := "succeeded"
if jobResult != "success" {
jobResultMessage = "failed"
}
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
}
func setJobOutputs(ctx context.Context, rc *RunContext) {
if rc.caller != nil {
// map outputs for reusable workflows
callerOutputs := make(map[string]string)
ee := rc.NewExpressionEvaluator(ctx)
for k, v := range rc.Run.Workflow.WorkflowCallConfig().Outputs {
callerOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value))
}
rc.caller.runContext.Run.Job().Outputs = callerOutputs
}
}
func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor { func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, stepModel.String(), stage.String())
rawLogger := common.Logger(ctx).WithField("raw_output", true) rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {

View File

@@ -15,15 +15,15 @@ import (
func TestJobExecutor(t *testing.T) { func TestJobExecutor(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, secrets}, {workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms},
{workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets}, {workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
{workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets}, {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
{workdir, "uses-github-root", "push", "", platforms, secrets}, {workdir, "uses-github-root", "push", "", platforms},
{workdir, "uses-github-path", "push", "", platforms, secrets}, {workdir, "uses-github-path", "push", "", platforms},
{workdir, "uses-docker-url", "push", "", platforms, secrets}, {workdir, "uses-docker-url", "push", "", platforms},
{workdir, "uses-github-full-sha", "push", "", platforms, secrets}, {workdir, "uses-github-full-sha", "push", "", platforms},
{workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, secrets}, {workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms},
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets}, {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms},
} }
// These tests are sufficient to only check syntax. // These tests are sufficient to only check syntax.
ctx := common.WithDryrun(context.Background(), true) ctx := common.WithDryrun(context.Background(), true)
@@ -79,7 +79,6 @@ func (jim *jobInfoMock) result(result string) {
type jobContainerMock struct { type jobContainerMock struct {
container.Container container.Container
container.LinuxContainerEnvironmentExtensions
} }
func (jcm *jobContainerMock) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) { func (jcm *jobContainerMock) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
@@ -249,17 +248,7 @@ func TestNewJobExecutor(t *testing.T) {
sfm := &stepFactoryMock{} sfm := &stepFactoryMock{}
rc := &RunContext{ rc := &RunContext{
JobContainer: &jobContainerMock{}, JobContainer: &jobContainerMock{},
Run: &model.Run{
JobID: "test",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"test": {},
},
},
},
Config: &Config{},
} }
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
executorOrder := make([]string, 0) executorOrder := make([]string, 0)
jim.On("steps").Return(tt.steps) jim.On("steps").Return(tt.steps)

View File

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

View File

@@ -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",
}
}

Some files were not shown because too many files have changed in this diff Show More